twemproxy 支持一个 proxy 实例同时代理多个分布式集群(server pools),每个集群使用不同的网络端口实现数据流的隔离,下图中 port1 应用于 cluster1 代理,port2 应用于 cluster2 代理:
今天要介绍的是 twemproxy 对 redis 节点高可用的支持,拿上图的其中一个分布式集群进行示例,逻辑结构如下:
客户端 client 流入的请求,在 proxy 上进行路由分片,然后转发到后端的 redis 节点上存储或者读取。事实上,大家已经注意到后端的 redis 节点只有一个点,在云计算环境下,是很容易掉线的。按 twemproxy 的设计,它可以自动识别失效节点并将其剔除,同时落在原来节点上的请求会分摊到其余的节点上。这是分布式缓存系统的一种通用做法,但需要忍受这个失效节点上的数据丢失,这种情况是否可以接受?
在业内,redis 虽然被定位为缓存系统,但事实上,无论哪种业务场景(我们接触过的)都不愿意接受节点掉线带来的数据丢失,因为那样对他们系统的影响实在太大了,更有甚者在压力大的时候引起后端数据库被击穿的风险。所以,我们打算改造 twemproxy,前后总共有几个版本,下面分享给各位的是我们目前线上在跑的版本。
在上图的基础上,我们增加了与 manager 交互的模块、增加了与 sentinel(redis-sentinel)交互的模块,修改了 redis 连接管理模块,图中三个红色虚线框所示:
增加连接 manager 的客户端交互模块,用于发送心跳消息,从心跳应答包里获取 group 名称列表和 sentinel 列表(IP/PORT信息),即整个分布式集群的配置信息,其中心跳消息带有版本信息,发送间隔可配置。
增加与 sentinel 客户端交互模块(IP/PORT信息来自于 manager),发送 group 名称给 sentinel 获取 redis 主节点的 IP/PORT 信息,一个 group 对应一个主节点。取到所有主节点后,订阅主从切换频道,获取切换消息用于触发 proxy 和主节点间的连接切换。这里需要解析 sentinel 的响应消息,会比较繁琐一些。当 proxy 开始与 sentinel 节点的交互过程,需要启动定时器,用以控制交互结果,当定时器超时交互未结束(或者 proxy 未正常工作),proxy 将主动切换到下一个 sentinel 节点,并启动新的交互过程。考虑到 proxy 与 sentinel 之间网络连接的重要性(连接假死,proxy 收不到主从切换消息,不能正常切换),增加了定时心跳机制,确保这条 TCP 链路的可用性。
原先 redis 节点的 IP/PORT 信息来自于静态配置文件,是固定的,而改造以后这些信息是从 sentinel 节点获取。为了确保获取到的 IP/PORT 信息的准确性,需要向 IP/PORT 对应的节点验证是否是主节点的逻辑,只有返回确认是主节点,才认为是合法的。整个过程,按官方指导实现,不存在漏洞。
为了清晰的描述 proxy 的内部处理逻辑,制作了如下消息流图:
绿色为业务通道,用于透传业务层数据;
紫色为命令通道(红线的细化),用于初始化和节点主从切换:
箭头1:manager heartbeat req;
箭头2:manager heartbeat rsp;
箭头3:sentinel get-master-addr-by-name req;
箭头4:sentinel get-master-addr-by-name rsp;
箭头5:redis auth & role req;
箭头6:redis auth & role rsp;
箭头7:sentinel psubscribe +switch-master req;
箭头8:sentinel psubscribe +switch-master rsp;
箭头9:sentinel pmessage;
命令通道命令顺序按数字1-8进行,7/8是proxy与sentinel的心跳消息,9是主从切换消息;
在 sentinel 节点切换的过程中,存在 proxy 正在对外提供业务服务的状态,这时候正在处理的数据将继续处理,不会受到影响,而新接入的客户端连接将会被拒绝,已有的客户端连接上的新的业务请求数据也会被拒绝。sentinel 节点切换,对系统的影响是毫秒级别,前面的设计对业务系统来讲会显得比较友好、不那么粗鲁;
而 redis 节点的主从切换对系统的影响,主要集中在 proxy 发现主节点异常到 sentinel 集群做出主从切换这个过程,这段时间内落在该节点上的业务都将失败,而该时间段的长度主要依赖在 sentinel 节点上的 down-after-milliseconds 配置字段。
作为代理中间件,支持 pipeline 的能力有限,容易产生消息积压,导致客户端大量超时,所以慎用pipeline功能;
高负荷下容易吃内存,struct msg 和 struct mbuf 对象会被大量缓存在进程内(内存池化);
zero copy,对于多个连续请求(TCP粘包)进行拆分,拷贝是无法避免的,但是有优化空间;