随着云原生在企业应用的场景越来越多,业务程序在容器等技术的加持下也越来越灵活,高弹性、易伸缩、多活需求的业务程序,给传统的缓存也带来了挑战,怎么演变才能更好的服务业务?本次技术公开课,我们邀请到作业帮 DBA 顾雅各,了解面对日活用户量千万级别的压力,作业帮的缓存架构演变和自研历程。
顾雅各:现就职于作业帮,担任 DBA;近十年数据库相关工作经验,致力于提供易用、高效、稳定的数据库服务,专注于数据库管理及数据库周边生态如自动化平台、Proxy、DTS 等产品的研发工作。
今天的分享将从四个方面展开。大家都知道技术是为了更好地服务业务:
首先介绍作业帮的业务特点,这样能够更好地理解我们的缓存架构为何这样设计;接下来讲讲作业帮的缓存架构演变之路,详细介绍作业帮缓存架构随着业务发展,都做了那些变化;
第三,分享作业帮自研 RedisProxy 的一些特性,帮助大家更好地理解我们自研的 proxy 在哪些方向做了针对性的优化;
最后,分享现在和未来要做的事情。
作业帮业务特点
作业帮的产品形态,决定了以下这些业务特点:
第一个特点是稳定性。作业帮的用户量大,一些流量型服务用户量日活达数千万级别,任何能达到这个用户量的产品,对于稳定性都有着极高的要求。另外,在线教育类业务有明显的低峰期和高峰期,这期间流量差异明显,甚至有数十倍以上的差异。
第二个特点:多云。基于对稳定性的要求,而且随着近几年对云原生的应用越来越广,高弹性、易扩容的容器技术非常适合作业帮低峰期、高峰期差异明显的业务特点,所以作业帮选择了通过多云的方案来提升服务的整体稳定性。现在作业帮已经基本完成了多云建设以及容器化建设,实现了所有核心服务的多云和容器化。作业帮的多云,一方面是指服务在多个云都完整部署,另一方面是指日常的业务流量也会根据需求动态的在多云间去做调度分配。
第三个特点是自主可控。当整体技术架构都是基于多云来建设的时候,我们在数据存储服务上基本也就需要走一条自建 PaaS 的道路。如果现在有公司做多云的话可能有更多的方案可选,但在当时,因为作业帮做多云建设和容器化改造比较早,站在作业帮开始做多云建设和容器化改造的时间点来看,还有从数据安全的角度来看,数据存储服务自建 PaaS 是我们比较好的选择。
作业帮缓存架构演变
下面介绍作业帮缓存架构的演变之路。首先介绍作业帮最初的缓存架构,我们可以先看一下架构图,这里展示的是两个云的情况,每个云的一侧主要分为三部分。
最上层是 LB,这是我们缓存服务的入口,主要功能是转发请求到代理层;
代理层我们使用的是 codis-proxy 集群,这一层的作用是承接处理业务请求,将请求转发到 server,然后返回 server 处理的结果给客户端;
从图上可以看出来,上面这两部分每个云都有自己的服务部署,下面最后一层是 server 层,我们采用的是 codis-server 集群,这一层是跨云部署的集群,也就是说一个集群中,分片 master 在腾讯云的话,这个 master 会在腾讯云和阿里云都有 slave 的节点,用来保证处理本侧云的请求。
另外还有一些组件,从图上可以看出:管理集群的 codis-dashboard,这个是单节点服务,一般部署在一个机房;还有负责 server 层高可用的 sentinel 集群,这个是需要跨云部署的。整体方案部署的话,我们用的是 4c32G 的虚拟机部署,一个集群中,会有一个 codis-proxy 和 codis-server 部署在这台服务器上。
这个架构下,完整的业务请求是怎么流转的?当用户请求从腾讯云进来后,经过客户端 App,然后经过 LB 的转发,路由到本侧云 codis-proxy,再由 codis-proxy 转发请求到 codis-server 集群本侧云节点处理,最后将结果返回给客户端,这就完成了一个完整的请求。
这个架构最初是满足作业帮业务需求的,运行也比较稳定,不过随着业务的发展以及业务的多云建设,还有容器化改造的情况,这个架构在使用上也遇到了不少的挑战,逐渐暴露出一些问题,下面从这几个问题展开说一下:
首先是跨云主从全量复制。我们都知道 codis-server 是基于 redis3.2 版本改造的,之前的 redis 版本主从复制还是使用的 psync 1.0 版本。当我们在主从切换的时候或者是故障触发主从切换的时候,codis-server 都需要重新全量复制,而且我们的业务是在多云架构下部署的,并且业务流量经常需要变化,所以我们需要经常地切换主节点所在机房。
举个例子简单说明一下这种情况,假如说,我们业务流量分配比在腾讯云和阿里云是 7:3,我们需要把 codis-server 集群所有分片的主节点切换到腾讯云,相对的,之后业务流量变化为腾讯云与阿里云 3:7,我们还需要再次把主节点切换到阿里云。如果业务流量经常在各个云之间调度,我们就需要经常切换集群主节点所在机房,这个版本下,主从切换就带来了不少问题,我们在完成主从切换后,就会触发 codis-server 的主从全量复制,在主从全量复制的过程中,主节点 bgsave 的时候可能对于服务的稳定性带来一定的影响,而且传送 RDB 的时候会占用跨云带宽,而我们数据层的跨云带宽是公用的,这样可能会影响到其他的服务。
第二是多云服务配置不一样。前面说到过我们的业务都是多云部署的,因为每一个云 codis-proxy 入口,也就是 LB 是不一样的,导致业务都需要配置本侧云的连接信息,这样就增加了业务在不同云部署的复杂度,不符合我们的预期。
第三,是 proxy、server 的扩容。由于我们的业务高峰期、低峰期流量差异明显,很多时候需要临时扩容 proxy 和 server 来满足需求。codis 的架构扩容比较慢,虽然有平台功能支持,但是牵扯到服务器申请、部署程序等步骤,不能快速地扩容完成,而且不是很灵活。
第四,codis 依赖的组件比较多。像管理集群的 codis-dashboard,server 层负责高可用的 sentinel,在多云架构下,依赖的组件过多也会带来一些问题,比如,多云下依赖组件不方便平台调度,另外跨云的部署增加了管理组件和高可用组件的复杂性,不利于集群的稳定性。
第五,codis-server 是基于 redis3.2 版本改造的,导致 redis 新版本的一些有助于提高稳定性的新特性无法使用。比如说增量复制、混合持久化、主动碎片整理等,而且后续升级的话需要改造 redis 源码以适用 codis 架构,改造成本比较高。
最后,资源利用率不高,成本优势不明显。这个架构下,我们的部署方案是采用 4c32G 的虚拟机部署,很大程度上,资源利用率不高,比如说有的小集群,单个分片最大内存是 8G,就需要占用一整台虚拟机,而且每台服务器都需要预留做 RDB 的内存,这样的架构部署,特别是在集群数量和实例数量上来以后,资源成本优势就不明显了。另外就是我们的 codis-proxy 和 codis-server 部署在一台服务区上,压力大的时候,存在资源争用的风险。以上这些就是我们当时遇到的问题。
针对 codis 架构的这些问题我们是 case by case 去解决?还是设计一个新的架构方案去解决呢?最终,我们选择了去设计一个新的缓存架构。
上图是目前作业帮线上的缓存架构,大家可以从图中了解到我们这里称之为 redis cluster 的架构。目前这个架构相比之前的 codis 架构,主要针对性做了几处改造,图上也可以简单看出来。
第一处:代理层。基于 codis-proxy 的一些问题,代理层换成了我们自研的 RedisProxy,并且进行了容器化,通过 K8s 部署。
第二处:server 层。基于 codis-server 的一些问题,server 层我们换成了基于 redis5.0 的 redis-cluster。
第三处:综合成本的考虑,我们采用了资源池混部的方式部署 redis-cluser。
当然,针对新的架构,作业帮的平台也进行了全面地改造,所有的运维操作均通过自动化平台实现。
在这个架构下,我们先来看一下完整的业务请求是怎么流转的。依然还是两个云的情况,前面介绍过我们的业务基本全部完成了容器化改造,redis-cluster 架构下,当用户请求从腾讯云一侧进来以后,经过 K8s 的 SVC 解析转发,然后到自研的 RedisProxy,然后 proxy 将请求转发给本侧云的 redis-cluster 节点处理后,最后将结果反馈给客户端。
基于以上的改造,演变了作业帮目前的缓存架构,相比较之前的 codis 架构,现在的架构有以下几点优势:
第一是增量复制特性,这个是说我们目前使用的 redis5.0 版本主从复制采用 psync 2.0 新版本,可以增量复制,这个增量复制是指主从切换后主从节点无需全量复制,从而降低了主从切换对于线上业务影响的风险和对跨云网络带宽的影响。
第二是,K8s 服务注册、业务多云配置一致。我们自研的 proxy 采用容器化部署,通过 K8s 部署完之后自动注册 SVC,容器化后的业务程序就可以通过 K8s 的 SVC 访问。这样业务在每个云的连接配置均一致,从而大大降低了业务部署的复杂性。
第三是 proxy 容器化后扩缩容便捷。我们自研的 proxy 无依赖组件,无状态,非常适合容器化,可以实现秒级部署及扩缩容,能够更好地应对业务高峰期的压力。并且可以利用 K8s 的 HPA 技术,HPA 技术就是说在 K8s 中会对 Pod 运行中的各项运行指标进行检测,这些指标包括 CPU 占用、内存占用、网络请求量等指标,实现了对实例个数的动态新增和减少。
第四是无依赖组件,后续 redis 升级方便,当前架构使用的是社区版的 redis,相比 codis 而言,与我们自研的 RedisProxy 无耦合,后续可以按需升级 redis 版本。
第五,server 基于资源池混部超额分配,资源利用率大大提升,成本优势明显。通过我们长时间的监控采集数据分析,发现我们的 redis 实例使用的内存,平均在已分配内存的 50%左右,基于这个数据我们按照内存超额分配的方式,采用服务器资源池混部的方式部署 redis-cluster,实现超卖。比如说 100G 内存的服务器资源,分配出最大内存共 150G 的 redis 实例,并且不用预留 redis 做 RDB 需要的内存,这样就提高了资源利用率。目前我们的分配比稳定在 150%左右,资源利用率大大提升,成本优势特别明显。
前面介绍作业帮目前的缓存架构,也就是 redis-cluster 的架构,我们从最初的 codis 架构演变到现在这个架构,期间也遇到过不少的困难,这里简单介绍一下遇到了哪些问题,很大程度上这也是我们自研 RedisProxy 的原因。
当初架构变更的时候我们有两个方向可选,一个是选择继续使用 codis,升级改造新版本 redis 兼容 codis 架构,可以解决一些 server 层的问题,不过考虑到组件过多,改造成本比较高,所以选择了放弃。另一个选择是使用开源 proxy 加 redis-cluster 的方案,通过调研测试开源 proxy 发现多少存在一些问题,而且不是特别满足作业帮的场景和业务方式,所以也选择了放弃,最后才演变了现在的缓存架构。
新的架构在设计之初也带来一些新的挑战。
1、稳定性
最大的挑战就是稳定性,稳定性主要涉及到两个方面,分别是 proxy 和 rediscluster 的稳定性:proxy 层需要稳定,另外也需要灵活的扩缩容,满足我们的业务需求;redis-cluster 对稳定性也有极高的要求,另外也需要在变更的时候,比如说节点上下线、扩缩容方面尽可能对业务无感。
2、资源池混部下 redis-cluster 管理
前面说到基于成本的考虑我们选择了采用资源池混部的方式使用 redis-cluster,接下来的挑战就是 redis-cluster 的管理,资源池混部下超额分配资源会带来一些问题。
实例分布,最直接的就是部署的端口问题,最主要是需要防止端口冲突,也方便运维管理,另外也需要资源分配均衡。因此,我们采用了按照集群端口递增的方式分配端口,也就是说一个集群内所有的 redis 节点端口一致,每个集群的端口是不同的,一个集群内所有的节点分布在资源池不同的服务器上,这样就解决了端口问题,同时也增加了集群的容错性,防止单个服务器的故障影响集群的整体可用性,最后通过分配策略打造资源分配均衡。
在 bgsave 的影响方面,缓存作为数据层,我们通过定制备份保证数据完整性。备份是通过 redis RDB 实现的,因为我们采用的是大配置型服务器做资源池,最终体现在每台服务器上的 redis 实例数量大约在二三十个,并且我们是超额分配内存,所以每台服务器上做 bgsave 的时候都需要考虑 bgsave 带来的影响,比如 bgsave 时需要额外的内存,我们是通过设计完整的任务调度来实现的,用于防止这个风险的发生。
在切换的需求方面,前面说到过,多活下我们的业务流量经常在各个云中间调度,这个调度一般都是以业务线为单位的,有的业务线下相关集群达四五十个,这种情况下就涉及到一个批量切云的操作。
在监控告警方面,每个集群的告警规则都可以定制化,还有服务器替换等方面,这些都是通过自动化平台去解决的。
3、跨云读写
由于我们是多云部署,所以业务还存在跨云读写的问题。
4、redis-cluster slot 迁移
然后 redis-cluster slot 迁移,这个是说 redis-cluster 在扩缩容分片的时候需要重新分配 slot,redis-cluster slot 迁移过程中会有 moved、ask 重定向的问题,不处理的话,业务请求就会返回错误,这些问题都需要处理,并且要对业务无感。
基于以上这些方面,有些问题,比如说 redis-cluster 的管理、日常运维,可以通过自动化平台或者是运维手段规避;还有一些方面,比如多云下跨云读写,还有 redis-cluster 的变更需要对业务无感,由于对于 proxy 层没有特别好的解决方案,所以我们选择了自研 RedisProxy。
以上就是我们在设计新架构的时候所遇到的主要问题,当然这里大家可能会想问一下,设计新的架构遇到这么多问题,那么新的架构最终给我们带来了什么样的收益呢?
目前的缓存架构已经上线很长一段时间了,我主要从三个方面介绍一下新的缓存架构为我们带来的收益。
首先是稳定性,自研 RedisProxy + rediscluster 自上线以后都一直稳定运行,SLA 从最初的 99.9% 提升到 99.99%,并且稳定保持,而且也经历过公司的多次故障演练验证,同时也经历过一些真实的故障考验,表现均在我们预期之内。另外,新的架构运维操作,均通过平台实现,比如说操作 redis-cluster 大部分操作都是在分钟级内完成,容器化的 proxy 都在秒级内完成,大大提高了运维效率,而且通过平台运维也减少了传统运维操作带来的风险。
第二是业务时延,自研 proxy 通过本地读功能,大大降低了业务跨云请求耗时,进而提高了用户体验。
第三是成本收益,采用新的架构后,RedisProxy 容器化部署和 redis-cluster 资源池混部的方式,相比之前的架构总体缩减 50%的支出,以上就是我们新架构带来的主要收益。
自研 RedisProxy 的实践
下面介绍一下我们在自研 RedisProxy 的实践。这里通过介绍一些特性,来了解我们在自研 RedisProxy 的实践。
第一,支持读写分离,动态开启关闭指定节点流量。我们都知道 redis-cluster 默认从库不提供读功能,从库如要接收读流量提供读功能,就需要在 client 在会话中执行 readonly 命令,之后才能接收流量。我们自研的 proxy 默认开启从库读流量,并且可以通过 API 动态开启和关闭指定节点的读流量。比如集群中下线一个从节点的时候,可以先关闭这个节点的流量,这样操作节点下线就对业务无感了。
第二,无需依赖组件,实现 redis-cluster slot 变化感知及路由。我们自研的 proxy 无依赖组件,可以自动实现 redis-cluster 节点变化和 slot 变化的感知,正确地将请求路由到对应的节点。
第三,自动处理 slot 迁移过程中 moved、ask 重定向请求。在 redis-cluster 集群操作的时候,比如添加分片,缩减分片都需要迁移 slot,在 slot 迁移的过程中访问到正在迁移 slot 中的 key 的时候,会有 moved、ask 重定向的问题,不处理的话,就会将重定向问题返回给客户端。我们自研的 proxy 实现了自动处理机制,对于业务无感,可以将正常的结果反馈给客户端。
第四,本侧云优先读功能。在多云环境下经过 proxy 的读流量会优先路由到本侧云节点,解决了业务跨云读的问题,本侧云优先读这个策略,可以通过配置文件很方便地调整。
第五,从库 loading 过程中不接收流量,这个是指在 redis-cluster 分片新添加从库的时候,从库 loading 不接收流量。说到这个特性,先简单说一下 redis 主从复制的大致步骤,比如说集群中一个 master 节点添加一个新的 slave 节点的过程大致分为三个步骤,第一步 slave 加入集群,请求 master 主从建立主从复制;第二步 master 节点收到 slave 的请求,开始做 RDB,然后发送给 slave 节点;第三步 slave 节点接收到 master 节点发送来的 RDB,然后加载入内存,然后完成全量同步后进入正常的增量复制阶段,经过这三个步骤后才完成主从复制的建立。
其中在步骤一的时候,proxy 从集群拓扑中就已经发现了新添加的 slave 节点,但是这时候是不应该往该节点分发流量的,因为在步骤二的时候,新的 slave 节点可以接收请求,但是没有加载 master 的 RDB,也没有建立主从复制,这时候是请求不到数据的。在步骤三的时候,新的 slave 节点在加载 RDB 到内存中,这时候接收请求,会返回 redis 常见的错误。所以正常是在步骤三完成后,主从复制建立进入正常的增量阶段,新的 slave 才开始接收流量。这个情况我们自研的 proxy 通过内部机制自动处理,在主从复制进入到增量复制的阶段才会往新的 slave 节点分发流量。
第六,黑名单功能,黑名单功能可以动态屏蔽指定 client 的连接,也可以动态屏蔽 key 的指定操作,这个操作主要是用于线上突发紧急情况的时候止损使用。比如说获取一个大 key 的操作影响到服务,在业务允许的情况下可以暂时屏蔽对该 key 的操作。
以上通过介绍我们自研 RedisProxy 的一些特性,让大家了解我们在 RedisProxy 的实践,希望对大家有所帮助。后面我们计划将自研的 RedisProxy 在 11 月进行开源,后面会有更详细的介绍,感兴趣的同学后面可以关注一下。
未来展望及调优规划
目前的架构还有一些有待完善的地方,也是我们现在正在做的以及以后要做的,主要是分两部分。
第一部分是自研 RedisProxy。一方面,我们希望加入自动感知热 key、大 key 的功能,大家知道缓存使用过程中热 key 和大 key 对它的影响比较大,所以我们希望提前发现问题并介入解决。另一方面,希望加入热 key 缓存的功能,实际应用中缓存遇到热 Key 是一个比较难处理的问题,因为通常情况下热 key 比较集中,Key 值比较小,不能通过扩容分片的方式解决,一般只能通过添加热 key 所在的 slave 节点分担压力来解决,这种办法性价比不是特别高。所以为了减少热点 key 对 server 层的冲击,提高业务的响应,我们考虑将热 key 缓存到 proxy 层,这是因为 proxy 容器化部署之后扩缩容特别方便,从运维角度和成本来看,热 key 放在 proxy 层缓存,性价比相对比较高。
第二部分是 server 层。第一块是 Tair,Tair 是阿里云自研的缓存数据库,使用持久化内存作为存储介质。这里简单说一下我们为什么考虑 Tair,一是从成本来看,Tair 持久化内存作为存储介质使用成本是目前 redis 的 60%到 70%左右,而且性能方面经过我们实际的测试,Tair 与 redis 基本一致。二是 Tair 持久化内存方案,天生持久化,在断电的情况下也能保证数据不会丢失,拥有极高的数据可靠性。并且持久化不需要借助 AOF 和 RDB,这样在 server 做 RDB 期间造成的性能抖动情况就不存在了,从监控上看就没有毛刺。当然 Tair 还有其他特性,今天就不做过多介绍了,有感兴趣的同学可以去了解一下。目前我们已经完成了阿里云所有 redis 节点替换为 Tair 节点,并且对于业务使用没有任何的影响。
第二块,slot 迁移方式优化,目前扩缩容的时候 slot 的迁移方式也是采用 redis-cluster 原生的迁移方式,只是基于迁移的 slot 数量做了任务拆分,小批量迁移 slot,以降低对服务的影响,后续会调研考虑用更优的方式处理 redis-cluster 的 slot 迁移。
第三块,多云集群双写,目前的架构虽然解决了跨云读的问题,但是业务还是需要跨云写,后面正在调研及测试集群跨云双写的方式,比如两个集群在两个云环境通过 DTS 同步实现双写,达到每个云的业务程序读写都在本侧云。