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