崛起的 Kafka
本文译自 Braedon Vickers 发布在 Movio 上的一篇文章,详尽的探讨了在微服务架构升级的过程中,如何使用 Kafka 将微服务之间耦合降到最低,同时能让整个系统在保证高可用的前提下做到高可扩展。
随着微服务的流行,很多公司都在尝试将现有的系统进行架构升级。促成 Movio 公司架构改造的一项关键技术就是 Kafka 消息队列。Kafka 是一个开源的分布式消息队列,在可靠性和可扩展性方面有非常大的优势。 我们正处于把系统迁移到微服务新世界的过程中。
促成这次架构改造的一项关键的技术就是 Apache Kafka 消息队列。它不仅成为了我们基础架构的关键组成部分,还为我们正在创建的系统架构提供了依据。
1.Kafka 概览
虽然这篇文章的目的不是在宣扬 Kafka 比其他消息队列系统更优秀,但是本文讨论的某些部分是专门针对它的。对于外行来说,Kafka 是一个开源的分布式消息队列系统。它最初是由 LinkedIn 研发,现在由 Apache 软件基金会维护。和其他的消息队列系统一样,你可以给它发送消息,同时也可以读取消息。用 Kafka 的说法就是 “生产者” 发送消息,“消费者”接收它们。
Kafka 的独特性在于它同时提供简单的文件系统和桥接这两种功能。一个 Kafka 的代理器(broker)最基本的任务就是尽可能快地将消息写到磁盘上的日志中,并从中读取消息。消息队列中的消息在持久化之后就不会丢失了,这是整个项目的核心所在。
队列(Queue)在 Kafka 中被称为 “主题”(topic),同一个主题共享 1 个或多个 “分区”(partition)。每条消息都有一个 “偏移”(offset)- 一个代表它所在分区位置的偏移量。这使得消费者可以记录它们当前读到的位置,并向代理(broker)请求读取接下来一条(或多条)消息。多个消费者可以同时读取同一个分区的数据,每个消费者从某个位置上读取数据都是独立于其他消费者的。它们甚至可以在整个分区内随意跳跃式前进或后退。多个 Kafka 代理组合到一起成为一个集群。分区是在分散在整个集群中的,这样就提供了可扩展性。它们也可以复制到集群中的多个节点上,实现了高可用性。综合复制与分区持久化特点,进而达到高可靠性。
2. 构建微服务的系统架构
为了帮助我们理解 Kafka 的用途和影响力,想象一个通过 REST API 从外部接收数据的系统,进而用某种方式进行变换,最后将结果保存到数据库中。我们想要把这个系统升级为微服务架构,所以首先把这个系统切分为两个服务 - 一个用来提供外部的 REST 接口 (Alpha 服务),另外一个用来做数据变换(Beta 服务)。简单起见,Beta 服务同时会负责存储变换后的数据。
由一对微服务组成的系统会比提供整体服务(往往是糟糕的)的系统要好一点,所以我们定义一个接口用于从 Alpha 服务将数据发送到 Beta 服务,用来减少耦合。和使用 REST 接口实现的系统相比,让我们看看使用 Kafka 实现这个接口将会如何影响该系统的设计和运行。
3. 系统的可用性和性能
当数据被提交到 Alpha 的服务,它在响应客户端之前需要确保数据安全地存储在某个地方,或者已经失败 - 客户端需要知道,因为如果出现了错误它可以重新发送(或用一些其他的方式进行恢复), 使用 REST 接口,Alpha 服务在响应客户端之前将需要一直等待 Beta 服务的响应,直到 Beta 服务将数据存储到数据库中。
这种做法存在两个问题。首先,Alpha 服务现在要求 Beta 服务是启动状态并且可以响应请求 - 它的正常运行依赖于 Beta 服务。其次,Alpha 服务要等到 Beta 服务的响应才能响应客户端 - 它的性能也依赖于 Beta 服务!
在这两种情况下,Alpha 服务耦合于 Beta 服务。Beta 服务失败的频率是多少?它需要停机维护的频率是多少?它的峰值性能是多少?难道真的只有在数据被安全的存储之后才可以响应,或者说提前响应就是不可行的?如果它依赖于其他服务,相应的会需要进一步延伸耦合链?像这样的系统好坏程度取决于其最薄弱(weakest)的服务。
如果我们使用 Kafaka 消息队列做为接口替换上面描述的,我们将会得到一个完全不同的结果,Kafka 有一招:一个 Kafka 消息队列是持久化存储的。当数据已安全地放到队列中时,Alpha 服务就可以响应请求了;我们可以确信数据将最终会存储到数据库中,因为 Beta 服务致力于处理队列中的消息。Alpha 服务现在仅仅依赖于 Kafka 的正常运行和性能了 - 这两个指标都可能远远好于系统中的其他微服务。它是如此松耦合于 Beta 服务,它甚至都不需要知道它的存在!
当然,消息队列从来就不是性能问题的灵丹妙药 - 它并不能神奇的改善系统的整体性能(举起手来,如果在你曾经参加的会议中有人相信的话)。然而,它确实允许你应对来自外部系统的可变负载,或者甚至是你自己系统中的微服务(例如那些必须做某种形式的批处理)。消息队列能够应对峰值负载,从而使下游服务可以平滑的速度处理。一个给定的服务的性能只需要大于系统的平均负载,而不是峰值负载。
使用 REST 接口时,你可以通过在每个服务中存储数据来打破依赖链并实现类似的效果。然而,为了达到这一点,在每一个服务中你都需要自己实现消息代理模块。使用现有的服务你自己不必再设计、构建和维护一套(或多套)类似的系统。
4. 服务间松耦合
在设计系统的时候,我们发现如果 Alpha 服务返回的结果是转换后的数据,一些客户可能会发现它会很方便。
如果使用 REST 接口这将变得非常容易 - Beta 服务可以返回转换后的数据,Alpha 服务直接透传给客户端。同时我们在两个服务之间增加了一个新的依赖关系 - Alpha 服务现在依赖于 Beta 服务的转换功能。这和上面小节中 Alpha 服务依赖于 Beta 服务存储数据是类似的情景。同样的问题出现了:如果 Beta 服务不能及时并且正确执行其功能,Alpha 服务同样的也不能返回结果给其客户端。
和存储数据不同的是 Kafka 针对这个问题没有提供有效的解决方案。事实上,用 Kafka 接口来达到这个目的将会更复杂(但绝不是不可能)。因为消息队列是单向通信信道,从 Beta 服务获取转换后的数据需要相反方向的第二条信道。匹配这些异步响应结果和等待的客户端也会需要一些额外的工作。
然而,用消息队列不容易做到这个事实的给了我们一个启发,那就是我们新生的系统有些地方做的并不好,我们应该重新评估。纵观我们加诸于系统的额外功能与数据流的关系,结果表明不管它是如何实现的,是该功能引入了服务之间耦合。要想让 Alpha 返回转换后的数据则他必须依赖于真正做数据转换的服务,我们架构的系统不可能绕过这个事实。
这让我们停下来检查我们是否确实需要这个功能。有没有其他的方法可以提供访问转换后的结果数据并且不会引入这个问题?客户端是否需要所有转换后的数据?如果这个功能是必要,表明我们在切分微服务的时候并不是最优的,我们应该将数据转换这步放到 Alpha 服务中。如果不是必要的,我们只是阻止了自己添加不必要的或设计不当的功能,这些功能未来可能会影响我们系统的发展和性能。系统的功能一旦添加上往往很难移除掉了。
为了避免在系统中引入沉重的包袱,在实现之前辨别有疑问的功能(或者功能设计)是非常必要的。Kafka 消息队列的自身的局限性可以指导我们设计出更好的系统。当你试图砍掉一个紧急的功能时,他们可能会感到沮丧,但是从长远来看是值得被称赞的。
5. 提高可用性和扩展性
随着负载的增加,我们的系统应该保持高可用性,可扩展性。作为实现这一目标的一部分,我们希望能够运行多个 Beta 实例。如果其中一个实例宕机,其他的实例会(希望)仍然能够正常运行。可以增加更多的实例来处理增加的负载。
使用 REST 接口时,我们可以在 Beta 服务实例前面加一个负载均衡器,并且把 Alpha 服务从直接指向 Beta 服务的实例替换为指向这个负载均衡器。负载均衡器需要能够自动检测宕机的实例,从而一旦有实例宕机可以将负载转移到别的实例上。
一个 Kafka 消息队列默认支持可变数量的消费者(例如 Beta 服务),不需要额外的基础架构的支持。多个消费者可以组成一个 “消费组”,只需要在连接集群的时候指定一个相同的组名。Kafka 会将每个主题所有分区的数据共享传输给整个组的消费者。只要我们使用的主题有足够多的分区,我们可以持续增加 Beta 服务的实例,这么它们将会分担一部分负载。如果某些实例不可用,他们的分区将由剩余的实例处理。
6. 增加更多服务
我们需要在我们的系统中添加一些新的功能(这并不重要),我们将把它放在一个新的 Gamma 服务中。它依赖 Alpha 服务的数据的方式和 Beta 服务依赖 Alpha 服务的数据的方式是一样的,并且数据是用同样的接口提供的。数据必须经过 Gamma 服务处理才算被整个系统完整的处理。
如果使用 REST 接口,则 Alpha 服务将会耦合一个额外的服务,加剧前面讨论的服务间耦合的问题。随着下游服务的增加,依赖会变得原来越多。
此外,一组数据可能成功的被一个下游的服务处理,但是在另外一个服务中却处理失败。这种情况下,可能很难通知到客户端和做到自动恢复,特别是如果它们可以在状态不一致的情况下就离开。
使用 Kafka 接口允许新的 Gamma 服务和 Beta 服务一样简单地从同一消息队列中读取数据。只要它使用一个不同于 Beta 服务的消费组,两者之间不会互相干扰。由于 Alpha 服务不需要知道它写入数据的消息队列有什么服务在使用,我们可以持续的添加服务而不会对它造成任何影响。
数据被安全地存储在 Kafka 中,所以各个服务可以在瞬时故障的情况下重试,例如数据库死锁或者是网络问题。非瞬时错误,例如脏数据,和使用 REST 接口一样都会存在类似一些问题。检查数据合法性越早越好,例如在阿尔法服务,是减少这些问题的关键。
7. 总结
以上的例子都是直接取自于我们在 Movio 推进微服务化过程中的真实案例。在这个过程中它已经证明了是一个非常宝贵的工具,催生了优秀的系统架构,并可以简单和快速的实现它。我们将继续探索使用它的新方法,并期望我们的使用会促进系统的进一步发展。LinkedIn 和 Apache 的团队创造了一件伟大的作品。
针对学习这件事,我想想讲讲我的看法,
第一:绝对的三个字:靠自己,任何人都帮不了你。
第二:切勿眼高手低,好说懒做,动动嘴比谁都快,动手的时候就怂了
,这不是一个技术人应该有的态度。
第三:别人给你的只会是思路与方法,实际内容还需要自己去填充,
没有坐等这回事,绝逼没有。
同意的老铁请点赞、转发此文章支持! 分享一个交流学习群:群号是: 558787436