第一章 引言
本书主要关注构建不同规模的事件驱动系统,但更深入地探讨了跨多个团队的软件开发。这正是面向服务的架构(SOA)的领域:这一理念兴起于本世纪初,指的是公司围绕执行通用功能的共享服务进行重组。
这种理念迅速流行起来。亚马逊曾公开禁止所有非服务接口的系统间通信。后来,新兴公司 Netflix 全力投入微服务架构,许多其他基于 Web 的初创公司也纷纷效仿。大型企业也做了类似的事情,但通常使用消息系统,其动态特性略有不同。在此期间,人们积累了丰富的经验,并取得了显著的进步,但过程并非一帆风顺。
当时普遍存在一个教训:基于服务的方法会显著增加凌晨三点接到紧急呼叫的概率,因为此时一个或多个服务可能会宕机。事后看来,这并不令人意外。如果你把一组原本各自独立的应用程序变成一个高度互联的网络,不难想象,一个重要但不稳定的服务可能会产生深远的影响,最坏的情况下甚至会导致整个系统瘫痪。正如 Steve Yegge 在他那篇著名的亚马逊/谷歌文章中所说:“按服务来组织,教会了团队在大多数方面彼此不信任,就像他们本就不该信任外部开发人员那样。”
然而,对亚马逊来说真正奏效的是,他们彻底转型为服务型组织所带来的组织变革。服务团队将他们的软件视为一台庞大机器中的一个齿轮。正如 Ian Robinson 所说:“要成为网络的一部分,而不是藏身于网络之后。”这与人们以往构建应用程序的方式截然不同,在以往,系统间通信只是团队事后才勉强添加的功能。但服务模型将交互提升为“一等公民”。突然间,你的用户不再仅仅是客户或业务人员;他们也是其他应用程序,他们非常关心你的服务的可靠性。因此,应用程序逐渐演变为平台,而构建平台并非易事。
LinkedIn 就深切体会到了这一点,它从最初的单体 Java 应用程序发展成为拥有 800 到 1100 个服务的庞大系统。复杂的依赖关系导致了系统不稳定,版本控制问题造成了痛苦的同步发布,而且在早期阶段,新架构是否真的有所改进也并不明朗。
LinkedIn 的发展路径与众不同之处在于它使用了自主研发的消息系统:Kafka。Kafka 为架构添加了异步发布/订阅模型,使得每天数万亿条消息能够在组织内部传输。这对于一家高速发展的公司来说至关重要,因为它允许在不破坏驱动前端的脆弱的同步交互网络的情况下,接入新的应用程序。
但这种围绕事件重构系统的理念并非新鲜事——事件驱动架构已经存在了几十年,而企业消息传递等技术也已成为一项庞大的业务,尤其是在(不出所料)大型企业中。大多数企业都已运营多年,其系统都是通过多次迭代或收购自然发展而来的。消息系统天然契合这种复杂且彼此割裂的局面,原因与 LinkedIn 的情况类似:事件解耦,这意味着公司的不同部门可以彼此独立运作。这也意味着更容易将新系统接入实时事件流。
一个很好的例子是 2018 年 1 月冲击金融行业的监管规定,该规定要求交易活动必须在发生后一分钟内报告给监管机构。在计算机领域,一分钟似乎很长,但只要某个业务孤岛的关键路径上存在一个批处理驱动的系统,就可能导致无法达到这一要求。因此,那些投入精力部署实时交易事件系统并将其整合到所有产品相关部门的银行,轻松应对了这些规定。而对于大多数尚未这样做的银行来说,这却是一项艰巨的任务,通常只能得到敷衍了事、不尽如人意的解决方案。
因此,大型企业起步时往往复杂且彼此割裂:许多独立的、异步的“孤岛”——通常各自拥有不同的用户——大多独立运行。互联网公司则不同,它们最初只是简单的面向用户的Web应用程序,用户点击按钮即可触发相应操作。大多数互联网公司最初都是单体架构,并且会保持这种状态一段时间(可以说比它们应有的时间更长)。但随着互联网公司的发展壮大,业务变得越来越复杂,它们也经历了类似的异步化转变。新的团队和部门不断涌现,它们需要独立运行,摆脱前端的同步束缚。因此,用户对在线实用工具(例如支付或更新购物车)的普遍需求,正逐渐被对数据集日益增长的需求所取代,这些数据集可以自由使用和演进,而无需任何特定应用程序的锁定。
但消息传递并非万能灵药。例如,企业服务总线(ESB)就饱受诟病,而传统的消息传递系统本身也存在诸多问题。它们通常用于在组织内部传输数据,但由于缺乏历史概念,其价值受到限制。因此,即使近期事件通常比旧事件更有价值,业务运营仍然需要历史数据——无论是用户查询账户历史记录、某个服务需要客户列表,还是需要运行分析以生成管理报告。
另一方面,具有 HTTP 前端接口的数据服务使得查找变得简单。任何人都可以访问并运行查询。但它们并没有让数据传输变得容易。要提取数据集,最终需要运行查询,然后定期轮询服务以获取更改。这有点取巧,而且通常负责被轮询服务的运维人员不会对此表示感谢。
但是,像 Kafka 这样的可重放日志可以扮演事件存储的角色:介于消息系统和数据库之间。 (如果你不了解 Kafka,别担心——我们将在第 4 章深入探讨。)可重放日志能够像消息系统一样将各个服务彼此解耦,但它们还提供了一个容错且可扩展的中心存储点——一个任何应用程序都可以随时依赖的共享事实来源。
事实证明,共享的事实来源非常有用。例如,微服务之间不会共享数据库(这被称为集成数据库反模式)。这样做是有充分理由的:数据库拥有非常丰富的 API,这些 API 本身就非常有用,但当它们被广泛共享时,就很难确定一个应用程序是否以及如何影响其他应用程序,无论是数据耦合、争用还是负载。但是,服务选择共享的业务事实是最重要的事实。它们是构建其余业务的基础。Pat Helland 早在 2006 年就指出了这种区别,并将其称为“外部数据”。
但可重放日志为存储这类数据提供了更合适的场所,因为(有点违反直觉)你无法查询它!它纯粹是存储数据并将其推送到新的位置。这种纯粹的数据移动理念至关重要,因为外部数据——即服务共享的数据——耦合度最高,而且生态系统中的服务越多,这些数据的耦合度就越高。解决方案是将数据移动到耦合度更低的位置,这意味着将其移动到你的应用程序中,在那里你可以随心所欲地操作它。因此,数据移动赋予应用程序一种直接运行时依赖无法实现的可操作性和控制力。这种保留控制权的理念至关重要——这也是共享数据库模式在实践中效果不佳的原因。
因此,这种基于日志的可重放方法有两个主要优势。首先,它提供了一套专门用于处理当前事件的工具集,方便用户对正在发生的事件做出快速反应。其次,它提供了一个中央存储库,可以将整个数据集推送到任何需要的地方。如果你运营一家业务遍及全球、数据中心分布于世界各地的企业,需要快速启动新项目或构建其原型,进行一些临时数据探索,或者构建一个可以自由独立演进的复杂服务生态系统,那么这种方法就非常有用。
因此,事件驱动方法具有一些明显的优势(当然,REST/RPC 模型也有其优势)。但这实际上只是故事的一半。流处理并非仅仅是 RPC 的一种替代方案,它并非碰巧更适用于高度互联的用例;它是一种更为根本的思维方式转变,需要将你的业务重新思考为一个不断演进的数据流,并将你的服务重新思考为将这些数据流转换为新内容的函数。
这可能会让人感觉不太自然。我们许多人从小就习惯于提问或发出命令然后等待回复的编程风格。过程式或面向对象程序就是这样工作的,但罪魁祸首或许是数据库。近半个世纪以来,数据库在系统设计中扮演着核心角色,它比任何其他工具都更深刻地影响着我们编写(和思考)程序的方式。从某种程度上来说,这令人遗憾。
本书逐章展开,逐步构建了一种处理数据的微妙而不同的方法,这种方法将数据库拆解、解绑、解构,并彻底地由内而外翻转。这些概念听起来或许有些奇怪,甚至新颖,但它们就像软件领域的许多事物一样,是旧思想的演变,只不过这些思想在各种技术亚文化中各自独立地涌现。长期以来,主流程序员一直使用事件驱动架构、事件溯源和 CQRS(命令查询职责分离)来摆脱扩展以数据库为中心的系统的种种难题。大数据领域也遇到了类似的问题,数 TB 级的数据集凸显了批处理数据管理固有的不切实际之处,进而促使人们转向流处理。函数式编程领域则在某种程度上心知肚明地保持观望,偶尔也会对大众的命令式思维提出质疑。
但这些看似迥异的进展——将数据库由内而外翻转、解构、CQRS、解绑——都有一个共同点。它们都只是对以下需求的简单隐喻:我们需要把嵌入每个数据库之中、相互混杂的各种概念分离开来,将它们解耦,以便分别管理,从而提高效率。
我们想要这样做有很多原因,但也许最重要的原因是,它使我们能够构建更大、功能更多样化的系统。因此,虽然以数据库为中心的方法对于单个应用程序来说非常有效,但我们生活在一个并非由单个应用程序构成的世界里。我们生活在一个由相互连接的系统构成的世界里——各个组件虽然各自有价值,但实际上都是一个更大拼图的一部分。我们需要一种能够适应这个复杂互联世界的数据共享机制。事件将引领我们实现这一目标。它们不断地将数据推送到我们的应用程序中。这些应用程序会做出响应,将数据流融合在一起,构建视图,改变状态,并推动自身向前发展。在流处理模型中,不存在共享数据库。数据库本身就是事件流,应用程序只是将其重新塑造成新的内容。
公平地说,流处理系统仍然具备一些数据库的属性,例如用于查找的表和用于原子性的事务,但其方法截然不同,更接近函数式或数据流语言(流处理和函数式编程社区之间也存在着大量的交叉融合)。
因此,在处理数据时,我们应该明确系统的共享事实。毕竟,它们是我们业务的本质。事实可能会随着时间的推移而演变,以不同的方式应用,甚至被重新诠释以适应不同的上下文,但它们始终应该与一条不可更改的真理线索相连,所有其他真理都由此衍生而来——它是支撑和驱动所有现代数字业务的中枢神经系统。
本书专门探讨了 Apache Kafka 在这一问题上的应用。在第一部分,我们将介绍流处理并了解 Kafka 的工作原理。第二部分重点介绍构建事件驱动型程序所需的模式和技术:事件溯源、事件协作、CQRS 等。第三部分将这些理念进一步延伸,应用于多团队系统(包括微服务和面向服务的架构),重点关注将事件流作为事实来源,以及前文提到的“系统和公司都可以被重新构想为一个由内而外翻转的数据库”这一理念。在最后一部分,我们将重点放在更实际的应用上,使用 Kafka Streams(和 KSQL)构建一个小型流处理系统。