干货 | 携程Redis容器化实践
作者简介
李剑,携程CIS资深软件工程师。加入携程之前主要从事音视频流媒体的开发,目前主要负责Redis和Mysql容器化和服务化的研发。
本文来自李剑在“2018携程技术峰会”上的分享。
携程的Redis使用规模有200T+,并且每天有百万亿次的访问频率,如此大规模的Redis容器化对于我们来说是个不小的挑战,本文分享携程Redis容器化落地的一些实践经验。
一、背景
携程大部分应用是基于CRedis客户端通过集群来访问到实际的Redis的实例,集群是访问Redis的基本单位,多个集群对应一个Pool,一个Pool对应一个Group,每个Group对应一个或多个实例,Key是通过一致性hash散列到每个Group上,集群拓扑图如截图所示。
这个图里面我们可以看到集群,Pool,Group还有里面的实例,这是携程Redis一个比较常见的拓扑图,如下图:
1.1 为什么要容器化
标准化和自动化
Redis之前是直接部署在物理机上,而DBA是根据物理机上设定的Redis的版本来选择需要部署的物理机,携程的各个版本的Redis非常分散而且不容易维护,如下图所示,容器天然支持标准化,另外容器基于K8S自动化部署的效率,根据我们估算,相比人工部署提高了59倍。
规模化
有别于社区的方案比如官方Redis Cluster或代理方案而言,携程的技术演进方案需要对大的实例进行分拆 (内部称为CRedis水平扩容),实例分拆后,单个实例的内存小了,QPS降低,单个实例挂掉的影响小很多,可以说是利国利民的项目,但会带来一个问题,实例数急剧膨胀。容器化后我们能对分拆后的实例更好地管理和运维。
另外,分拆过程中需要大量中间状态的实例Buffer作为过渡,比如一对60G的实例分拆为5G,中间状态的Buffer需要24个60G的实例,纯人工分拆异常艰难,而且容易出错,依靠容器自动调度生成实例会极大降低DBA分拆时的心智负担,极大提升了分拆的效率并减少出错的概率。
提高资源利用率
借助于容器化和上层的K8S的编排系统,我们很轻易的就可以做到资源利用率的提升,至于怎么做到的,后面细节部分会涉及。
1.2 能不能容器化
既然Redis容器化后好处这么多,那么Redis能不能容器化呢?对比测试最能说明问题。
实际上我们在容器化前做了很多测试,甚至因为测试模式的细微差别在各个部门之间还有过长时间的争论,但最终下面这几张图的数据获得了大家的一致认可,容器化才得以继续推广下去。
我们A/B对比测试都是基于相同硬件的容器和物理机,不挂slave,图上我们可以看到,Redis的响应相比物理机要慢一点,QPS也能看到差距很小,这些差异主要是容器化后经过多个虚拟网卡带来的性能损失。
这第三张图就更明显了,这是我们测试对比生产实际物理机的流量对比,我们测试的流量远高于生产实际运行的单台物理机的流量。
因此总结下来就是,容器与物理机的性能有细微的差别,大概5-10%,并且携程的使用场景Redis完全可以容器化。
二、架构和细节
2.1 总体架构
以上介绍无非是容器化前的一些调研和可行性分析工作。
具体的架构如下图所示,首先最上层的是运维和治理工具CRedis和Rat,这个在携程内部是属于框架和DBA两个部门,CRedis不但提供应用访问Redis的客户端,本身也做CMS的工作,存储Redis实例最基本的元数据。
PaaS层为Credis/Rat提供统一的Redis Group/实例的创建删除接口,下面的Redis微服务提供实例申请具体的调度策略,基础设施有很多,这里其实只列举了一部分比较重要的,如网络相关的ovs和neutron,与磁盘配额相关的quota,以及监控相关的telegraf等。xpipe是携程内部的跨IDC的DR方案,sentinel就是官方的哨兵。
2.2 容器化遇到的一些问题
在我们容器化方案落地前遇到过一些具体的问题,例如:
1)Redis实际上是被应用直连的,我们需要IP和宿主机固定,并且master/slave不能在一台宿主机上。
2)部署之前是在物理机上,通过端口来区分不同的实例,所有的监控通过端口来区分。
3)重启实例Redis.conf文件配置不能丢失,这个在容器之前甚至不算需求,但放在容器上就有点麻烦。
4)Master挂了不希望K8S立刻把它拉起来,希望哨兵来感知到它,因为K8S如果在哨兵感知前拉起了它,导致哨兵还没切换Master/Slave,Master就活过来并且数据都丢失,这时候一同步到Slave上数据也全没有了,等于执行了一个清空操作,这对于业务和DBA来说是不能接受的。
5)实例几乎没有任何的内存控制,就是说实例不管写多大,都是得让maxmemory一直加上去,一直加到必须迁移走开始,再把实例迁移走,而不能控制maxmemory,让应用那边直接写报错。这个是最大的问题,决定了容器化是否能进行下去。如果不控制内存,K8S的某些功能形同虚设,但如果控制内存,与携程之前的运维习惯和流程不太相符,业务也无法接受。
以上都是我们遇到的一些主要问题,有些K8S的原生策略就可以很好地支持,有些则不行,需自研策略来解决。
2.3 K8S原生策略
首先,我们的容器基于K8S的Statefulset,这个几乎没有任何疑问,毕竟Redis是有状态的。
其次,nodeAffinity保证了调度到指定标签的宿主机,podAntiAffinity保证同一个Statefulset的Pod不调度到同一台宿主机上,toleations保证可以调度到taint的宿主机上,而该宿主机不会被其他资源类型调度到,如Mysql,App等,也就是说宿主机被Redis独占,只能调度Redis的实例。
上面提到的分拆其实也是基于nodeAffinity,podAntiAffinity等特性,我们内部划分出一块虚拟区域叫slaughterhouse,专门用于分拆,分拆完成再迁到常规区域。
2.4 自研策略
宿主机固定,这个是自研的调度sticky-scheduler来提供支持,如下图所示,在创建实例的时候会看annontation有没有对应host,有的话直接会跳过调度固化到该宿主机上,如果没有则进入默认的调度宿主机的流程。
虽然Redis对磁盘需求不多,但我们还是得防止log或rdb文件过大将磁盘撑爆,自研的chostpath和cemptydir都是基于xfs的quotas很好的支持磁盘配额,并且我们将Redis.conf和data目录挂载出来,保证重启容器后配置文件不丢失,还可保证容器重启后可以读rdb数据。
比如我们在做风险操作升级kubelet时候可能会引起相关的Pod重启,但我们先对相关的Redis bgsave下,哪怕重启pod也会读取对应的rdb数据,不会导致完全没有数据的尴尬场面。
监控方面,之前Redis部署在物理机上,通过端口来区分不同的实例,所有的监控通过端口来区分,但容器化后每个Pod都有一个IP,自然监控策略要变。
我们的方案是每个Pod两个容器,一个是Redis本身的实例,一个是监控程序telegraf,每60秒采集一次数据发送到公司的统一监控平台Hickwall,所有的telegraf脚本固化在物理机上,一旦修改方便统一的推送,并且对于Redis实例没有任何影响。
实践证明这种监控方案最为理想,比如有一次我们生产迁移集群后,DBA需要集群的聚合页面,也就是把所有的实例聚合在一起的按集群维度查看的页面,我们修改telegraf的脚本将集群的信息随着实例推送过去立刻就能显示在监控页面上,非常方便。
下面两张图清晰地展示出容器的监控页面和物理机完全没有区别。
为了解决上文提到的Master挂了不希望K8S立刻把它拉起来,希望哨兵来感知到它,我们用Supervisord作为容器的1号进程。当Redis挂了,Supervisord默认不会拉起它,但容器还是活的,Redis进程却不存在了,想让Redis活过来很简单,删除掉Pod即可。K8S会自动重新拉起它。
最后再来看看最困难的,实例几乎没有任何的内存限制。实际上在容器上我们对CPU和内存也几乎没有限制。
CPU不限制主要是几个方面原因,首先,12核的机器上CPU quota/period =12, 按理是占满了整个机器,但压测时CPU居然有throttle,这明显不符合我们的客观直觉,我们怀疑Linux的cfs是有问题的,而且很神奇的是我们设置一个很大的quota值后,也就是将CPU限额设置到50核,throttle消失了。
其次Redis是单线程,最多能用一个CPU,如果一个CPU跑一个Redis实例,肯定没问题,实际上我们设置两个实例分配到一个核也是完全可行的。
最后一个原因也是最主要的,Redis在物理机上运行是没有任何CPU隔离的。基于上面三个原因,我们让CPU超分。
关于内存超分,下面这张图清晰地说明了问题所在,对于只有一个100G的宿主机,只要放上2个实例,每个实例50G,它的内存就超了。内存超分好处很多,比如物理机迁移过来很平滑,用户也很能接受,运维工具几乎不需要修改就能套上去,但是,超分大法好,但OOM了怎么办?
方案是不让OOM发生,只要策略合适,这显然是可以做到的,在说到杜绝OOM的策略之前,先看下普通的调度策略。
我们在调度时对集群重要性进行了划分,主要分为以下几种:
1、基础集群,比如账号相关的,登陆相关的,虽然订单无关但比订单相关都重要。
2、接入XPIPE,订单相关的。
3、没有接入XPIPE,订单相关的。
4、订单无关但相对重要的。
5、既订单无关的又不重要的。
这样划分后,我们就可以很方便地让集群根据重要性按机器的高中低配来调度,并且让集群是否在多Region上打散。为了方便理解,这里一个Region可以简单等同于一个K8S集群。
单个Region如下图,一个Statefulset两个Pod分别是Master/Slave,每个Pod里面有两个容器,一个是Redis本身,一个是监控程序telegraf,部署在两个Host上。
多个Region如下图,这时候,其实是有2个Statefulset,这种方案可以扩散到更多Region,这样哪怕是某个K8S集群挂了,重要的集群仍然有对外提供服务的能力。
介绍完一般的调度策略后,接着说上文提到的杜绝OOM的策略。首先,调度之前,对于不同配置的宿主机限定不通的Pod数量,此外设定10%的占位策略,如下图所示,并且设定Pod的request == maxmemory。
调度中,我们会基于宿主机实际的可用内存进行打分,在K8S默认调度后,优选时我们会将实际剩余内存的打分赋值一个非常高的权重,当然基于其他策略的调度比如说CPU,网络流量之类我们也在研究,但目前最优先考虑的是实际剩余的物理内存。
以上这些策略可以杜绝大部分OOM,但还不够,因为Redis后续还是会自然增长的,所以在运维过程中,我们会有Job定时轮询宿主机,看可用内存和上面的Pod分配是否合理,对于不合理的Pod,Job会自动触发迁移任务,将一些Pod迁移到内存更空的机器上去,以达到宿主机整体可用内存方差最小。
还有一些其他的调度后的策略,比如动态调整Redis实例的HZ,我们曾遇到一个情况就是,在物理机上跑着一个实例大小都是10多个G,但跑到容器上后2天增加了20多个G。
我们排查后发现Redis的HZ值设置的过小,导致大量过期的Key没时间来得及清理,清理完成后发现,usedmemory是下来了,但rss还保持稳定,也就是碎片率很高,所以我们会动态打开自动碎片整理,整理一次完成后再关闭它,因为同时打开,消耗的CPU过高,目前情况下还不是很适合。
最后还有个保底的,基于宿主机内存告警,一般设置为80%即可,这种保底策略到目前为止也就触发过一次。
小结下,Redis跑在容器上,尤其在生产上大规模部署,需要多个组件共同协作才能达成。其次,携程的现状决定了我们必须超分,那么超分后如何不OOM是关键,我们从调度过程前中后容器层面和Redis层面分别都有相应的策略,调度上的闭环不但保证了Redis在容器上的平稳运行,而且资源利用率(如下图所示)也做到了非常大的提升。
三、一些坑
最后再分享一下实践过程中的一些坑,这些坑其实本身不是Redis的问题,但都是在Redis容器化过程中发现的。
3.1 System Load有规模毛刺
首先是System Load有规模毛刺,每7小时一次,我们可以看到监控上,增加Pod后毛刺上升,但看上去跟CPU利用率没什么关系。
降低Pod数量,毛刺减小,但还存在,所以跟Pod数量正相关,
后来我们发现是telegraf监控脚本的问题。所有瞬间会产生很多进程的Job都会导致System Load升高。
对于Redis宿主机load异常情况,主要是因为监控程序每1min生成很多进程采集一次数据, System Load采集则是每5.001s采集一次,当telegraf的第一次采集点命中System Load采集点后,第二次则需要5s*(5/0.001)=25000s,导致Load有规律每7小时飙高。
我们修改telegraf中的collection_jitter值,用来设置一个随机的抖动来控制telegraf采集前的休眠时间,确保瞬间不会爆发上百个进程,修改后,毛刺消失了,如下图所示:
3.2 Slowlog的异常
其次是Slowlog的异常,该问题根因在于4.9-4.13的内核的一个bug,会导致skylake服务器的时钟变慢,而该时钟不断地被NTP修正,所以导致Slowlog的两次打点时间过长,升级内核到4.14即解决该问题。
详细的分析可见这篇文章:携程一次Redis迁移容器后Slowlog“异常”分析
3.3 Xfs bugs
还有一个是Xfs的bugs,Xfs我们发现的有两个比较严重的问题,第一个是字节对齐的问题,这个比较隐蔽,简答地说就是内核态的Xfs header跟用户态的Xfs header里面定义不同,导致内核在写Xfs的时候会越界。下图中就是很明显的症状,我们升级4.14的内核对内存对齐打了patch解决了该问题。
Xfs第二个问题是xfsaild进入D状态缓慢导致宿主机大量D状态进程和僵尸进程,最终导致宿主机僵死,典型的现象如下图。
这个现象在4.10内核发现很多次,并且猜测与khugepaged有关系,我们升级到4.14并Backport 4.15-4.19的Xfs bugfix,压测问题还是存在,但比4.10要难以复现,在free内存超过3G后不会再复现。目前升级到4.14.67 Backport的新内核实际运行中还没出现这个问题。
2018携程技术峰会PPT和视频可见这里。
【推荐阅读】