写点什么

网易有道 | REDIS 云原生实战

  • 2021 年 12 月 27 日
  • 本文字数:7039 字

    阅读完需:约 23 分钟

网易有道 | REDIS 云原生实战

摘要 本次以 Redis 为范例,阐述了有道基础架构团队在基础设施容器化道路上的实践,主要将从声明式管理,Operator 工作原理,容器编排,主从模式,集群模式,高可用策略,集群扩缩容等方面展开。


目录

  • 背景

  • 面临的挑战

  • 声明式管理

  • Operator 工作原理

  • 容器编排

  • 主从模式

• 主从拓扑图 • 调和原理

  • 集群模式

• 集群拓扑图 • 调和原理

  • 高可用策略

• Kubernetes 保证的高可用 • Redis 集群的高可用

  • 监控观测

  • 集群扩缩容

  • 总结与展望

背景

Redis 是业务系统中较为常用的缓存服务,常用于流量高峰、数据分析、积分排序等场景,并且通过中间件可以实现系统之间的解耦,提升系统的可扩展性。

传统物理机部署中间件,需要运维人员手动搭建,启动时间较长,也不利于后期维护,无法满足业务快速发展的需求。

云原生相较于传统 IT,可以助力业务平滑迁移、快速开发、稳定运维,大幅降低技术成本,节约硬件资源。

云原生中间件是指依托容器化、服务网格、微服务、Serverless 等技术,构建可扩展的基础设施,持续交付用于生产系统的基础软件,在功能不变的前提下,提高了应用的可用性与稳定性。

在这种大趋势下,有道基础架构团队开始了云原生中间件的实践,除了本文介绍的 Redis,还包括 Elasticsearch、ZooKeeper 等。

面临的挑战

利用云原生技术可以解决当前 Redis 部署缓慢,资源利用率低等问题,同时容器化 Redis 集群也面临着一些挑战:

• Kubernetes 如何部署 Redis 有状态服务 • 容器 Crash 后如何不影响服务可用性; • 容器重启后如何保证 Redis 内存中的数据不丢; • 节点水平扩容时如何做到 slots 迁移时不影响业务; • pod ip 变化后集群的状态如何处理。

声明式管理

对于一个 Redis 集群,我们的期望是能够 7x24 小时无间断提供服务,遇故障可自行修复。这与 Kubernetes API 的声明式特点如出一辙。

所谓“声明式”, 指的就是我们只需要提交一个定义好的 API 对象来“声明”我所期望的状态是什么样子,Kubernetes 中的资源对象可在无外界干扰的情况下,完成当前状态到期望状态的转换,这个过程就是 Reconcile 过程。例如,我们通过 yaml 创建了一个 Deployment ,Kubernetes 将“自动的”根据 yaml 中的配置,为其创建好 Pod,并拉取指定存储卷进行挂载,以及其他一系列复杂要求。

因此,我们的 Redis 集群是否可以使用一个类似的服务去完成这个过程呢?即我们需要定义这样的对象,定义服务 Reconcile 的过程。Kubernetes 的 Operator 刚好可以满足这个需求,可以简单的理解 Operator 由资源定义和资源控制器构成,在充分解读集群和 Operator 的关系后,我们将整体架构图设计如下 

 Operator 集群本身采用 Deployment 部署,由 ETCD 完成选主,上层与 Kubernetes 的 Api Server 、Controller Manager 等组件进行通信,下层持续调和 Redis 集群状态。

哨兵模式中 Redis 服务用一套哨兵集群,使用 StatefulSet 部署,持久化配置文件。Redis server 也采用 StatefulSet 部署, 哨兵模式的实例为一主多从。

集群模式中的每个分片使用 StatefulSet 部署,代理采用 Deployment 部署。原生 Pod、StatefulSet、Service、调度策略等由 Kubernetes 本身负责。

Redis 的资源定义在 ETCD 中存储一份即可,我们只需要预先提交自定义资源的 yaml 配置。如下所示为创建三个副本的 Redis 主从集群


apiVersion: Redis.io/v1beta1kind: RedisClustermetadata: name: my-releasespec: size: 3 imagePullPolicy: IfNotPresent resources: limits: cpu: 1000m memory: 1Gi requests: cpu: 1000m memory: 1Gi config: maxclients: "10000"
复制代码

其中,kind 定义使用的 CR 名称,size 为副本数,resources 定义资源配额,config 对应 Redis Server 的 config,该定义存储在 Kubernetes 的 ETCD 数据库中,后续的具体资源申请与使用由 Operator 的 Controller 完成。

Operator 工作原理

Operator 是 Kubernetes 的扩展模式,由 CRD、Controller 构成。它利用定制资源管理特定应用及其组件,Operator 遵循 Kubernetes 的理念。

Operator 无需任何修改,即可从 Kubernetes 核心中获得许多内置的自动化功能,如使用 Kubernetes 自动化部署和运行工作负载, 甚至可以自动化 Kubernetes 自身。

Kubernetes 的 Operator 模式可在不修改 Kubernetes 自身的代码基础上,通过控制器关联到一个以上的定制资源,即可以扩展集群的行为。Operator 是 Kubernetes API 的客户端,核心功能是充当定制资源的控制器。 


CRD: Custom Resource Definition, 在 Kubernetes 中一切皆是资源,资源就是 CRD,用户自定义的 Kubernetes 资源是一个类型 ,比如默认自带的由 Deployment,Pod ,Service 等。

CR: Custom Resource 是实现 CRD 的具体实例。

用户创建一个 CRD 自定义资源,ApiServer 把 CRD 转发给 webhook,webhook 进行缺省值配置 验证配置和修改配置,webhook 处理完成后的的配置会存入 ETCD 中 ,返回给用户是否创建成功信息。Controller 会监测到 CRD,按照预先写的业务逻辑,处理这个 CRD,比如创建 Pod、处理新节点与旧集群关系等,保证运行的状态与期望的一致。

容器编排

Redis 集群在 Kubernetes 中的最小部署单位为 Pod,因此在架构设计之前,需预先考虑 Redis 特性、资源限制、部署形态、数据存储、状态维护等内容,为不同类型的 Redis 集群配置合适的部署方式。

资源限制

Kubernetes 采用 request 和 limit 两种限制类型来对资源进行分配。

• request(资源需求):即运行 Pod 的节点必须满足运行 Pod 的最基本需求才能启动。

• limit(资源限制):即运行 Pod 期间,可能内存使用量会增加,那最多能使用多少内存,这就是资源限额。


Redis 基本不会滥用 cpu,因此配置 1-2 个核即可。内存根据具体业务使用分配,考虑到部分场景下会 fork 较多的内存,例如 aof 频繁刷写,aof 重写过程中,Redis 主程序称依旧可以接收写操作,这时会采用 copy on write (写时复制)的方法操作内存数据,若业务使用特点为“写多读少”,那么刷写期间将产生大量的内存拷贝,从而导致 OOM,服务重启。

一个有效的解决方式为减少刷写次数,将刷写操作放在夜间低流量时段进行。减少刷写次数的方法为适当增加 auto-aof-rewrite-min-size 的大小,可配置使用内存的 5 倍甚至更大的最小刷写量;其次可以主动触发刷写,判断内存使用达到的配额两倍时进行刷写,实际部署时一般也会预留 50%的内存防止 OOM。

部署的基本形态

依据数据是否需要持久化或是否需要唯一标识区分服务为无状态和有状态的服务,Redis 集群需要明确主从、分片标识,大部分场景也需要数据持久化,Kubernetes 使用 StatefulSet 来满足这一类需求。StatefulSet 的顺序部署、逆序自动滚动更新更能提高 Redis 集群的可用性。

具体的:

• Redis Server 使用 StatefulSet 启动,为标识为{StatefulSetName}-0 的 Pod 设置 Master 角色,给其他 Pod 设置为该 Master 的从节点。

• Proxy 无需存储任何数据,使用 Deployment 部署,便于动态扩展。

配置文件

Redis Server 启动时需要一些配置文件,里面涉及到用户名和密码,我们使用 Configmap 和 Secret 来存储的。Configmap 是 Kubernetes 的 Api 对象,常用于存储小于 1MB 的非机密键值对。而 Secret 可以用于存储包含敏感信息的密码、令牌、密钥等数据的对象。

两种资源均可以在 Pod 运行的时候通过 Volume 机制挂载到 Pod 内部。

存储

存储使用的是 PVC(PersistentVolumeClaim) 加 PV (Persistent Volumes),PV 为 Kubernetes 集群中的资源,由存储类 StorageClass 来动态供应,PV 支持多种访问模式:ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany,通过 PV 定义存储资源,PVC 申请使用该存储资源。另外通过根据存储的 StorageClass 字段 可抽象不同的存储后端,如 Cephfs、Cephrbd、Openebs、LocalStorage 等。

主从模式

主从拓扑图

Redis 容器化后建立的每个 CR 表示一个完整的 Redis 服务,具体的服务模式包括哨兵模式和集群模式两种,在进行容器化过程中,除覆盖裸服务器部署结构外,也对架构进行了一定程度的优化。

原生哨兵模式 原生哨兵模式为每套实例配一组哨兵。 


共用哨兵模式 所有实例共用一组哨兵将进一步提高实例启动速度,并在一定程度上可提高硬件资源利用率,实测单组哨兵可轻松应对百规模的主从集群。 


调和原理

Reconcile 实现持续监测并对主从集群进行修复的功能。 


  1. 检查是否按照预期启动了全部的 Pod,比如创建 3 个 Server,那么需要按照预期启动三个才能继续进行后面的操作。

  2. 检查 Master 的数量,确保该实例仅有一个主节点(数量为 0 主动选一个;数量大于 1 手动修复)。

  3. 检查哨兵:

(1)所有的哨兵是否监控了正确的 Master;

(2)所有的哨兵均知道相同的 Slave;

(3)再次检查哨兵的数量,确保哨兵均可用。

  1. 检查 Service,使 Service 的 Endpoints 指向正确的 Master。

  2. 检查 Redis config 是否有做修改,有则对所有节点重写 config 参数。

集群模式

集群拓扑图

Redis Cluster + Proxy 模式

通过在传统 Redis Cluster 架构中引入代理功能,实现动态路由分发,并基于 Kubernetes 原生动态扩缩容特性,更易应对突发流量,合理分配使用资源。

代理基础转发规则如下

• 对于操作单个 Key 的命令,Proxy 会根据 Key 所属的 Slot(槽)将请求发送给所属的数据分片。

• 对于操作多个 Key 的命令,如果这些 Key 是储存在不同的数据分片,Proxy 会将命令拆分成多个命令分别发送给对应的分片。


服务部署前,也对代理的部分功能进行了补充,例如移除不可用节点等。



调和原理

reconcile 实现持续监测并对 Redis Cluster 进行修复功能。 


确保集群健康的步骤

  1. 等待所有 Pod 状态变为 Ready 且每个节点相互识别后,Operator 会在每个 StatefulSet 的 Pod 中挑选一个作为 Master 节点,其余节点为该 Master 的 Slave。

  2. 获取实例集群所有 Pod 的 ip、所有 Pod 的 cluster info(包含 nodeIP,主从关系等)。

  3. 进入恢复流程

    (1)处理失败节点, 对部分节点重启后的无效 ip、状态为 noaddr 的僵尸节点进行 forget 操作;

    (2)处理不可信节点 (所有 handshake 状态的节点),发生于某一个节点被移除(由 forget node 触发),但试图加入集群时,即该 Pod 在 Operator 角度下存在,但实际集群节点并不需要该节点,处理方式为删掉这个 Pod,并再次做 forget 操作直到 Pod 被删除。

  4. 任选一个节点,使用 CLUSTER MEET 给该节点加入所有已知节点。

  5. 为 StatefulSet 中的 Pod 建立主从关系,同时给其分配 Slots。若当前 Master 数量同预期不一致,则对应扩缩容操作,具体见’集群扩缩容’的横向扩缩容小节。

  6. 检查 Redis config 是否有做修改,有则对所有节点重写 config 参数。

确保代理健康的步骤

  1. 获取所有 Running 状态代理的 Pod ip。

  2. 从代理获取 Redis Server 信息,将集群信息同步到所有的代理上,代理中不存在的 Server ip 做移除操作。

  3. 若代理中无可用 Redis Server, 表示被全部移除,则添加一个,代理可自动发现集群其他 Redis 节点。

高可用策略

Kubernetes 保证的高可用

(1) 容器部署保证高可用:

Redis 部署最小资源对象为 Pod,Pod 是 Kubernetes 创建或部署的最小/最简单的基本单位。

当启动出错,例如出现“CrashLoopBackOff”时,Kubernetes 将自动在该节点上重启该 Pod,当出现物理节点故障时,Kubernetes 将自动在其他节点上重新拉起一个。 Pod 未出问题,但程序不可用时,依托于健康检查策略,Kubernetes 也将重启该 Redis 节点。

(2) 滚动升级:

节点纵向扩容时,使用 StatefulSet 的滚动升级机制,Kubernetes 将逆序重启更新每个 Pod,提高了服务的可用性。

(3) 调度的高可用:

Kubernetes 本身不处理 Redis 多个 Pod 组建的集群之间的部署关系,但提供了部署策略,为保证特定场景下的高可用,如因物理节点导致所有 Redis 节点均宕机,CRD 在设计中加入了亲和与反亲和字段。 默认使用 podAntiAffinity 做节点打散,如下所示实例 instance1 的所有 Pod 将被尽可能调度到不同的节点上。

spec:      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - podAffinityTerm:              labelSelector:                matchLabels:                  Redis.io/name: instance1              topologyKey: Kubernetes.io/hostname            weight: 1
复制代码

Redis 集群的高可用

Redis 服务运行期间不可避免的出现各种特殊情况,如节点宕机、网络抖动等,如何持续监测这类故障并进行修复,实现 Redis 集群的高可用,也是 Operator 需解决的问题,下面以哨兵模式模式为例描述集群如何进行故障恢复。

主节点宕机:因物理节点驱逐、节点重启、进程异常结束等导致的 Redis 主节点宕机情况,哨兵会进行切主操作,然后 Kubernetes 会在可用物理节点上重新拉起一个 Pod。

从节点宕机:哨兵模式的 Redis 集群未开启读写分离,从节点宕机对服务无影响,后续 Kubernetes 会重启拉起一个 Pod,Operator 会将该 Pod 设置为新主节点的从节点。

集群全部节点宕机:发生概率极小,但基于持久化可将服务影响降至最低,集群恢复后可继续提供服务。

节点网络故障:主从模式下配置了三个哨兵用于集群选主操作,哨兵集群的每一个节点会定时对 Redis 集群的所有节点发心跳包检测节点是否正常。如果一个节点在 down-after-milliseconds 时间内没有回复 Sentinel 节点的心跳包,则该 Redis 节点被该 Sentinel 节点主观下线。

当节点被一个 Sentinel 节点记为主观下线时,并不意味着该节点肯定故障了,还需要 Sentinel 集群的其他 Sentinel 节点共同判断为主观下线才行。

该 Sentinel 节点会询问其他 Sentinel 节点,如果 Sentinel 集群中超过 quorum 数量的 Sentinel 节点认为该 Redis 节点主观下线,则该 Redis 客观下线。

如果客观下线的 Redis 节点是从节点或者是 Sentinel 节点,则操作到此为止,没有后续的操作了;如果客观下线的 Redis 节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。

集群模式故障转移与上述类似,不过不需要哨兵干预,而是由节点之间通过 PING/PONG 实现。

监控观测

Redis 的监控采用经典的 Exporter+Promethus 的方案,Exporter 用于指标采集,数据存储在 Prometheus 或其他数据库中,最终 Grafana 前端将服务状态可视化。

集群扩缩容

(1)纵向扩缩容 

纵向扩缩容主要指 Pod 的 CPU、内存资源的调整,基于 Kubernetes 的特性,只需修改实例对应的 spec 字段,Operator 的调和机制将持续监测参数变化,并对实例做出调整 。当修改 cpu 、内存等参数时,Operator 同步更新 StatefulSet 的 limit、request 信息,Kubernetes 将逆序滚动更新 Pod,滚动更新时,若停掉的是主节点,主节点的 preStop 功能会先通知哨兵或者集群进行数据保存,然后做主从切换操作,从而将服务的影响降至最低。更新后的主从关系建立以及哨兵 monitor 主节点功能也由 Operator 一并处理,全过程对客户端无感知。主从版、集群版在该场景下均支持秒级断闪。


(2)横向扩缩容 

横向扩缩容主要指副本数或节点数的调整,得益于 Kubernetes 的声明式 API,可以通过更改声明的资源规模对集群进行无损弹性扩容和缩容。

Redis Server 扩容操作时,主从版本中 Operator 将获取新节点 ip, 新启动节点将在下一轮调和时触发 slaveof 主节点操作,且同步过程中,哨兵不会将该节点选为主节点。集群版本中 Operator 将在同步节点信息后进行分片迁移,保证所有节点上的 Slots 尽可能均匀分布。

Redis Server 缩容操作时,主从版本中 Operator 将逆序销毁 Pod,销毁时会先询问哨兵,自己是否为主节点,若为主节点则进行先 failover 操作再退出。集群版本中 Operator 中会先进行分片迁移,再对该节点做删除操作。

代理的扩缩容,更易实现,根据流量波峰波谷规律,可手动定期在波峰到来时对 Proxy 进行扩容,波峰过后对 Proxy 进行缩容;也可根据 HPA 实现动态扩缩容,HPA 也是 Kubernetes 的一种资源,可以依据 Kubernetes 的 Metrics API 的数据,实现基于 CPU 使用率、内存使用率、流量的动态扩缩容。

总结与展望

本次以 Redis 为范例,阐述了有道基础架构团队在基础设施容器化道路上的实践,Redis 上云后将大幅缩短集群部署时间,支持秒级部署、分钟级启动、启动后的集群支持秒级自愈,集群依托于哨兵和代理的特性,故障切换对用户无感知。

有道架构团队最终以云平台的形式提供中间件能力,用户无需关注基础设施的资源调度与运维,重点关注具体业务场景,助力业务增长。未来,将进一步围绕 Redis 实例动态扩缩容、故障分析诊断、在线迁移、混合部署等内容展开探索。


Redis 容器化后有哪些优势?

Kubernetes 是一个容器编排系统,可以自动化容器应用的部署、扩展和管理。

Kubernetes 提供了一些基础特性:

部署:部署更快,集群建立无需人工干预。容器部署后可保证每个的 Redis 节点服务正常,节点启动后将由 Operator 持续监测调和 Redis 集群状态,包括主从关系、集群关系、哨兵监控、故障转移等。

资源隔离:如果所有服务都用同一个集群,修改了 Redis 集群配置的话,很可能会影响到其他的服务。但如果你是每个系统独立用一个 Redis 群的话,彼此之间互不影响,也不会出现某一个应用不小心把集群给打挂了,然后造成连锁反应的情况。

故障恢复

(1)实例的重启:容器化后的健康检查可以实现服务自动重启功能; (2) 网络故障:因宿主机网络故障带来的实例延迟高,哨兵可进行主从切换,而为了保证集群的健康,将由 Operator 负责同步集群信息。

扩缩容:容器部署可根据 limit 和 request 限制实例的 cpu 和内存,也可以进行扩缩容操作,扩容后的故障恢复由 Operator 处理。

节点调整:基于 Operator 对 CRD 资源的持续调和,可在 Operator 的 Controller 中为每个 Redis 实例进行状态维护,因此,节点调整后带来的主副关系建立、集群 Slots 迁移等均可自动完成。

数据存储:容器化可挂载 Cephfs、LocalStorage 等多种存储卷。

监控与维护:实例隔离后搭配 Exporter、Prometheus 等监控工具更容易发现问题。


-- END --

用户头像

高效学习,从有道开始 2021.03.10 加入

分享有道人的技术思考与实践。

评论

发布
暂无评论
网易有道 | REDIS 云原生实战