转载:Redis 如何分布式,来看京东金融的设计与实践
作者 | 李竟成
编辑 | 雨多田光
标签 | 分布式缓存
前 言
R2M 是京东金融线上大规模应用的分布式缓存系统,目前管理的机器总内存容量超过 60TB,近 600 个 Redis Cluster 集群,9200 多个 Redis 实例。
其主要功能包括:全 web 可视化运维、缓存集群一键部署、资源池统筹管理、在线扩容及快速数据迁移、多机房切换及容灾、完善的监控及告警、Redis API 兼容等。
本文将从 R2M 系统架构、资源管理、集群扩容与迁移、数据冷热交换、多机房容灾等多方面进行深入剖析,希望读者能有所收获。
业务使用及运维上的优点
极简化接入
R2M 接入简单,以 Java 客户端为例,只需在业务程序中引入客户端 jar 包,配置好 appname 及 ZK 地址,即可使用。
配置自动下发
R2M 客户端提供了诸如连接池、读写超时时间、重试次数等配置,有些情况下,需要对这些配置进行调优,比如 618、双十一大促来临时,有些业务需要减小客户端读写超时时间,避免超时问题引发的连锁反应。
为了修改配置,业务不得不重新上线所有的客户端,不仅费时费力,还有可能引发故障。因此,为了避免这个问题,R2M 提供了配置自动下发功能,业务客户端无需重新上线,管理端修改配置后,所有客户端即时生效。
多种数据类型 API 支持
完全兼容 Redis 各数据类型 API 接口,提供包括哈希、集合、有序集合、列表、发布 / 订阅等近百个接口。
支持数据冷热交换
在保证高性能和数据一致性的前提下,实现了基于 LFU 热度统计的数据动态冷热交换,使得冷数据被交换到 SSD 上,热数据被加载到 Redis 上,取得高性能和低成本的高平衡。
全 web 可视化运维
R2M 实现了包括集群部署、扩容、数据迁移、监控、集群下线、数据清理、故障节点替换及机器下线等在内的所有功能的可视化运维,运维效率及可用性大大提升。
多机房一键切换
通过改造 Redis Cluster 支持多机房灾备、防止集群脑裂,以及各系统组件针对多机房切换选举的支持,实现了在 Web 控制台的多机房一键切换。
多种方式的集群同步及数据迁移工具
R2M 提供了专门的迁移工具组件,支持从原生 Redis 迁移至 R2M,实现了基于 Redis RDB 文件传输及增量同步的迁移机制。
同时,还可以指定规则比如按照前缀匹配或者反向匹配进行部分数据迁移。由于内部历史原因,京东金融部分业务以前用的是京东商城的 Jimdb,R2M 同样也支持了从 Jimdb 集群进行迁移。
R2M 迁移工具还支持数据实时同步的功能,包括 R2M 集群间的数据同步和 Jimdb 源集群的数据同步。
非 Java 语言代理
我们的业务开发人员主要用的是 Java,对于非 Java 语言的客户端,比如 Python、C 等等,则通过 R2M Proxy 组件接入,各种运维操作在 Proxy 层进行,对业务方屏蔽。同时,Proxy 也提供了高可用保障方案。
系统架构
组件功能
Web console
是 R2M 缓存系统的可视化运维控制台。所有运维操作均在 Web console 进行。
Manager
整个系统的管理组件。负责所有运维操作的下发、监控数据的收集等。运维操作包括集群创建、数据迁移、扩容、多机房切换等等。
Agent
每台物理机上部署一个 Agent 组件,Agent 负责本机 Redis 实例的部署和监控,并进行数据上报。
缓存集群节点
每个集群的节点分布在不同机器上,若干个节点构成一个分布式集群,去中心化,无单点。
Client
客户端由业务方引入,如:Java 通过 jar 包方式引入。
Proxy
对于非 Java 客户端的业务,通过 Proxy 提供缓存接入和服务。
缓存集群一键部署
分布式集群的部署通常来说是比较麻烦的,涉及到多台机器、多个节点及一系列的动作和配置,比如部署 Redis Cluster,就包括节点安装与启动、节点握手、主从分配、插槽分配、设置复制这些步骤。
虽然官方提供了构建集群的工具脚本,但机器推荐、节点安装及配置仍然没有自动化,对于需要大规模部署和运维 Redis Cluster 的企业来说,仍然不够灵活和便捷。
因此,R2M 基于 Golang 实现了在 Web 控制台一键完成从机器推荐、节点部署、集群构建、到节点配置的所有动作,这个过程中每台机器上的 Agent 组件负责下载并安装 RPM 包、在指定端口上启动实例,Manager 组件则负责完成集群构建和节点配置过程。
自动部署过程中,还有一些必要的验证条件和优先规则,主要是以下几点:
检查主节点数量和主备策略,是否满足分布式选举及高可用的最低配置条件:三个以上主节点、一主一从。
避免同一个集群多个节点部署在相同机器上,防止机器故障,优先推荐符合条件的机器。
根据机器可用内存,优先推荐剩余可用内存多的机器。
根据主节点数量,均衡分配插槽数量。
资源规划与统筹管理
由于机房、机器、业务数量众多,要做好平台化管理,需要对资源进行合理的事先规划,事先规划包括:申请接入时业务方对容量进行预估,合理分配缓存实例大小和数量、机器的预留内存,为了方便管理和统计,通常还需要根据机房对机器进行分组、或者根据业务进行机器分组。
做好事先规划的同时,还需要对资源使用情况做到一目了然,避免出现超用或严重超配的情况。
严重超配,就是实际使用量远小于预估容量的情况,这种情况还是很多的,因为很多时候容量很难准确预估,或者有的业务开发为了保险起见或担心不好扩容,申请的容量往往远大于实际需要,对于这种情况,我们可以进行一键缩容,防止资源浪费。
对于超用,也就是机器实际资源使用量超过了标准,则是要非常注意的,缓存机器超用比如 Redis 机器超用可能导致非常严重的后果,比如 OOM、发生数据淘汰、性能急剧下降,为了避免超用,需要进行合理的资源预留、选择合适的淘汰策略,同时平台要有完善的监控和实时的报警功能。
R2M 通过对机房、分组、机器、实例的层级管理和监控,方便我们更好地对资源进行规划,并最大限度的统筹和平衡资源利用,防止资源浪费和机器超用。
扩容及数据迁移
业务在申请缓存时,我们都会要求预估容量,根据预估值进行分配,但预估值经常是不准确的,计划也永远赶不上变化,业务场景拓展、业务量和数据的增长总是无法预测的。
因此,良好的扩容机制显得尤为重要,扩容做得好不好,决定了系统的扩展性好不好。在 R2M 中,我们将水平扩容解耦为两个步骤,添加新节点和数据迁移,添加新节点其实就是一个自动部署的过程,很多步骤与集群创建过程是相同的,那么关键就在于如何解决数据迁移问题了。
数据迁移主要解决以下问题:
如何做到不影响业务的正常读写,即业务方对迁移是基本无感知的?
如何保证迁移的速度?
当一条数据处于迁移中间状态时,如果此时客户端需要对该数据进行读写,如何处理?
迁移过程中收到读操作,是读源节点还是目标节点,如何确保客户端不会读到脏数据?
迁移过程中是写源节点还是写目标节点,如果确保不写错地方、不会由于迁移使得新值被旧值覆盖?
迁移完成,如何通知客户端,将读写请求路由到新节点上?
为了解决这些问题,我们充分吸取了 Redis Cluster 的优点,同时也解决了它的一些不足之处和缺点。
数据迁移原理
上图就是 Redis Cluster 的数据迁移原理图,迁移过程是通过源节点、目标节点、客户端三者共同配合完成的。
Redis Cluster 将数据集分为 16384 个插槽,插槽是数据分割的最小单位,所以数据迁移时最少要迁移一个插槽,迁移的最小粒度是一个 key,对每个 key 的迁移可以看成是一个原子操作,会短暂阻塞源节点和目标节点上对该 key 的读写,这样就使得不会出现迁移“中间状态”,即 key 要么在源节点,要么在目标节点。
如上图,假设正在迁移 9 号插槽,首先会在源节点中将 9 号插槽标记为“正在迁移”状态,将目标节点中 9 号插槽标记为“正在导入”状态,然后遍历迁移该插槽中所有的 key。
此时,如果客户端要对 9 号插槽的数据进行访问,如果该数据还没被迁移到目标节点,则直接读写并返回,如果该数据已经被迁移到目标节点,则返回 Ask 响应并携带目标节点的地址,告诉客户端到目标节点上再请求一次。
如果 9 号插槽的数据被全部迁移完成,客户端还继续到源节点进行读写的话,则返回 Moved 响应给客户端,客户端收到 Moved 响应后可以重新获取集群状态并更新插槽信息,后续对 9 号插槽的访问就会到新节点上进行。这样,就完成了对一个插槽的迁移。
多节点并行数据迁移
Redis Cluster 是基于 CRC16 算法做 hash 分片的,在插槽数量差不多且没有大 key 存在的情况下,各节点上数据的分布通常非常均衡,而由于数据迁移是以 key 为单位的,迁移速度较慢。
当数据量暴涨需要紧急扩容时,如果一个接一个地进行主节点数据迁移,有可能部分节点还没来得及迁移就已经把内存“撑爆”了,导致发生数据淘汰或机器 OOM。
因此,R2M 实现了多节点并行数据迁移,防止此类问题发生,同时也使数据迁移耗时大大缩短,另外,结合 Redis 3.0.7 之后的 pipeline 迁移功能,可以进一步减少网络交互次数,缩短迁移耗时。
可控的数据迁移
水平扩容添加新节点后,为了做数据 / 负载均衡,需要把部分数据迁移到新节点上,通常数据迁移过程是比较耗时的,根据网络条件、实例大小和机器配置的不同,可能持续几十分钟至几个小时。
而数据迁移时可能对网络造成较大的压力,另外,对于正在迁移的 slot 或 keys,Redis Cluster 通过 ASK 或 MOVED 重定向机制告诉客户端将请求路由至新节点,使得客户端与 Redis 多发生一次请求响应交互。
并且通常客户端的缓存读写超时比较短 (通常在 100~500ms 以内),在多重因素的作用下,有可能造成大量读写超时情况,对在线业务造成较大的影响。
基于此,我们实现了迁移任务暂停和迁移任务继续,当发现迁移影响业务时,随时可以暂停迁移,待业务低峰期再继续进行剩余的数据迁移,做到灵活可控。
自动扩容
R2M 还提供了自动扩容机制,开启自动扩容后,当机器可用内存足够时,如果实例已使用容量达到或超过了预设的阀值,则自动对容量进行扩大。
对于一些比较重要的业务,或不能淘汰数据的业务,可以开启自动扩容。当然,自动扩容也是有条件的,比如不能无限制的自动扩容,实例大小达到一个比较高的值时,则拒绝自动扩容,还要预留出一部分内存进行 Fork 操作,避免机器发生 OOM。
数据冷热交换存储
由于我们线上存在很多大容量 (几百 GB 或几个 TB) 缓存集群,缓存机器内存成本巨大,线上机器总内存容量已达到 66TB 左右。
经过调研发现,主流 DDR3 内存和主流 SATA SSD 的单位成本价格差距大概在 20 倍左右,为了优化基础设施 (硬件、机房机柜、耗电等) 综合成本,因此,我们考虑实现基于热度统计的数据分级存储及数据在 RAM/FLASH 之间的动态交换,从而大幅度降低基础设施综合成本,并达到性能与成本的高平衡。
R2M 的冷热交换存储的基本思想是:基于 key 访问次数 (LFU) 的热度统计算法识别出热点数据,并将热点数据保留在 Redis 中,对于无访问 / 访问次数少的数据则转存到 SSD 上,如果 SSD 上的 key 再次变热,则重新将其加载到 Redis 内存中。
思想很简单,但实际上做起来就不那么容易了,由于读写 SATA SSD 相对于 Redis 读写内存的速度还是有很大差距的,设计时为了避免这种性能上的不对等拖累整个系统的性能、导致响应时间和整体吞吐量的急剧下降,我们采用了多进程异步非阻塞模型来保证 Redis 层的高性能,通过精心设计的硬盘数据存储格式、多版本 key 惰性删除、多线程读写 SSD 等机制来最大限度的发挥 SSD 的读写性能。
多进程异步模型,主要是两个进程,一个是 SSD 读写进程,用于访问 SSD 中的 key,一个是深度改造过的 Redis 进程,用于读写内存 key,同时如果发现要读写的 key 是在 SSD 上,则会将请求转发给 SSD 读写进程进行处理。
Redis 进程这一层,最开始我们其实是基于 Redis 3.2 做的,但 Redis 4 出了 RC 版本之后尤其是支持了 LFU、Psync2、内存碎片整理等功能,我们就果断的切到 Redis 4 上进行改造开发了。
SSD 读写进程,起初是基于开源的 SSDB 进行开发,不过由于 SSDB 的主从复制实现性能很差,数据存储格式设计还不够好,与 Redis API 有很多的不兼容问题,最终除了基本的网络框架外,基本重写了 SSDB。
另外由于 SSDB 默认采用的存储引擎是 leveldb,结合功能特性、项目活跃度等方面的原因,我们改成了比较流行的 RocksDB,当然,其实它也是发源于 leveldb 的。
目前我们内部已完成了该项目的开发,并进行了全面的功能、稳定性和性能测试,即将上线。由于冷热交换存储涉及的内容较多,由于篇幅原因,这里不再详述。该项目已命名为 swapdb,并在 Github 开源
https://github.com/JRHZRD/swapdb
欢迎有兴趣的同学关注,欢迎加 star~
多机房切换及容灾支持
多机房切换是一个从上到下各组件协调联动的过程,要考虑的因素非常多,在 R2M 中,主要包括缓存集群、路由服务 (如:ZooKeeper、etcd) 及各个组件 (Manager、Web console、客户端) 的多机房切换。
数据层的多机房切换——即缓存服务的多机房切换
对于多机房支持,关键在于如何避免脑裂问题。我们先来看看脑裂是如何发生的。
正常情况下,一个缓存集群的节点部署在同一个机房,按最低三主、一主一从进行部署,每个主节点负责一部分数据,如果主节点挂了,剩余主节点通过选举将它的从节点提升为新的主节点,即自动 failover。
如果要进行机房切换或对重要的业务进行多机房容灾,则需要在另一个机房对每个主节点再添加一个从节点,那么每个主节点将有两个从,运行过程中,如果集群中有节点发生自动 failover,主节点可能被 failover 到另一个机房,结果,就会出现同一个集群的主节点分布在不同机房的情况。
这种情况下,如果机房间网络链路出现问题,A 机房的主节点与 B 机房的主节点会互相认为对方处于 fail 状态,假设多数主节点都在 A 机房,那么 A 机房的主节点会从同机房的从节点中选出新的主节点来替代 B 机房的主节点,导致相同的分片同时被分属两个机房的主节点负责,A 机房的客户端把这些分片的数据写到了 A 机房新选出的主节点,B 机房的客户端仍然把数据写到了 B 机房的主节点上,从而造成脑裂导致数据无法合并。
熟悉 Redis Cluster 的同学可能知道,Redis Cluster 有一个cluster-require-full-coverage的参数,默认是开启的,该参数的作用是:只要有节点宕机导致 16384 个分片没被全覆盖,整个集群就拒绝服务,大大降低可用性,所以实际应用中一定要关闭。但带来的问题就是,如果出现上述问题,就会造成脑裂带来的后果。
为了解决这个问题,我们在 Redis 的集群通信消息中加入了datacenter标识,收到自动 failover 请求时,主节点会将自己的 datacenter 标识与被提名做 failover 的从节点的 datacenter 标识进行比较,如果一致,则同意该节点的自动 failover 请求,否则,拒绝该请求。
保证自动 failover 只会发生在同机房从节点上,避免主节点分布在多个机房的情况。而对于手动 failover 或强制 failover,则不受此限制。针对 Redis 多机房支持的功能,已经向 Redis 官方提交了 pull request,作者 antirez 大神在 Redis 4.2 roadmap 中表示会加入这块功能。
目前,R2M 的机房正常切换及容灾切换都已经实现了在 Web console 的一键切换。当需要做机房正常维护或迁移时,就可以通过手动 failover 将主节点批量切到跨机房的从节点上。
值得一提的是,正常切换过程保证切换前后主从节点的数据一致。当机房由于断电或其它原因出故障时,通过强制 failover 将主节点批量切到跨机房的从节点上,由于是突发事件,少量数据可能还未同步给从节点,但这里的主要目的是容灾、及时恢复服务,可用性重要程度要远大于少量的数据不一致或丢失。
系统组件的多机房切换
缓存集群路由服务 (ZK) 的多机房切换
业务客户端通过路由服务拿到对应的缓存节点地址,在我们的生产环境中,每个机房的 ZK 都是独立部署的,即不同机房的 ZK 实例属于不同的 ZK 集群,同时每个机房的业务客户端直接访问本机房的 ZK 获取缓存节点。
在 R2M 中,每个机房的 ZK 路由节点存储的配置全部相同,我们保持 ZK 路由节点中的信息尽量简单,所有集群状态相关的东西都不在 ZK 上,基本是静态配置,只有扩容或下线节点时才需要更改配置。所以,当机房切换时,不需要对 ZK 做任何变更。
客户端的多机房切换
业务客户端本身是多机房部署的,不存在多机房问题,但客户端需要在缓存集群发生多机房切换后及时把服务路由到切换后的机房,这就需要通知分布于多个机房的各个客户端,并与每个客户端维持或建立连接,无疑是一大麻烦事。
在 R2M 中,由于客户端是 smart client,当感知到异常时,可以从存活的节点中重新获取集群状态,自动感知节点角色变更并切换,也就不存在通知的问题了。
Manager 组件的多机房切换
Manager 是缓存集群的管理组件,运维操作包括机房切换操作都是通过它来进行,所以必须做到 Manager 本身的多机房才能保证可以随时进行机房容灾切换或正常切换。
需要注意的是,由于不同机房的 ZK 是独立的,Manager 的多机房切换不能直接依赖于 ZK 来实现,因为有可能刚好被依赖的那个 ZK 所在的机房挂掉了,所以,我们实现了 Manager 的多机房选举 (类 Raft 机制) 功能,多个 Manager 可以自行选举 leader、自动 failover。
Web console 的多机房切换
对于 Web console 的多机房切换,这个就相对简单了。由于 Web 是无状态的,直接通过 Nginx 做负载均衡,只要有任意一个机房的 Web 组件可用就可以了。
监控、告警及问题排查
业务出现调用超时、性能指标出现抖动怎么办?
一次业务调用可能涉及多个服务,比如消息队列、数据库、缓存、RPC,如何确认不是缓存的问题?
如果是缓存服务的问题,是机器问题、网络问题、系统问题、还是业务使用不当问题?
当出现上述问题时,面对大量机器、集群以及无数的实例,如果没有一套完善的监控与告警系统,没有一个方便易用的可视化操作界面,那只能茫然无措、一筹莫展,耐心地一个一个实例的看日志、查 info 输出,查网络、查机器,所谓人肉运维。
因此,R2M 提供了各种维度的监控指标和告警功能,及时对异常情况进行预警,当问题出现时,也能够快速定位排查方向。
机器和网络指标
每台机器的网络 QoS(丢包率、包重传次数、发送接收错误数)、流入流出带宽、CPU、内存使用量、磁盘读写速率等。
系统参数指标
每个节点的内存使用量、网络流入流出流量、TPS、查询命中率、客户端 TCP 连接数、key 淘汰数、慢查询命令记录、实例运行日志等。
实时监控及历史统计图表
实时和历史统计图表是对各项参数指标的实时反馈和历史走势的直观展示,不仅能在出问题时快速给出定位的方向,还能够对运维和业务开发提供非常有价值的参考数据,这些参考数据也反过来促进业务系统进行优化、促进更好地运维。
对于实例的历史监控统计数据,由每个机器上部署的 Agent 组件负责收集并上报。由于实例数及监控项非常多,监控数据量可能会非常大。
为了避免监控数据占用大量数据库空间,对于历史统计数据,我们会保留展示最近 12 小时以分钟为维度、最近一个月以小时为维度、以及最近一年以天为维度的数据,12 小时以前的分钟数据自动合并为小时维度的数据,一个月以前的小时数据自动合并为天维度的数据,在合并时,保留这个时间段的指标最高值、最低值、以及累加后的平均值。
对于实时监控图表,则是用户请求查看时才与对应的缓存实例建立连接,直接从实例获取实时信息。
客户端性能指标
每个客户端的 TP50、TP90、TP99、TP999 请求耗时统计,快速定位问题 IP。
告警项
容量告警、流入流出流量、TPS、实例阻塞、实例停止服务、客户端连接数等。
总 结
限于篇幅原因,这里只能对 R2M 缓存系统大致的设计思路和功能做一个简要的介绍,很多细节和原理性的东西没能一一详述,比如数据的冷热交换,实际上做这个东西的过程中我们遇到了很多的挑战,也希望以后能对这块做详细的介绍。最后,也希望有更多的技术同仁拥抱和参与开源,让大家有更多更好的轮子用。
作者介绍
李竟成(微信号:lijingcheng87),任职于京东金融杭州研发中心,京东金融 R2M 分布式缓存系统负责人,略通 Redis 内核,爱好开源,关注 KV 存储、分布式架构及数据一致性相关领域。