kafka

Apache Kafka是一个由Java开发的分布式发布-订阅消息系统,它最初由LinkedIn公司开发,之后成为Apache项目的一部分。Kafka是一种快速、可扩展的、设计内在就是分布式的,分区的和可复制的提交日志服务。

Apache Kafka具有良好的扩展性和极高的性能,灵活的订阅数据方式使得它已经成为海量消息处理的标杆。

基本概念

  1. 数据的切分
    对于每一个topic的数据,将分成多个数据块存储在系统集群中。
    分块采用主/从的方式保证数据的可靠性。同一时间只有主节点负责用户接收请求,从节点只被动的同步数据。
  2. 生产者
    生产者可以根据自己需求将消息发送给指定的topic对应的指定分块,也可随机选择一个分块。
  3. 消费者
    Kafka最大的特点是保证消费者处理消息的强时序性。它严格限制了,同一时间一个分块最多只有一个消费者。
    同时也对使用者提出了两个要求:
  4. 为了保证系统的并发,需在启动之初,选择好每个topic的分块数量。
  5. 对于需要保证时序的多个操作,生产者需要指定同样的分块。

实现要点

数据写入

  • 每个topic分块实际上对应的就是linux上的一个文件。
  • 采用批量Append方式,不支持数据修改和删除。牺牲用户的响应时间,追求系统性能。
  • 采用sendfile命令,直接将用户的数据写到指定文件中,避免从用户态到内核态数据复制的损失。

数据读取

  • 系统不使用任何Cache,全部交由操作系统处理,
  • 读取数据时,消费者需提供数据的offset。
    后端保存消费者处理到的offset在元数据中。
  • 消费者消费完数据后,数据不会删除。

许多消息队列采用客户ACK方式,确保用户某条消息已经正确被消费,同时删除此条数据,但这带来了两个问题。

  1. 消息可能被消费多次。
    考虑一种情形,当消费者处理完某条消息后,发送ACK确认期间宕机。当客户再次启动时,由于服务器没有收到消息的确认信息,消息将被重新消费。
  2. 性能开销
    每条消息都多了一条确认请求。

Kafka提出了新的方案。当用户消费完消息后,数据将不会被马上删除。将根据设置,批量的删除历史数据(如2周前)。
这样带来的好处是:

  1. 减少了元数据信息。
    后端只需保存每个用户消费到的offset。使得客户端与服务器端沟通代价更小。服务器端定期保存offset数据即可。
  2. 消息可以重读
    当用户出现BUG时,允许用户回退读取过期数据。这对于用户遇到问题时是个很大帮助。

网络的优化

  1. 基于binary的通讯协议。
    降低Producer,Broker和Consumer之间因为小数据包带来的性能损失。
  2. 批量发送数据 & 对数据进行压缩。
    开发者发现对于很多场景,系统的性能瓶颈在于网络带宽。
    Kafka在后端向消费者发送数据时,采用批量策略。并只对大数据,进行数据进行压缩。消费者在收到数据后,将针对压缩数据进行解压,返回给上层用户的只是未压缩数据。
    详细资料在此[https://cwiki.apache.org/confluence/display/KAFKA/Compression]

数据的推-拉模型

系统的数据处理方式是推拉结合,生产者发送数据到后端,消费者从后端拉取数据。
Scribe和Flume采用拉推模式。上游将发送的数据存在本地,后端定期从上游端数据中获取文件,并且向下游发送数据。
对于上游的拉方式,其优点在于上游客户操作延迟低。但是存在数据丢失的风险。同时对于有大量的上游用户场景不太适用。
对于下游的推方式,其优点在于后端可以性能最大化,有效的平衡CPU,磁盘IO和网络带宽。但缺点在于后端数据发送的速率可能过快,超过了下游数据处理能力。
考虑到kafka对用户发送数据的可靠性要求和可能存在的大量生产者,最后选择了推-拉模型。

消息消费原语

消息队列一般存在三种消息消费原语,它包括:

  1. 最多消费一次:消息最多被消费者处理一次
  2. 最少消费一次:消息最少被消费者处理一次
  3. 恰好消费一次:消息正好被消费者处理一次。这个当然是现实中最想要的,但是通常存在很高的代价。
    这三种原语实际上对应的是,客户端处理操作的顺序。
    客户端在处理消息时一般包含三个步骤:
  4. 从消息队列取得数据
  5. 处理该消息
  6. 通知消息队列数据消费完成
  • 对于最少消费一次原语,其步骤恰好同上面相同。因为在2和3之间可能客户端出现错误,当消费者再次启动时,将重复消费该条数据。
  • 对于最多消费一次原语,需将2和3对换过来。先告知后端处理完成,再处理消息。 同样2和3之间可能客户端出现错误,此时将导致某条数据可能还没背消费。
  • 对于消费一次原语,需要保证2和3两个步骤作为一次事务,共同完成或共同失败。事务的一般做法是二阶段提交。其流程描述如下:
  1. 拿分布式锁。
  2. 标记job开始做,记录此条消息涉及的数据之前的状态。实际上对应数据库中的undo log。
  3. 处理消息
  4. 通知消息队列
  5. 释放锁
    但该做法在实现过程中存在两个问题。一是在于消息相关数据状态难于获取,二是在于拿锁性能可能存在问题。特别是对于Kafka这类实时性要求较高的系统,很难接受如此大的开销。
    对于消息处理没有状态的场景,存在更搞笑的解决办法。可先运行得到处理结果,再将结果和完成通知一并写在一个能保证数据安全的地方(如ZK)。
    为了平衡效率,Kafka最终提供了1和2原语,而没提供3这个看上去”最好”的原语。

复制

Kafka通过多副本,主/从复制的方式保证数据的高可靠性。从节点实际上也是一个消费者,它订阅了主节点的所有topic,仅充当数据备份,而不接受用户请求。从节点需定期汇报自己的运行状态(如同步到的数据offset位置)到ZK中,只有处于”同步中”(与主节点的数据差异小于指定消息数目)的节点才能在主节点宕机后,被选为主节点,从而降低主节点宕机造成的数据丢失风险,更多细节可查看浅谈集群中的数据一致性