为什么我们从RabbitMQ切换到apache kafka?

Trello过去三年一直在使用RabbitMQ,在RabbitMQ之前,我们还使用了于Redis Pub-Sub实现。最近,由于RabbitMQ在发生网络分区时出现了可疑行为,我们已经切换到了Kafka。

这篇博文深入介绍了我们的RabbitMQ实现,为什么我们最终选择选择了Kafka和基于Kafka的架构。

当前状况
Trello使用15个RabbitMQ实例的集群进行所有websocket更新,我们的服务器将消息发布到集群,然后我们的websocket实例从队列中提取消息,但是会涉及一些配置特性。

插曲:RabbitMQ的工作原理
RabbitMQ能让你使用一个带key的路由将消息发布到交换机,每个交换机都有一个与之关联的路由策略:fanout、单个路由key、前缀等。队列使用路由ley绑定到交换机,交换机尝试根据路由key和交换机配置将已发布的消息与队列进行匹配。

创建队列时,你可以将它们指定为瞬态; 一旦TCP连接创建后就会关闭,并且所有关联的绑定都被删除,它们立即被销毁。

插曲2:Trello Websocket协议
我们使用的websocket协议非常简单; 有一个最小的请求/响应机制,我们支持的唯一命令是对通道进行订阅和取消订阅。

订阅命令包含Trello模型类型(board,memeber,组织,card)及其各自的模型ID。(banq注:Trello是提供敏捷看板项目管理的网站)

消息路由
我们让每个websocket进程(每实例8个进程)连接到RabbitMQ并为自己创建一个临时队列来设置这个系统,当进程获得websocket连接并接收订阅命令时,它将对这个订阅创建一个绑定,以便更新交换机。

RabbitMQ Sharding
通过RabbitMQ的消息按其模型ID进行16个以上的分片。

Trello Server使用客户端计算的分片键将所有消息发布到3个实例的rabbitmq入站群集的单个交换机上,这16个不同的分片键都有自己的绑定,绑定到16个不同的队列上。然后我们使用 shovel插件将这16个队列分配给4个不同的rabbitmq-outbound出站集群(每个集群有3个实例),每个集群包含4个队列。websocket客户端服务器连接到所有RabbitMQ集群,订阅所需的队列,这取决于连接用户的请求方式。

这背后的理论是负载分配并水平扩展RMQ基础设施,但是,由于群集本身不可靠(单实例故障或网络中断可能导致整个群集完全失败),入站群集仍然是单点故障。

问题
Rabbit的主要问题体现在它处理分区和通常集群中断上,结果略有不同,但范围基本都是从裂脑到完全集群失败,更糟糕的是,从死群中恢复通常需要完全重置它,在我们的例子中,这意味着我们必须删除所有socket并强制Web客户端重新连接以确保它们可以重新检索错过的更新,然而,这可能还不足以在裂脑情况下完全恢复 - 网络客户端可能已经错过了一条消息而收到了后面的一条消息,一切就无法知道了。

此外,还有另一个问题 - 在RabbitMQ中创建队列和进行绑定既缓慢又昂贵,销毁队列和绑定也很昂贵,每次我们丢失套接字服务器时,我们都会看到取消订阅和重新订阅的风暴,因为客户端websockets被丢弃并尝试重新连接,这需要RMQ花费一些时间来处理。虽然我们可以重新启动一个服务器的简单情况下处理它,但如果我们丢失了所有的websocket连接并且必须重新连接它们(发生的次数比我们想要的多),那么大量的绑定的添加/删除命令将导致RMQ群集变得无响应,甚至对监视命令或正常进程信号也无视,这会导致集群故障。

为了解决这个问题,我们在将断开连接传播到RMQ服务器时引入了一些抖动。这对大规模套接字丢弃有很大帮助,但网络分区仍然是一个问题。

可用解决方案
比较了多个候选方案后,我们认为kafka是最好的选择。希望Redis流将在未来实际可用; Redis是一个简洁的工具,可以实现更高效的架构。

然后比较了Kafka驱动器kafka-node和node-rdkafka,
因为我们需要故障转移,所以选择node-rdkafka,当我们对这两个进行故障测试时,发现kafka-node故障转移不起作用,我们感到非常困惑,我们发现node-rdkafka是我们想要的一切,并没有进一步调查为什么会这样。

重要的是要注意,node-rdkafka它实际上是一个包装librdkafka,“官方”(如:由Confluent员工开发)Kafka的C ++客户端。

结果

Socket服务器现在具有主-客架构,主服务器订阅整个主题并接收所有增量更新,根据客户端向用户转发所需的模型在本地进行过滤。这种方式从一开始就给我们的服务器带来了更多的负担,但是扩展它相对容易(通过获得更大的CPU)。当客户端收到订阅请求时,它会检查权限,然后将请求转发给主服务器,从而将模型ID保存在映射中。

“客户端”实际上接受来自用户的套接字连接,处理其身份验证,并将订阅请求转发给主服务器。

当增量更新进入时,主服务器检查是否有任何客户端对该特定模型感兴趣并将消息转发给它,然后分发给用户。

度量
现在,卡夫卡的所有情况都有非常好的指标!以前,RabbitMQ仪表板中只提供了一些指标,如消息速率。现在我们将所有Kafka指标导入我们自己的存储,这使我们可以对所有内容发出警报。

消费者滞后(consumer lag)的指标(从队列服务器和客户端的角度来看!)以前RMQ没有以这种有组织的方式提供给我们。虽然可以为Rabbit构建,但我们只是在重写过程中添加了它。


与以前相比,内存使用量下降了大约33%,而CPU使用率增加到大约2倍。内存减少是由于所需队列数量减少,而CPU增加是由于本地过滤造成的。


停机
幸运的是,我们只经历过一个小小的停机!虽然我们最近才切换到新的基于Kakfa的架构,但该集群已经启用并已发布超过一个月,我们还没有停电!与转换前RabbitMQ在一个月内造成的4次中断相比,这是一个好消息。

在RabbitMQ升级期间(trusty→ xenial),我们设法崩溃并重新连接整个服务器场,kafka的max_open_file的限制数值未正确设置也导致某些进程无法连接。

成本
少了很多!虽然不是主要的激励因素,但降低成本非常简洁。

RMQ由大量c3.2xlarge实例组成。现在卡夫卡由几个Zookeeper的m4.large和kafka的i3.large组成。这些变化导致成本降低了5倍。好极了!


Why We Chose Kafka For The Trello Socket Architect