分布式系统之缓存的微观应用经验谈(二) [主从和主备高可用篇]
前言
近几个月一直在忙些琐事, 几乎年后都没怎么闲过. 忙忙碌碌中就进入了 2018 年的秋天了, 不得不感叹时间总是如白驹过隙, 也不知道收获了什么和失去了什么. 最近稍微休息, 买了两本与技术无关的书, 其一是 Yann Martel 写的《The High Mountains of Portugal》(葡萄牙的高山), 发现阅读此书是需要一些耐心的, 对人生暗喻很深, 也有足够的留白, 有兴趣的朋友可以细品下. 好了, 下面回归正题, 尝试写写工作中缓存技术相关的一些实战经验和思考.
正文
在分布式 web 程序设计中, 解决高并发以及内部解耦的关键技术离不开缓存和队列, 而缓存角色类似计算机硬件中 CPU 的各级缓存. 如今的业务规模稍大的互联网项目, 即使在最初 beta 版的开发上, 都会进行预留设计. 但是在诸多应用场景里, 也带来了某些高成本的技术问题, 需要细致权衡. 本系列主要围绕分布式系统中服务端缓存相关技术, 也会结合朋友间的探讨提及自己的思考细节. 文中若有不妥之处, 恳请指正.
为了方便独立成文, 原谅在内容排版上的一点点个人强迫症.
第二篇这里尝试聊聊缓存的主从(Master-Slave), 以及相关的高可用实现(High-Availability)(具体应用依然以 Redis 举例)
另见: 分布式系统之缓存的微观应用经验谈(一) [基础细节篇] (https://www.cnblogs.com/bsfz/p/9568591.html )
一, 先简单谈谈主从分离(Master-Slave)
(注: 由于目前个人工作中大多数情况应用的是 Redis 3.x, 以下若有特性关联, 均是以此作为参照说明.)
1.1 关于主从分离的取舍观点
是否采用主从分离(这里特指读写分离), 个人目前的观点是, 它在很多场景里, 并不是一个很好的方案.
我更想说的是, 甚至任何涉及数据同步的环节, 包括 DB 读写分离, 缓存数据复制等等, 如果没有特殊场景强制要求, 那么尽量规避.
虽然在互联网的一些应用场景里, 读远大于写, 也演变了一套看似完整的读写实践方案, 大体归为 "主写从读","一写多读","多读多写". 但本质上, 在大多数环境下, 均存在一定缺陷, 无论是基于服务底层的数据同步延迟, 还是开发中的逻辑切换, 都带来了一些可能本末倒置的隐患和生硬. 同时, 在集群模式下读写分离成本实在过高, 不仅仅是一致性问题, 必须慎重考虑和权衡.
如果没明白读写分离方案基于什么本质什么条件, 包含哪些细节问题, 那你的设计可能就很勉强, 甚至出现某些关键问题时, 反而很难去分析和解决. 去年跟一前辈朋友取经, 他们一个业务服务兜兜转转实际测出的结果是读 QPS 约 2000, 写 QPS 不到 500, 这些的分离哭笑不得, 程序性能也没得到优化, 反而增加了完全浪费的技术成本, 以及因为读写分离带来的程序本不该处理的例外问题, 也是折腾.
1.2 主从分离的一些细节
以 Redis 为例, 每个 Redis 实例 (Instance) 都是作为一个节点(node), 并且默认主节点(master node), 它们均可以手动转换为从节点(slave node). 每个 slave node 只能隶属于一个 master node, 而每个 master node 可以拥有 n 个 slave node. 任何主从同步都离不开复制的概念, 在 Redis 中主要命令是 slaveof host port (指定一个 master 即可), 这样 master node 的数据将自动异步的同步到各个 slave 中, 这算是最基本的形态了.
在进行相关软性配置时, 个人推荐最好保持配置文件 (config-file) 的一致, 这里指多个 salve node. 对于 slave node 的操作默认是只读(read-only), 也建议保持这个设置. 如果更改为可写权限, 那么对 slave node 的修改是不会反向同步至 master node 中的, 而且就算通过其他方式实现了反向同步, 中间将大量存在类似传统 RDBMS 里的幻读问题, 这里并发不大但足够繁琐, 追踪到具体原因也是得不偿失.(而对应程序开发中, 往往写操作也是都直接进 master node 执行.)
另外顺便提下, 主从的硬件配置可以一致, 但是我依然推荐 slave node 可以稍微高一些. 另外, 稍微注意, 从节点内存尽量不要小于主节点的预算内存.
对于 node 之间的数据延迟问题, 外在因素一般都是网络 I/O 影响为主, CPU 影响为次, 换句话说, 往往 CPU 的负载都足够(详见上一篇中提到的一些关联处), 而网络 I/O 则会比较明显. 在部署时候, 没有特殊场景, 都是同机房内联而对外隔离, 但即使这样, 也需要额外注意延迟的接收程度, 每次同步复制的 TCP 数据包, 并非是真正的实时处理, 这个类似于之前提到的 AOF 和 RDB 的设计思想, 分为实时复制和间隔性复制, 前者更及时但带宽消耗大, 而后者正好相反. 在 Redis 中主要以 repl-disable-tcp-nodelay 切换, 默认使用前者, 个人也较为推荐, 但是在单次数据量较频较大, 业务场景的时效要求不高, 完全可以设置为后者, 从而节省不少性能, 也连锁提升了一定稳定性.
对于拓扑结构的设计, 应用最多的就是单层拓扑, 针对大量类似 keys 全表扫描的操作, slave node 会做到分担性能压力的作用. 如果还想极致一些, 把整体阻塞降到最低, 可以将拓扑结构转换为树状, 最简单的做法, 将某个 slave node 直接转移到最底部, 但会带来更多时效上的牺牲, 所以需要考虑场景的接受程度了. 同时, 这里可能在具体架构落地的环节里, 会比较分身乏术, 需要开始考虑交由专业的运维来参与部署了, 涉及上下节点间的通信, 带宽监控, 级联之间的复制问题, 以及一些更独立的高频率统计和管理. ps 下, 这点一直作为备用, 但在截止到目前的工作生涯里还没有找到必要的机会去采用.
对于复制 / 同步数据本身, 无论是全量, 还是增量, 由于异步性 (个人认为不可能设计为同步) 和一定的时效损耗, 必定存在一个偏移值(offset). 以 Redis 举例, master node 和 slave node 中, 各自对自己的当前复制时的 offset 做记录, 如果两个角色的偏移值存在较大差异(可参考查询对应 repl_offset), 那么大概率存在频繁的阻塞, 包括网络和自身脚本命令的阻塞. 一般内部网络都是专线环境, 并且都是独立部署, 所以优先排查命令执行效率, 和不必要的扫描问题(可参考上篇讨论). 但是无论如何, 延迟或者说偏移过大的问题, 总不可能完全规避, 所以在开发里要么利用专业的监控服务, 要么使用不同驱动库定时判断, 这也无疑增加了编码复杂度, 哪怕一些开源库已经尽力做了一些工作.
二, 尝试谈谈相关的高可用(High-Availability)
缓存既然作为一种通用的中间件 (当然, 某些场景里也可能是最后一层, 见第一篇), 决定了在诸多场景里其交互频率(包括 QPS) 大多远远高于其他服务, 这就需要其具备极高的稳定性, 可用性. 个人在前面阐述了一些关于主从分离的细节, 下面尝试谈谈相关的 HA 方案和一些思考.
2.1 关于高可用说明
这里声明下, 我认为主从分离和高可用本质上是没有任何一丝关系的, 只是有些刚刚好的作用使之结合形成了一些 HA 方案.
前面提到过, 单个相对可靠的缓存服务, 除了本身所在服务器自身的内存负载, 设计时更需要充分考虑网络 I/O,CPU 的负载, 以及某些场景下的磁盘 I/O 的代价. 而这些条件全部都会拥有瓶颈, 除此, 你永远无法避免的问题还有服务器造成的直接宕机, 服务自身的缺陷造成的某些时候的不可用 (单点问题) 等等. 一套相对能够落地的高可用缓存方案, 必然还需要拥有足够健壮的承载和相对完善的内部故障转移机制, 从而达到对外提供的是整套程序化的高可用的缓存服务.
2.2 实现 HA 的原始步骤
以 Redis 为例, 其本身的设计已经足够优秀和成熟, 但在负载过大导致延迟过高, 甚至崩溃的过程里, 比较原始的方式是这样去操作: 将一个关联的备用 slave node 升级为 master node, 可以一个 slaveof no one 基本处理. 然后分析是否还做了业务层面上的主从分离, 如果存在, 那么还需要手工修改其他 slave node 里的旧 master node 指向, 映射为当前 master node. 最后, 当 master node 重新上线时, 修改自身角色并重新加入集群.
2.3 谈谈程序化 HA 方案的部分实践
上面提到的主干思路看起来并不复杂, 但实际以人工去操作每一个环节所需要解决的问题往往不止这些, 比如对于 node 的不可用的判定, 确认后的选举逻辑, 程序客户端的事件通知处理, 服务的同步处理细节等. 在 Redis 中比较成熟的 HA 方案, 目前主要包括依赖独立 node 的 Sentinel 和自身基于 Gossip 传播的 Cluster 方案.
从宏观角度来谈, Sentinel 和 Cluster 对于 HA 的设计均有互相借鉴, 但后者 Cluster 更多是偏向提供一套可行性集群分片方案, 与这里主题关联性不是很大(后续我会尝试单独写一篇, 这里不延伸), 围绕 Sentinel 的 HA 实践更直接.
Sentinel 的本质逻辑就是对所有 node 作定期巡视, 当发现存在共同投票认为不可达的 master node, 会对其做下线标识, 同时进行必要的 master 选举升级, 并将事件状态返回给信号方及客户端. Sentinel 的故障转移是针对 master node 的, 通常是把 slave node 作为 master 的一个热备.
这里依然以 Redis 3.x 为主, 在 Redis Sentinel 方案里, 通常指 n 个 Sentinel node 来自动监听 Redis 普通 node. 准确的说, 每个 Sentinel node 其实会监听任何一个 node, 包括其他 Sentinel node.
对于选举和审定的控制, 可以调整配置 monitor 的 quorum 来确认严格性, 比如, 在大多数场景里, 设置为 (n / 2 + 1) 个, 这样代表过半的票数认同, 即认为指定 node 当前宕机. 同时, 当需要选举新的领导者 master, 这样也至少是趋势性客观判断. 是否可以设置更小? 当然可以, 只是要注意的一个问题是, 这样对失败的认定流程更短更快, 但是误差也相对越大了, 需要看看场景是否适配. 个人在权衡时会尽量优先设置为前者.
对于内部故障转移自然可以得到相应的事件通知, 一般还可以写入到对应执行脚本, 理论上会适合自动化这块, 但个人目前尚未应用, 这个偏向纯运维了, 个人这里依然保持针对架构和开发来做一些讨论.
对于监听通信, 可以适度调整 failover-timeout 等相关配置, 这里并没有相应的计算方式, 在大多数情况没太多讲究, 但是也需要关注不能过度调整. 个人目前采取的方式是, 优先设置一个较大值, 比如审定时间 30 秒, 五个实例, 那么同步转移超时时间则不低于 150 秒.
对于选举完成后, 发起新的数据复制流程, 由于 master node 会面向多个 slave node 进行瞬间同步, 默认并发复制, 而很多时候服务器环境有限, 没有很足够的配置, 甚至经常同一服务器上存在几个理想上本应该独立的服务, 这里则需要重点考虑下网络 IO 和磁盘 IO 的问题, 根据实际情况临时调整, 除此之外, 在高峰时这也起到了限流作用.
额外再强调一下, 基于主从的 HA 方案, 依然存在 master node 同步到 slave node 的延迟问题, 这个基本是任何类似热备方案均存在的问题, 系统交互越是密集, 或者 slave node 的不断增加, 都会明显增大这个延迟, 所以在权衡的时候, 需要考虑业务的初衷, 到底能够接受到什么程度.
任何服务里的应用, 都不是看起来越多越好. 假如你打算手动实现一套自定义的 HA 方案, 或者相关的热备思路, 你甚至可以考虑在业务程序里, 具体点可以直接是在相关的驱动库 (比如 JAVA 的 Jedis, 和. Net 的 StackExchange.Redis) 修改, 插入数据的同时, 插入到另一个备用库中. 这在一些非缓存场景里, 也有类似设计, 并不是一定不被采用的, 毕竟架构设计的初衷一定是考虑整体可行性和利弊权衡.
结语
来源: https://www.cnblogs.com/bsfz/p/9769503.html