04 | 如何利用事务消息实现分布式事务?
- 消息队列也有事务,事务主要是解决消息生产者和消费者数据一致性的问题
- 比如电商场景中或者是一些在线视频网站等不是重点的业务场景的东西,比如评论数+1,这种都是可以异步完成的
- 单体的关系型数据库实现事务可以,但是在分布式系统中同时实现ACID四种特性非常困难了,所以出现了残血版的一致性:顺序一致性,最终一致性
- 在不同的场景中做一些妥协
- 在实际业务中常见的分布式事务实现有2PC(二阶段提交)、TCC、事务消息,每种实现都有其特定的使用场景,都不是完美的解决方案
- 事务消息适用的场景是需要异步更新数据,只要保证最终一致就可以了
- 事务消息需要消息队列提供相应的功能才能实现,Kafka和RocketMQ都提供了事务的相关功能
- RocketMQ的事务反查机制
05 | 如何确保消息不会丢失?
- 在保证消息可靠性传递上实现原理都是一样的
- 消息队列最尴尬的不是丢消息,而是消息丢了还不知道
- 一般的大型互联网公司都会有消息总线,分布式消息追踪系统
- 还有一种简单的方式来检查消息是否丢失,可以咋producer端生成自增序列,在consumer判断连续
- 大多数消息队列的客户端都支持拦截器机制,可以不入侵业务实现
- kafka和RocketMQ这样的消息队列不保证在topic有序,而是在分区保证顺序
- 最好保证分区和consumer一致
- 很多丢消息是因为使用了异步发送,却没有在回调中结果
- 总结:
1.在生产阶段,你需要捕获消息发送的错误,并重发消息。
2.在存储阶段,你可以通过配置刷盘和复制相关的参数,让消息写入到多个副本的磁盘上,来确保消息不会因为某个 Broker 宕机或者磁盘损坏而丢失。
3.在消费阶段,你需要在处理完全部消费业务逻辑之后,再发送消费确认。
- 如何解决重复性操作:幂等性接口
06 | 如何处理消费过程中的重复消息?
- 消息队列本身不提供消息不重复的服务
- MQTT协议中,给出了三种服务质量标准:
- At most once: 至多一次:允许丢数据
- At least once: 至少一次:允许重复
- exactly once:恰好一次:最高等级
- 大多数都是至少一次
- 幂等性的实现方法:
- 利用数据库的唯一约束,redis的setnx
- 操作的设置
- 事后检查
07 | 消息积压了该如何处理?
- 一般消息队列的性能都是非常好的,应用服务器是没有那么高并发的
- 水平扩展consumer
08 | 答疑解惑(一) : 网关如何接收服务端的秒杀结果?
- 并行消费的开销很大,如果想提升消费效率,还是要增加队列的数量
- topic是没有顺序的
- 可以使用一致性hash算法将相同的key放在同一个队列上,可以保证相同的key的消息是有序的,如果不考虑队列扩容可以考虑队列数量取模来简单实现
10 | 如何使用异步设计提升系统性能?
- 同步模型会阻塞线程
11 | 如何实现高性能的异步网络传输?
- IO密集型适合用异步
- 因为SSD的速度越来越快,所以主要是网络的异步IO
- 在发送之前会将数据暂存在缓存中,操作系统会将缓存中的数据传输到对端的服务器上
- 只要缓存不满或者发送的速度没有超过网卡传输速度的上限,发送的速度仅仅是一次写入内存的时间,没有必要异步
- 麻烦的是接受数据,因为不知道什么时候会接受到数据,只能用一个线程阻塞在那等着,当有数据来的时候操作系统会先将数据写入缓存,然后开始读取数据,然后继续阻塞
- 在处理少量连接的时候是没有问题的,但是如果要同时处理多个请求,同步的IO模型就力不从心了。
- 每个连接都需要阻塞一个线程来等待数据,大量的连接需要相同数量的接受线程,当这些TCP连接都在进行收发数据会导致大量的线程来抢占CPU时间,造成频繁的CPU上下文切换,导致CPU负载升高,整个系统的性能下降
- Netty 自己维护一组线程来执行数据收发的业务逻辑。如果说,你的业务需要更灵活的实现,自己来维护收发数据的线程,可以选择更加底层的 Java NIO。其实,Netty 也是基于 NIO 来实现的
- 在TCP的连接上,它传输数据的基本形式就是二进制流,也就是001010101,在一般编程语言或者网络框架中的API中,传输的基本形式是字节Byte,一个字节就是8个二进制位,8bit,所以二进制流和字节流本质是一样的,对于编写的程序来说数据是结构化的,一个数组,一个json等等,那么要使用网络API框架来传输结构化的数据就先要实现结构化的数据和字节流的双向转换,也就是序列化和反序列化
- 序列化还有个重要的用途是将结构化的数据保存在文件中,因为文件中保存数据的形式也是二进制序列,序列化同样适用于将结构化数据保存在文件中
- 很多处理海量数据的场景中,都需要将对象序列化后,把它们暂时从内存转移到磁盘中,等需要用的时候,再把数据从磁盘中读取出来,反序列化成对象来使用,这样不仅可以长期保存不丢失数据,而且可以节省有限的内存空间。
- 可以将对象转化成字符串并打印出来,这个字符串只要转化成字节序列就可以在网络上传输或保存在文件中了
- 但是千万不要这么使用,这种方式仅仅是能用,但绝不是好的选择,有很多通用的序列化实现,java和go都内置了序列化实现,也有开源的实现,protobuf等,向json、xml这些标准的数据格式也可以作为序列化的实现来使用
- 根据业务选择可读性和性能
- 绝大部分系统,使用上面这两类通用的序列化实现都可以满足需求,而像消息队列这种用于解决通信问题的中间件,它对性能要求非常高,通用的序列化实现达不到性能要求,所以,很多的消息队列都选择自己实现高性能的专用序列化和反序列化。
- 使用专用的序列化方法,可以提高序列化性能,并有效减小序列化后的字节长度。
13 | 传输协议:应用程序之间对话的语言
- 传输协议的断句问题:HTTP1用的是\r \n
- 正常的交流是单工通信,HTTP1就是,效率是比较低的,解决办法是浏览器创建多个连接
- TCP是全双工的通道,可以同时收发,是互相不受影响的,要提高吞吐量应用层协议也必须支持双工通信
- 但是出现了个问题,在并发情况下顺序不能保证,解决方法是加一个序号,加上编号就能实现双工通信
- 断句问题:分隔符和前置长度
- 使用 ID 来标识请求与响应对应关系”的方法,是一种比较通用的实现双工通信的方法,可以有效提升数据传输的吞吐量。
- 解决了断句问题,实现了双工通信,配合专用的序列化方法,你就可以实现一套高性能的网络通信协议,实现高性能的进程间通信。很多的消息队列、RPC 框架都是用这种方式来实现它们自己的私有应用层传输协议。
14 | 内存管理:如何避免内存溢出和频繁的垃圾回收?
- 自动的内存管理机制也会带来一些问题:内存回收的
- 申请内存的底层原理:
计算要创建对象所需要占用的内存大小;
在内存中找一块儿连续并且是空闲的内存空间,标记为已占用;
把申请的内存地址绑定到对象的引用上,这时候对象就可以使用了。
- 现代的GC算法大多数采用的是标记清除算法,标记阶段从GC ROOT开始标记所有可达的对象,因为程序所有在用的一定都会被这GC ROOT直接或间接引用
- 清除阶段:遍历所有对象,找出没有标记的对象,没有标记的直接清除就可以了
- 这个算法有个重大的问题就是在执行标记和清除的过程中,必须把进程暂停,否则计算的结果不准确,这也就是在内存回收的时候程序会卡死,后续产生的变种的算法,可以减少进程暂停的时间,但是都不能避免暂停进程
- 完成对象回收之后还需要进行内存碎片的整理
- 垃圾回收完成后,还需要进行内存碎片整理,将不连续的空闲内存移动到一起,以便空出足够的连续内存空间供后续使用
- 和垃圾回收算法一样,内存碎片整理也有很多非常复杂的实现方法,但由于整理过程中需要移动内存中的数据,也都不可避免地需要暂停进程。
- 虽然自动内存管理机制有效地解决了内存泄漏问题,带来的代价是执行垃圾回收时会暂停进程,如果暂停的时间过长,程序看起来就像“卡死了”一样。
- 为什么在高并发的情况下程序会卡死?
- 内存不够用是一定会触发Full GC的
- 在高并发短时间会创建大量的对象,所以会导致恶心循环,当垃圾回收的速度赶不上对象创建的速度会导致内存溢出
- 从根本上解决可以自己管理但是如果管理不当会出现内存泄露
- 占用内存差不多的比较适合池化
- 改用String对象池,而不是每次新new 一个String
15 | Kafka如何实现高性能IO?
- Apache Kafka 是一个高性能的消息队列,在众多消息队列产品中,Kafka 的性能绝对是处于第一梯队的
- 在一台配置比较好的服务器上,对 Kafka 做过极限的性能压测,Kafka 单个节点的极限处理能力接近每秒钟 2000 万条消息,吞吐量达到每秒钟 600MB。
- 其中提到了像全异步化的线程模型、高性能的异步网络传输、自定义的私有传输协议和序列化、反序列化等等,这些方法和优化技巧,你都可以在 Kafka 的源代码中找到对应的实现。
- 批量处理是一种非常有效的提升系统吞吐量的方法。在 Kafka 内部,消息都是以“批”为单位处理的
- 虽然它提供的 API 每次只能发送一条消息,但实际上,Kafka 的客户端 SDK 在实现消息发送逻辑的时候,采用了异步批量发送的机制。
- 当你调用 send() 方法发送一条消息之后,无论你是同步发送还是异步发送,Kafka 都不会立即就把这条消息发送出去。它会先把这条消息,存放在内存中缓存起来,然后选择合适的时机把缓存中的所有消息组成一批,一次性发给 Broker。
- 在服务端,Kafka 不会把一批消息再还原成多条消息,再一条一条地处理,这样太慢了。Kafka 这块儿处理的非常聪明,每批消息都会被当做一个“批消息”来处理。也就是说,在 Broker 整个处理流程中,无论是写入磁盘、从磁盘读出来、还是复制到其他副本这些流程中,批消息都不会被解开,一直是作为一条“批消息”来进行处理的。
- Consumer 从 Broker 拉到一批消息后,在客户端把批消息解开,再一条一条交给用户代码处理。
- 构建批消息和解开批消息分别在发送端和消费端的客户端完成,不仅减轻了 Broker 的压力,最重要的是减少了 Broker 处理请求的次数,提升了总体的处理能力。
- 相比于网络传输和内存,磁盘 IO 的速度是比较慢的。对于消息队列的服务端来说,性能的瓶颈主要在磁盘 IO 这一块
- kafka在磁盘IO做了优化
- 对于磁盘来说,它有一个特性,就是顺序读写的性能要远远好于随机读写。在 SSD(固态硬盘)上,顺序读写的性能要比随机读写快几倍,如果是机械硬盘,这个差距会达到几十倍。
- 因为会寻址,有寻址算法,寻址是机械运动,咔咔生就是在寻址
- 顺序读写只需要一次寻址就可以连续的读写下去
- kafka将producer的信息顺序的写到文件中,写满了就开启新的文件顺序的继续写,消费的时候也是从全局的位置开始,也就是log文件的某个位置顺序的把消息读出来
- kafka的PageCache:就是操作系统给磁盘的文件建立的缓存,在现代操作系统中都会有
- 无论我们使用什么语言编写的程序,在调用系统的 API 读写文件的时候,并不会直接去读写磁盘上的文件,应用程序实际操作的都是 PageCache,也就是文件在内存中缓存的副本。
- 也是先往pagecache里写数据,后往磁盘写
- 会出现两种情况:一是pagecache有数据,就直接读取,当pagecache没有数据操作系统会引发一个缺页中断,应用程序的读取线程会被阻塞,操作系统将文件中的数据复制到pagecache中,然后再从pagecache中把数据读出来
- 当pagecache满了之后操作系统并不会立刻清除这个pagecache,而是尽可能用物理内存保存pagecache,基本上就会lru或者变种算法
- 在大多数都会命中读请求
- ZeroCopy:零拷贝技术:
在服务端处理消息的逻辑:首先从文件中找到信息读取到内存,然后通过网络发送给客户端
- 这个过程数据实际做了2,3此复制
- 从文件复制数据到pagecache中,如果命中pagecache可以省掉
- 从pagecache复制到应用程序的内存空间中,就是我们可以操作的对象所在内存
- 从应用程序的内存空间复制到socket的缓冲区,这个过程就是调用网络框架的API发送的过程
- kafka将23步优化成一步
- 下面是这个零拷贝对应的系统调用:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- 它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
- 所有语言都有API,都可以用零copy提升性能
- 总结:
批量处理
pagecache
零copy
顺序读写
- 要真正实现一个高性能的消息队列,是非常不容易的,你需要熟练掌握非常多的编程语言和操作系统的底层技术。
16 | 缓存策略:如何使用缓存来减少磁盘IO?
- 而内存的随机读写速度是磁盘的 10 万倍
- 缓存的命中率问题,缓存能否返回最新数据,缓存数据过期了怎么办
- 选择只读缓存还是读写缓存?唯一的区别是,更新数据是否经过缓存
- pagecache就是读写缓存,先写缓存,后异步的更新磁盘
- 读写缓存是不可靠的,在数据写到 PageCache 中后,它并不是同时就写到磁盘上了,这中间是有一个延迟的。操作系统可以保证,即使是应用程序意外退出了,操作系统也会把这部分数据同步到磁盘上。但是,如果服务器突然掉电了,这部分数据就丢失了。
- 可以sync等系统调用,强制将缓存中的数据同步到磁盘文件中去,这个同步的过程是很慢的,也就失去了缓存的意义
- 写缓存是很复杂的,一般情况下不推荐读写缓存
- Kafka 它并不是只靠磁盘来保证数据的可靠性,它更依赖的是,在不同节点上的多副本来解决数据可靠性问题,这样即使某个服务器掉电丢失一部分文件内容,它也可以从其他节点上找到正确的数据,不会丢消息。
- Kafka 其实在设计上,充分利用了 PageCache 这种读写缓存的优势,并且规避了 PageCache 的一些劣势
- 和 Kafka 一样,大部分其他的消息队列,同样也会采用读写缓存来加速消息写入的过程,只是实现的方式都不一样。
- 不同于消息队列,我们开发的大部分业务类应用程序,读写比都是严重不均衡的,一般读的数据的频次会都会远高于写数据的频次。从经验值来看,读次数一般都是写次数的几倍到几十倍。这种情况下,使用只读缓存来加速系统才是非常明智的选择。
- 只读缓存应该考虑什么问题:
对于只读缓存,缓存中的数据来源只有磁盘,当数据更新的时候,磁盘中的数据和缓存中额副本都需要更新,在分布式系统中除非是使用事务或者分布式一致性算法来保证数据的一致性,否则由于节点宕机,网络传输故障等情况存在,是无法保证磁盘和缓存中的数据是一致的,如果数据不一致一定是以磁盘为准
- 异步更新怎么保证更新的时序?
- 比如,我先把一个文件中的某个数据设置成 0,然后又设为 1,这个时候文件中的数据肯定是 1,但是缓存中的数据可不一定就是 1 了。因为把缓存中的数据更新为 0,和更新为 1 是两个并发的异步操作,不一定谁会先执行。
- 可以使用分布式事务来解决,只是付出的性能、实现复杂度等代价比较大。
- 另外一种比较简单的方法就是,定时将磁盘上的数据同步到缓存中。一般的情况下,每次同步时直接全量更新就可以了,因为是在异步的线程中更新数据,同步的速度即使慢一些也不是什么大问题。如果缓存的数据太大,更新速度慢到无法接受,也可以选择增量更新,每次只更新从上次缓存同步至今这段时间内变化的数据,代价是实现起来会稍微有些复杂。
- 这种定时同步缓存的方法,缺点是缓存更新不那么及时,优点是实现起来非常简单,鲁棒性非常好。
- 很多情况下,更新缓存不及时系统是可以接受的,但是对于交易类的系统对数据的一致性要求高,这种场景一般都不使用缓存或者在更新数据的同时更新缓存
- 一般来说,我们都会在数据首次被访问的时候,顺便把这条数据放到缓存中。随着访问的数据越来越多,总有把缓存占满的时刻,这个时候就需要把缓存中的一些数据删除掉,以便存放新的数据,这个过程称为缓存置换。来提高缓存的命中率。
- 命中率最高的置换策略,一定是根据你的业务逻辑,定制化的策略。比如,你如果知道某些数据已经删除了,永远不会再被访问到,那优先置换这些数据肯定是没问题的。再比如,你的系统是一个有会话的系统,你知道现在哪些用户是在线的,哪些用户已经离线,那优先置换那些已经离线用户的数据,尽量保留在线用户的数据也是一个非常好的策略。
- 另外一个选择,就是使用通用的置换算法。一个最经典也是最实用的算法就是 LRU 算法,也叫最近最少使用算法。这个算法它的思想是,最近刚刚被访问的数据,它在将来被访问的可能性也很大,而很久都没被访问过的数据,未来再被访问的几率也不大。
- Kafka 使用的 PageCache,是由 Linux 内核实现的,它的置换算法的就是一种 LRU 的变种算法
- 挖坟问题:用户访问旧的数据导致新的数据在缓存中被替换掉,可以加入权重来解决
- 总结:
更新缓存的三种方法:第一种是在更新数据的同时去更新缓存,第二种是定期来更新全部缓存,第三种是给缓存中的每个数据设置一个有效期,让它自然过期以达到更新的目的。这三种方法在更新的及时性上和实现的复杂度这两方面,都是依次递减的,你可以按需选择。
对于缓存的置换策略,最优的策略一定是你根据业务来设计的定制化的置换策略,当然你也可以考虑 LRU 这样通用的缓存置换算法。
17 | 如何正确使用锁保护共享数据,协调异步线程?
- 在群里报名是经典的并发读写造成的数据错误
- 在上面微信群报名的例子中,如果说我们的微信群中有一把锁,想要报名的人必须先拿到锁,然后才能更新报名名单。这样,就避免了多个人同时更新消息,报名名单也就不会出错了。
- 如果能不用锁,就不用锁;如果你不确定是不是应该用锁,那也不要用锁
- 加锁和解锁性能降低,等待锁是线程阻塞的状态,还容易造成死锁
- 只有在并发环境中,共享资源不支持并发访问,或者说并发访问共享资源会导致系统错误的情况下,才需要使用锁。
- 很多语言都有异常机制,当抛出异常的时候,不再执行后面的代码。如果在访问共享资源时抛出异常,那后面释放锁的代码就不会被执行,这样,锁就一直无法释放,形成死锁。所以,你要考虑到代码可能走到的所有正常和异常的分支,确保所有情况下,锁都能被释放。
- 大部分编程语言都提供了可重入锁,如果没有特别的要求,你要尽量使用可重入锁
- 经典的死锁案例:
import threading
def func1(lockA, lockB):
while True:
print("Thread1: Try to accquire lockA...")
with lockA:
print("Thread1: lockA accquired. Try to accquire lockB...")
with lockB:
print("Thread1: Both lockA and LockB accrquired.")
def func2(lockA, lockB):
while True:
print("Thread2: Try to accquire lockB...")
with lockB:
print("Thread2: lockB accquired. Try to accquire lockA...")
with lockA:
print("Thread2: Both lockA and LockB accrquired.")
if __name__ == '__main__':
lockA = threading.RLock();
lockB = threading.RLock()
t1 = threading.Thread(target=func1, args=(lockA, lockB,))
t2 = threading.Thread(target=func2, args=(lockA, lockB,))
t1.start()
t2.start()
- 避免死锁的方式:
- 再次强调一下,避免滥用锁,程序里用的锁少,写出死锁 Bug 的几率自然就低。
- 对于同一把锁,加锁和解锁必须要放在同一个方法中,这样一次加锁对应一次解锁,代码清晰简单,便于分析问题。
- 尽量避免在持有一把锁的情况下,去获取另外一把锁,就是要尽量避免同时持有多把锁。
- 如果需要持有多把锁,一定要注意加解锁的顺序,解锁的顺序要和加锁顺序相反。比如,获取三把锁的顺序是 A、B、C,释放锁的顺序必须是 C、B、A。
- 给你程序中所有的锁排一个顺序,在所有需要加锁的地方,按照同样的顺序加解锁。比如我刚刚举的那个例子,如果两个线程都按照先获取 lockA 再获取 lockB 的顺序加锁,就不会产生死锁。
- 读访问可以并发执行。
- 写的同时不能并发读,也不能并发写。
18 | 如何用硬件同步原语(CAS)替代锁?
- 硬件同步原语是计算机硬件提供的一组原子操作
- CAS compare and swap:先比较再替换
- FAA fetch and add:先获取变量再增加
- 如果我们用编程语言来实现,肯定是无法保证原子性的。而原语的特殊之处就是,它们都是由计算机硬件,具体说就是 CPU 提供的实现,可以保证操作的原子性。
- CAS 和 FAA 在各种编程语言中,都有相应的实现,可以来直接使用,无论你是使用哪种编程语言,它们底层的实现是一样的,效果也是一样的。
- 可以使用 CAS 原语 + 反复重试的方式来保证数据安全
- 线程之间的碰撞不能太频繁,否则太多重试会消耗大量的 CPU 资源,反而得不偿失。
19 | 数据压缩:时间换空间的游戏
20 | RocketMQ Producer源码分析:消息生产的实现过程
21 | Kafka Consumer源码分析:消息消费的实现过程
22 | Kafka和RocketMQ的消息复制实现的差异点在哪?
- 写入节点越多,性能会下降但是复制对消费的性能影响不大
- 保证不丢消息和严格的顺序,必须采用主从的复制方式
- 数据不一致以主节点为准,在任何一个时刻只要保证主节点是一个就行
- 使用第三方服务来管理这些节点,当主节点宕机,由管理服务指定一个新的主节点,但是引入管理服务会带来一系列问题,管理服务本身的高可用和数据一致性的问题
- 还可通过自主选举的方式,但是投票的实现比较复杂,选举的过程也比较慢,在选出主节点之前服务一直不可用
- RocketMQ 的这种主从复制方式,牺牲了可用性,换取了比较好的性能和数据一致性
- 那 RocketMQ 又是如何解决可用性的问题的呢?一对儿主从节点可用性不行,多来几对儿主从节点不就解决了?RocketMQ 支持把一个主题分布到多对主从节点上去,每对主从节点中承担主题中的一部分队列,如果某个主节点宕机了,会自动切换到其他主节点上继续发消息,这样既解决了可用性的问题,还可以通过水平扩容来提升 Topic 总体的性能。
- kafka也是写入足够多的节点就返回写入成功,这个数量由用户自己决定
- Kafka 使用 ZooKeeper 来监控每个分区的多个节点,如果发现某个分区的主节点宕机了,Kafka 会利用 ZooKeeper 来选出一个新的主节点,这样解决了可用性的问题。
- 没有完美的方式能够兼顾高性能、高可用、一致性
- 每一个分布式的服务都需要一个类似于NameServer(RocketMQ)服务来帮助访问集群中的客户端节点,在Dubbo中注册中心就提供了这种服务,Flink中的JobManager承担了NamwService的职责
- NameServer提供寻址服务,找到主题对应的Broker地址,还负责监控Broker的存活状态
- Broker定期会上报信息,同时起了心跳的作用
- 每个NameServer提供独立的完整信息服务,每次先会查询NameServer上Broker的路由信息,然后在获取物理的地址,再连接Broker节点进行消费
- 当心跳中断,NameServer会立刻移除掉Broker的路由信息,当客户端查询失败之后会自动切换
- 总结:
Broker 会与所有 NameServer 节点建立长连接,定期上报 Broker 的路由信息。客户端会选择连接某一个 NameServer 节点,定期获取订阅主题的路由信息,用于 Broker 寻址。
给客户端提供路由寻址服务的方式可以有两种,一种是客户端直接连接 NamingService 服务查询路由信息,另一种是,客户端连接集群内任意节点查询路由信息,节点再从自身的缓存或者从 NamingService 上进行查询。
- 在 RocketMQ 的 NameServer 集群中,各节点之间不需要互相通信,每个节点都可以独立的提供服务。课后请你想一想,这种独特的集群架构有什么优势,又有什么不足?
24 | Kafka的协调服务ZooKeeper:实现分布式系统的“瑞士军刀”
- ZooKeeper,它是一个分布式的协调服务,它的核心服务是一个高可用、高可靠的一致性存储,在此基础上,提供了包括读写元数据、节点监控、选举、节点间通信和分布式锁等很多功能,这些功能可以极大方便我们快速开发一个分布式的集群系统。
- 不要往zk写大量的数据,ZooKeeper 的选举过程是比较慢的,而它对网络的抖动又比较敏感,一旦触发选举,这段时间内的 ZooKeeper 是不能提供任何服务的。
- Kafka 主要使用 ZooKeeper 来保存它的元数据、监控 Broker 和分区的存活状态,并利用 ZooKeeper 来进行选举。
- Kafka 在 ZooKeeper 中保存的元数据,主要就是 Broker 的列表和主题分区信息两棵树。这份元数据同时也被缓存到每一个 Broker 中。客户端并不直接和 ZooKeeper 来通信,而是在需要的时候,通过 RPC 请求去 Broker 上拉取它关心的主题的元数据,然后保存到客户端的元数据缓存中,以便支撑客户端生产和消费。
- 如果你需要要部署大规模的 Kafka 集群,建议的方式是,拆分成多个互相独立的小集群部署,每个小集群都使用一组独立的 ZooKeeper 提供服务。这样,每个 ZooKeeper 中存储的数据相对比较少,并且如果某个 ZooKeeper 集群故障,只会影响到一个小的 Kafka 集群,故障的影响面相对小一些。
25 | RocketMQ与Kafka中如何实现事务?
- 事务可以增加本地事务和消息的操作,但是RocketMQ支持事务反查
26 | MQTT协议:如何支持海量的在线IoT设备?
29 | 流计算与消息(一):通过Flink理解流计算的原理
30 | 流计算与消息(二):在流计算中使用Kafka链接计算任务
- 存储和计算分离:引入配置中心