携程 Redis 容器化实践

栏目: 数据库 · 发布时间: 7年前

内容简介:携程大部分应用是基于CRedis客户端通过集群来访问到实际的Redis的实例,集群是访问Redis的基本单位,多个集群对应一个Pool,一个Pool对应一个Group,每个Group对应一个或多个实例,Key是通过一致性hash散列到每个Group上,集群拓扑图如截图所示。这个图里面我们可以看到集群,Pool,Group还有里面的实例,这是携程Redis一个比较常见的拓扑图,如下图:

作者简介

李剑,携程CIS资深软件工程师。加入携程之前主要从事音视频流媒体的开发,目前主要负责 RedisMysql 容器化和服务化的研发。

本文来自 李剑 在“ 2018携程技术峰会 ”上的分享。

携程的Redis使用 规模有200T+,并且每天有百万亿次的访问频率,如此大规模的Redis容器化对于我们来说是个不小的挑战,本文分享携程Redis容器化落地的一些实践经验。

一、背景

携程大部分应用是基于CRedis客户端通过集群来访问到实际的Redis的实例,集群是访问Redis的基本单位,多个集群对应一个Pool,一个Pool对应一个Group,每个Group对应一个或多个实例,Key是通过一致性hash散列到每个Group上,集群拓扑图如截图所示。

这个图里面我们可以看到集群,Pool,Group还有里面的实例,这是携程Redis一个比较常见的拓扑图,如下图:

携程 Redis 容器化实践

1.1 为什么要容器化

  • 标准化和自动化

Redis之前是直接部署在物理机上,而DBA是根据物理机上设定的Redis的版本来选择需要部署的物理机,携程的各个版本的Redis非常分散而且不容易维护,如下图所示,容器天然支持标准化,另外容器基于K8S自动化部署的效率,根据我们估算,相比人工部署提高了59倍。

携程 Redis 容器化实践

  • 规模化

有别于社区的方案比如官方Redis Cluster或代理方案而言,携程的技术演进方案需要对大的实例进行分拆 (内部称为CRedis水平扩容),实例分拆后,单个实例的内存小了,QPS降低,单个实例挂掉的影响小很多,可以说是利国利民的项目,但会带来一个问题,实例数急剧膨胀。容器化后我们能对分拆后的实例更好地管理和运维。

另外,分拆过程中需要大量中间状态的实例Buffer作为过渡,比如一对60G的实例分拆为5G,中间状态的Buffer需要24个60G的实例,纯人工分拆异常艰难,而且容易出错,依靠容器自动调度生成实例会极大降低DBA分拆时的心智负担,极大提升了分拆的效率并减少出错的概率。

携程 Redis 容器化实践

  • 提高资源利用率

借助于容器化和上层的K8S的编排系统,我们很轻易的就可以做到资源利用率的提升,至于怎么做到的,后面细节部分会涉及。

1.2 能不能容器化

既然Redis容器化后好处这么多,那么Redis能不能容器化呢?对比测试最能说明问题。

实际上我们在容器化前做了很多测试,甚至因为测试模式的细微差别在各个部门之间还有过长时间的争论,但最终下面这几张图的数据获得了大家的一致认可,容器化才得以继续推广下去。

我们A/B对比测试都是基于相同硬件的容器和物理机,不挂slave,图上我们可以看到,Redis的响应相比物理机要慢一点,QPS也能看到差距很小,这些差异主要是容器化后经过多个虚拟网卡带来的性能损失。

携程 Redis 容器化实践

携程 Redis 容器化实践

这第三张图就更明显了,这是我们测试对比生产实际物理机的流量对比,我们测试的流量远高于生产实际运行的单台物理机的流量。

携程 Redis 容器化实践

因此总结下来就是,容器与物理机的性能有细微的差别,大概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就是官方的哨兵。

携程 Redis 容器化实践

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 容器化实践

虽然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的脚本将集群的信息随着实例推送过去立刻就能显示在监控页面上,非常方便。

下面两张图清晰地展示出容器的监控页面和物理机完全没有区别。

携程 Redis 容器化实践

携程 Redis 容器化实践

为了解决上文提到的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了怎么办?

携程 Redis 容器化实践

方案是不让OOM发生,只要策略合适,这显然是可以做到的,在说到杜绝OOM的策略之前,先看下普通的调度策略。

我们在调度时对集群重要性进行了划分,主要分为以下几种:

1、基础集群,比如账号相关的,登陆相关的,虽然订单无关但比订单相关都重要。

2、接入XPIPE,订单相关的。

3、没有接入XPIPE,订单相关的。

4、订单无关但相对重要的。

5、既订单无关的又不重要的。

这样划分后,我们就可以很方便地让集群根据重要性按机器的高中低配来调度,并且让集群是否在多Region上打散。为了方便理解,这里一个Region可以简单等同于一个K8S集群。

单个Region如下图,一个Statefulset两个Pod分别是Master/Slave,每个Pod里面有两个容器,一个是Redis本身,一个是监控程序telegraf,部署在两个Host上。

携程 Redis 容器化实践

多个Region如下图,这时候,其实是有2个Statefulset,这种方案可以扩散到更多Region,这样哪怕是某个K8S集群挂了,重要的集群仍然有对外提供服务的能力。

携程 Redis 容器化实践

介绍完一般的调度策略后,接着说上文提到的杜绝OOM的策略。首先,调度之前,对于不同配置的宿主机限定不通的Pod数量,此外设定10%的占位策略,如下图所示,并且设定Pod的request == maxmemory。

携程 Redis 容器化实践

调度中,我们会基于宿主机实际的可用内存进行打分,在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的问题,但都是在Redis容器化过程中发现的。

3.1 System Load有规模毛刺

首先是System Load有规模毛刺,每7小时一次,我们可以看到监控上,增加Pod后毛刺上升,但看上去跟CPU利用率没什么关系。

携程 Redis 容器化实践

降低Pod数量,毛刺减小,但还存在,所以跟Pod数量正相关,

携程 Redis 容器化实践

后来我们发现是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采集前的休眠时间,确保瞬间不会爆发上百个进程,修改后,毛刺消失了,如下图所示:

携程 Redis 容器化实践

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解决了该问题。

携程 Redis 容器化实践

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和视频可见 这里

【推荐阅读】


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Types and Programming Languages

Types and Programming Languages

Benjamin C. Pierce / The MIT Press / 2002-2-1 / USD 95.00

A type system is a syntactic method for automatically checking the absence of certain erroneous behaviors by classifying program phrases according to the kinds of values they compute. The study of typ......一起来看看 《Types and Programming Languages》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具