You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

138 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 38 | 通信开销限制Redis Cluster规模的关键因素
你好,我是蒋德钧。
Redis Cluster能保存的数据量以及支撑的吞吐量跟集群的实例规模密切相关。Redis官方给出了Redis Cluster的规模上限就是一个集群运行1000个实例。
那么,你可能会问,为什么要限定集群规模呢?其实,这里的一个关键因素就是,**实例间的通信开销会随着实例规模增加而增大**在集群超过一定规模时比如800节点集群吞吐量反而会下降。所以集群的实际规模会受到限制。
今天这节课我们就来聊聊集群实例间的通信开销是如何影响Redis Cluster规模的以及如何降低实例间的通信开销。掌握了今天的内容你就可以通过合理的配置来扩大Redis Cluster的规模同时保持高吞吐量。
## 实例通信方法和对集群规模的影响
Redis Cluster在运行时每个实例上都会保存Slot和实例的对应关系也就是Slot映射表以及自身的状态信息。
为了让集群中的每个实例都知道其它所有实例的状态信息实例之间会按照一定的规则进行通信。这个规则就是Gossip协议。
Gossip协议的工作原理可以概括成两点。
一是每个实例之间会按照一定的频率从集群中随机挑选一些实例把PING消息发送给挑选出来的实例用来检测这些实例是否在线并交换彼此的状态信息。PING消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息以及Slot映射表。
二是一个实例在接收到PING消息后会给发送PING消息的实例发送一个PONG消息。PONG消息包含的内容和PING消息一样。
下图显示了两个实例间进行PING、PONG消息传递的情况。
![](https://static001.geekbang.org/resource/image/5e/86/5eacfc36c4233ae7c99f80b1511yyb86.jpg)
Gossip协议可以保证在一段时间后集群中的每一个实例都能获得其它所有实例的状态信息。
这样一来即使有新节点加入、节点故障、Slot变更等事件发生实例间也可以通过PING、PONG消息的传递完成集群状态在每个实例上的同步。
经过刚刚的分析我们可以很直观地看到实例间使用Gossip协议进行通信时通信开销受到**通信消息大小**和**通信频率**这两方面的影响,
消息越大、频率越高,相应的通信开销也就越大。如果想要实现高效的通信,可以从这两方面入手去调优。接下来,我们就来具体分析下这两方面的实际情况。
首先,我们来看实例通信的消息大小。
### Gossip消息大小
Redis实例发送的PING消息的消息体是由clusterMsgDataGossip结构体组成的这个结构体的定义如下所示
```
typedef struct {
char nodename[CLUSTER_NAMELEN]; //40字节
uint32_t ping_sent; //4字节
uint32_t pong_received; //4字节
char ip[NET_IP_STR_LEN]; //46字节
uint16_t port; //2字节
uint16_t cport; //2字节
uint16_t flags; //2字节
uint32_t notused1; //4字节
} clusterMsgDataGossip;
```
其中CLUSTER\_NAMELEN和NET\_IP\_STR\_LEN的值分别是40和46分别表示nodename和ip这两个字节数组的长度是40字节和46字节我们再把结构体中其它信息的大小加起来就可以得到一个Gossip消息的大小了即104字节。
每个实例在发送一个Gossip消息时除了会传递自身的状态信息默认还会传递集群十分之一实例的状态信息。
所以对于一个包含了1000个实例的集群来说每个实例发送一个PING消息时会包含100个实例的状态信息总的数据量是 10400字节再加上发送实例自身的信息一个Gossip消息大约是10KB。
此外为了让Slot映射表能够在不同实例间传播PING消息中还带有一个长度为 16,384 bit 的 Bitmap这个Bitmap的每一位对应了一个Slot如果某一位为1就表示这个Slot属于当前实例。这个Bitmap大小换算成字节后是2KB。我们把实例状态信息和Slot分配信息相加就可以得到一个PING消息的大小了大约是12KB。
PONG消息和PING消息的内容一样所以它的大小大约是12KB。每个实例发送了PING消息后还会收到返回的PONG消息两个消息加起来有24KB。
虽然从绝对值上来看24KB并不算很大但是如果实例正常处理的单个请求只有几KB的话那么实例为了维护集群状态一致传输的PING/PONG消息就要比单个业务请求大了。而且每个实例都会给其它实例发送PING/PONG消息。随着集群规模增加这些心跳消息的数量也会越多会占据一部分集群的网络通信带宽进而会降低集群服务正常客户端请求的吞吐量。
除了心跳消息大小会影响到通信开销如果实例间通信非常频繁也会导致集群网络带宽被频繁占用。那么Redis Cluster中实例的通信频率是什么样的呢
### 实例间通信频率
Redis Cluster的实例启动后默认会每秒从本地的实例列表中随机选出5个实例再从这5个实例中找出一个最久没有通信的实例把PING消息发送给该实例。这是实例周期性发送PING消息的基本做法。
但是这里有一个问题实例选出来的这个最久没有通信的实例毕竟是从随机选出的5个实例中挑选的这并不能保证这个实例就一定是整个集群中最久没有通信的实例。
所以,这有可能会出现,**有些实例一直没有被发送PING消息导致它们维护的集群状态已经过期了**。
为了避免这种情况Redis Cluster的实例会按照每100ms一次的频率扫描本地的实例列表如果发现有实例最近一次接收 PONG消息的时间已经大于配置项 cluster-node-timeout的一半了cluster-node-timeout/2就会立刻给该实例发送 PING消息更新这个实例上的集群状态信息。
当集群规模扩大之后因为网络拥塞或是不同服务器间的流量竞争会导致实例间的网络通信延迟增加。如果有部分实例无法收到其它实例发送的PONG消息就会引起实例之间频繁地发送PING消息这又会对集群网络通信带来额外的开销了。
我们来总结下单实例每秒会发送的PING消息数量如下所示
> PING消息发送数量 = 1 + 10 \* 实例数最近一次接收PONG消息的时间超出cluster-node-timeout/2
其中1是指单实例常规按照每1秒发送一个PING消息10是指每1秒内实例会执行10次检查每次检查后会给PONG消息超时的实例发送消息。
我来借助一个例子带你分析一下在这种通信频率下PING消息占用集群带宽的情况。
假设单个实例检测发现每100毫秒有10个实例的PONG消息接收超时那么这个实例每秒就会发送101个PING消息约占1.2MB/s带宽。如果集群中有30个实例按照这种频率发送消息就会占用36MB/s带宽这就会挤占集群中用于服务正常请求的带宽。
所以,我们要想办法降低实例间的通信开销,那该怎么做呢?
## 如何降低实例间的通信开销?
为了降低实例间的通信开销从原理上说我们可以减小实例传输的消息大小PING/PONG消息、Slot分配信息但是因为集群实例依赖PING、PONG消息和Slot分配信息来维持集群状态的统一一旦减小了传递的消息大小就会导致实例间的通信信息减少不利于集群维护所以我们不能采用这种方式。
那么,我们能不能降低实例间发送消息的频率呢?我们先来分析一下。
经过刚才的学习,我们现在知道,实例间发送消息的频率有两个。
* 每个实例每1秒发送一条PING消息。这个频率不算高如果再降低该频率的话集群中各实例的状态可能就没办法及时传播了。
* 每个实例每100毫秒会做一次检测给PONG消息接收超过cluster-node-timeout/2的节点发送PING消息。实例按照每100毫秒进行检测的频率是Redis实例默认的周期性检查任务的统一频率我们一般不需要修改它。
那么就只有cluster-node-timeout这个配置项可以修改了。
配置项cluster-node-timeout定义了集群实例被判断为故障的心跳超时时间默认是15秒。如果cluster-node-timeout值比较小那么在大规模集群中就会比较频繁地出现PONG消息接收超时的情况从而导致实例每秒要执行10次“给PONG消息超时的实例发送PING消息”这个操作。
所以为了避免过多的心跳消息挤占集群带宽我们可以调大cluster-node-timeout值比如说调大到20秒或25秒。这样一来 PONG消息接收超时的情况就会有所缓解单实例也不用频繁地每秒执行10次心跳发送操作了。
当然我们也不要把cluster-node-timeout调得太大否则如果实例真的发生了故障我们就需要等待cluster-node-timeout时长后才能检测出这个故障这又会导致实际的故障恢复时间被延长会影响到集群服务的正常使用。
为了验证调整cluster-node-timeout值后是否能减少心跳消息占用的集群网络带宽我给你提个小建议**你可以在调整cluster-node-timeout值的前后使用tcpdump命令抓取实例发送心跳信息网络包的情况**。
例如执行下面的命令后我们可以抓取到192.168.10.3机器上的实例从16379端口发送的心跳网络包并把网络包的内容保存到r1.cap文件中
```
tcpdump host 192.168.10.3 port 16379 -i 网卡名 -w /tmp/r1.cap
```
通过分析网络包的数量和大小就可以判断调整cluster-node-timeout值前后心跳消息占用的带宽情况了。
## 小结
这节课我向你介绍了Redis Cluster实例间以Gossip协议进行通信的机制。Redis Cluster运行时各实例间需要通过PING、PONG消息进行信息交换这些心跳消息包含了当前实例和部分其它实例的状态信息以及Slot分配信息。这种通信机制有助于Redis Cluster中的所有实例都拥有完整的集群状态信息。
但是随着集群规模的增加实例间的通信量也会增加。如果我们盲目地对Redis Cluster进行扩容就可能会遇到集群性能变慢的情况。这是因为集群中大规模的实例间心跳消息会挤占集群处理正常请求的带宽。而且有些实例可能因为网络拥塞导致无法及时收到PONG消息每个实例在运行时会周期性地每秒10次检测是否有这种情况发生一旦发生就会立即给这些PONG消息超时的实例发送心跳消息。集群规模越大网络拥塞的概率就越高相应的PONG消息超时的发生概率就越高这就会导致集群中有大量的心跳消息影响集群服务正常请求。
最后我也给你一个小建议虽然我们可以通过调整cluster-node-timeout配置项减少心跳消息的占用带宽情况但是在实际应用中如果不是特别需要大容量集群我建议你把Redis Cluster 的规模控制在400~500个实例。
假设单个实例每秒能支撑8万请求操作8万QPS每个主实例配置1个从实例那么400~ 500个实例可支持 1600万~2000万QPS200/250个主实例\*8万QPS=1600/2000万QPS这个吞吐量性能可以满足不少业务应用的需求。
## 每课一问
按照惯例我给你提个小问题如果我们采用跟Codis保存Slot分配信息相类似的方法把集群实例状态信息和Slot分配信息保存在第三方的存储系统上例如Zookeeper这种方法会对集群规模产生什么影响吗
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事。我们下节课见。