zoukankan      html  css  js  c++  java
  • 【整理】互联网服务端技术体系:高可用之复制技术

    数据拷贝是互联网世界的一半天空。

    综述入口见:“互联网应用服务端的常用技术思想与机制纲要”

    引子

    高可用的基础是冗余。而冗余的基础是复制技术。

    本文总结复制相关的技术。主要包括如下内容:

    • 数据拷贝:复制技术的基础,主要包括赋值与 IO 操作;
    • 复制机制:复制思路、复制延迟及方案、复制容错;
    • 复制实现:MySQL 和 Redis 的复制机制。

    数据拷贝

    赋值

    数据在内存中的两个变量之间进行拷贝。赋值可分为深拷贝和浅拷贝。浅拷贝只拷贝数据的引用(引用两份,数据一份),引用的数据是共享的;深拷贝则是创建了完全相同的数据,即存在两份同样的数据,引用的数据是隔离互不影响的。

    深拷贝

    要特别注意的是,对象的引用拷贝是浅拷贝,深拷贝意味着引用的对象也要拷贝。简单粗暴的深拷贝可以使用 JSON 作为中间变量,对象转 JSON ,JSON 再转对象。适合比较简单的情形。

    深拷贝的算法如下:

    • 简单情形:如果是无嵌套的对象,则该对象要实现 clone 接口,实现克隆能力;
    • 稍复杂情形:如果对象是含有子对象的嵌套对象,则嵌套的子对象也要实现 clone 接口,实现克隆能力,然后递归克隆;
    • 数组情形:如果数组的元素是原子类型,则建新的数组,然后依次拷贝值即可;如果数组的元素是对象,则对象要实现 clone 接口,实现克隆能力,依次将每个指向的对象克隆,然后新的数组的引用指向克隆的新对象。

    IO

    数据在网络/磁盘和内存之间传输。Linux 将磁盘、网络等设备抽象为文件,从而能够用统一的方式来处理 IO。

    要访问一个设备时,内核返回一个非负整数的文件描述符(进程中当前没有打开的最小描述符),用来操作这个对应于设备的文件。内核中保持一个偏移量,用来定位读写到的文件位置。

    一个打开的文件包括三部分:描述符(文件的引用,进程专享)、文件表项(文件位置、引用计数等,所有进程共享)、v_node 引用项(指向所有进程共享的 v_node 表,存放文件元数据)。

    IO 要考虑效率,使用缓冲机制、 IO 中断和 DMA 技术来实现。同时,IO 也要考虑健壮性,处理各种读写不足情况。

    缓冲机制

    数据先从网络/磁盘拷贝到内核的数据缓冲区,再拷贝到用户程序空间的数据区。反之亦然。用户程序只与操作系统的数据缓冲区打交道,而与磁盘传输解耦。还有一种直接 IO 的方式,应用程序不经过内核缓冲区来传输数据,需要自己去设计一套缓存机制,比如 DBMS 。

    I/O 中断与 DMA

    PIO:CPU 发出 IO 请求,磁盘将数据放入磁盘缓冲区,然后向 CPU 发出中断信号,CPU 将数据从磁盘缓冲区拷贝到内核缓冲区再拷贝到用户缓冲区,全程需要耗费 CPU 资源;

    DMA:磁盘将数据放入磁盘缓冲区后,系统主内存与磁盘或网络之间的数据传输可以绕开 CPU 的全程调度,DMA 会与外部设备交互,完成数据在外部设备和内核缓冲区之间的传输。

    **IO 健壮性及 API **

    • 健壮性。 IO 读写会遇到不足值(传输字节数比所需要的少)的问题。磁盘 IO 遇到不足值通常是 EOF,而网络传输考虑缓冲约束和延迟,要频繁处理不足值问题。这就需要一个健壮读写 IO 的系统函数 RIO。
    • IO SDK。磁盘读写首选标准 IO, 网络读写首选 RIO。标准 IO 和 RIO 都难以满足需求时,使用系统 IO。

    零拷贝技术

    即使是 DMA ,也会多一步内核缓冲区与用户缓冲区之间的传输开销,耗费 CPU。零拷贝技术是为了避免这层开销。主要通过 mmap 来实现。 mmap 是基于虚拟内存管理实现的。mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了数据在内核读缓冲区(read buffer)到用户缓冲区(user buffer)之间的传输过程。

    复制机制

    分布式冗余机制:数据分片存储、冗余副本、宕机检测、自动副本迁移、多余副本删除。其中冗余副本和副本迁移均需要进行复制。

    复制思路

    • 复制拓扑:主从 Leader - Followers 或 Master - Slave ; 多主 Multi-Leaders ; 无主 Leaderless 。大多数采用的是主从拓扑。
    • 复制依据:复制通常要根据某个类似日志文件来进行。MySQL 基于 binlog 日志,Redis 基于 redis 写命令的快照文件。可以基于语句、二进制日志、逻辑日志。
    • 复制方式:同步或异步。同步可以保证至少有一个 backup 可用,但会阻塞写操作。由于复制节点的不可靠、网络延迟等可能导致复制节点回复时间长,因此,复制操作通常采用异步机制。复制可以自动配置、基于条件触发。

    复制步骤

    历史数据复制和增量复制。主要分两个步骤:

    • STEP1:首先 Leader 生成一份某个时间点的快照文件,发送给 Follower , Follower 执行快照文件里的所有操作变更;
    • STEP2:Follower 请求自快照开始创建时间点之后的所有数据变更请求,并以此处理后,就与 Leader 的数据保持一致了。

    历史数据往往采用快照文件机制,而增量复制则采用偏移量加复制缓冲区方式。

    复制延迟

    复制延迟会导致主从节点的数据在一段时间内的不一致,这种不一致会给用户带来困扰。在实现最终一致性时,需要评估复制延迟对系统的影响,并判断需要作出哪些一致性的保证。

    Read-After-Write

    写数据后要求立即能读到。方案主要有

    • 可以检测数据是否被修改过,若修改过读主,否则读从。可以通过 profile 信息来记录和判断是否修改过;
    • 监控主从延迟。 在延迟内读主,延迟外读从;
    • 客户端记录最近更新的时间戳,服务端可以根据这个时间戳决定由主还是由某个已更新的从来响应读。

    跨设备或跨数据中心的 Read-After-Write 一致性需要考虑更多的因素,比如节点的时钟不同步。

    Monitonic Reads

    复制延迟可能导致在不同的 Followers 上读到的数据不一致。比如用户反复刷新页面时,在一个已更新的 Follower 读到最新数据,随后在一个没有更新的 Follower 读不到数据,就像时间回退了一样(Moving Backward in time)。方案:保证同一个请求被转发到同一个 Follower,比如使用 ID 的哈希映射到特定节点上。

    Consistent Prefix Reads

    复制延迟可能导致果在因之前看到。

    复制容错

    复制容错主要是基于分布式容错机制。

    • Follow Failure : Follow 会持续保存一份读取从 Leader 接收的 replica log ,请求自 Failed 的时间点之后的数据变更,并执行到所在节点。

    • Leader Failure: 首先要基于一致性协议选择一个新的 Leader ,并配置使用新的 Leader 。客户端的写请求会发给新的 Leader 。Leader Failure 需要考虑的问题 -- 判断原来 Leader 宕机的超时时间选择; 新 Leader 不一定跟上了原来 Leader 的数据更新;有多个新 Leader(脑裂)。


    复制机制实现

    MySQL复制机制

    • 复制用途:多数据中心的备份、密集读操作的负载均衡、避免单点失败、故障切换、升级测试。
    • 复制方案:选择性复制-分库分表复制-水平数据划分、OLTP 与 OLAP 功能分离、备库上实现数据归档、日志服务器。
    • 复制基础:binlog 日志。在主库记录 binlog 日志( SQL 或 被更新的数据记录),在备库重放日志的方式实现异步的数据复制。可以将读操作指向备库,从而增强读操作的可扩展性。
    • 复制开销:读取二进制日志、锁竞争、主库处于高吞吐量时。

    复制拓扑

    一个主库可以对应多个备库,一个备库只能对应一个主库且必须有唯一的服务器ID(唯一ID用于打破一些复制无限循环)。

    备库可以用于:不同角色使用、待用主库、容灾、数据恢复、培训测试等。常用推荐拓扑:一主多备、主动-被动模式下的主-主复制、主库-分发主库-备库、树形。

    复制步骤

    参与者:主库线程,备库 IO 线程, 备库 SQL 线程; binlog 日志, relaylog 日志。

    • STEP1:主库在每次准备提交事务前,按照事务提交的顺序将数据更新的事件记录到 binlog 中。记录 binlog 后,通知引擎提交事务;
    • STEP2:备库 IO 线程将主库的 binlog 复制到本地的 relaylog 里。步骤为:备库起IO线程 -> 与主库建立客户端连接 -> 主库起特殊的二进制转储线程读取主库的二进制日志 -> 备库IO线程将二进制日志读取到 relaylog 里;
    • STEP3:备库 SQL 线程从 relaylog 里读取事件并在备库执行。

    复制方式

    • 基于语句的复制:实现简单、二进制日志紧凑,占用存储和网络带宽很少、操作灵活、容易定位问题。缺点是:语句执行是否成功及快慢、结果可能依赖环境因素、使用触发器或存储过程的语句的复制可能失败、执行代价很高但只选择或更新很少行的语句复制成本高、只能串行需要加更多锁。mysqlbinlog 工具是基于语句复制的好工具;
    • 基于行的复制:将实际数据记录在二进制日志里。适用场景广泛、可以正确复制每一行、执行代价高但更新少数行的语句更高效复制。缺点:有些简单语句的更新行数多,二进制日志很多,增加主库的负载;无法判断执行了哪些 SQL,难以排查问题。默认基于语句的复制。

    主从复制延迟

    • 主库并发,从库单线程,通过 sharding 打散或升级 Mysql 5.7 开启基于逻辑时钟的并行复制;
    • 主库大事务拆分为小事务,大语句拆分为小语句;
    • 对大表执行 DDL,找到阻塞的 DDL 并干掉;
    • 缺乏主键或唯一索引,检查表设计,确保有自增主键和合适索引;
    • 主从配置不一致,尽量统一配置;
    • 从库压力过大,分解为多个从库。

    Redis复制机制

    Redis 复制机制基于 BGSAVE 的快照机制。

    BGSAVE快照机制

    使用 BGSAVE 创建快照。 BGSAVE 是异步的,会 fork 子进程来写入硬盘,父进程仍然处理请求。SAVE 是同步的,无法在创建快照的同时响应命令。配置 save timeInSecs writingTimes 表示从最近一次创建快照开始,在 timeInSecs 秒内如果有 writingTimes 次写入,则会使用 BGSAVE 创建快照。如果有多个 save 配置,则任一个满足都会创建快照。

    主从复制机制

    通常采用一主多从拓扑。Master-Slave 。从服务器用来减轻读请求对主服务器的负载压力。Redis 不支持主主复制。

    主从复制过程:

    • STEP1:Master 向 Slave 发送 SLAVEOF host port ,建立主从复制连接;
    • STEP2:Slave 向 Master 发送 SYNC 命令;
    • STEP3:Master 接收到 SYNC 命令后,执行 BGSAVE 生成快照文件,并往缓冲区记录自执行 BGSAVE 命令时刻点后的增量写命令;
    • STEP4:Master 的快照文件生成之后,会发送给 Slave,快照文件发送完毕之后,会发送缓冲区写入的增量写命令;
    • STEP5:Slave 解析快照文件,覆盖原有数据; Slave 在同步时,会清空原有的数据;
    • STEP6:执行完快照文件里的命令后,Slave 接受 Master 发送来的缓冲区增量写命令,执行命令实现增量同步;
    • STEP7:Master 发送完缓冲区的增量写命令后,每接收一条写命令,执行完后就向 Slave 发送相同的写命令,Slave 接收到写命令并执行,从而与 Master 保持一致。STEP7 称为“命令传播”。

    在命令传播期间,从服务器断开重连问题:

    • 方案一:Full Sync 模式。Slave 重新连接 Master 后,发送 SYNC ,开始一次全量复制过程。 SYNC 命令是非常耗时耗资源的:Master 生成快照文件耗费 CPU 和内存;发送快照文件耗费网络带宽;从服务器载入快照文件期间无法响应命令;
    • 方案二:Partial Sync 模式。Slave 向 Master 发送 PSYNC 命令,通过 +CONTINUE 通信确认主从双方使用 Partial Sync 方式;Master 将 Slave 断线时刻点偏移量之后的写命令发送给 Slave ,Slave 执行完后与 Master 保持一致。

    PSYNC 命令实现复制模式:

    • 依赖 RunningID 和 offset 机制来实现;
    • Redis 主从服务器均有一个唯一的由 40 个随机十六进制字符组成的 RunningID;在初次 Full Sync 复制时,Master 将自己的 Running ID 发给 Slave 保存起来,便于后续 PSYNC 时识别连接的是否断开之前的 Master;
    • Master 会维护一个偏移量 master-offset 和一个复制缓冲区 backlog (偏移量及写命令的映射数组) ,Slave 会维护一个偏移量 slave-offset;偏移量以字节为单位或者以复制块为单位;对比 master-offset 和 slave-offset 即可知道主从是否处于一致状态;
    • 在命令传播期间,Master 不仅会将写命令发给所有从服务器,也会在 backlog 写入一条偏移量数据;
    • Slave 发送 PSYNC Master-RunningID slave-offset ,发送自己的偏移量 slave-offset 和存储的 Master-RunningID 给 Master,Master 首先根据 Master-RunningID 判断自己是否是 Slave 断开之前连接的主服务器,如果不相同,则要执行 Full Sync;如果相同,则可执行 Partial Sync;
    • Master 通过 slave-offset 与 buffer 进行对比,如果 slave-offset 之后的偏移量数据都在 backlog 里,则将这些偏移量数据发送给 Slave,否则进行 Full Sync ;
    • 复制缓冲区大小:断开重连平均时间(s) * 每秒写命令数 * factor ; factor 是因子,可取 2-3 ;
    • 执行 Full Sync: Slave 初次复制发送 PSYNC?-1 ;Master 回复 Slave 要用 FULLERSYNC Master-RunningID slave-offset ;
    • 在执行主从复制之前,会建立 TCP 连接,发送 PING 命令检测网络连接状况,进行身份验证;
    • 心跳检测:Slave 默认每隔 1s 向 Master 发送 REPLCONF ACK slave-offset 命令。

    参考资料

  • 相关阅读:
    java 的异常和错误,有哪些
    java里的15种锁
    Netty知识点总结(一)——NIO
    Java中的路径问题
    Java定时任务-Timer
    安装Idea后需要做的3件事
    线程中的队列(queue)
    信号量(Semaphore)
    python线程的同步事件Event
    python中的GIL
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/14158712.html
Copyright © 2011-2022 走看看