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