RESP 协议
概述
通信协议
通信协议是指双方实体完成通信或服务所必须遵循的规则和约定。通过通信信道和设备互连起来的多个不同地理位置的数据通信系统,要使其能协同工作实现信息交换和资源共享,它们之间必须具有共同的语言。交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。这个规则就是通信协议。
Redis 使用协议
- 连接: 在 Redis 中客户端与服务端之间的通信协议是在
TCP
协议之上构建的。 - 数据格式: Redis 制定了
RESP
(REdis Serialization Protocol,Redis 序列化协议)实现客户端与服务端的正常交互,这种协议简单高效、既能够被机器解析、又容易被人类识别。
RESP 协议
该协议是专门为 Redis 设计的,可以序列化不同的数据类型,如integers
(整数),strings
(字符串),arrays
(数组)。它还使用了一个特殊的类型来表示errors
(错误)。请求以字符串数组的形式来表示要执行命令的参数从客户端发送到 Redis 服务器。Redis 使用command-specific
(命令特有)数据类型作为回复。RESP
协议是二进制安全的,并且不需要处理从一个进程传输到另一个进程的块数据的大小,因为它使用prefixed-length
(前缀长度)的方式来传输块数据的。 需要注意的是该协议是仅用于 客户端 - 服务器(Client-Server)的通信。 Redis 集群使用不同的二进制协议来交换节点之间的消息。
发送命令格式
RESP 的规定一条命令的格式如下,CRLF 代表\r\n
。
*< 参数数量 > CRL
$< 参数 1 的字节数量 > CRLF
< 参数 1> CRLF
...
$< 参数 N 的字节数量 > CRLF
< 参数 N> CRLF
复制代码
以set hello world
这条命令举例
## 参数数量为3个,因此第一行为:
*3
## 参数字节数分别是355,因此后面几行为:
$3
SET
$5
hello
$5
world
复制代码
需要注意的是,上面只是格式化显示的结果,实际传输格式为如下代码
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
复制代码
响应结果格式
Redis 的返回结果类型分为以下五种:
- 状态回复:在
RESP
中第一个字节为+
。 - 错误回复:在
RESP
中第一个字节为-
。 - 整数回复:在
RESP
中第一个字节为:
。 - 字符串回复:在
RESP
中第一个字节为$
。 - 多条字符串回复:在
RESP
中第一个字节为*
。
在redis-cli
只能看到最终的执行结果(例如执行set hello world
,返回结果是OK
,并没有看到类似于+
符合),那是因为redis-cli
本身就是按照RESP
进行结果解析的,所以看不到中间结果,redis-cli.c 源码对命令结果的解析结构如下:
static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
sds out = sdsempty();
switch (r->type) {
case REDIS_REPLY_ERROR:
// 处理错误回复
case REDIS_REPLY_STATUS:
// 处理状态回复
case REDIS_REPLY_INTEGER:
// 处理整数回复
case REDIS_REPLY_STRING:
// 处理字符串回复
case REDIS_REPLY_NIL:
// 处理空
case REDIS_REPLY_ARRAY:
// 处理多条字符串回复
return out;
}
复制代码
那好哥哥要问了,怎么才能看到 Redis 服务端返回的“真正”结果呢?可以使用 nc 命令、telnet 命 令、甚至写一个 socket 程序进行模拟。以nc
命令举个栗子
## 连接到Redis
nc 127.0.0.1 6379
## 执行set命令
set hello world
## 状态回复返回
+OK
## 执行一个不存在的命令
sethx
## 错误回复
-ERR unknown command 'sethx'
## 执行加法操作
incr counter
## 整数回复
:1
## 执行取值操作
get hello
## 字符串回复
$5
world
## 批量设置多个键值对
mset java jedis python redis-py
## 批量获取mget
mget java python
## 返回结果条数
*2
##字符串回复
$5
jedis
$8
redis-py
## 如果key不存在,返回的就是 $-1
get not_exist
## 无论是字符串回复还是多条字符串回复,如果有nil值,那么会返回 $-1。
$-1
复制代码
总结
注意点
由于上面讲的协议都是基于请求-响应,但是需要排除以下功能点:
- Redis 支持管道操作,不熟悉的好哥哥们可以看看Redis Pipeline 这一篇就够了(整个系列基本基础都有了)。所以客户可以一次发送多个命令,稍后等待回复。
- 当 Redis 客户端订阅
Pub/Sub
模式的通道时,协议会改变语义变成推送协议,也就是说,客户端不再需要发送命令,因为服务器一旦收到消息就会自动向客户端发送该新消息(对于订阅了通道的客户端)。 - Redis 集群使用的是不同的二进制协议来交换节点之间的消息,并不是该协议。
Gossip协议
概述
确定不了解一下 Redis cluster
的通讯协议吗?其实这一块可以选择不弄的,主要吧cluster
的通讯协议有一个别名,叫做疫情传播算法
或者流行病协议
。受疫情影响,所以就还是一起来了解一下,因为本身我对这一块了解的还是比较少的(我是个只会CRUD
初级攻城狮)。
首先呢,cluster
的通讯协议使用的是Gossip
协议。上面也说了,aka 流行病协议
、疫情传播算法
。为什么这么叫等下看看Gossip
协议对应的原理好哥哥们就懂了。
Gossip
协议是一个去中心化思路的分布式通信协议。Gossip
协议于 1987 年在ACM
上发表的论文Epidemic Algorithms for Replicated Database Maintenance
中被提出,主要用在分布式数据库系统中各个副本节点间的数据同步。这种场景的一个最大特点就是组成网络的节点都是对等的,网络中即使有的节点因宕机而重启,或有新节点加入,但经过一段时间后,这些节点的状态也会与其他节点达成一致,也就是说,Gossip
天然具有分布式容错的优点。
另外Gossip
是一个带冗余的容错算法,是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在最终
(最终
是一个现实中存在,但理论上无法证明的时间点)所有节点一致。实际上Gossip
可以用于众多能接受最终一致性
的领域,比如说失败检测
、路由同步
、Pub/Sub
等。但是Gossip
的缺点也很明显,冗余通信会对网路带宽、CPU
资源造成很大的负载,而这些负载又受限于通信频率,该频率又影响着算法收敛的速度,因此,针对不同的应用场景,也有很多的优化方法。
Gossip
传播消息的过程主要如下图:
注意点
Gossip
过程是由一个种子节点(可以理解成“毒王”)发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机
的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。Gossip
过程是异步的,也就是说发消息的节点不会关注对方是否收到,即不等待响应。不管对方有没有收到,它都会每隔1 秒
(假设)向周围节点发消息。异步是它的优点,而消息冗余则是它的缺点。
Cluster 中的 Gossip
Gossip
协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息。在cluster
中,信息的种类分为ping消息
、pong消息
、meet消息
、fail消息
等(下面会介绍这几种消息), 整体的节点通讯的过程大概如下:
- 集群中的每个节点都会单独开辟一个
TCP
通道,用于节点之间彼此通信,通信端口号在基础端口上加10000
。 - 每个节点在固定周期内通过特定规则选择几个节点发送
ping
消息。 - 接收到
ping消息
的节点用pong消息
作为响应。
集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong
消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的(最终一致性)。
Gossip 消息
Gossip
协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip
消息,常用的Gossip
消息就是上面提到的那四种。在cluster
中的每个节点都维护了一份自己视角下的当前整个集群的状态,主要包括:
-
当前集群状态。
-
集群中各节点所负责的
slots
信息,及其migrate
状态。 -
集群中各节点的
master-slave
状态。 -
集群中各节点的存活状态及怀疑
fail
状态。 节点信息通讯模式如下图:
-
meet消息
用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet
消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong 消息交换。 -
ping消息
是集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息
,用于检测节点是否在线和交换彼此状态信息。ping消息
发送封装了自身节点和部分其他节点的状态数据。 -
pong消息
是当接收到ping
、meet
消息时,作为响应消息回复给发送方确认消息正常通信。pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息
来通知整个集群对自身状态进行更新。 -
fail消息
是当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息
,其他节点接收到fail消息
之后把对应节点更新为下线状态。
节点选择
上面已经把常用的消息弄清楚了,在注意点中也提到了Gossip
是随机来选择节点。那cluster
是怎么选择节点的呢?
cluster
集群下节点通信采用固定频率
(定时任务每秒执行 10 次)。因此节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。因此cluster
集群的Gossip
协议需要兼顾信息交换实时性和成本开销。通讯过程如下图:
集群内每个节点维护定时任务默认每秒执行10次
,每秒会随机
选取5个节点
找出最久没有通信的节点发送 ping 消息,用于保证Gossip
信息交换的随机性。每100毫秒
都会扫描本地节点列表,如果发现节点最近一次接受pong消息
的时间大于cluster_node_timeout/2
,则立刻发送ping消息
,防止该节点信息太长时间未更新。
关于这个定时任务是不是很熟悉,在 图解 Redis 哨兵模式 这一篇中就是用了三个定时任务来监控节点的状态,有不熟悉的好哥哥可以翻一翻。
总结
今天这篇的话针对于cluster
的通讯协议写的也不是很深,主要理解一下cluster
是怎么通讯的。