大数据技术栈速览之:Kafka

https://blog.csdn.net/lingbo229/article/details/80761778

Kafka的特性:

- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。

- 可扩展性:kafka集群支持热扩展

- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失

- 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)

- 高并发:支持数千个客户端同时读写

 

Kafka的使用场景:

- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。

- 消息系统:解耦和生产者和消费者、缓存消息等。

- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。

- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。

- 流式处理:比如spark streaming和storm

- 事件源

 

1) Producer端使用zookeeper用来"发现"broker列表,以及和Topic下每个partition leader建立socket连接并发送消息.

2) Broker端使用zookeeper用来注册broker信息,已经监测partition leader存活性.

3) Consumer端使用zookeeper用来注册consumer信息,其中包括consumer消费的partition列表等,同时也用来发现broker列表,并和partition leader建立socket连接,并获取消息。

Leader的选择

Kafka的核心是日志文件,日志文件在集群中的同步是分布式数据系统最基础的要素。

如果leaders永远不会down的话我们就不需要followers了!一旦leader down掉了,需要在followers中选择一个新的leader.但是followers本身有可能延时太久或者crash,所以必须选择高质量的follower作为leader.必须保证,一旦一个消息被提交了,但是leader down掉了,新选出的leader必须可以提供这条消息。大部分的分布式系统采用了多数投票法则选择新的leader,对于多数投票法则,就是根据所有副本节点的状况动态的选择最适合的作为leader.Kafka并不是使用这种方法

Kafka动态维护了一个同步状态的副本的集合(a set of in-sync replicas),简称ISR,在这个集合中的节点都是和leader保持高度一致的,任何一条消息必须被这个集合中的每个节点读取并追加到日志中了,才会通知外部这个消息已经被提交了。因此这个集合中的任何一个节点随时都可以被选为leader.ISR在ZooKeeper中维护。ISR中有f+1个节点,就可以允许在f个节点down掉的情况下不会丢失消息并正常提供服。ISR的成员是动态的,如果一个节点被淘汰了,当它重新达到“同步中”的状态时,他可以重新加入ISR.这种leader的选择方式是非常快速的,适合kafka的应用场景。

一个邪恶的想法:如果所有节点都down掉了怎么办?Kafka对于数据不会丢失的保证,是基于至少一个节点是存活的,一旦所有节点都down了,这个就不能保证了。

实际应用中,当所有的副本都down掉时,必须及时作出反应。可以有以下两种选择:

1. 等待ISR中的任何一个节点恢复并担任leader。

2. 选择所有节点中(不只是ISR)第一个恢复的节点作为leader.

这是一个在可用性和连续性之间的权衡。如果等待ISR中的节点恢复,一旦ISR中的节点起不起来或者数据都是了,那集群就永远恢复不了了。如果等待ISR意外的节点恢复,这个节点的数据就会被作为线上数据,有可能和真实的数据有所出入,因为有些数据它可能还没同步到。Kafka目前选择了第二种策略,在未来的版本中将使这个策略的选择可配置,可以根据场景灵活的选择。

这种窘境不只Kafka会遇到,几乎所有的分布式数据系统都会遇到。

一个partition只能被一个消费者消费(一个消费者可以同时消费多个partition)

Zookeeper 协调控制

1. 管理broker与consumer的动态加入与离开。(Producer不需要管理,随便一台计算机都可以作为Producer向Kakfa Broker发消息)

2. 触发负载均衡,当broker或consumer加入或离开时会触发负载均衡算法,使得一个consumer group内的多个consumer的消费负载平衡。(因为一个comsumer消费一个或多个partition,一个partition只能被一个consumer消费)

3.  维护消费关系及每个partition的消费信息。

Zookeeper上的细节:

1. 每个broker启动后会在zookeeper上注册一个临时的broker registry,包含broker的ip地址和端口号,所存储的topics和partitions信息。

2. 每个consumer启动后会在zookeeper上注册一个临时的consumer registry:包含consumer所属的consumer group以及订阅的topics。

3. 每个consumer group关联一个临时的owner registry和一个持久的offset registry。对于被订阅的每个partition包含一个owner registry,内容为订阅这个partition的consumer id;同时包含一个offset registry,内容为上一次订阅的offset。

 

ack: 0-不用通知; 1-leader写成功,ack通知成功; -1 - 所有副本写成功,才ack通知成功。

 

生产实践:

1)选举出了一个新的kafka controller,但是原来的controller在shut down的时候总是不成功,这个时候producer进来的message由于Kafka集群中存在两个kafka controller而无法落地。导致数据淤积。

 2) ack=1的时候,一旦有个broker宕机导致partition的follower和leader切换,会导致丢数据。 

 

Kafka中的优秀设计

https://mp.weixin.qq.com/s?__biz=MzIzODIzNzE0NQ==&mid=2654418186&idx=1&sn=c18ae8ad03d0907d2f71f91fd6fb00ca&chksm=f2fff1bcc58878aab50300aa88a4d2e62acd62569e056ab3c92dfa84a9a275b473abe3c46502&mpshare=1&scene=2&srcid=&sharer_sharetime=1577675009037&sharer_shareid=1cfbb4ec295246e3649598007e341b6f&key=507d39fb4abb5ee532a1815c45e582d247ea75676634d979254980c9fd586c0d42d206c810113aab7dde341f058ffa91c893427dce20111d22d18a11045f08874cacc32d3db233ae0c4b22cb1e20ee6e&ascene=14&uin=MjUwOTQyMjQyMw%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&exportkey=AWh6IKI1CwKaf4RapvyE6G4%3D&pass_ticket=S16nnpvopBoyVP9yirPmcmmNT4Jt47BP%2F5FAx8xhQKd1Bcw8Hd%2BnbidoS9axPYmV

优秀设计之基于NIO编程 

Kafka 底层的 IO 用的是 NIO,这个事虽然简单,但是也需要提一提。我们开发一个分布式文件系统的时候避免不了需要思考需要什么样的 IO?BIO 性能较差,NIO 性能要比 BIO 要好很多,而且编程难度也不算大,当然性能最好的那就是 AIO 了,但是 AIO 编程难度较大,代码设计起来较为复杂,所以 Kafka 选择的是 NIO,也是因为这些原因,目前我们看到很多开源的技术也都是用的 NIO。

优秀设计之高性能网络设计 

个人认为 Kafka 的网络部分的代码设计是整个 Kafka 比较精华的部分。我们接下来一步一步分析一下 Kafka Server 端为了支持超高并发是如何设计其网络架构的?

我们先不看 kafka 本身的网络架构,我们先简单了解一下 Reactor 模式:

   图1  Reactor模型

(1) 首先服务端创建了 ServerSocketChannel 对象并在 Selector 上注册了 OP_ACCEPT 事件,ServerSocketChannel 负责监听指定端口上的连接。
(2)当客户端发起到服务端的网络连接请求时,服务端的 Selector 监听到 OP_ACCEPT 事件,会触发 Acceptor 来处理 OP_ACCEPT 事件.
(3)当 Acceptor 接收到来自客户端的 socket 请求时会为这个连接创建对应的 SocketChannel,将这个 SocketChannel 设置为非阻塞模式,并在 Selector 上注册它关注的 I/O 事件。如:OP_WRITER,OP_READ 事件。此时客户端与服务端的 socket 连接正式建立完成。
(4)当客户端通过上面建立好的 socket 连接向服务端发送请求时,服务端的 Selector 会监听到 OP_READ 事件,并触发对应的处理逻辑(read handler)。服务端像客户端发送响应时,服务端的 Selector 可以监听到 OP_WRITER 事件,并触发对应的处理逻辑(writer handler)。

我们看到这种设计就是将所有的事件处理都在同一个线程中完成。这样的设计适合用在客户端这种并发比较小的场景。如果并发量比较大,或者有个请求处理逻辑要较为复杂,耗时较长,那么就会影响到后续所有的请求,接着就会导致大量的任务超时。要解决这个问题,我们对上述的架构稍作调整,如下图所示:

图2 Reactor 改进模型

Accept 单独运行在一个线程中,这个线程使用 ExecutorService 实现,因为这样的话,当 Accept 线程异常退出的时候,ExecutorService 也会创建新的线程进行补偿。Read handler 里面也是一个线程池,这个里面所有的线程都注册了 OP_READ 事件,负责接收客户端传过来的请求,当然也是一个线程对应了多个 socket 连接。Read handler 里的线程接收到请求以后把请求都存入到 MessageQueue 里面。Handler Poll 线程池中的线程就会从 MessageQueue 队列里面获取请求,然后对请求进行处理。这样设计的话,即使某个请求需要大量耗时,Handler Poll 线程池里的其它线程也会去处理后面的请求,避免了整个服务端的阻塞。当请求处理完了以后 handler Pool 中的线程注册 OP_WRITER 事件,实现往客户端发送响应的功能。

通过这种设计就解决了性能瓶颈的问题,但是如果突然发生了大量的网络 I/O。单个 Selector 可能会在分发事件的时候成为性能瓶颈。所以我们很容易想的到应该将上面的单独的 Selector 扩展为多个,所以架构图就变成了如下的这幅图:

图3 Reactor 改进模型

如果我们理解了上面的设计以后,再去理解 Kafka 的网络架构就简单多了,如下图所示:

图4 Kafka 网络模型

这个就是 Kafka 的 Server 端的网络架构设计,就是按照前面的网路架构演化出来的。Accepetor 启动了以后接收连接请求,接收到了请求以后把请求发送给一个线程池(Processor)线程池里的每个线程获取到请求以后,把请求封装为一个个 SocketChannel 缓存在自己的队列里面。接下来给这些 SocketChannel 注册上 OP_READ 事件,这样就可以接收客户端发送过来的请求了,Processor 线程就把接收到的请求封装成 Request 对象存入到 RequestChannel 的 RequestQueue 队列。接下来启动了一个线程池,默认是 8 个线程来对队列里面的请求进行处理。处理完了以后把对应的响应放入到对应 ReponseQueue 里面。每个 Processor 线程从其对应的 ReponseQueue 里面获取响应,注册 OP_WRITER 事件,最终把响应发送给客户端。

个人觉得 Kafka 的网络设计部分代码设计得很漂亮,就是因为这个网络架构,保证了 kafka 的高性能。

  优秀设计之顺序写 

一开始很多人质疑 kafka,大家认为一个架构在磁盘之上的系统,性能是如何保证的。这点需要跟大家解释一下,客户端写入到 Kafka 的数据首先是写入到操作系统缓存的(所以很快),然后缓存里的数据根据一定的策略再写入到磁盘,并且写入到磁盘的时候是顺序写,顺序写如果磁盘的个数和转数跟得上的话,都快赶上写内存的速度了!

 优秀设计之跳表、稀松索引、零拷贝

上面我们看到 kafka 通过顺序写的设计保证了高效的写性能,那读数据的高性能又是如何设计的呢?kafka 是一个消息系统,里面的每个消息都会有 offset,如果消费者消费某个 offset 的消息的时候是如何快速定位呢?

 

01 /  跳 表

如下截图是我们线上的 kafka 的存储文件,里面有两个重要的文件,一个是 index 文件,一个是 log 文件。

 

图5 Kafka 存储文件

 

log 文件里面存储的是消息,index 存储的是索引信息,这两个文件的文件名都是一样的,成对出现的,这个文件名是以 log 文件里的第一条消息的 offset 命名的,如下第一个文件的文件名叫 00000000000012768089,代表着这个文件里的第一个消息的 offset 是 12768089,也就是说第二条消息就是 12768090 了。

 

在 kafka 的代码里,我们一个的 log 文件是存储是 ConcurrentSkipListMap 里的,是一个 map 结构,key 用的是文件名(也就是 offset),value 就是 log 文件内容。而 ConcurrentSkipListMap 是基于跳表的数据结构设计的。

 

图6 concurrentSkipListMap设计

 

这样子,我们想要消费某个大小的 offset,可以根据跳表快速的定位到这个 log 文件了。

02 /  稀松索引

经过上面的步骤,我们仅仅也就是定位了 log 文件而已,但是要消费的数据具体的物理位置在哪儿?,我们就得靠 kafka 的稀松索引了。假设刚刚我们定位要消费的偏移量是在 00000000000000368769.log 文件里,如果说要整个文件遍历,然后一个 offset 一个 offset 比对,性能肯定很差。这个时候就需要借助刚刚我们看到的 index 文件了,这个文件里面存的就是消息的 offset 和其对应的物理位置,但 index 不是为每条消息都存一条索引信息,而是每隔几条数据才存一条 index 信息,这样 index 文件其实很小,也就是这个原因我们就管这种方式叫稀松索引。

 

图7 稀松索引

 

比如现在我们要消费 offset 等于 368776 的消息,如何根据 index 文件定位呢?(1)首先在 index 文件里找,index 文件存储的数据都是成对出现的,比如我们到的 1,0 代表的意思是,offset=368769+1=368770 这条信息存储的物理位置是 0 这个位置。那现在我们现在想要定位的消息是 368776 这条消息,368776 减去 368769 等于 7,我们就在 index 文件里找 offset 等于 7 对应的物理位置,但是因为是稀松索引,我们没找到,不过我们找到了 offset 等于 6 的物理值 1407。

(2)接下来就到 log 文件里读取文件的 1407 的位置,然后遍历后面的 offset,很快就可以遍历到 offset 等于 7(368776)的数据了,然后从这儿开始消费即可。

03 /  零拷贝

接下来消费者读取数据的流程用的是零拷贝技术,我们先看一下如下是非零拷贝的流程:

(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入 socket 缓冲区;
(4)操作系统将数据从 socket 缓冲区复制到网卡接口,此时数据才能通过网络发送。

 

图8 非零拷贝流程

 

上图我们发现里面会涉及到两次数据拷贝,Kafka 这儿为了提升性能,所以就采用了零拷贝,零拷贝”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作,提升了整个读数据的性能。

 

图9 零拷贝流程

 优秀设计之批处理 

在 kafka-0.8 版本的设计中,生产者往服务端发送数据,是一条发送一次,这样吞吐量低,后来的版本里面加了缓冲区和批量提交的概念,一下子吞吐量提高了很多。下图就是修改过后的生产者发送消息的原理图:(1) 消费先被封装成为 ProducerRecord 对象.
(2)对消息进行序列化(因为涉及到网络传输).
(3)使用分区器进行分区(到这儿就决定了这个消息要被发送到哪儿了).
(4)接着下来这条消息不着急被发送出去,而是被存到缓冲区里.
(5)会有一个 sender 线程,从缓冲区里取数据,把多条数据封装成一个批次,再一把发送出去,因为有了这个批量发送的设计,吞吐量成倍的提升了。

图10 缓存区设计

这个缓存区里的代码技术含量很高,感兴趣的同学,可以自己去阅读以下源码。

总结:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值