知乎存储平台团队基于开源Redis 组件打造的知乎 Redis 平台,通过不断的研发迭代,目前已经造成了一整套完整自动化运维服务体系,提供不少强大的功能。本文做者陈鹏是该系统的负责人,本次文章深刻介绍了该系统的方方面面,值得互联网后端程序员仔细研究。
<ignore_js_op>git
陈鹏:现任知乎存储平台组 Redis 平台技术负责人,2014 年加入知乎技术平台组从事基础架构相关系统的开发与运维,从无到有创建了知乎 Redis 平台,承载了知乎高速增加的业务流量。程序员
知乎做为知名中文知识内容平台,每日处理的访问量巨大 ,如何更好的承载这样巨大的访问量,同时提供稳定低时延的服务保证,是知乎技术平台同窗须要面对的一大挑战。
知乎存储平台团队基于开源 Redis 组件打造的 Redis 平台管理系统,通过不断的研发迭代,目前已经造成了一整套完整自动化运维服务体系,提供一键部署集群,一键自动扩缩容, Redis 超细粒度监控,旁路流量分析等辅助功能。
目前,Redis 在知乎的应用规模以下:
github
根据业务的需求,咱们将Redis实例区分为单机(Standalone)和集群(Cluster)两种类型,单机实例一般用于容量与性能要求不高的小型存储,而集群则用来应对对性能和容量要求较高的场景。
而在集群(Cluster)实例类型中,当实例须要的容量超过 20G 或要求的吞吐量超过 20万请求每秒时,咱们会使用集群(Cluster)实例来承担流量。集群是经过中间件(客户端或中间代理等)将流量分散到多个 Redis 实例上的解决方案。知乎的 Redis 集群方案经历了两个阶段:客户端分片(2015年前使用的方案)与 Twemproxy 代理(2015年至今使用的方案)。
下面将分别来介绍这两个类型的Redis实例在知乎的应用实践状况。web
对于单机实例,咱们采用原生主从(Master-Slave)模式实现高可用,常规模式下对外仅暴露 Master 节点。因为使用原生 Redis,因此单机实例支持全部 Redis 指令。
对于单机实例,咱们使用 Redis 自带的哨兵(Sentinel)集群对实例进行状态监控与 Failover。Sentinel 是 Redis 自带的高可用组件,将 Redis 注册到由多个 Sentinel 组成的 Sentinel 集群后,Sentinel 会对 Redis 实例进行健康检查,当 Redis 发生故障后,Sentinel 会经过 Gossip 协议进行故障检测,确认宕机后会经过一个简化的 Raft 协议来提高 Slave 成为新的 Master。
一般状况咱们仅使用 1 个 Slave 节点进行冷备,若是有读写分离请求,能够创建多个 Read only slave 来进行读写分离。
<ignore_js_op>
如上图所示,经过向 Sentinel 集群注册 Master 节点实现实例的高可用,当提交 Master 实例的链接信息后,Sentinel 会主动探测全部的 Slave 实例并创建链接,按期检查健康状态。客户端经过多种资源发现策略如简单的 DNS 发现 Master 节点,未来有计划迁移到如 Consul 或 etcd 等资源发现组件 。
当 Master 节点发生宕机时,Sentinel 集群会提高 Slave 节点为新的 Master,同时在自身的 pubsub channel +switch-master 广播切换的消息,具体消息格式为:redis
switch-master <master name> <oldip> <oldport> <newip> <newport>算法
watcher 监听到消息后,会去主动更新资源发现策略,将客户端链接指向新的 Master 节点,完成 Failover,具体 Failover 切换过程详见 Redis 官方文档(Redis Sentinel Documentation - Redis)。
实际使用中须要注意如下几点:
后端
早期知乎使用 redis-shard 进行客户端分片,redis-shard 库内部实现了 CRC3二、MD五、SHA1 三种哈希算法 ,支持绝大部分 Redis 命令。使用者只需把 redis-shard 当成原生客户端使用便可,无需关注底层分片。
<ignore_js_op>
基于客户端的分片模式具备以下优势:
缓存
可是也存在以下问题:
安全
具体特色详见:github.com/zhihu/redis…
早期知乎大部分业务由 Python 构建,Redis 使用的容量波动较小, redis-shard 很好地应对了这个时期的业务需求,在当时是一个较为不错解决方案。服务器
2015 年开始,业务上涨迅猛,Redis 需求暴增,原有的 redis-shard 模式已经没法知足日益增加的扩容需求,咱们开始调研多种集群方案,最终选择了简单高效的 Twemproxy 做为咱们的集群方案。
由 Twitter 开源的 Twemproxy 具备以下优势:
具体特色详见:github.com/twitter/twe…
可是缺点也很明显:
对此,咱们将集群实例分红两种模式,即缓存(Cache)和存储(Storage):
若是使用方能够接收经过损失一部分少许数据来保证可用性,或使用方能够从其他存储恢复实例中的数据,这种实例即为缓存,其他状况均为存储。
咱们对缓存和存储采用了不一样的策略,请继续往下读。
<ignore_js_op>
对于存储咱们使用 fnv1a_64 算法结合 modula 模式即 取模哈希对 Key 进行分片,底层 Redis 使用 单机模式结合 Sentinel 集群实现高可用,默认使用 1 个 Master 节点和 1 个 Slave 节点提供服务,若是业务有更高的可用性要求,能够拓展 Slave 节点。
当集群中 Master 节点宕机,按照单机模式下的高可用流程进行切换,Twemproxy 在链接断开后会进行重连,对于存储模式下的集群,咱们不会设置 auto_eject_hosts, 不会剔除节点。
同时,对于存储实例,咱们默认使用 noeviction 策略,在内存使用超过规定的额度时直接返回 OOM 错误,不会主动进行 Key 的删除,保证数据的完整性。
因为 Twemproxy 仅进行高性能的命令转发,不进行读写分离,因此默认没有读写分离功能,而在实际使用过程当中,咱们也没有遇到集群读写分离的需求,若是要进行读写分离,可使用资源发现策略在 Slave 节点上架设 Twemproxy 集群,由客户端进行读写分离的路由。
考虑到对于后端(MySQL/HBase/RPC 等)的压力,知乎绝大部分业务都没有针对缓存进行降级,这种状况下对缓存的可用性要求较数据的一致性要求更高,可是若是按照存储的主从模式实现高可用,1 个 Slave 节点的部署策略在线上环境只能容忍 1 台物理节点宕机,N 台物理节点宕机高可用就须要至少 N 个 Slave 节点,这无疑是种资源的浪费。
<ignore_js_op>
因此咱们采用了 Twemproxy 一致性哈希(Consistent Hashing)策略来配合 auto_eject_hosts 自动弹出策略组建 Redis 缓存集群。
对于缓存咱们仍然使用使用 fnv1a_64 算法进行哈希计算,可是分布算法咱们使用了 ketama 即一致性哈希进行 Key 分布。缓存节点没有主从,每一个分片仅有 1 个 Master 节点承载流量。
Twemproxy 配置 auto_eject_hosts 会在实例链接失败超过 server_failure_limit 次的状况下剔除节点,并在 server_retry_timeout 超时以后进行重试,剔除后配合 ketama 一致性哈希算法从新计算哈希环,恢复正常使用,这样即便一次宕机多个物理节点仍然能保持服务。
<ignore_js_op>
在实际的生产环境中须要注意如下几点:
在方案早期咱们使用数量固定的物理机部署 Twemproxy,经过物理机上的 Agent 启动实例,Agent 在运行期间会对 Twemproxy 进行健康检查与故障恢复,因为 Twemproxy 仅提供全量的使用计数,因此 Agent 运行时还会进行定时的差值计算来计算 Twemproxy 的 requests_per_second 等指标。
后来为了更好地故障检测和资源调度,咱们引入了 Kubernetes,将 Twemproxy 和 Agent 放入同一个 Pod 的两个容器内,底层 Docker 网段的配置使每一个 Pod 都能得到独立的 IP,方便管理。
最开始,本着简单易用的原则,咱们使用 DNS A Record 来进行客户端的资源发现,每一个 Twemproxy 采用相同的端口号,一个 DNS A Record 后面挂接多个 IP 地址对应多个 Twemproxy 实例。
初期,这种方案简单易用,可是到了后期流量日益上涨,单集群 Twemproxy 实例个数很快就超过了 20 个。因为 DNS 采用的 UDP 协议有 512 字节的包大小限制,单个 A Record 只能挂接 20 个左右的 IP 地址,超过这个数字就会转换为 TCP 协议,客户端不作处理就会报错,致使客户端启动失败。
当时因为状况紧急,只能创建多个 Twemproxy Group,提供多个 DNS A Record 给客户端,客户端进行轮询或者随机选择,该方案可用,可是不够优雅。
以后咱们修改了 Twemproxy 源码, 加入 SO_REUSEPORT 支持。
Twemproxy with SO_REUSEPORT on Kubernetes:
<ignore_js_op>
同一个容器内由 Starter 启动多个 Twemproxy 实例并绑定到同一个端口,由操做系统进行负载均衡,对外仍然暴露一个端口,可是内部已经由系统均摊到了多个 Twemproxy 上。
同时 Starter 会定时去每一个 Twemproxy 的 stats 端口获取 Twemproxy 运行状态进行聚合,此外 Starter 还承载了信号转发的职责。
原有的 Agent 不须要用来启动 Twemproxy 实例,因此 Monitor 调用 Starter 获取聚合后的 stats 信息进行差值计算,最终对外界暴露出实时的运行状态信息。
咱们在 2015 年调研过多种集群方案,综合评估多种方案后,最终选择了看起来较为陈旧的 Twemproxy 而不是官方 Redis 集群方案与 Codis,具体缘由以下:
1)MIGRATE 形成的阻塞问题:
Redis 官方集群方案使用 CRC16 算法计算哈希值并将 Key 分散到 16384 个 Slot 中,由使用方自行分配 Slot 对应到每一个分片中,扩容时由使用方自行选择 Slot 并对其进行遍历,对 Slot 中每个 Key 执行 MIGRATE 命令进行迁移。
调研后发现,MIGRATE 命令实现分为三个阶段:
通过调研,咱们认为这种模式并不适合知乎的生产环境。Redis 为了保证迁移的一致性, MIGRATE 全部操做都是同步操做,执行 MIGRATE 时,两端的 Redis 均会进入时长不等的 BLOCK 状态。
对于小 Key,该时间能够忽略不计,但若是一旦 Key 的内存使用过大,一个 MIGRATE 命令轻则致使 P95 尖刺,重则直接触发集群内的 Failover,形成没必要要的切换
同时,迁移过程当中访问处处于迁移中间状态的 Slot 的 Key 时,根据进度可能会产生 ASK 转向,此时须要客户端发送 ASKING 命令到 Slot 所在的另外一个分片从新请求,请求时延则会变为原来的两倍。
一样,方案初期时的 Codis 采用的是相同的 MIGRATE 方案,可是使用 Proxy 控制 Redis 进行迁移操做而非第三方脚本(如 redis-trib.rb),基于同步的相似 MIGRATE 的命令,实际跟 Redis 官方集群方案存在一样的问题。
对于这种 Huge Key 问题决定权彻底在于业务方,有时业务须要不得不产生 Huge Key 时会十分尴尬,如关注列表。一旦业务使用不当出现超过 1MB 以上的大 Key 便会致使数十毫秒的延迟,远高于平时 Redis 亚毫秒级的延迟。有时,在 slot 迁移过程当中业务不慎同时写入了多个巨大的 Key 到 slot 迁移的源节点和目标节点,除非写脚本删除这些 Key ,不然迁移会进入进退两难的地步。
对此,Redis 做者在 Redis 4.2 的 roadmap 中提到了 Non blocking MIGRATE 可是截至目前,Redis 5.0 即将正式发布,仍未看到有关改动,社区中已经有相关的 Pull Request ,该功能可能会在 5.2 或者 6.0 以后并入 master 分支,对此咱们将持续观望。
2)缓存模式下高可用方案不够灵活:
还有,官方集群方案的高可用策略仅有主从一种,高可用级别跟 Slave 的数量成正相关,若是只有一个 Slave,则只能容许一台物理机器宕机, Redis 4.2 roadmap 提到了 cache-only mode,提供相似于 Twemproxy 的自动剔除后重分片策略,可是截至目前仍未实现。
3)内置 Sentinel 形成额外流量负载:
另外,官方 Redis 集群方案将 Sentinel 功能内置到 Redis 内,这致使在节点数较多(大于 100)时在 Gossip 阶段会产生大量的 PING/INFO/CLUSTER INFO 流量,根据 issue 中提到的状况,200 个使用 3.2.8 版本节点搭建的 Redis 集群,在没有任何客户端请求的状况下,每一个节点仍然会产生 40Mb/s 的流量,虽然到后期 Redis 官方尝试对其进行压缩修复,但按照 Redis 集群机制,节点较多的状况下不管如何都会产生这部分流量,对于使用大内存机器可是使用千兆网卡的用户这是一个值得注意的地方。
4)slot 存储开销:
最后,每一个 Key 对应的 Slot 的存储开销,在规模较大的时候会占用较多内存,4.x 版本之前甚至会达到实际使用内存的数倍,虽然 4.x 版本使用 rax 结构进行存储,可是仍然占据了大量内存,从非官方集群方案迁移到官方集群方案时,须要注意这部分多出来的内存。
总之,官方 Redis 集群方案与 Codis 方案对于绝大多数场景来讲都是很是优秀的解决方案,可是咱们仔细调研发现并非很适合集群数量较多且使用方式多样化的咱们,场景不一样侧重点也会不同,但在此仍然要感谢开发这些组件的开发者们,感谢大家对 Redis 社区的贡献。
对于单机实例,若是经过调度器观察到对应的机器仍然有空闲的内存,咱们仅需直接调整实例的 maxmemory 配置与报警便可。一样,对于集群实例,咱们经过调度器观察每一个节点所在的机器,若是全部节点所在机器均有空闲内存,咱们会像扩容单机实例同样直接更新 maxmemory 与报警。
可是当机器空闲内存不够,或单机实例与集群的后端实例过大时,没法直接扩容,须要进行动态扩容:
Resharding 过程:
<ignore_js_op>
原生 Twemproxy 集群方案并不支持扩容,咱们开发了数据迁移工具来进行 Twemproxy 的扩容,迁移工具本质上是一个上下游之间的代理,将数据从上游按照新的分片方式搬运到下游。
原生 Redis 主从同步使用 SYNC/PSYNC 命令创建主从链接,收到 SYNC 命令的 Master 会 fork 出一个进程遍历内存空间生成 RDB 文件并发送给 Slave,期间全部发送至 Master 的写命令在执行的同时都会被缓存到内存的缓冲区内,当 RDB 发送完成后,Master 会将缓冲区内的命令及以后的写命令转发给 Slave 节点。
咱们开发的迁移代理会向上游发送 SYNC 命令模拟上游实例的 Slave,代理收到 RDB 后进行解析,因为 RDB 中每一个 Key 的格式与 RESTORE 命令的格式相同,因此咱们使用生成 RESTORE 命令按照下游的 Key 从新计算哈希并使用 Pipeline 批量发送给下游。
等待 RDB 转发完成后,咱们按照新的后端生成新的 Twemproxy 配置,并按照新的 Twemproxy 配置创建 Canary 实例,从上游的 Redis 后端中取 Key 进行测试,测试 Resharding 过程是否正确,测试过程当中的 Key 按照大小,类型,TTL 进行比较。
测试经过后,对于集群实例,咱们使用生成好的配置替代原有 Twemproxy 配置并 restart/reload Twemproxy 代理,咱们修改了 Twemproxy 代码,加入了 config reload 功能,可是实际使用中发现直接重启实例更加可控。而对于单机实例,因为单机实例和集群实例对于命令的支持不一样,一般须要和业务方肯定后手动重启切换。
因为 Twemproxy 部署于 Kubernetes ,咱们能够实现细粒度的灰度,若是客户端接入了读写分离,咱们能够先将读流量接入新集群,最终接入所有流量。
这样相对于 Redis 官方集群方案,除在上游进行 BGSAVE 时的 fork 复制页表时形成的尖刺以及重启时形成的链接闪断,其他对于 Redis 上游形成的影响微乎其微。
这样扩容存在的问题:
1)对上游发送 SYNC 后,上游 fork 时会形成尖刺:
2)切换过程当中有可能写到下游,而读在上游:
3)一致性问题,两条具备前后顺序的写同一个 Key 命令在切换代理后端时会经过 1)写上游同步到下游 2)直接写到下游两种方式写到下游,此时,可能存在应先执行的命令却经过 1)执行落后于经过 2)执行,致使命令前后顺序倒置:
实际使用过程当中,若是上游分片安排合理,可实现数千万次每秒的迁移速度,1TB 的实例 Resharding 只须要半小时左右。另外,对于实际生产环境来讲,提早作好预期规划比遇到问题紧急扩容要快且安全得多。
因为生产环境调试须要,有时会须要监控线上 Redis 实例的访问状况,Redis 提供了多种监控手段,如 MONITOR 命令。
但因为 Redis 单线程的限制,致使自带的 MONITOR 命令在负载太高的状况下会再次跑高 CPU,对于生产环境来讲过于危险,而其他方式如 Keyspace Notify 只有写事件,没有读事件,没法作到细致的观察。
对此咱们开发了基于 libpcap 的旁路分析工具,系统层面复制流量,对应用层流量进行协议分析,实现旁路 MONITOR,实测对于运行中的实例影响微乎其微。
同时对于没有 MONITOR 命令的 Twemproxy,旁路分析工具仍能进行分析,因为生产环境中绝大部分业务都使用 Kubernetes 部署于 Docker 内 ,每一个容器都有对应的独立 IP,因此可使用旁路分析工具反向解析找出客户端所在的应用,分析业务方的使用模式,防止不正常的使用。
因为 Redis 5.0 发布在即,4.0 版本趋于稳定,咱们将逐步升级实例到 4.0 版本,由此带来的如 MEMORY 命令、Redis Module 、新的 LFU 算法等特性不管对运维方仍是业务方都有极大的帮助。
知乎架构平台团队是支撑整个知乎业务的基础技术团队,开发和维护着知乎几乎全量的核心基础组件,包括容器、Redis、MySQL、Kafka、LB、HBase 等核心基础设施,团队小而精,每一个同窗都独当一面负责上面提到的某个核心系统。
随着知乎业务规模的快速增加,以及业务复杂度的持续增长,咱们团队面临的技术挑战也愈来愈大,欢迎对技术感兴趣、渴望技术挑战的小伙伴加入咱们,一块儿建设稳定高效的知乎云平台。
转自https://juejin.im/entry/5ba07c9c5188255c7663eecb