简介:Redis 是目前最受欢迎的 kv 类数据库,当然它的性能越来越多,早已不限定在 kv 场景,音讯队列就是 Redis 中一个重要的性能。Redis 从 2010 年公布 1.0 版本就具备一个音讯队列的雏形,随着 10 多年的迭代,其音讯队列的性能也越来越欠缺,作为一个全内存的音讯队列,适宜利用与要求高吞吐、低延时的场景。本文未来盘一下 Redis 音讯队列性能的倒退历程,历史版本有哪些有余,后续版本是如何来解决这些问题的。
作者 | 丕天
起源 | 阿里技术公众号
Redis 是目前最受欢迎的 kv 类数据库,当然它的性能越来越多,早已不限定在 kv 场景,音讯队列就是 Redis 中一个重要的性能。
Redis 从 2010 年公布 1.0 版本就具备一个音讯队列的雏形,随着 10 多年的迭代,其音讯队列的性能也越来越欠缺,作为一个全内存的音讯队列,适宜利用与要求高吞吐、低延时的场景。
咱们来盘一下 Redis 音讯队列性能的倒退历程,历史版本有哪些有余,后续版本是如何来解决这些问题的。
一 Redis 1.0 list
从狭义上来讲音讯队列就是一个队列的数据结构,生产者从队列一端放入音讯,消费者从另一端读取音讯,音讯保障先入先出的程序,一个本地的 list 数据结构就是一个过程维度的音讯队列,它能够让模块 A 写入音讯,模块 B 生产音讯,做到模块 A / B 的解耦与异步化。但想要做到利用级别的解耦和异步还须要一个音讯队列的服务。
1 list 的个性
Redis 1.0 公布时就具备了 list 数据结构,利用 A 能够通过 lpush 写入音讯,利用 B 通过 rpop 从队列中读取音讯,每个音讯只会被读取一次,而且是依照 lpush 写入的程序读到。同时 Redis 的接口是并发平安的,能够同时有多个生产者向一个 list 中生产音讯,多个消费者从 list 中读取音讯。
这里还有个问题,消费者要如何晓得 list 中有音讯了,须要一直轮询去查问吗。轮询无奈保障音讯被及时的解决,会减少延时,而且当 list 为空时,大部分轮询的申请都是有效申请,这种形式大量节约了系统资源。好在 Redis 有 brpop 接口,该接口有一个参数是超时工夫,如果 list 为空,那么 Redis 服务端不会立即返回后果,它会期待 list 中有新数据后在返回或是期待最多一个超时工夫后返回空。通过 brpop 接口实现了长轮询,该成果等同于服务端推送,消费者能立即感知到新的音讯,而且通过设置正当的超时工夫,使系统资源的耗费降到很低。
# 基于 list 实现音讯的生产和生产
#生产者生产音讯 msg1
lpush listA msg1
(integer) 1
#消费者读取到音讯 msg1
rpop listA
"msg1"
#消费者阻塞式读取 listA,如果有数据立即返回,否则最多期待 10 秒
brpop listA 10
1) "listA"
2) "msg1"
应用 rpop 或 brpop 这样接口生产音讯会先从队列中删除音讯,而后再由利用生产,如果利用利用在解决音讯前异样宕机了,音讯就失落了。但如果应用 lindex 这样的只读命令先读取音讯处理完毕后在删除,又须要额定的机制来保障一条音讯不会被其余消费者反复读到。好在 list 有 rpoplpush 或 brpoplpush 这样的接口,能够原子性的从一个 list 中移除一个音讯并退出另一个 list。
应用程序能够通过 2 个 list 组和来实现音讯的生产和确认性能,应用 rpoplpush 从 list A 中生产音讯并移入 list B,等音讯处理完毕后在从 list B 中删除音讯,如果在解决音讯过程中利用异样宕机,复原后利用能够从新从 list B 中读取未解决的音讯并解决。这种形式为音讯的生产减少了 ack 机制。
# 基于 2 个 list 实现音讯生产和确认
#从 listA 中读取音讯并写入 listB
rpoplpush listA listB
"msg1"
#业务逻辑解决 msg1 结束后,从 listB 中删除 msg1,实现音讯的确认
lrem listB 1 msg1
(integer) 1
2 list 的不足之处
通过 Redis 1.0 就引入的 list 构造咱们就能实现一个分布式的音讯队列,满足一些简略的业务需要。但 list 构造作为音讯队列服务有一个很致命的问题,它没有播送性能,一个音讯只能被生产一次。而在大型零碎中,通常一个音讯会被上游多个利用同时订阅和生产,例如当用户实现一个订单的领取操作时,须要告诉商家发货,要更新物流状态,可能还会进步用户的积分和等级,这些都是不同的上游子系统,他们全副会订阅领取实现的操作,而 list 一个音讯只能被生产一次在这样简单的大型零碎背后就顾此失彼了。
可能你会说那弄多个 list,生产者向每个 list 中都投递音讯,每个消费者解决本人的 list 不就行了吗。这样第一是性能不会太好,因为同一个音讯须要被反复的投递,第二是这样的设计违反了生产者和消费者解耦的准则,这个设计下生产者须要晓得上游有哪些消费者,如果业务发生变化,须要额定减少一个消费者,生产者的代码也须要批改。
3 总结
劣势
- 模型简略,和应用本地 list 基本相同,适配容易
- 通过 brpop 做到音讯解决的实时性
- 通过 rpoplpush 来联动 2 个 list,能够做到音讯先生产后确认,防止消费者利用异常情况下音讯失落
有余
音讯只能被生产一次,不足播送机制
二 Redis 2.0 pubsub
list 作为音讯队列利用场景受到限制很重要的起因在于没有播送,所以 Redis 2.0 中引入了一个新的数据结构 pubsub。pubsub 尽管不能算作是 list 的替代品,但它的确能解决一些 list 不能解决的问题。
1 pubsub 个性
pubsub 引入一个概念叫 channel,生产者通过 publish 接口投递音讯时会指定 channel,消费者通过 subscribe 接口订阅它关怀的 channel,调用 subscribe 后这条连贯会进入一个非凡的状态,通常不能在发送其余申请,当有音讯投递到这个 channel 时 Redis 服务端会立即通过该连贯将音讯推送到消费者。这里一个 channel 能够被多个利用订阅,音讯会同时投递到每个订阅者,做到了音讯的播送。
另一方面,消费者能够会订阅一批 channel,例如一个用户订阅了浙江的新闻的推送,但浙江新闻还会进行细分,例如“浙江杭州 xx”、“浙江温州 xx”,这里订阅者不须要获取浙江的所有子类在挨个订阅,只须要调用 psubscribe“浙江 *”就能订阅所有以浙江结尾的新闻推送了,这里 psubscribe 传入一个通配符表白的 channel,Redis 服务端依照规定推送所有匹配 channel 的音讯给对应的客户端。
# 基于 pubsub 实现 channel 的匹配和音讯的播送
#消费者 1 订阅 channel1
subscribe channel1
1) "subscribe"
2) "channel1"
3) (integer) 1
#收到音讯推送
1) "message"
2) "channel1"
3) "msg1"
#消费者 2 订阅 channel*
psubscribe channel*
1) "psubscribe"
2) "channel*"
3) (integer) 1
#收到音讯推送
1) "pmessage"
2) "channel*"
3) "channel1"
4) "msg1"
1) "pmessage"
2) "channel*"
3) "channel2"
4) "msg2"
#生产者公布音讯 msg1 和 msg2
publish channel1 msg1
(integer) 2
publish channel2 msg2
(integer) 1
在 Redfis 2.8 时退出了 keyspace notifications 性能,此时 pubsub 除了告诉用户自定义音讯,也能够告诉零碎内部消息。keyspace notifications 引入了 2 个非凡的 channel 别离是__keyevent@__: 和__keyspace@__:,通过订阅__keyevent 客户端能够收到某个具体命令调用的回调告诉,通过订阅__keyspace 客户端能够收到指标 key 的增删改操作以及过期事件。应用这个性能还须要开启配置 notify-keyspace-events。
# 通过 keyspace notifications 性能获取零碎事件
#写入申请
set testkey v EX 1
#订阅 key 级别的事件
psubscribe __keyspace@0__:testkey
1) "psubscribe"
2) "__keyspace@0__:testkey"
3) (integer) 1
#收到告诉
1) "pmessage"
2) "__keyspace@0__:testkey"
3) "__keyspace@0__:testkey"
4) "set"
1) "pmessage"
2) "__keyspace@0__:testkey"
3) "__keyspace@0__:testkey"
4) "expire"
1) "pmessage"
2) "__keyspace@0__:testkey"
3) "__keyspace@0__:testkey"
4) "expired"
#订阅所有的命令事件
psubscribe __keyevent@0__:*
1) "psubscribe"
2) "__keyevent@0__:*"
3) (integer) 1
#收到告诉
1) "pmessage"
2) "__keyevent@0__:*"
3) "__keyevent@0__:set"
4) "testkey"
1) "pmessage"
2) "__keyevent@0__:*"
3) "__keyevent@0__:expire"
4) "testkey"
1) "pmessage"
2) "__keyevent@0__:*"
3) "__keyevent@0__:expired"
4) "testkey"
2 pubsub 的不足之处
pubsub 既能单播又能播送,还反对 channel 的简略正则匹配,性能上曾经能满足大部分业务的需要,而且这个接口公布的工夫很早,在 2011 年 Redis 2.0 公布时就曾经具备,用户根底很宽泛,所以当初很多业务都有用到这个性能。但你要深刻理解 pubsub 的原理后,是必定不敢把它作为一个一致性要求较高,数据量较大零碎的音讯服务的。
首先,pubsub 的音讯数据是刹时的,它在 Redis 服务端不做保留,publish 发送到 Redis 的音讯会立即推送到所有过后 subscribe 连贯的客户端,如果过后客户端因为网络问题断连,那么就会错过这条音讯,当客户端重连后,它没法从新获取之前那条音讯,甚至无奈判断是否有音讯失落。
其次,pubsub 中消费者获取音讯是一个推送模型,这意味着 Redis 会按音讯生产的速度给所有的消费者推送音讯,不论消费者解决能力如何,如果消费者利用解决能力有余,音讯就会在 Redis 的 client buf 中沉积,当沉积数据超过一个阈值后会断开这条连贯,这意味着这些音讯全副失落了,在也找不回来了。如果同时有多个消费者的 client buf 沉积数据但又还没达到断开连接的阈值,那么 Redis 服务端的内存会收缩,过程可能因为 oom 而被杀掉,这导致了整个服务中断。
3 总结
劣势
- 音讯具备播送能力
- psubscribe 能按字符串通配符匹配,给予了业务逻辑的灵活性
- 能订阅特定 key 或特定命令的零碎音讯
有余
- Redis 异样、客户端断连都会导致音讯失落
- 音讯不足沉积能力,不能削峰填谷。推送的形式不足背压机制,没有思考消费者解决能力,推送的音讯超过消费者解决能力后可能导致音讯失落或服务异样
三 Redis 5.0 stream
音讯失落、音讯服务不稳固的问题重大限度了 pubsub 的利用场景,所以 Redis 须要从新设计一套机制,来解决这些问题,这就有了起初的 stream 构造。
1 stream 个性
一个稳固的音讯服务须要具备几个要点,要保障音讯不会失落,至多被生产一次,要具备削峰填谷的能力,来匹配生产者和消费者吞吐的差别。在 2018 年 Redis 5.0 退出了 stream 构造,这次思考了 list、pubsub 在利用场景下的缺点,对标 kafka 的模型从新设计全内存音讯队列构造,从这时开始 Redis 音讯队列性能算是能和支流音讯队列产品 pk 一把了。
stream 的改良分为多个方面
老本:
存储 message 数据应用了 listpack 构造,这是一个紧凑型的数据结构,不同于 list 的双向链表每个节点都要额定占用 2 个指针的存储空间,这使得小 msg 状况下 stream 的空间利用率更高。
性能:
- stream 引入了消费者组的概念,一个消费者组内能够有多个消费者,同一个组内的消费者共享一个音讯位点(last_delivered_id),这使得消费者可能程度的扩容,能够在一个组内退出多个消费者来线性的晋升吞吐,对于一个消费者组,每条 msg 只会被其中一个消费者获取和解决,这是 pubsub 的播送模型不具备的。
- 不同消费者组之前是互相隔离的,他们各自保护本人的位点,这使得一条 msg 能被多个不同的消费者组反复生产,做到了音讯播送的能力。
- stream 中消费者采纳拉取的形式,并能设置 timeout 在没有音讯时阻塞,通过这种长轮询机制保障了音讯的实时性,而且生产速率是和消费者本身吞吐相匹配。
音讯不失落:
- stream 的数据会存储在 aof 和 rdb 文件中,这使 Redis 重启后可能复原 stream 的数据。而 pubsub 的数据是刹时的,Redis 重启意味着音讯全副失落。
- stream 中每个消费者组会存储一个 last_delivered_id 来标识曾经读取到的位点,客户端连贯断开后重连还是能从该位点持续读取,音讯不会失落。
- stream 引入了 ack 机制保障音讯至多被解决一次。思考一种场景,如果消费者利用曾经读取了音讯,但还没来得及解决利用就宕机了,对于这种曾经读取但没有 ack 的音讯,stream 会标示这条音讯的状态为 pending,等客户端重连后通过 xpending 命令能够从新读取到 pengind 状态的音讯,持续解决。如果这个利用永恒宕机了,那么该消费者组内的其余消费者利用也能读取到这条音讯,并通过 xclaim 命令将它归属到本人上面持续解决。
# 基于 stream 实现音讯的生产和生产,并确保异样状态下音讯至多被生产一次
#创立 mystream,并且创立一个 consumergroup 为 mygroup
XGROUP CREATE mystream mygroup $ MKSTREAM
OK
#写入一条音讯,由 redis 主动生成音讯 id,音讯的内容是一个 kv 数组,这里蕴含 field1 value1 field2 value2
XADD mystream * field1 value1 field2 value2
"1645517760385-0"
#消费者组 mygroup 中的消费者 consumer1 从 mystream 读取一条音讯,> 示意读取一条该消费者组从未读取过的音讯
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
1) 1) "mystream"
2) 1) 1) "1645517760385-0"
2) 1) "field1"
2) "value1"
3) "field2"
4) "value2"
#生产实现后 ack 确认音讯
xack mystream mygroup 1645517760385-0
(integer) 1
#如果消费者利用在 ack 前异样宕机,复原后从新获取未解决的音讯 id。XPENDING mystream mygroup - + 10
1) 1) "1645517760385-0"
2) "consumer1"
3) (integer) 305356
4) (integer) 1
#如果 consumer1 永远宕机,其余消费者能够把 pending 状态的音讯挪动到本人名下后持续生产
#将音讯 id 1645517760385- 0 挪动到 consumer2 下
XCLAIM mystream mygroup consumer2 0 1645517760385-0
1) 1) "1645517760385-0"
2) 1) "field1"
2) "value1"
3) "field2"
4) "value2"
Redis stream 保障了音讯至多被解决一次,但如果想做到每条音讯仅被解决一次还须要应用逻辑的染指。
音讯被反复解决要么是生产者反复投递,要么是消费者反复生产。
- 对于生产者反复投递问题,Redis stream 为每个音讯都设置了一个惟一递增的 id,通过参数能够让 Redis 主动生成 id 或者利用本人指定 id,利用能够依据业务逻辑为每个 msg 生成 id,当 xadd 超时后利用并不能确定音讯是否投递胜利,能够通过 xread 查问该 id 的音讯是否存在,存在就阐明曾经投递胜利,不存在则从新投递,而且 stream 限度了 id 必须递增,这象征了曾经存在的音讯反复投递会被回绝。这套机制保障了每个音讯能够仅被投递一次。
- 对于消费者反复生产的问题,思考一个场景,消费者读取音讯后业务处理完毕,但还没来得及 ack 就产生了异样,利用复原后对于这条没有 ack 的音讯进行了反复生产。这个问题因为 ack 和生产音讯的业务逻辑产生在 2 个零碎,没法做到事务性,须要业务来革新,保障音讯解决的幂等性。
2 stream 的有余
stream 的模型做到了音讯的高效散发,而且保障了音讯至多被解决一次,通过应用逻辑的革新能做到音讯仅被解决一次,它的能力对标 kafka,但吞吐高于 kafka,在高吞吐场景下老本比 kafka 低,那它又有哪些有余了。
首先音讯队列很重要的一个性能就是削峰填谷,来匹配生产者和消费者吞吐的差别,生产者和消费者吞吐差别越大,持续时间越长,就意味着 steam 中须要沉积更多的音讯,而 Redis 作为一个全内存的产品,数据沉积的老本比磁盘高。
其次 stream 通过 ack 机制保障了音讯至多被生产一次,但这有个前提就是存储在 Redis 中的音讯自身不会失落。Redis 数据的长久化依赖 aof 和 rdb 文件,aof 落盘形式有几种,通过配置 appendfsync 决定,通常咱们不会配置为 always 来让每条命令执行完后都做一次 fsync,线上配置个别为 everysec,每秒做一次 fsync,而 rdb 是全量备份时生成,这象征了宕机复原可能会丢掉最近一秒的数据。另一方面线上生产环境的 Redis 都是高可用架构,当主节点宕机后通常不会走复原逻辑,而是间接切换到备节点持续提供服务,而 Redis 的同步形式是异步同步,这意味着主节点上新写入的数据可能还没同步到备节点,在切换后这部分数据就失落了。所以在故障复原中 Redis 中的数据可能会失落一部分,在这样的背景下无论 stream 的接口设计的如许欠缺,都不能保障音讯至多被生产一次。
3 总结
劣势
- 在老本、性能上做了很多改良,反对了紧凑的存储小音讯、具备播送能力、消费者能程度扩容、具备背压机制
- 通过 ack 机制保障了 Redis 服务端失常状况下音讯至多被解决一次的能力
有余
- 内存型音讯队列,数据沉积老本高
- Redis 自身 rpo>0,故障复原可能会丢数据,所以 stream 在 Redis 产生故障复原后也不能保障音讯至多被生产一次。
四 Tair 长久内存版 stream
Redis stream 的有余也是内存型数据库个性带来的,它领有高吞吐、低延时,但大容量下老本会比拟高,而利用的场景也不齐全是相对的大容量低吞吐或小容量高吞吐,有时利用的场景会介于二者之间,须要均衡容量和吞吐的关系,所以须要一个产品它的存储老本低于 Redis stream,但它的性能又高于磁盘型音讯队列。
另一方面 Redis stream 在 Redis 故障场景下不能保障音讯的不失落,这导致业务须要本人实现一些简单的机制来回补这段数据,同时也限度了它利用在一些对一致性要求较高的场景。为了让业务逻辑更简略,stream 利用范畴更广,须要保障故障场景下的音讯长久化。
兼顾老本、性能、长久化,这就有了 Tair 长久内存版。
1 Tair 长久内存版个性
更大空间,更低成本
Tair 长久内存版引入了 Intel 傲腾长久内存(上面称作 AEP),它的性能略低于内存,但雷同容量下老本低于内存。Tair 长久内存版将次要数据存储在 AEP 上,使得雷同容量下,老本更低,这使同样单价下 stream 能沉积更多的音讯。
兼容社区版
Tair 长久内存版兼容原生 Redis 绝大部分的数据结构和接口,对于 stream 相干接口做到了 100% 兼容,如果你之前应用了社区版 stream,那么不须要批改任何代码,只须要换一个连贯地址就能切换到长久内存版。并且通过工具实现社区版和长久内存版数据的双向迁徙。
数据的实时长久化
Tair 长久内存版并不是简略将 Redis 中的数据换了一个介质存储,因为这样仅能通过 AEP 降低成本,但没用到 AEP 断电数据不失落的个性,对长久化能力没有任何晋升。
开源 Redis 通过在磁盘上记录 AppendOnlyLog 来长久化数据,AppendOnlyLog 记录了所有的写操作,相当于 redolog,在宕机复原时通过回放这些 log 复原数据。但受限于磁盘介质的高延时和 Redis 内存数据库应用场景下对低延时的要求,并不能在每次写操作后 fsync 长久化 log,最新写入的数据可能并没有长久化到磁盘,这也是数据可能失落的根因。
Tair 长久内存版的数据恢复没有应用 AppendOnlyLog 来实现,而是将将 redis 数据结构存储在 AEP 上,这样宕机后这些数据结构并不会失落,并且对这些数据结构减少了一些额定的形容信息,宕机后在 recovery 时可能读到这些额定的形容信息,让这些 redis 数据结构从新被辨认和索引,将状态复原到宕机前的样子。Tair 通过将 redis 数据结构和形容信息实时写入 AEP,保障了写入数据的实时长久化。
HA 数据不失落
Tair 长久内存版保障了数据的长久化,但生产环境中都是高可用架构,少数状况下当主节点异样宕机后并不会等主节点重启复原,而是切换到备节点持续提供服务,而后给新的主节点增加一个新的备节点。所以在故障产生时如果有数据还没从主节点同步到备节点,这部分数据就会失落。
Redis 采纳的异步同步,当客户端写入数据并返回胜利时对 Redis 的批改可能还没同步到备节点,如果此时主节点宕机数据就会失落。为了防止在 HA 过程中数据失落,Tair 长久内存版引入了半同步机制,确保写入申请返回胜利前相干的批改曾经同步到备节点。
能够发现开启半同步性能后写入申请的 RT 会变高,多出主备同步的耗时,这部分耗时大略在几十微秒。但通过一些异步化的技术,尽管写申请的 RT 会变高,但对实例的最大写吞吐影响很小。
当开启半同步后生成者通过 xadd 投递音讯,如果返回胜利,音讯肯定同步到备节点,此时产生 HA,消费者也能在备节点上读到这条音讯。如果 xadd 申请超时,此时音讯可能同步到备节点也可能没有,生产者没法确定,此时通过再次投递音讯,能够保障该音讯至多被生产一次。如果要严格保障音讯仅被生产一次,那么生产者能够通过 xread 接口查问音讯是否存在,对于不存在的场景从新投递。
2 总结
劣势
- 引入了 AEP 作为存储介质,目前 Tair 长久内存版价格是社区版的 70%。
- 保障了数据的实时长久化,并且通过半同步技术保障了 HA 不丢数据,大多数状况下做到音讯不失落(备库故障或主备网络异样时会降级为异步同步,优先保障可用性),音讯至多被生产一次或仅被生产一次。
五 将来
音讯队列次要是为了解决 3 类问题,利用模块的解耦、音讯的异步化、削峰填谷。目前支流的音讯队列都能满足这些需要,所以在理论选型时还会思考一些非凡的性能是否满足,产品的性能如何,具体业务场景下的老本怎么样,开发的复杂度等。
Redis 的音讯队列性能并不是最全面的,它不心愿做成一个大而全的产品,而是做一个小而美的产品,服务好一部分用户在某些场景下的需要。目前用户选型 Redis 作为音讯队列服务的起因,次要有 Redis 在雷同老本下吞吐更高、Redis 的延时更低、利用须要一个音讯服务但又不想额定引入一堆依赖等。
将来 Tair 长久内存版会针对这些述求,把这些劣势持续放大。
- 吞吐
通过优化长久内存版的长久化流程,让吞吐靠近内存版甚至超过内存版吞吐。
- 延时
通过 rdma 在多正本间同步数据,升高半同步下写入数据的延时。
原文链接
本文为阿里云原创内容,未经容许不得转载。