原文地址:https://blog.xstudio.mobi/a/230.html
1. 概述
“架构师图谱”是一个很宏大的命题,特别是优秀的架构师自身也是“由点到面再到图”,一点点成长积累起来。
尝试写这篇文章的目的更多的是结合自身的一些架构、研发、管理经验对现阶段做一个复盘总结,所以这里更偏向于后端图谱,依赖于开源技术、云原生或者其他第三方服务。
这里会重点介绍一些技术栈、设计理念以及适应场景,这些可以作为我们选型时的依据。所谓“架构即决策”,是在一个有约束的盒子中寻求最优解。
这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等编织、掺杂在一起的综合体。
本质上无优劣,但是存在恰当的架构用在合适的软件系统中,而这些就是决策的结果。
一个技术图谱:
本文重点聚焦在微服务和常用的消息队列,包括相关的选型以及一些理论基础。
完整的思维导图:
微服务
微服务是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关的 API 集相互通信。
微服务架构有别于更为传统的单体服务,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署。
这也体现了可扩展的基本思想:将原本大一统的系统拆成多个小部分,扩展时只修改其中一部分,通过这种方式减少改动范围,降低改动风险。
微服务架构涵盖了服务的多个方面,包括网关、通信协议、服务注册/发现、可观察性、如何合理的划分等等。
| 理论基础
微服务的理论基础主要用来指导微服务架构设计、服务拆分,确定合适的服务粒度和边界。
在做微服务之前我们首先要想明白我们现有系统面临什么样的问题,为什么需要微服务,随后才是怎么做。
微服务很多核心理念其实在半个世纪前的一篇文章中就被阐述过了,而且这篇文章中的很多论点在软件开发飞速发展的这半个世纪中竟然一再被验证,这就是康威定律(Conway’s Law)。
在康威的这篇文章中,最有名的一句话就是:
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.
中文直译大概的意思就是:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。
最初这篇文章只是描述作者自己的发现和总结,后来“人月神话”中,引用这个观点,并将其“吹捧”成现在熟知的“高位定律”。
其中的一些核心观点可以概括如下:
组织沟通方式决定系统设计,对于复杂的系统,聊设计就离不开聊人与人的沟通,解决好人与人的沟通问题,才能有一个好的系统设计
时间再多一件事情也不可能做的完美,但总有时间做完一件事情,这与架构设计的“简单、合适、演化”思维不谋而合
线型系统和线型组织架构间有潜在的异质同态特征,更直白的说,你想要什么样的系统,就搭建什么样的团队,定义好系统的边界和接口,团队内应该是自治的,这样将沟通成本维持在系统内部,每个子系统就会更加内聚
大的系统组织总是比小系统更倾向于分解,面对复杂的系统及组织,往往可以采用分而治之
但是当我们的业务和组织架构复杂度比较高的时候,很多概念只从技术角度很难去抽象,这就需要我们自上而下,建立起通用语言,让业务人员和研发人员说一样的话,把思考层次从代码细节拉到业务层面。越高层的抽象越稳定,越细节的东西越容易变化。
通过对不同领域的建模,逐步确定领域范围和业务边界,这也就是领域驱动设计(DDD)。
DDD 是一种在面向高度复杂的软件系统时,关于如何去建模的方法论,它的关键点是根据系统的复杂程度建立合适的模型,DDD 中的界限上下文也完美匹配了微服务的高内聚、低耦合特性,这也为我们微服务的划分提供了强有力的基础。
DDD 实施的一般步骤是:
根据需求划分出初步的领域和限界上下文,以及上下文之间的关系
进一步分析每个上下文内部,识别出哪些是实体,哪些是值对象
对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根
为聚合根设计仓储,并思考实体或值对象的创建方式
在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的地方并重构
但是 DDD 也不是银弹,特别是在一些新业务场景,本身就充满了很多的不确定性,一次性把边界划清楚并不是一件很容易的事。
大家在一个进程里,调整起来会相对容易,然后让不同的界限上下文各自演化,等到了一定程度之后再考虑微服务也是一个不错的选择。
| 网关
作为微服务的统一入口,也肩负着整个微服务的流量接入、管理、聚合、安全等,从服务分层的角度可以划分为接入网关和业务网关。
接入网关接入网关提供最基础的流量接入和安全防护能力,侧重于全局,与业务无关。
域名&DNS:作为服务的流量入口,对外通过域名和 DNS 提供服务,国内域名厂商一般都依托于共有云或被共有云厂商收购,用来完善自由的云生态。
像阿里的万网,腾讯的 DNSPod 等,也有国外的 AWS,GoDaddy 和 Namecheap 等,可以用作 .me 等国内无法托管或备案域名的管理。
其次也可以借助DNS(HTTPDNS、EDNS)实现跨地域、运营商网络等负载均衡,实现异地多活、就近访问、容灾等。
负载均衡(LB):主要负责请求的转发代理,按机器负载来分配流量等,对外提供 VIP,这里的负载可以宽泛的理解为系统的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量。
负载均衡器按服务层级来划分,除了前边提到的 DNS,还有集群级别的硬件负载均衡,以及机器级别的软件负载均衡。
DNS/硬件负载均衡(F5/A10)主要用来应对海量用户的访问,中小量用户使用无疑会增加更多的维护和采购成本。
软件负载均衡可以选择自研或上云,LVS、Keepalived 主要用于四层(IP+端口)的负载均衡,在四层的基础之上如果要实现应用层(域名/URL/用户会话)等的 7 层负载均衡,可以使用 Nginx、Keepalived 的组合。
除此之外,网关也负责服务整体的安全防护,SSL,IPV6 等:
安全防护目的是保护服务数据以及可用性,例如防范常见的 DDOS/CC 网络攻击,反爬虫,自定义访问控制,自研成本往往比较高,可以借助云上一系列的高防、防火墙服务。
SSL(TLS)用来提供外部 https 访问,https 可以防止数据在传输过程中不被窃取、改变,确保数据的完整性,在支付或者用户登录等敏感数据场景,可以起到一定的保护作用,同时 https 页面对搜索引擎也比较友好。
IPV6,全球 43 亿 IPV4 地址已经在 2019 年年底耗尽,网信办在 2018 年开始就已经推行各大运营商、CDN 厂商、互联网核心产品支持 IPV6,我们公司之前也是试点之一。IPV6 的支持只需要增加一条“AAAA”DNS记录,将域名解析到自持 IPV6 的 IP/VIP 即可。IPV4 到 IPV6 由于存在兼容性等问题,一定是长期共存的,过渡方案可以采用 IPV6 代理(IPV6 代理转发到 IPV4 服务)或者双栈(同时支持 IPV6 和 IPV4)。
业务网关:
业务网关作为业务的最上层出口,一般承担起业务接入或者 BFF 的工作,例如基础的路由、鉴权、限流、熔断降级、服务聚合、插件化能力,并可以通过可视化界面管理网关配置。
可选框架有基于 OpenResty 的 Kong、APISIX 以及其他语言相关的 SpringCloud Gateway、gRPC-Gateway 等等。
国内开源的 Goku、Kratos、go-zero go 框架,有很多比较有意思的组件实现,我们日常业务上也可以借鉴。
鉴权:鉴权的目的是为了验证用户、请求等的有效性,例如用户身份鉴权(JWT/Oauth2/Cookie),请求鉴权(请求签名、请求加密),鉴权逻辑也花样繁多,大多需要基于业务定制化,通过网关插件能很好的集成进来。
限流:限流是为了做一定的流量控制,防止对系统产生过大压力从而影响整个服务。可以基于单台机器或整个集群限流,常见的方式有限制总量和限制速率,超过的则排队或丢弃,例如令牌桶(弹性)/漏桶(匀速)算法。
熔断降级:熔断作为服务断路器,当下游的服务因为某种原因突然变得不可用或响应过慢(这里既可以指单次请求也可以指一段时间),上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,这样也能对整体链路起到保护作用。
如果目标服务情况好转则恢复调用,同时结合降级策略提升服务的鲁棒性。常见的有Hystrix/Resilience4J(Hystrix 虽然已停止更新,但现有功能已经能满足大多业务场景)。
重试:大量网络 IO,避免不了会出现因网络抖动,出现连接失败或者超时,重试可以提高请求的最终成功率,削平服务毛刺。
但重试也有可能放大故障,所以可以结合退避策略(backoff)、限制单点重试、限制链路重试这些策略进行优雅的重试,同时也可以采用更加激进的“对冲请求”提前(tp99 时间未响应时)发起重试请求,降低系统时延。
插件化:各个网关集成插件的方式尽不相同,但是目的都是为了集成技术人员编写的一些业务相关的通用能力,例如前边提到的身份鉴权、请求鉴权等等。
另外作为业务网关插件,也可以编写一些基础业务(API 鉴权、请求格式化)逻辑,直接透传请求到服务层,省去很多 BFF 和上下游对接的工作。
BFF:Backend For Frontend,可以按照业务逻辑,以串行、并行和分支等结构编排多个服务 API,为服务提供聚合、适配、裁剪(只返回需要的字段)功能,核心是 API 的动态编排以满足日益增长的业务逻辑,降低前端与微服务之间的对接成本。
BFF 并不意味着只能由后端实现,也可以在前端通过 GraphQL 等 API 查询语言实现。
| 协议
服务间的通信方式是在采用微服务架构时需要做出一个最基本的决策,统一的协议标准也能大大降低服务的联调和维护成本。
HTTP REST:REST 更确切的讲是指的 API 设计风格,而不是协议标准。通常基于使用 HTTP,URL,和 JSON 这些现有的广泛流行的协议和标准。符合 REST 设计风格的 API 称作 RESTful API。
在实际应用中大多实现的是伪 REST API,例如用 POST 请求同时实现资源的增删改,或者为了请求的扩展性,资源的增删改查都使用 POST JSON。
RPC:RPC 协议描绘了客户端与服务端之间的点对点调用流程,包括 stub、通信、RPC 消息协议部分。可以基于 TCP,也可以基于 http。
在实际应用中,还需要考虑服务的高可用、负载均衡等问题,所以产品级的 RPC 框架除了点对点的 RPC 协议的具体实现外,还应包括服务的发现与注册、提供服务的多台 Server 的负载均衡、服务的高可用等更多的功能。
目前的 RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理(Dubbo、Motan),另一种偏重于跨语言调用(Thrift/GRPC)。
RPC vs HTTP REST 优点:
更清晰的 API 定义,例如 gRPC 协议的定义文件 proto,自身就可以作为很好的 API 文档,日常开发中也可以把 proto 文件独立版本库管理,精简目录结构,方便不同的服务引用。
更好的传输效率,通过序列化和反序列化进一步压缩网络传输数据,不过序列化、反序列化也会有一定的性能损耗,protobuf 可以说很好的兼顾了这两点。
更合适的容错机制,可以基于实际的业务场景,实现更合适的超时控制与异常重试机制,以应对网络抖动等对服务造成的影响。
在一些特定场景,例如:OpenAPI、BFF 等,HTTP REST 可以更大程度上降低外部团队的接入成本。并且 RPC 也有调试不便、多语言互通需要对应的 SDK 支持这些问题,各有利弊。
综合考虑来看,除了一些特定场景,如果我们已经有相对完善的基础设施支撑(RPC 框架、服务治理),RPC 可以为一个更合适的选择。
| 服务注册/发现
服务注册主要是通过将微服务的后端机器 IP、端口、地域等信息注册起来,并结合一定的发现机制使客户端的请求能够直连具体的后端机器。
从实现方式上可以分为服务端模式与客户端模式:
服务端模式:也可以说是传统模式,通过借助负载均衡器和 DNS 实现,负载均衡器负责健康检查、负载均衡策略,DNS 负责实现访问域名到负载均衡器 IP/VIP 的映射。通过直接暴露域名和端口的方式提供客户端访问。
客户端模式:可以借助注册中心实现,注册中心负责服务的注册与健康检查,客户端通过监听配置变更的方式及时把配置中心维护的配置同步到本地,通过客户端负载均衡策略直接向后端机器发起请求。
从两种模式的实现方式上可以看出:
①服务端模式注册与发现都由服务端完成,这样可以使客户端专注在自身的业务实现,但是由于依赖负载均衡器,也就是集中式的 proxy,proxy 需要维护双向连接,也很容易使自己成为系统瓶颈,可用性的高低直接决定了服务质量。
并且 DNS 缓存机制也会导致故障发生时,迁移并不能及时完成。当然在服务量少,且负载均衡器有 VIP 的情况下,我们也可以不使用 DNS。
②客户端模式注册与发现由配置中心和客户端共同完成,通过分布式的方式,可以避免出现 proxy 节点性能瓶颈问题,但是可靠性与性能瓶颈很容器出现在配置中心上,并且客户端的也需要一定的接入成本。
好在开源的已经有很成熟的架构方案与丰富的客户端 SDK,例如 etcd/ZooKeeper/Consul。
Consul 提供开箱即用的功能,etcd 社区和接入易用性方面更优一些,他们之间的一些具体区别:
| 配置中心
配置中心从使用场景来讲,一类是前边讲到的服务注册、发现和 KV 存储,例如 etcd/ZooKeeper/Consul,在 Kubernetes 场景下也可以通过 ConfigMap/Secret 将配置写入本地文件、环境变量或者共享的 Volume 中。
这样没有了中心服务的依赖和客户端的接入,可以实现一些老旧服务的无侵入式改造。
但是作为配置中心,除了基础的配置数据,一些情况下还要开放给非开发人员(测试、运维、产品)使用,完善的控制台、权限管理、Dashbord 的支持,也非常重要,这类可以参考 Nacos(阿里开源)/Apollo(携程开源)。
Nacos 在读写性能上优于 Apollo,但是功能特性(例如权限管理)稍逊于 Apollo。
| 可观察性
在控制论中,可观察性是用系统输出到外部的信息来推断系统内部运运行状态的一种度量方式。
在云原生时代,容器和服务的生命周期是紧密联系在一起的,相较在传统的单体服务运行在物理主机或者虚拟机当中,排查问题的时候显得非常不便,这种复杂性导致了一个定义研发运营效率的 MTTR(平均故障修复时间)指标急剧增加。
所以这里更强调的是微服务的可观察性,需要提前想好我们要如何观察容器内的服务以及服务之间的拓扑信息、各式指标的搜集等,这些监测能力相当重要。
可观察性三大支柱围绕 Tracing(链路追踪)、Logging(日志)和 Metrics(度量)展开,这三个维度几乎涵盖了应用程序的各种表征行为,开发人员通过收集并查看这三个维度的数据时刻掌握应用程序的运行情况。
很长一段时间,这三者是独立存在的,随着时间的推移,这三者已经相互关联,相辅相成。
①链路追踪
链路追踪为分布式应用的开发者提供了完整的调用链路还原、调用请求量统计、链路拓扑、应用依赖分析等工具,可以帮助开发者快速分析和诊断分布式应用架构下的性能瓶颈,提高微服务时代下的开发诊断效率以及系统的可观察性。
为了解决不同的分布式系统 API 不兼容的问题,诞生了 OpenTracing 规范,OpenTracing 中的 Trace 可以被认为是由多个 Spacn 组成的 DAG 图。
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 插件实现。
②日志
服务间的链路日志能否帮助我们判断错误发生的具体位置,这类业务日志主要集中在访问日志/打点日志等等。
随着大数据的兴起,我们对数据的分析解读能力越来越强,日志作为原始数据则体现出了更大的价值,例如用户的行为分析,反垃圾,舆情分析等等。
业务日志:这类日志重点在于通过不同级别的日志,及时发现分析系统存在的异常。
RFC 5424 定义的 8 中日志级别:
Emergency:system is unusable
Alert:action must be taken immediately
Critical:critical conditions
Error:error conditions
Warning:warning conditions
Notice:normal but significant condition
Informational:informational messages
Debug:debug-level messages
在实际使用过程中可能会对日志级别进行简化和调整,一般来讲 Warning 及以上的日志是需要重点关注的,需要做好及时的监控告警,Warning 以下的日志也可以辅助问题的定位。
日志写入可以选择写入消息队列,也可以选择落地磁盘,将关心的结构化或非结构化日志、业务模块信息(如果是细粒度的微服务,可以选择将日志放同一模块收集),以及级别、时间(who、when、where、how、what)等要素正确的写入正确写入后再收集到日志服务。
写入消息队列需要考虑消息队列的选型以及做好可用性和积压监控,写入磁盘需要考虑写入性能以及日志的切割清理,例如 Golang 的 zap+rotatelogs 组合。
日志收集的话,由于 Logstash 资源消耗相对比较大,虚拟机环境中可以使用 Filebeat 来替代,更严苛的线上或容器环境,可以使用 Fluentd/Fluentd Bit。日志最终汇总到 ES 和 Kibana 做展示,通过 Esalert 定制告警策略。
大数据日志:大数据日志本质上也对应着我们一定的业务场景,但大多是海量日志、高吞吐量场景,所以对海量日志的收集和存储是较大的挑战。
实现方案我们可以采用高吞吐量的流式中间件,例如 Kafka/Plusar 等,在结合流式处理(Flink)或者批处理(Spark)系统,将数据汇总到 Hadoop 进行分析,这里涉及到的中间件和数据库可参考后续章节。
③指标
指标是有关系统的离散的数据点,这些指标通常表示为计数或度量,并且通常在一段时间内进行汇总或计算。
一般用来做基础的资源监控和业务监控:
资源监控:CPU、内存、IO、fd、GC等
业务监控:QPS、模调、耗时分布等
Zabbix 作为老牌的监控系统,适合更复杂的物理机、虚拟机、数据库等更复杂的场景,同时也拥有更丰富的图形化界面。
但是 Prometheus 作为云原生的代表作,与 Kubernetes、容器等能更好的结合,协同 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 模式:
Pull:服务暴露 metrics 接口,指标内存更新,Prometheus server 定时拉取,性能更好,但要考虑易失性
Push:指标推送 push-gateway,Promethus server 从 gateway 拉取,适用瞬时业务场景(定时任务)
| Service Mesh
我们前边讲的服务发现、熔断降级、安全、流量控制、可观察性等能力。这些通用能力在 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 对于微服务基础设施的一种演进,但不代表他已经非常成熟了,相反像迁移成本高,甚至一些可用性设计还不如业务自己做那么灵活。
这些现实的问题还摆在面前,我觉得这也是属于技术进化的一种趋势,当一项技术足够成熟的时候,又回衍生出新的复杂度问题,从而又需要发展出新技术解决。
消息队列
在计算机科学中,消息队列是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。
消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设备的种类,以及特定的输入参数。
也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。
实际应用场景中,消息队列也经常作为中间件,用于异步解耦、削峰填谷、数据广播、错峰与流控、最终一致性等,在一些核心的大数据分析、交易支付等场景也经常扮演重要角色。
关于服务解耦,会有很多人质疑,消息队列是否能真正解耦,我的理解是:数据要发生流转,系统之间要有依赖关系。
例如上游服务直接读写下游存储、中间件进行数据交互,解耦则更侧重于将易于变化的复杂度转移,对下游存储、中间件的依赖,通过消息队列转化为双方的弱接口(消息payload)依赖。
但如果上游是本身是依赖的下游 API,这种方式就需要考虑有多个下游时,自身复杂度和可用性的变化。
消息队列的选型主要侧重以下几点:
HA:自身的高可用性保障,避免消息队列的引入而影响整体服务的可用性
高吞吐:在面对海量数据写入能否保持一个相对稳定、高效的数据处理能力
功能丰富性:是否支持延迟消息、事务消息、死信队列、优先级队列等
消息广播:是否支持将消息广播给消费者组或者一组消费者
消息堆积能力:在数据量过大时,是否允许一定消息堆积到broker
数据持久性:数据持久化策略的采用,也决定着数据在宕机恢复后是否会丢失数据
重复消费:是否支持ack机制,在消费者未正确处理消息时,支持重新消费
消息顺序性:针对顺序消费的场景保证数据按写入时间的顺序性
这里着重对比一下 Redis、RabbitMQ/RocketMQ、Kafka、Plusar。
| Redis
Redis 实现消息队列可以通过 List 类型、Pub/Sub、Stream(Redis 5.0)类型来实现,HA 使用多副本或者集群的方式。
作为消息队列使用起来非常方便,但是也有很多的弊端:
功能丰富性:只支持普通的消息类型
数据持久性:Pub/Sub 只提供缓冲区广播能力,不进行持久化,List/Stream 即使基于 aof 和 rdb 持久化策略,但是并没有事务性保障,在宕机恢复后还是存在丢失数据的可能性
消息堆积能力:List 随长度增大,内存不断增长;Pub/Sub 只在缓冲区内堆积,缓冲区满消费者强制下线;Stream 创建时可以指定队列最大长度,写满后剔除旧消息
除此之外,List 类型无法支持消息广播,和 Pub/Sub 一样也不支持重复消费。
结合整体来看 Redis 作为消息队列大多数只应用在数据量小,对丢失数据不敏感的业务场景,适用范围较小,复杂业务并且有一定运维支撑的情况下,可以直接考虑企业级消息中间件。
| RabbitMQ vs Kafka vs RocketMQ
这几个可以作为企业级消息中间件的代表,而 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 只有基础的消息类型,RabbitMQ 支持优先级队列,通过 TTL 和死信队列可以实现消息的延迟和重试,但是需要提前创建好对应重试频率的队列。
例如:1s 重试队列,10s 重试队列,RocketMQ 则内置了 18 个重试频率“1s 5s 10s 30s 1m 2m……”,另外也具有独有的 2PL 事务消息,很好的保障业务逻辑与消息发送的一致性。
重复消费:他们三者都采用 ACK 机制保障了单条消息重复消费的能力,Kafka 通过 offset 和 partition 特殊的 ttl 机制(segment 过期,按文件名顺序清理),能支持通过重置 offset 来回溯历史数据。
消息顺序性:RabbitMQ 和 RocketMQ 可以保证写入同一 topic 的顺序性,但是在多个消费者同时消费的情况下还是会出现乱序的情况。
在数据量较大的时候,我们也可以通过单个消费者消费,再按照一定的分发策略分配给多个消费者执行,只不过会提升整体复杂度,同时会带来更多的 HA、维护成本考量。
Kafka 可以保障单个 partition 的顺序性,并且每个 partiton 只允许一个消费者来消费(N:1),这就从策略上避免了多消费者的情况,在数据量较大的情况下,可以通过划分更多的 partition 提升数据处理能力。
综合来讲,RabbitMQ、RocketMQ 使用 Queue 模型,丰富的消息队列功能,更多的应用在业务场景,Kafka 基于 Streaming 模型,结合批处理、流式处理,更多的应用在大数据分析场景。
| Pulsar
Pulsar 作为 Apache 开源、云原生的消息中间件,诞生之初就引发了很大的关注。
设计上避免了 Kafka 遇到的功能丰富性、扩容等方面的问题,采用计算、存储分离的架构,broker 层只作为“API 接口层”,存储交给更专业的 bookeeper,由于 broker 层的无状态性,结合 Kubernetes 等非常方便的进行扩容。
并且 Pulsar 支持多个消费模型提升消费者处理能力,例如:exclusive、failover、shared、key-shared 等。
可以说综合了 Kafka 和其他消息中间件的众多优点:
HA、高吞吐:和 Kafka 类似,通过多 partition 和选举机制功,除此之外,还支持丰富的跨地域复制能力
功能丰富性:可以支持秒级的延迟消息,以及独特的重试队列和私信队列
消息顺序性:为了实现 partition 消息的顺序性,和 Kafka 一样,都需要将消息写入到同一 broker,区别是 Kafka 会同时存储消息在该 broker,broker 和 partiton 绑定在一起,而 Pulsar 可以将消息分块(segment)后,更加均匀的分散到 bookeeper 节点上,broker 只需要记录映射关系即可,这样在资源扩容时,可以更加快速便捷
像能量守恒定律一样,系统的复杂度往往也是守恒的,实现即高性能又高可用的消息中间件需要的技术复杂性,不会凭空消失,只会从一个地方转移到另一个地方。
消息队列本质上可以理解为 feature+fs,只不过存储、计算分离架构,将各层间的职责分离,使每一层都能专注在自身领域,以应对海量数据和更加复杂多变的环境,这也是现在新技术发展的一个趋势。
作为后起之秀,的确可以站在巨人的肩膀上,避免很多设计上的不足,同时引入一些新的架构理念,但是要成功的在其中分一杯羹,同样也要面临用户学习成本高、缺少杀手级应用、如何迁移等等这些现实性的问题。
不过依靠良好的社区和技术先驱,随着时间的变迁,这些短板也会逐步补齐,真正适应当前时代的技术一定会脱颖而出。