“架构师图谱”是一个很宏大的命题,特别是优秀的架构师自身也是“由点到面再到图”,一点点成长积累起来,尝试写这系列文章的目的更多的是结合自身的一些经验和理解,来解读工程师和架构师所应具备的技能模型,这里会更偏向于后端技能,依赖于开源技术、云原生或者其他第三方服务。重点介绍一些技术栈、设计理念和适应场景,这些可以作为我们选型时的依据。所谓“架构即决策”,是在一个有约束的盒子中寻求最优解。这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等编织、掺杂在一起的综合体。本质上无优劣,但是存在恰当的架构用在合适的软件系统中,而这些就是决策的结果。
一个技术图谱:
计划会分三个篇章来介绍:
完整的思维导图:
谈到微服务,通常会和SOA、微内核等架构进行比较,不过SOA粗粒度服务、庞大的ESB,在互联网更注重敏捷交付的场景,落地较少。微内核架构和微服务架构没有本质上的区别,但是更多的面向插件化场景,在一些类似营销、风控、工作流、管线等场景,对应的微服务可以采用微内核架构。
微服务(英语:Microservices)是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic)的API集相互通信。
微服务架构有别于更为传统的单体巨石服务,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署。这也体现了可扩展的基本思想:将原本大一统的系统拆成多个小部分,扩展时只修改其中一部分,通过这种方式减少改动范围,降低改动风险。
微服务架构涵盖了服务的多个方面,包括理论基础、网关、通信协议以及服务注册/发现、可观察性等基础设施。
微服务的理论基础主要用来指导微服务架构设计、服务拆分,确定合适的服务粒度和边界。在做微服务之前我们首先要想明白我们现有系统面临什么样的问题,为什么需要微服务,随后才是怎么做。
微服务很多核心理念其实在半个世纪前的一篇文章中就被阐述过了,而且这篇文章中的很多论点在软件开发飞速发展的这半个世纪中竟然一再被验证,这就是康威定律(Conway’s Law)。在康威的这篇文章中,最有名的一句话就是:
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations. – Melvin Conway(1967)
中文直译大概的意思就是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。 最初这篇文章只是描述作者自己的发现和总结,后来“人月神话”中,引用这个观点,并将其“吹捧”成现在熟知的“康威定律”,其中的一些核心观点可以概括如下:
“康威定律”更多的阐述了影响微服务实施的一些软性因素,再具体到业务上,我们需要结合业务现状、复杂度、团队现状进行进一步的评估、分析,再采用合适的方式进行拆分。面向业务拆分,我们一般需要考虑:
当我们的业务和组织架构复杂度比较高的时候,很多概念只从技术的角度很难去抽象。但是为了达到可复制,足够的抽象是必须的,越高层的抽象越稳定,越细节的东西越容易变化。因此我们需要把思考层次从代码细节、技术架构拉到业务层面。
我们不妨自上而下,建立起通用语言,通过对不同领域的建模,逐步确定领域范围和业务边界,这也就是领域驱动设计(DDD)。 DDD 是一种在面向高度复杂的软件系统时,关于如何去建模的方法论,它的关键点是根据系统的复杂程度建立合适的模型,DDD中的限界上下文也完美匹配了微服务的高内聚、低耦合特性,这也为我们微服务的划分提供了强有力的基础。
除了微服务,在平台和中台的建设上,我们也经常谈及DDD,平台是解决公共能力的复用问题,防止重复造轮子,中台则是“企业级能力的复用”,中台从业务和平台服务中不断抽象和聚合,建立领域模型,形成的一整套可复用的平台级解决方案,这点和DDD战略设计不谋而合。平台和中台建模之后,我们就需要通过DDD的架构设计和微服务完成系统架构,平台、中台和微服务可以说是DDD一个落地的最佳场景,这也是DDD逐渐火热的重要原因。
DDD实施的一般步骤是:
DDD并不是银弹,首先是团队对DDD的掌握程度以及领域专家(可以是研发、产品、测试等)的水平,一定程度上都会影响到DDD的落地,这里有几本关于DDD的书籍,由浅及深,非常推荐去阅读:
其次,在一些新业务场景,本身就充满了很多的不确定性,一次性把边界划清楚并不是一件很容易的事。大家在一个进程里,调整起来会相对容易,然后让不同的限界上下文各自演化,等到了一定程度之后再考虑微服务也是一个不错的选择。
作为微服务的统一入口,也肩负着整个微服务的流量接入、管理、聚合、安全等,从服务分层的角度可以划分为接入网关和业务网关。
接入网关 接入网关提供最基础的流量接入和安全防护能力,侧重于全局,与业务无关。
业务网关
业务网关作为业务的最上层出口,一般承担起业务接入或者BFF的工作,例如基础的路由、鉴权、限流、熔断降级、服务聚合、插件化能力,并可以通过可视化界面管理网关配置。可选框架有基于openrestry的kong、apisix以及其他语言相关的spring cloud gateway、grpc-gateway等等,国内开源的goku、kratos、go-zero go框架,有很多比较有意思的组件实现,我们日常业务上也可以借鉴。
服务间的通信方式是在采用微服务架构时需要做出一个最基本的决策,统一的协议标准也能大大降低服务的联调和维护成本。
RPC vs HTTP REST优点
在一些特定场景,例如:OpenApi、BFF等,HTTP REST可以更大程度上降低外部团队的接入成本。并且RPC也有调试不便、多语言互通需要对应的SDK支持这些问题,各有利弊。综合考虑来看,除了一些特定场景,如果我们已经有相对完善的基础设施支撑(RPC框架、服务治理),RPC可以为一个更合适的选择。
服务注册主要是通过将微服务的后端机器IP、端口、地域等信息注册起来,并结合一定的发现机制使客户端的请求能够直连具体的后端机器。从实现方式上可以分为服务端模式与客户端模式:
客户端模式,可以借助注册中心实现,注册中心负责服务的注册与健康检查,客户端通过监听配置变更的方式及时把配置中心维护的配置同步到本地,通过客户端负载均衡策略直接向后端机器发起请求。
从两种模式的实现方式上可以看出
服务端模式注册与发现都由服务端完成,这样可以使客户端专注在自身的业务实现,但是由于依赖负载均衡器,也就是集中式的proxy,proxy需要维护双向连接,也很容易使自己成为系统瓶颈,可用性的高低直接决定了服务质量,并且DNS缓存机制也会导致故障发生时,迁移并不能及时完成。当然在服务量少,且负载均衡器有VIP的情况下,我们也可以不使用DNS。
客户端模式注册与发现由配置中心和客户端共同完成,客户端先通过服务发现获取到真实的后端节点,再与后端节点直接通信,通过去中心化的方式,可以避免出现双向链接等proxy模式的性能问题,但是可靠性很容易出现在配置中心上,并且客户端的也需要一定的接入成本。好在开源的已经有很成熟的架构方案与丰富的客户端SDK,例如etcd/zookeeper/consul,consul提供开箱即用的功能,etcd社区和接入易用性方面更优一些,他们之间的一些具体区别:
FEATURE | ETCD | ZOOKEEPER | CONSUL |
---|---|---|---|
服务健康检查 | 连接心跳 | 连接心跳 | 心跳、内存、硬盘等 |
一致性 | raft | paxos | raft |
接口 | http/grpc | 客户端sdk(依赖较重) | http/dns |
metrics | 支持 | 不支持 | 支持 |
安全 | https | acl | acl/https |
kv存储 | 支持 | 支持 | 支持 |
配置中心从使用场景来讲,一类是前边讲到的服务注册、发现和KV存储,例如etcd/zk/consul,在k8s场景下也可以通过configmap/secret将配置写入本地文件、环境变量或者共享的volume中,这样没有了中心服务的依赖和客户端的接入,可以实现一些老旧服务的无侵入式改造。但是作为配置中心,除了基础的配置数据,一些情况下还要开放给非开发人员(测试、运维、产品)使用,完善的控制台、权限管理、dashbord的支持,也非常重要,这类可以参考nacos(阿里开源)/apollo(携程开源)。nacos在读写性能上优于apollo,但是功能特性(例如权限管理)稍逊于apollo。
在控制论中,可观测性是用系统输出到外部的信息来推断系统内部运运行状态的一种度量方式
可观测性三大支柱围绕Tracing(链路追踪)、Logging(日志)和Metrics(度量)展开,这三个维度几乎涵盖了应用程序的各种表征行为,开发人员通过收集并查看这三个维度的数据时刻掌握应用程序的运行情况。很长一段时间,这三者是独立存在的,随着时间的推移,这三者已经相互关联,相辅相成。
可观测性,乍一看好像是在卖弄概念,微服务出现之前也有日志、指标、链路追踪这些,其实不然,而是随着微服务、云原生、容器这些技术的出现,服务间的调用链路变的越来越复杂,所以相比于微服务出现之前,会更强调服务的观测性,目标是能准确快速的度量当前服务的状态,当出现问题时能快速定位修复,要达到这些,就要去构建强大的观测系统,能够实现像统一观测协议,三合一平台,推出配套的工具,迁移接入等等。
链路追踪为分布式应用的开发者提供了完整的调用链路还原、调用请求量统计、链路拓扑、应用依赖分析等工具,可以帮助开发者快速分析和诊断分布式应用架构下的性能瓶颈,提高微服务时代下的开发诊断效率以及系统的可观察性。 为了解决不同的分布式系统API不兼容的问题,诞生了OpenTracing规范,OpenTracing中的Trace可以被认为是由多个Spacn组成的DAG图。
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G 在 Span F 后被调用, FollowsFrom)
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
OpenTracing专注在tracing,除此之外还有包含了metrics的OpenCensus标准,以及由CNCF推出,融合OpenTracing和OpenCensus的OpenTelemetry。OpenTelemetry旨在实现云原生时代可观察性指标(Tracing、Logging、Metrics)的统一收集和处理,同时提供推动这些标准实施的组件和工具。 OpenTracing中的佼佼者当属Jaeger、Zipkin、Skywalking。他们之间的一些对比:
Zipkin开源时间长,社区相对丰富,Jaeger更加轻量,也是Istio推荐方案,Skywalking支持部分语言(Java、PHP、Python等)的无侵入式接入。另外APM(应用性能)监控的支持也会影响到我们的选型。 除此之外,这几种默认支持的都是头部连贯策略,在入口处决定是否采样,面对线上海量请求,也通常需要借助一些尾部连贯采样的策略,把我们关心的,例如链路中发生了错误、部分请求耗时过高等请求,都采集下来,可以通过结合opentelemetry-collector以及开箱即用的tailsamplingprocessor构建pipeline插件实现。
服务间的链路日志能否帮助我们判断错误发生的具体位置,这类业务日志主要集中在访问日志/打点日志等等。随着大数据的兴起,我们对数据的分析解读能力越来越强,日志作为原始数据则体现出了更大的价值,例如用户的行为分析,反垃圾,舆情分析等等。
在实际使用过程中可能会对日志级别进行简化和调整,一般来讲Warning及以上的日志是需要重点关注的,需要做好及时的监控告警,Warning以下的日志也可以辅助问题的定位。 日志写入可以选择写入消息队列,也可以选择落地磁盘,将关心的结构化或非结构化日志、业务模块信息(如果是细粒度的微服务,可以选择将日志放同一模块收集),以及级别、时间(who、when、where、how、what)等要素正确的写入正确写入后再收集到日志服务。写入消息队列需要考虑消息队列的选型以及做好可用性和积压监控,写入磁盘需要考虑写入性能以及日志的切割清理,例如golang的zap+rotatelogs组合。日志收集的话,由于Logstash资源消耗相对比较大,虚拟机环境中可以使用filebeat来替代,更严苛的线上或容器环境,可以使用Fluentd/Fluentd Bit。日志最终汇总到ES和Kibana做展示,通过esalert定制告警策略。
在业务记录一些错误日志时,通常可以选择,在错误发生处附带堆栈信息;将error抛给调用者,在顶层打印日志,而非层层打印;错误降级时,选择性打印日志。
指标是有关系统的离散的数据点,这些指标通常表示为计数或度量,并且通常在一段时间内进行汇总或计算,一般用来做基础的资源监控和业务监控:
Zabbix作为老牌的监控系统,适合更复杂的物理机、虚拟机、数据库等更复杂的场景,同时也拥有更丰富的图形化界面,但是Prometheus作为云原生的代表作,与k8s、容器等能更好的结合,协同grafana实现可定制化的界面,另外存储基于TSDB,相比于关系型数据库也有更好的扩展性。以Prometheus为例,支持的数据类型有:
Counter 只增不减的计数器,例如请求数(http_requests_total)。基于此数据模型,使用Prometheus提供的强大PromQL表达式能够拓展出更加适合开发观察的指标数据。 分钟增量请求:increase(http_requests_total[1m]) 分钟QPS:rate(http_requests_total[1m])
Gauge 可增可减的时刻量,例如Go语言协程数(go_goroutines)、消息队列积压量 波动量:delta(go_goroutines[10m])
Histogram 直方图,不同区间内样本的个数。例如,耗时50ms-100ms每分钟请求量,100ms-150ms每分钟请求量。
Summary 概要,反应百分位值。例如,某RPC接口,95%的请求耗时低于150ms,99%的请求耗时低于200ms。
Prometheus指标支持pull和push模式:
我们前边讲的服务发现、熔断降级、安全、流量控制、可观察性等能力。这些通用能力在Service Mesh出现之前,由Lib/Framework通过一些切面的方式完成,这样就可以在开发层面上很容易地集成到我们的应用服务中。但是并没有办法实现跨语言编程,有什么改动后,也需要重新编译重新发布服务。理论上应该有一个专门的层来干这事,于是出现了 Sidecar,
第一代Service Mesh,像Linkerd,后边又出现了第二代Service Mesh,Istio,职责分明,分离出处数据面和控制面。但是Sidecar作为代理层,避免不了性能损耗(CPU序列化反序列化 UDS),所以proxyless service mesh重新被提起,和之前的 「RPC + 服务发现治理」区别是啥?感觉这个名词营销味道略重。其实不能简单的 “Proxyless Service Mesh” 理解为 “一个简单的RPC框架,暴露了几个超时参数到配置中心来控制”,它重在统一协议、API,这样就便于基于统一的协议实现proxyless mesh和proxy mesh的互通,可以同时满足性能敏感型和快速迭代型的业务场景。
他们相辅相成,丰富了service mesh的形态。
servicemesh对于微服务基础设施的一种演进,但不代表他已经非常成熟了,相反像迁移成本高,甚至一些可用性设计还不如业务自己做那么灵活,这些现实的问题还摆在面前,我觉得这也是属于技术进化的一种趋势,当一项技术足够成熟的时候,又回衍生出新的复杂度问题,从而又需要发展出新技术解决。
在计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。
实际应用场景中,消息队列也经常作为中间件,用于服务解耦、削峰填谷、数据广播、错峰与流控、最终一致性等,在一些核心的大数据分析、交易支付等场景也经常扮演重要角色。
关于服务解耦,会有很多人质疑,消息队列是否能真正解耦,我的理解是:数据要发生流转,系统之间要有依赖关系,例如上游服务直接读写下游存储、中间件进行数据交互,解耦则更侧重于将易于变化的复杂度转移,对下游存储、中间件的依赖,通过消息队列转化为双方的弱接口(消息payload)依赖。但如果上游是本身是依赖的下游API,这种方式就需要考虑有多个下游时,自身复杂度和可用性的变化。
消息队列的选型主要侧重以下几点:
这里着重对比一下Redis、Rabbitmq/Rocketmq、Kafka、Plusar
redis实现消息队列可以通过List&Zset类型、Pub/Sub、Stream(redis 5.0)类型来实现,HA使用多副本或者集群的方式。作为消息队列使用起来非常方便,但是也有很多的弊端:
除此之外,List&Zset类型无法支持消息广播,和Pub/Sub一样也不支持重复消费。结合整体来看redis作为消息队列大多数只应用在数据量小,对丢失数据不敏感的业务场景,适用范围较小,复杂业务并且有一定运维支撑的情况下,可以直接考虑企业级消息中间件。
这几个可以作为企业级消息中间件的代表,Rabbitmq和kafka的一些详细对比,可以参考之前写的这篇文章《消息队列Rabbitmq与Kafka对比分析》。而Rocketmq在设计之初就借鉴了很多Rabbitmq、Kafka的设计理念,例如:Routing、多副本、顺序写(IO),也广泛应用在淘宝双十一等场景。
HA 在HA方面他们都是通过副本的方式,区别是Rabbitmq是集群级别的副本,Kafka是多partiton和ISR、选举机制,而Rocketmq通过多(master/slave)副本同时保障NameServer和Broker。
高吞吐 Kafka和Rocketmq通过直接操作文件系统,相比于Rabbitmq,顺序写能大幅度提升数据的处理速度。Kafka为了进一步提升消息的吞吐量,可以采用客户端缓冲队列的方式批量发送,但也会存在宕机丢失数据的可能性,可以通过设置 batch.size 与 linger.ms 来动态调整,相比于Rocketmq更加灵活。Kafka的partition机制的确会带来性能的提升,但是在Topic不断增多的情况下,众多的partition及副本也将顺序写逐步退化为随机写,并且扩容时,由于hash值的变化,也会涉及到大量partiton数据的迁移。Rocketmq采用commitlog的方式实现全局写,所以能支持更多的Topic,扩容也不涉及大量数据的迁移。除此之外Kafka和Rocketmq都有zero-copy的应用,通过sendfile和mmap,减少内核态到用户态的CPU拷贝。以及pagecache的应用,大幅度提升消费者生产者速度较为匹配场景的IO效率。
功能丰富性 Kafka只有基础的消息类型,Rabbitmq支持优先级队列,通过TTL和死信队列可以实现消息的延迟和重试,但是需要提前创建好对应队列,例如:1s延迟队列,10s延迟队列(rabbitmq3.5.7以上可以集成rabbitmq-delayed-message-exchange),阿里云版Rocketmq可以直接设置秒级的延迟时间实现延迟投递,以及针对有序消息阻塞重试,无序消息内置16个重试频率“10s 30s 1m 2m…”供集群(同一消费组)的消费者重试,另外Rocketmq也具有独有的2PL事务消息,很好的保障业务逻辑与消息发送的最终一致性。
重复消费 他们三者都采用Ack机制保障了单条消息重复消费的能力,Kafka通过offset和partition特殊的ttl机制(segment过期,按文件名顺序清理),能支持通过重置offset来回溯历史数据。
消息顺序性 Rabbitmq和Rocketmq可以保证写入同一topic的顺序性,但是在多个消费者同时消费的情况下还是会出现乱序的情况,在数据量较大的时候,我们也可以通过单个消费者消费,再按照一定的分发策略分配给多个消费者执行,只不过会提升整体复杂度,同时会带来更多的HA、维护成本考量。Kafka可以保障单个partition的顺序性,并且每个partiton只允许一个消费者来消费(N:1),这就从策略上避免了多消费者的情况,在数据量较大的情况下,可以通过划分更多的partition提升数据的并行处理能力。
综合来讲,Rabbitmq、Rocketmq使用Queue模型,丰富的消息队列功能,更多的应用在业务场景,Kafka基于Streaming模型,结合批处理、流式处理,更多的应用在大数据分析场景。
Pulsar作为Apache开源、云原生的消息中间件,诞生之初就引发了很大的关注。设计上避免了kafka遇到的功能丰富性、扩容等方面的问题,采用计算、存储分离的架构,broker层只作为“API接口层”,存储交给更专业的bookeeper,由于broker层的无状态性,结合k8s等非常方便的进行扩容。并且Pulsar支持多个消费模型提升消费者处理能力,例如:exclusive、failover、shared、key-shared等,可以说综合了kafka和其他消息中间件的众多优点。
像能量守恒定律一样,系统的复杂度往往也是守恒的,实现即高性能又高可用的消息中间件需要的技术复杂性,不会凭空消失,只会从一个地方转移到另一个地方,消息队列本质上可以理解为feature+fs,只不过存储、计算分离架构,将各层间的职责分离,使每一层都能专注在自身领域,以应对海量数据和更加复杂多变的环境,这也是现在新技术发展的一个趋势。 作为后起之秀,的确可以站在巨人的肩膀上,避免很多设计上的不足,同时引入一些新的架构理念,但是要成功的在其中分一杯羹,同样也要面临用户学习成本高、缺少杀手级应用、如何迁移等等这些现实性的问题,不过依靠良好的社区和技术先驱,随着时间的变迁,这些短板也会逐步补齐,真正适应当前时代的技术一定会脱颖而出。ps:腾讯云最近开源Rop,支持Rocketmq相对平滑的迁移至Pulsar
如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,
咱们下期见!答案获取方式:已赞 已评 已关~
学习更多JAVA知识与技巧,关注与私信博主(03)
。