【51CTO.com快译】序幕
近些年,微服务风格的应用架构正在扎根并迅速成长,它们可能已散布在企业生态系统的方方面面。在多云环境中组织以及运维微服务,围绕微服务组织数据,使数据尽可能实时传输,这些工作正成为我们面临的挑战。
由于事件驱动架构 (EDA) 平台(例如 Kafka)和数据管理技术(例如 Data Meshes 和 Data Fabrics)的发展,设计基于微服务的应用会变得更加容易。
然而,为了确保这些基于微服务的应用在对应的级别上运行,必须确保在设计期间就考虑到关键的非功能需求(NFRs)。
在一系列博客文章中,我和我的同事 Tanmay Ambre、Harish Bharti试图用统一的方法来描述NFR 设计。我们称之为基于用例的方法。
在第一部分中,我们将聊聊“性能”的设计,它是最关键的NFR需求。本文会介绍如何对于大容量、低延迟处理为基础的架构和设计进行决策。
为了让决策过程清晰易懂,我们将使用银行转账作为演示用例,在简化了用例的同时,将关注点放到性能上。
用例
电子资金转账 (ETF) 是当今通过数字渠道消费的一种非常重要的汇款和收款方式。这也是为什么我们使用该用例来解释与性能相关的决策,因为在用例中会涉及到海量请求以及多分布式协调等诸多问题,并且不用考虑误差范围(包括:可靠性和容错问题)。
在过去,资金转账需要数天时间。还会涉及前往银行或者开支票的过程。然而,随着数字化、新支付机制、支付网关的出现以及相关规定的影响下,资金转账在瞬间就可以完成。例如,2021 年 9 月,在 UPI 网络上就发生了 36 亿笔交易,交易总价值 约合6.5 万亿印度卢比。客户更趋向于通过各种渠道进行实时支付。PSD2、开放银行和特定国家/地区已经通过相关法规,法规明确规定向受信任的第三方应用程序开发人员公开其支付机制。
通常,银行客户会使用任意渠道(例如,移动应用程序、在线门户或访问机构)发起转账请求。当收到转账请求后,需要执行以下操作,如图1 所示:
1. 鉴定转账资格(Entitlement)——检查客户是否具备转账的资格
2. 检查操作权限(Operational limits)(是否具备使用转账通道和传输模式的权利)
3. 检查余额并锁定转账金额(Account Balance checks)
4. 制裁检查(Sanctions Check) —— 交易是否符合法规以及制裁检查
5. 欺诈检查(Fraud Checks) —— 检查交易是否具有欺诈性
6. 在支付网关中创建支付请求(Payment Gateway)
下图对此进行了概述:
图1 收到转账请求需要执行的操作
注意:就本文而言,以上用例所涉及到的操作会在发起请求的金融机构内部完成。它依赖于已存在的转账网关,支付网关并在此范畴内。
关键的非功能性需求
以下是关键的 NFR(非功能性需求):
1. 性能
- 事件的低延迟处理
- 高吞吐量
2. 弹性
- 可恢复性——从故障中恢复的能力。从故障点重新启动并具有重放事件的能力。最小化 RTO(恢复时间目标)
- 零数据丢失
- 保证结果的可靠性和一致性。
3. 可用性和可扩展性
- 高可用和容错——承担故障(包括承担数据中心/可用区域的中断情况)
- 随着负载增加而对系统进行水平扩展
- 能够承受突发的流量激增(弹性可扩展性)
- 在符合法规的前提下持久化海量数据
4. 安全
- 应用中核心组件安全性——左移(Shifting left 未雨绸缪)(备注:软件开发过程被视为一个从左到右的顺序过程。例如:定义需求、分析、设计、编码、测试和部署。左移就提前考虑某某问题,不要等到了这一步再思考这个问题,放到文中就是提前考虑核心组件安全性的问题。因此翻译为“未雨绸缪”)
- 认证和授权
- 审计
- 传输层安全
- 保证数据在传输状态和静止状态的安全性
架构
云原生技术的方方面面将会贯穿本案例的实现模型——例如:微服务、API、容器、事件流和分布式数据管理,以及使用最终一致性风格的数据持久化方式来保证数据的完整性。需要注意的是,基于事件驱动的微服务架构为本案例架构提供了最佳实践。
以下是为实现此案例需要考虑的关键架构模式:
1. 分段事件驱动架构 (SEDA Staged Event-Driven Architecture)
2. 事件流处理
3. 事件溯源
4. SAGA
5. CQRS
如图2所示,为本案例解决方案架构图:
图2 案例架构图
应用和数据流
应用架构是由一组可独立运行的微服务组成。此外,协调器服务(Enablers)作为单独的微服务负责协调整个事务,确保端到端流程执行到位。
资金转账(Fund Transfer)包含多个服务,作为转账事件的生产者、处理者和消费者它们相互连接。主要由如下 4 个主要处理器:
1. Fund Transfer Orchestrator (转账协调器)– 负责处理资金转移事件并修改和维护资金转账请求的状态。
2. Fund Transfer Request Router(转账请求路由器)–基于资金的状态转换请求生成事件,并确定事件所对应的路由。它可以将接受到的事件以多播的形式发送到一个或者多个系统。同时它也是一个可选组件,如果下游消费者不能从单个主题消费(例如,下游消费者是遗留服务或已经实现的服务,同时这些服务不能更改成从单个主题消费),则需要使用到它。
3. Fund Transfer Statistics Aggregator(转账统计聚合器) – 基于管理 KPI 所需的多维度进行汇总,并且维护转账过程中的统计数据。
4. Fund Transfer Exception Managements(转账异常管理) – 根据异常自动触发转账请求中的用户行为,包括取消转账,转账重试 。简而言之是为用户提供管理转账异常的功能。
5. Fund Transfer API (转账API)– 为不同转账渠道提供以下功能:(a) 请求转账,(b) 检查转账状态,(c) 管理转账(取消、重试等)。
API 向 Fund Transfer Orchestrator 的input topic(输入主题)发布事件,该主题是 Fund Transfer 请求的主要协调器。发送的事件作为“一等公民”会被持久化到Event/State Store中,同时这些时间会在Event/State Store中不断积累(启用事件溯源架构模式)。根据事件上下文和有效载荷,Fund Transfer Orchestrator将事件进行转换并将转账状态发布到另一个topic(主题)中。同样,转账记录的状态变更也会被持久化,在出现系统级故障时,可用于恢复转账状态。
接着,由Fund Transfer Request Router(转移请求路由器)处理通过topic 传递过来的转账状态信息,并由Fund Transfer Request Router决定该状态被路由到哪个系统中(单个或者多个)。这些系统(如图2 右边的Customer、Accounts、Sanctions等)将处理之后的结果作为事件发布到input topic(输入主题),再由Fund Transfer Orchestrator(转账协调器)进行关联和处理,最终完成转账状态的更新。Fund Transfer Orchestrator (转账协调器)也能够通过input topic 接受来自Fund Transfer Exception Managements(转账异常管理)的异常消息,并对其进行处理,然后更新转账请求的状态。
Fund Transfer Statistics Aggregator(转账统计聚合器)也可以通过topic接受来自Fund Transfer Orchestrator (转账协调器)的转账事件,并且对转账记录进行实时统计,Fund Transfer Statistics Aggregator(转账统计聚合器)还聚合了多维度的转账统计数据——以便运营团队可以实时地查看转账的统计数据。
技术构件和技术栈
为了实现上述应用架构,需要使用关键的技术构件如下:
1. Event backbone——用于在服务之间的发送消息。它还将确保发送事件的排序和序列。它还提供了数据真实性的单一来源。发生故障时——系统可以从故障点重启。它提供了构建事件和状态存储的机制。在发生重大中断的情况下,所存储的事件和状态存储可用于状态恢复。
2. In-memory data-grid——是一个分布式缓存可以用来提高系统的性能。它拥有从服务中存储和查找数据的能力。它使每个服务都有自己的一组缓存,并且这些缓存是可以被持久化(可选),支持“write-through”模式(同时往内存和磁盘写入信息)。它具有弹性并且可以承受中断。此外,在集群级故障(即整个缓存集群宕机)的情况下,缓存可以通过Event Backbone的持久化机制恢复(但是,需要处理器停止工作一段时间)。
3. Service Mesh——提供监控、安全和服务发现的能力。
我们可以通过如下技术栈来构建系统。技术栈中的工具大部分是开源的,也可以使用其他技术代替。例如,Quarkus 等。
解决NFR(非功能性需求)的设计决策
为了解决高度动态和复杂的 NFR,提出最重要的三项设计决策。
- Event Backbone——为了实现用例和满足架构要求需要建立Event backbone(事件主干)。其主要原因是可以让不同的转账过程并行,从而降低处理时间。此外,使用行业级的event backbone(事件主干)将提供/支持某些开箱即用的高质量架构(例如容错性、可恢复性、可扩展性和事件排序)。
- 数据流——使用in-memory data grid(内存数据网格)可以显著减少数据库请求。此外,数据和事件被分区的设计使得系统更容易扩展。为部署所有组件选择相同的使用区域——大大减少了网络延迟。
- 安全方面的“左移”–通过强制的手段保证每个组件的安全性。需要为每个组件(尤其是event backbone事件主干、in-memory data grid内存数据网格)实施适当的访问控制和身份验证机制。必须深入了解需要满足的安全标准和法规(本案例包括:PCI-DSS、GDPR 等)。传输层的安全性对于确保数据在传输过程中的加密来说至关重要。此外,必须强制执行与数据(特别是敏感的私人信息)相关的控制。避免将敏感信息放入缓存中(尽管这可能会影响性能)。因此,在NFR 测试期间 – 需要对安全性进行测试。静态代码质量分析工作需要检查代码中的安全漏洞,针对组件镜像的扫描也是为了检查安全漏洞。防火墙、DMZ、VPC 需要通过适当的 IAM 解决方案进行配置。同时云原生架构也需要关注另一个安全方面的问题,就是机密、密钥和证书管理。
上述三个设计决策将有助于解决以下类别的 NFR:
- 自动扩展——系统的每个组件(包括事件主干、内存数据网格)在容器化并部署在 Kubernetes 和 OpenShift 等平台上之后都可以自动扩展。
- 容错- 处理管道的每个组件都可以从故障点重新启动。这并不需要特殊的实现。事件主干如 Apache Kafka就提供上述开箱即用的功能。此外,Kafka Topics 兼作事件和状态的存储工作。因此,可以更加容易地恢复处理状态。此外,即便在集群丢失好几个节点的情况下,通过Kafka 和 Ignite 的复制和重新调整功能也让系统具备持续处理事件的能力。
- 高可观察性- 系统的每个组件都被检测起来,实时性能和资源利用率指标信息会被以流式的方式传递到 Prometheus 中。而 Grafana 会与Prometheus合作完成数据可视化和报警通知的工作。
- 弹性 – 有多级部署决策来处理弹性问题。每个组件都有多个实例运行在同一个可用区(包括 Kafka)。DR 位置维护在备用的不同可用区中。Kafka 的 active 和 dr 集群之间为 Kafka 启用了集群级复制。
在下一节,我们将围绕顶级 NFR – 性能的一些关键设计因素进行深入探讨。后续我们将发布其他有关 NFR 的设计的文章。
满足性能要求的设计要点
为了满足性能要求,在设计和实施过程中需要考虑以下几点:
性能建模 ——对性能目标有一个清晰的理解是很重要的。性能目标会影响到架构、技术、基础设施和设计决策。随着混合云和多云解决方案的日益普及,性能建模变得更加重要。由此产生了许多依赖于性能建模的架构折中方案。性能建模应涵盖 – 事务/事件清单、工作负载建模(并发性、访问峰量、不同事务/事件的预期响应时间)和基础设施建模。构建性能模型有助于创建部署模型(尤其是与可扩展性相关的模型),它有助于搭建架构和设计优化从而减少访问延迟,也有助于设计性能测试并验证系统的性能和吞吐量。
避免单体怪物——单体架构的集中处理。这意味着无法独立缩放不同的组件。事实上,服务的实现也需要使用 SEDA的方式将其分解为松耦合的组件,提供扩展每个组件的能力并使服务更具弹性。每个部署的组件都可以独立扩展并部署到一个集群中,以提高系统的并发性和弹性。
事件主干的选择——Event backbone(事件主干)的 选择会影响系统性能。从性能的角度来看,以下 5个特征显得尤为重要:
- 消息抽取的性能(特别是使消息持久化。即,将消息写入到磁盘中)。此外,性能需要始终保持一致。
- 消费者消息消费的性能。
- 扩展能力(即在集群中添加新节点/代理的能力)
- 节点/代理失败时重新调整所需的时间
- 当消费者实例离开或加入消费者组时重新调整所需的时间
Apache Kafka 是一个不错的选择,它在性能、容错性、可扩展性和可用性等方面都经受住了考验,因此从众多候选者中脱颖而出。它能够轻松面对上面提出的5个特征中的前三个,对于最后 2 点(重新平衡时间),Kafka 的性能处理效果视主题数据量而定。虽然Apache Pulsar 试图解决这个问题,但是鉴于 Kafka 的良好表现并不会立即切换方案,但我们会密切关注 Apache Pulsar 的发展动向。
善用缓存– 数据库查询作为IO操作,其使用开销是可想而知的。为了避免IO操作的开销需要善用缓存机制。例如,Redis、Apache Ignite 等都是优秀的缓存实践。通过使用缓存,可以创建有助于数据查找的“快速数据层”。所有关于数据的读取操作都会首先访问缓存,而不是从数据库或其他远程服务中获取数据。流数据处理管道也可以实时地存放(或更新)到缓存。接下来事件处理器只需要引用缓存中的数据即可,而不用查询数据库或对记录系统进行调用。事件处理器先将事件写入到 Ignite中,然后持久化处理器再将数据异步写入到数据库中。这种数据处理方式极大地提高了系统的性能。
在本案例中,我们选择了 Apache Ignite,因为它具备以下几个特性:
- 它提供分布式缓存(支持数据分区)
- 水平可扩展
- 符合 ANSI SQL
- 便于使用
- 支持复制——提供一定程度的容错
- Cloud-Ready(云就绪)
缓存可以持久化。即,缓存数据写入磁盘。但它会影响性能。不使用 SSD 时,性能损失非常显著。如果缓存是非持久性的,那么架构需要满足从数据/事件存储中重新混合缓存的需求。恢复性能对于减少平均恢复时间至关重要。在我们的架构中——利用 Kafka 主题来补充缓存。它们是我们的事件/状态存储。有一个多实例恢复组件,可以从 Kafka 主题中读取数据并对缓存进行再水化。
配合处理——当事件的生产者、消费者和事件主干配合工作时,系统性能处于最佳状态。然而,这意味着如果数据中心出现故障——它会导致整个平台瘫痪。为了避免这种情况的方式可以对事件主干设置复制/镜像。例如,Kafka MirrorMaker 2 (MM2) 就可用于设置跨数据中心/可用区的复制。
选择正确的消息格式——对消息进行序列化和反序列化操作的快慢会对性能产生影响。虽然,消息格式有多种选择,例如,XML、JSON、Avro、Protobuf、Thrift。但是对于本例而言,选择 Avro的消息格式, 是因为它的紧凑性、序列化/反序列化的性能以及对消息模式演化的支持。
并发相关的决策——在本例的架构设计中,并发的关键参数如下:
- Kafka topic(主题)的分区数
- 每个消费组的实例数
- 消费者/生产者实例的线程数
这些参数会对系统吞吐量产生直接影响。在 Kafka 中,每个分区都可以被同一消费组中的单个线程所消费。在 Kafka brokers(代理)中存在多个分区和驱动器,它们有助于事件的传播,同时也不必担心消息的顺序问题。在资源利用率限制(CPU、内存和网络)范围内,拥有多线程的消费者可以在一定程度上对性能提升有所帮助。在同一个消费者组中拥有多个实例从而将负载分摊到多个节点/服务器上,最终提升水平可扩展的能力。
使用分区——分区数越多,并发性越高。但是,在确定分区数时需要注意以下几个方面的问题。重视分区键的选择,它可以在不破坏事件排序的情况下在分区之间均匀分布事件。此外,在有些情况下过度分区会带来不良后果,例如:在打开文件句柄的时候、由于broker(代理)故障造成 Kafka topic(主题)不可用的时候、端点延迟的时候以及消费者对内存有更高需要的时候。
执行性能测试从而定义系统性能的标准,通过测试推断出所需的吞吐量和性能,它们有助于确定并发的 3 个关键参数的配置。
I/O 问题需要重视——I/O操作对系统延迟的影响很大,甚至说它影响到架构中的所有组件,包括事件主干、缓存、数据库和应用程序组件。
对于使用了持久化的分布式缓存组件而言,I/O操作是影响性能的关键因素之一。例如,对于 Apache Ignite - 强烈建议使用 SSD(固态硬盘)并且开启direct(直接) I/O模式,从而获得良好的性能。非持久性缓存无疑是最快的缓存方案——但是缺点也很明显,一旦缓存集群出现故障,缓存数据将会丢失。虽然,可以通过恢复过程来解决该问题 - 重新恢复缓存。然而,需要引起注意的是随着恢复时间变长——流媒体管道中的延迟也会变大。除非,缓存的恢复过程必须非常快。因此,需要通过运行多个恢复实例同时还要避免任何转换/业务逻辑的操作。
Kafka 不一定需要高性能磁盘(如 SSD)。建议使用多驱动器(和多个日志目录)从而获得良好的吞吐量。这里不建议与应用程序以及操作系统共享驱动器。此外,诸如配置“ noatime ”之类的挂载选项可以提升性能。下面链接讨论了依赖于文件系统类型的其他挂载项:https : //kafka.apache.org/documentation.html#diskandfs
从应用程序的角度来看 - 将日志记录保持在最低限度。专注于记录异常信息而不是面面俱到。尽管大多数日志框架都能够进行异步记录,但仍然会对延迟产生影响。
内存调优——事件流架构在很大程度上依赖于内存调优。对于 Kafka brokers(代理)和 Apache Ignite 服务器节点尤为如此。因此为处理器、消费者、Kafka 代理以及内存数据网格节点分配足够的内存空间就显得非常重要。
调整操作系统的内存设置也有助于在一定程度上提高性能。关键参数之一是“ vm.swappiness”。它负责控制进程在内存中的交换。它的值介于 0 和 100 之间的值,数字越大,就表示内存交换越积极。建议将此数字保持在较低水平以减少交换。
在 Kafka 的使用场景下,由于它严重依赖页面缓存,因此也可以调整“ vmdirty ratio”选项来控制数据刷新到磁盘的比例。
网络优化 ——为了有效利用网络,建议对大容量消息进行压缩,因为这个大家伙会降低网络利用率。但是对于大容量消息的压缩可以提升生产者、消费者以及代理的CPU 利用率。例如,Kafka 支持 4 种不同的压缩类型(gzip、lz4、snappy、zstd)。Snappy 的压缩类型让CPU 使用率、压缩率、速度和网络利用率处在折中的位置,让它们之间保持一种平衡关系。
关注相关消息——在发布订阅机制中,消费者有可能获得他们不感兴趣的消息,因此可以添加过滤器从而过滤掉他们不感兴趣的消息,让更多的注意力放到相关的消息上。即便如此仍然需要对事件进行反序列化后才能对时间内容进行过滤,为了提高效率避免无效的反序列化操作,可以通过在事件的标题中添加事件元数据来对事件进行描述。消费者通过查看事件标头来决定是否对事件进行反序列化,并解析其中的内容。这种做法明显提高了消费者处理事件的吞吐量并降低了对资源的使用,提升资源利用率。
解析需要的内容(尤其是 XML)– 在金融服务行业 – XML 被大量使用,事件流应用程序的输入很可能就是 XML。然而,解析 XML属于CPU 和内存密集型的操作。因此,XML 解析器的选择是与性能和资源利用率相关的重要决策。针对大 XML 文档的解析,建议使用 SAX 解析器。如果事件流应用程序不需要解析完全XML文档,可以预先配置 xpath 来定位到所需数据,然后通过数据构建事件负载,这样可能是一个更快的选择。但是,如果需要解析整个XML 数据——那么最好一次性解析整个文档并将其转换为事件流应用程序的消息格式,解析的操作使用处理器的多个实例完成,而不是让事件流管道中的每个处理器解析XML文档。
监控和识别性能瓶颈的工具——识别性能瓶颈应该是一件很容易的事情。可以通过使用轻量级检测框架(基于 AOP)来进行识别工作。在案例中,我们将 AOP 与 Spring-Boot Actuator 和 Micrometer 进行组合,并将接口暴露给Prometheus 端点。对于 Apache Kafka性能指标的采集,使用 Prometheus 的 JMX 导出器来完成。接着就是用 Grafana 构建一个内容丰富的仪表板,上面可以显示生产者、消费者、Kafka 以及 Ignite(基本包括架构的所有组件)的性能指标。
上面这一系列的操作提供了快速查询和展示瓶颈的能力,并不需要对日志进行分析,也不用将应用与日志进行关联。
对Kafka配置进行微调 ——Kafka 有大量的配置参数用于代理、生产者、消费者和 Kafka 流信息。以下是用来配置Kafka 延迟和吞吐量的参数:
- Broker(代理)
- log.dirs – 在不同驱动器上设置多个目录以加快 I/O读写速度。
- num.network.threads
- num.io.threads
- num.replica.fetchers
- socket.send.buffer.bytes
- socket.receive.buffer.bytes
- socket.request.max.bytes
- group.initial.rebalance.delay.ms
- Producer(消息生产者)
- linger.ms
- batch.size
- buffer.memory
- compression.type
- Consumer(消息消费者)
- fetch.min.bytes - 如果设置为默认值 1,它将改善延迟但吞吐量不会很好。所以一定要做好平衡。
- max.poll.records
- Streams(流)
- num.stream.threads
- buffered.records.per.partition
- cache.max.bytes.buffering
有关调整 Kafka 代理、生产者和消费者的其他信息,请参阅提供的参考资料。
GC调优 – 垃圾收集器的调优对避免长时间停顿和过多的 GC 开销也显得尤为重要。从 JDK 8 开始,建议大多数应用程序(具有大内存的多处理器机器)使用Garbage-First Garbage Collector (G1GC) 模式。这种模式试图实现暂停时间目标并试图实现高吞吐量。此外,在启动的时候不需要太多的配置。
我们确保为 JVM 拥有足够的内存。在 使用Ignite时,会通过堆外内存来存储数据。
-Xms 和 -Xmx 与“-XX:+AlwaysPreTouch”保持相同的值,以确保在启动期间从操作系统分配所有内存。
G1GC 中还有其他参数,例如以下可用于进一步调整吞吐量(因为在 EDA 中吞吐量很重要):
- -XX:MaxGCPauseMillis(增加其值以提高吞吐量)
- -XX:G1NewSizePercent, -XX:G1MaxNewSizePercent
- -XX:ConcGCThreads
更多细节请参考调优 G1GC。
调整 Apache Ignite – Apache Ignite 调整在其官方网站上有详细记录。以下是我们为提高其性能而采取的一些措施:
- 80%:20% 原则,同时为 Ignite 服务器节点分配内存(80% 分配给 Ignite 进程。20% 保留给操作系统)
- 使用 zookeeper 进行集群发现
- 使用堆外内存来存储数据
- 至少为 JVM 堆分配了 16GB
- 根据需求将堆外划分为不同的数据区域(引用数据区域、输入数据区域、输出数据区域等)
- 引用数据保存在“副本”缓存中
- 输入/输出(即事务数据)保存在“分区”缓存中。
- 确保每个缓存都定义了一个关联键,该键始终用于并置处理的查询
- 使用上一节中的配置指南调整 JVM GC
- 将缓存组用于逻辑相关的数据
- 在 JDBC 驱动程序中启用“延迟”加载
- 正确调整不同线程池的大小以提高性能
- 使用 JCache API 代替 SQL 查询
- 确保游标关闭
- 使用“NearCache”配置获取引用数据——这样查找它的时候就不需要通过客户端从远程服务器获取了。
- 增加索引的内联大小
- 将 vm.swappiness 减少到 1
- 使用 Direct- IO
- 引用数据缓存启用了本机持久化机制。但是,事务缓存不用开启持久化机制,因为事务缓存需要备份在 Kafka 上。在集群网络中断的情况下,事务数据使用单独的恢复处理器从 Kafka 恢复到 Ignite中。
结论
在本文中,我们专注于与性能相关的架构决策。因此保持架构每个组件的性能显得非常重要,因为任何组件的问题都可能导致流信息阻塞。因此,架构的每个组件都需要在不影响其他 NFR 的情况下针对性能进行调优。所以对这些组件有深入的技术理解是必不可少的,只有这样才能有效地进行调优工作。
译者介绍
崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过60万。《分布式架构原理与实践》作者。
原文标题:Designing High-Volume Systems Using Event-Driven Architectures,作者:Ram Ravishankar,Harish Bharti,Tanmay Ambre
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】