首先明确一下:读写分离与否没有绝对的优劣,它仅仅是一种架构设计,各自有适用的场景。
读写分离的适用场景
为什么MySQL与Redis要搞读写分离?因为它们的Master节点太累了,无论有多少台机器,都只有一台机器对外提供读写服务,负载过重,Master本身就是单点瓶颈,不得不引入读写分离,以此来分担Master的压力,把读请求分流给 Slave。
Redis和MySQL都支持主从读写分离,这和它们的使用场景有关。对于那种读操作很多而写操作相对不频繁的负载类型而言,采用读写分离是非常不错的方案------我们可以添加很多follower横向扩展,提升读操作性能。
反观Kafka,它的主要场景还是在消息引擎而不是以数据存储的方式对外提供读服务,通常涉及频繁地生产消息和消费消息,这不属于典型的读多写少场景,因此读写分离方案在这个场景下并不太适合。
Kafka的机制
Kafka分片机制设计的很巧妙,天生的分片架构。假设你有一个Kafka 集群,3台机器Broker A、B、 C。你有一个Topic,切了3个分区 P0、 P1、 P2。那么P0 的副本Leader在Broker A,P1的副本Leader在Broker B,P2 的副本Leader在 Broker C。发现了吧,在Kafka集群里,每一台机器都是 Leader,Broker A不仅是P0的Leader,但它同时也是P1和P2的Follower。这就是说,所有的机器都在同时承担读和写的压力,流量已经被Partition机制均匀地打散到了整个集群。Kafka不搞读写分离,是因为人家全员都是 Master。既然都已经实现了负载均衡,再搞读写分离,只会增加架构复杂度。
Kafka之所以吞吐量惊人,主要是依赖操作系统的Page Cache页缓存和零拷贝。
生产者发送一条消息给Leader。操作系统内核收到数据,直接写入Page Cache内存。注意这时候数据可能还没刷到硬盘上,但对Kafka来说已经收到了。消费者紧接着拉这条消息,发现在Page Cache里,Leader直接利用零拷贝技术,把内存里的数据直接射到网卡,发给消费者。这样全程走内存,根本不沾硬盘,延迟肯定低。
假设Kafka搞了读写分离
生产者把消息发给Leader,Leader要把数据通过网络发给Follower,Follower收到数据,写入自己的Page Cache,为了不丢数据,它要异步刷盘。消费者去找Follower读数据,因为有网络延迟和处理耗时,等消费者去读Follower的时候,这条消息在Follower的内存里可能就被新的数据挤出了Page Cache,或者还没来得及热起来。如果消费者读得稍微快一点,Follower还没同步到数据,消费者就得阻塞等待。如果消费者读得慢一点,Follower的Page Cache被污染了,消费者就得去读硬盘。所以放着内存不读,非要绕一圈去读硬盘,反而会很慢。
一致性问题
MySQL主从延迟几毫秒,用户刷新一下网页也还能接受。但在Kafka的Offset可是绝对的坐标,如果允许读 Follower,就会出现著名的时光倒流现象:消费者从Follower A读到了Offset 100。网络抖动,消费者重连到了Follower B,但它的同步比较慢,只到了Offset 90。消费者一看:"卧槽?怎么最新的数据没了?Offset 怎么倒退了?"要解决这个问题,Kafka还要引入元数据同步机制,确保所有 Follower都对齐了水位线之后才能开放读取,复杂度又上来了。为一个本来就不存在的负载瓶颈,去引入一套高成本的一致性方案,性价比太低了。
新版本开放适度的读写分离
为了防杠,提前说明,现在的Kafka(2.4版本以后)确实支持从Follower读取数据了。但Kafka开放这个功能,初衷可不是纯为了提升性能,主要减少跨机房流量,是为了省钱。比如我Leader在北京,Follower在上海,消费者也在上海。以前消费者必须跨越千里去连北京Leader,延迟高、流量费贵。现在允许上海消费者直接读上海的Follower。省钱是省钱,也是有代价的,上海的消费者必须忍受同步延迟。属于是种用一致性和时效性,换取网络成本。
架构设计循序渐进,为了解决一个问题,引入一个新东西,就会冒出来另一个新问题,根本没有一套万能通用的解决方案,完全看取舍。