前言
高可用需要解决的问题主要有单点故障和大流量。
Redis部署
架构 | 实现 | 备注 |
---|---|---|
单点部署 | 单点启动 | 可能造成单点故障 |
主从复制 | Slave主动请求,通过RDB同步 | 流量大导致RDB文件过大,同步慢 |
Codis | 代理模式+主从复制 | 分桶1024个,不支持KEYS |
Redis Cluster | 去中心化,客户端分片 | 分桶16384,不支持SELECT,官方出品 |
Codis架构
Codis-HA作为协调者也可能出现单点故障,同样需要主从部署。协调者会观测代理和集群的状态,一旦出现单点故障就会重新选举出主节点。Zookeeper作为分布式协调服务,保存代理和集群的映射关系。
Redis和Kafka作为消息队列对比
Redis
redis刚好提供了上述的数据结构——list。redis list支持:
- lpush:从队列左边插入数据;
- rpop:从队列右边取出数据。
这正好对应了我们队列抽象的push_front和pop_tail,因此我们可以直接把redis的list当成一个消息队列来使用。而且redis本身对高并发做了很好的优化,内部数据结构经过了精心地设计和优化。所以从某种意义上讲,用redis的list大概率比你自己重新实现一个list强很多。
但另一方面,使用redis list作为消息队列也有一些不足,比如:
- 消息持久化:redis是内存数据库,虽然有aof和rdb两种机制进行持久化,但这只是辅助手段,这两种手段都是不可靠的。当redis服务器宕机时一定会丢失一部分数据,这对于很多业务都是没法接受的。
- 热key性能问题:不论是用codis还是twemproxy这种集群方案,对某个队列的读写请求最终都会落到同一台redis实例上,并且无法通过扩容来解决问题。如果对某个list的并发读写非常高,就产生了无法解决的热key,严重可能导致系统崩溃。
- 没有确认机制:每当执行rpop消费一条数据,那条消息就被从list中永久删除了。如果消费者消费失败,这条消息也没法找回了。你可能说消费者可以在失败时把这条消息重新投递到进队列,但这太理想了,极端一点万一消费者进程直接崩了呢,比如被kill -9,panic,coredump…
- 不支持多订阅者:一条消息只能被一个消费者消费,rpop之后就没了。如果队列中存储的是应用的日志,对于同一条消息,监控系统需要消费它来进行可能的报警,BI系统需要消费它来绘制报表,链路追踪需要消费它来绘制调用关系……这种场景redis list就没办法支持了。
- 不支持二次消费:一条消息rpop之后就没了。如果消费者程序运行到一半发现代码有bug,修复之后想从头再消费一次就不行了。
对于上述的不足,目前看来第一条(持久化)是可以解决的。很多公司都有团队基于rocksdb leveldb进行二次开发,实现了支持redis协议的kv存储。这些存储已经不是redis了,但是用起来和redis几乎一样。它们能够保证数据的持久化,但对于上述的其他缺陷也无能为力了。
Kafka
特性
- 顺序存储,写入友好
- 游标记录,支持多订阅者与二次消费
- 一个partition同时只支持一个消费者消费,保证同partition的消息的有序性
- partition主从部署,避免单点故障
- partition分片存储segment稀疏索引,提高查找效率
不足
- 无法弹性扩容:对partition的读写都在partition leader所在的broker,如果该broker压力过大,也无法通过新增broker来解决问题;
- 扩容成本高:集群中新增的broker只会处理新topic,如果要分担老topic-partition的压力,需要手动迁移partition,这时会占用大量集群带宽;
- 消费者新加入和退出会造成整个消费组rebalance:导致数据重复消费,影响消费速度,增加e2e延迟;
- partition过多会使得性能显著下降:ZK压力大,broker上partition过多让磁盘顺序写几乎退化成随机写。
存算分离
这其实是目前非常流行的架构也是一种趋势,很多新型的存储都是这种”存算分离“的架构。比如tidb,底层存储其实是tikv这种kv存储。tidb是更上层的计算层,自己实现sql相关的功能。还有的例子就是很多"持久化"redis产品,大部分底层依赖于rocksdb做kv存储,然后基于kv存储关系实现redis的各种数据结构。