Redis有数万个实例,每天百万亿次Redis访问,支撑了几乎所有的产品线和生态链。
Redis部署在物理机上,不同业务间没有做资源隔离:
1、当 机器宕机 或 网络抖动,导致Redis节点下线,需要人工介入运维处理,运维成本高。
2、没有做CPU资源隔离,当slave节点打RDB或流量激增导致节点QPS高,会造成节点CPU使用率增加,导致无法预测的延迟增加,影响其他业务。
3、Redis分片采用Redis Cluster协议,集群自主分片。问题:应用开发人员需要一定程度了解Redis Cluster,同事需要使用智能客户端访问Redis Cluster。这些智能客户端配置参数繁多,应用开发人员不易掌握。
为了提升资源利用率,多个业务混合部署,当使用物理机时,混步的不同业务间会互相影响。一旦出现问题,不同业务间会互相影响,定位及解决问题需要花费一定时间,代价较高。
用K8S后,可以做资源隔离,不同业务不会互相影响。
部署到物理机时,需要人工介入:查找空余资源的机器,再手动修改配置文件,部署节点,在使用工具创建集群,新集群初始化需要1~2小时。
K8S通过StatefulSet部署K8S集群,部署新集群只需要几分钟,大大提高运维效率,降低了运维成本。
客户端通过LVS(LinuxVirtualServer,一个虚拟的服务器集群系统)的VIP统一接入,通过Redis Proxy转发服务请求到Redis Cluster集群。这里引入了Redis Proxy来转发请求:
Redis部署为StatefulSet, 作为有状态服务。
对于持久化存储部分,如RDB/AOF,可持久化到分布式存储中,重启后读取,性能会低于本地磁盘,但好在RDB/AOF是异步的,延迟一点可接受(延迟为100~200ms)
Proxy作为无状态服务,通过LB对外提供服务,很容器做到动态扩缩容。同时,为Proxy开发了动态切换后端Redis Cluster的功能,用于实现在线添加和切换Redis Cluster。
可以利用K8S对Proxy自动扩缩容
Redis Cluster的Redis客户端,需要配置集群部分节点的IP和Port, 用于客户端重启时查找Redis Cluster的入口。这对于物理机不是问题,物理机重启后IP和Port不会变化。
但在K8S上的Pod, 重启后IP会变化,这样客户端就可能找不到Redis Cluster的入口了。
解决方案:在客户端和Redis Cluster之间加上Proxy,可对客户端屏蔽调Redis Cluster的信息,由Proxy动态感知Redis CLuster上的节点变化,客户端只需要将LVS的IP:Port最欧文入口,请求转发到Proxy上即可,这样就可以像使用单机版Redis一样使用Redis Cluster集群了,也不需要Redis只能客户端。
在Redis 6.0版本之前,Redis都是单线程处理大部分任务的。当Redis节点连接较高时,Redis需要消耗大量的CPU资源处理这些连接,导致时延较高。有了Proxy智慧,大量连接都在Proxy上,而Proxy跟Redis实例之间只保持很少的连接,这样降低了Redis的负担,避免了连接增加而导致Redis时延升高。
集群迁移时,当前物理机的方案为:
但有了Proxy,可以将后端的创建、同步、切换集群 对客户端屏蔽掉。新老集群同步完成后,由Proxy切换地址,使客户端无感知。
Redis通过AUTH来做鉴权,客户端直连Redis,密码在客户端保存,存在安全风险。如果使用proxy, 密码保存在proxy,且proxy可限制flushdb, config set等操作,避免了客户端误用的情况。
proxy上增加了高危操作的日志保存功能,可以在不影响性能前提下提供审计能力。
多一跳会增加0.2~0.3ms的时延,不过通常这对业务来说是可以接受的。
proxy部署在k8s上,重启IP也会变化,通过LVS感知proxy IP变化,可动态把LVS的流量且到重启后的proxy上。
目前小米部署Redis实例,是通过端口来区分的,并且下线的端口不能服用,也就是说每个Redis实例都要有唯一的端口号,最多能用65535各。
通过K8S,每个Redis实例都有独立的IP,不存在端口数的限制了。
用户不必使用智能客户端,可降低客户端的负载。因为智能客户端需要再客户端对key进行hash以确定发送到哪个redis节点,在qps比较高的情况下回消耗客户端机器的CPU资源。
Proxy支持动态添加和切换Redis Cluster,这样Redis Cluster在集群升级和扩容切换时,业务端无感知。
举例说明:业务方使用了30个节点的Redis Cluster集群,当需要扩容2倍节点时,在原来的物理机上过程如下:
虽然Redis Cluster支持在线扩容,但扩容过程中slots迁移会对线上服务造成影响,同时迁移时间不可控,所以不采用线上扩容。
新的k8s集群扩容过程如下:
整个过程,业务无感知。
集群升级也很方便:如果业务方能接受一定的延迟毛刺,可在低峰时段通过StatefulSet滚动升级方式来实现;如果业务对延迟有要求,可以通过创建新集群迁移数据的方式实现。
通过k8s自带的资源隔离能力,实现和其他不同类型的应用混步,在提高资源利用率的同时,也能保证服务稳定性。
K8s的Pod遇到问题重启时,由于重启速度过快,会在Redis Cluster集群发现并切主前,将Pod重启。如果Pod上的Redis是slave,不会有任何影响。如果是master,并且没有AOF,重启后原来内存中的数据被清空,Redis会reload之前存储的RDB文件,但RDB文件并不是实时的数据。之后slave也会跟着把自己的数据同步成之前的RDB文件中的数据镜像,会造成部分数据丢失。
StatefulSet是有状态服务,部署的Pod名是固定格式(StatefulSet名+编号)。我们在初始化Redis Cluster时,将相邻编号的Pod设置为主从关系,在重启Pod时,通过Pod名确定他的slave,在重启Poad前向从节点发送cluster failover命令,强制将或者的从节点且住,这样在重启后,该节点会自动以从节点方式接入集群。
Proxy的Pod事通过LVS实现负载均衡的,LVS读以后端IP:Port的映射生效有一定时延,Proxy节点突然下线,会导致部分连接丢失。
对于正常的Proxy Pod下线,例如集群缩容、滚动更新proxy版本,及其他k8s可控的pod下线,在pod下线前,会发消息给LVS,并等待171秒,这段时间足够LVS将这个pod的流量逐渐切到其他pod上,对业务无感知。
k8s原生的StatefulSet不能完全满足Redis Cluster部署的要求:
1、Redis Cluster不允许同为主备关系的节点部署在同一台机器上。
2、Redis Cluster不允许机器超过一半的主节点失效。因为超过一半主节点失效,就无法有足够的节点投票来满足gossip协议的要求。因为Redis Cluster的主备是可能随时切换的,我们无法避免同一个机器上的所有节点都是主节点,所以在部署时,不能允许集群中超过1/4的节点部署在一台机器上。
为了满足上面的要求,原生StatefuleSet可以通过anti-affinity功能来保证相同集群在同一机器上只部署一个节点,但这样集群利用率很低。
因此,我们开发了基于StatefuleSet的CRD:RedisStatefulSet, 会采用多种策略部署Redis节点。同时,还在RedisStatefuleSet中接入了一些Redis管理功能。