news 2026/9/10 13:00:28

Dragonfly 主从复制深度解析:握手、全量/增量同步、稳定态流式与故障切换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dragonfly 主从复制深度解析:握手、全量/增量同步、稳定态流式与故障切换

Dragonfly 主从复制深度解析:握手、全量/增量同步、稳定态流式与故障切换

【免费下载链接】dragonflyA modern replacement for Redis and Memcached项目地址: https://gitcode.com/GitHub_Trending/dr/dragonfly

Dragonfly 是 Redis 与 Memcached 的现代替代品,其同构复制(Dragonfly 到 Dragonfly)在标准 RESP 协议之上构建了一套专有的DFLY子命令体系,实现了"每分片一条流"的高并发同步。本文以仓库 docs/replication.md 为主体,结合 src/server/dflycmd.cc、src/server/replica.cc、src/server/journal/journal_slice.cc 等源码,完整讲解复制的握手流程、逐流协商、全量快照与增量缓冲、稳定态流式同步、故障切换(failover)与接管(takeover)的底层机制,以及全部相关配置参数。读完本文,你将掌握 Dragonfly 复制状态机的每一处关键节点、部分同步得以成立的原理与边界,以及如何用这些参数调优大规模副本同步。

两个角色,两套状态机

复制涉及两个相互独立的状态机,它们通过一条控制连接(control connection)加上每个分片一条的流连接(flow)进行通信:

  • 主库侧(Master):每个已连接副本对应一个会话,依次经历PREPARATION -> FULL_SYNC -> STABLE_SYNC,任意状态下都可能进入CANCELLED
  • 副本侧(Replica):一组逐步累积的标志位(connected、greeted、syncing、sync-ok),随握手与同步推进逐级置位,任何失败都会整体清零重来。

主库会把每个副本的同步会话按主库分片数量切分成若干流,即"每个主库分片一条 TCP 连接";副本侧则用每流一个 worker 与之对应。状态名与源码中的SyncState枚举一一对应,见 src/server/dflycmd.cc:preparationfull_syncstable_sync(在--info_replication_valkey_compatible开启时显示为online)、cancelled

核心术语

  • LSN(Log Sequence Number):每个分片独立的、单调递增的日志序号,标识"下一个将被写入的日志条目"。LSN不可跨分片比较——每个分片各自维护独立序列。源码中DFLY REPLICAOFFSET正是按分片返回该 LSN 向量(src/server/dflycmd.cc)。
  • Flow:一个分片的复制连接。副本有N个主库分片就有N条流,通过 flow-id 映射到分片 id。
  • master-replid:主库进程生命周期内随机生成一次的 ID,用于让副本判断自己重连的是"同一个主库"还是"不同的主库"(例如主库重启后)。

1. 握手(每连接问候)

副本以普通 RESP 客户端连接主库,按顺序发送:

  1. PING——期望+PONG
  2. REPLCONF listening-port <port>——期望+OK
  3. REPLCONF ip-address <ip>(仅在设置--replica_announce_ip时发送)——尽力而为,即使收到错误回复也只记录警告(旧版主库可能不支持)。
  4. REPLCONF capa eof capa psync2——期望+OK
  5. REPLCONF capa dragonfly——主库的回复用于区分 Redis 主库(单元素+OK)与 Dragonfly 主库(多元素数组)。

握手的第一步实现在Replica::Greet()中(src/server/replica.cc):先PING校验PONG,再依次发送上述REPLCONF系列命令,并依据REPLCONF capa dragonfly回复的元素个数判断对端类型——1 个元素是 Redis,3 个及以上是 Dragonfly 主库。

主库侧对REPLCONF CAPA dragonfly有特殊处理:它会分配一个sync_id,为每个分片预留一个流槽位,然后回复一个 5 元素数组:

<master_replid> <sync_id "SYNCn"> <num_flows = shard count> <protocol version> <lineage_id>

副本解析该回复时(Replica::HandleCapaDflyResp(),src/server/replica.cc)有几处值得注意的行为:

  • 若广播的master_replid等于副本自身的 client id,则拒绝连接(防止意外地从自己复制数据)。
  • 若主库的master_replid与副本上次见到的不一致,且设置了--break_replication_on_master_restart,则直接中止复制——这是为了防止"同地址主库进程重启后带着全新的无关数据"导致副本被静默清空。否则,丢弃之前记住的每流 LSN,强制触发全量重同步(部分同步只会在副本上次见过的同一个master_replid下尝试,或走下述 故障切换后的部分同步 的特殊路径)。
  • 第 5 个字段lineage_id会被保存,但仅在实验性级联复制特性中使用(对应源码中的--experimental_cascaded_partial_syncsf_->GetLineageId()判断,见 src/server/dflycmd.cc);若主库未返回该字段,则回退为master_replid本身。

随后,仅在主库是 Dragonfly 时,副本继续发送:

  • REPLCONF CLIENT-ID <id>——让主库将会话标记为副本的稳定集群节点 ID(对应Replica::ConfigureDflyMaster()service_.cluster_family().MyID(),src/server/replica.cc)。
  • REPLCONF CLIENT-VERSION <version>——副本自身的协议版本,存储于会话上,后续用于门控部分同步行为与 RDB 特性封装(如搜索索引 blob 等)。对应DflyVersion::CURRENT_VER(src/server/replica.cc),并记录了"在REPLCONF capa dragonfly上回传版本号"这一能力,见 src/server/version.h。

之后副本将自身标记为 greeted。

2. 逐流协商(DFLY FLOW

num_flows中的每个分片,副本新建一条 TCP 连接并发送:

DFLY FLOW <master_repl_id> <dfly_session_id> <flow_id> [<lsn>] [<last_master_id> <lsn-vec>]
  • <lsn>:仅当副本记得本流在上次断连时、同主库的可续传 LSN,且主库广播的版本支持、且开启--replica_partial_sync时才附加。
  • <last_master_id> <lsn-vec>(上一个主库的 id 与按-连接的每分片 LSN 向量):仅当副本记住了自己曾跟随的另一个主库的同步数据(见 故障切换后的部分同步),且主库版本支持时才附加。解析逻辑见DflyCmd::Flow()(src/server/dflycmd.cc),其中ParseLsnVec-切分 LSN 向量(src/server/dflycmd.cc)。

主库侧,流处理器会校验master_id(不匹配返回bad master id,见 src/server/dflycmd.cc)、解析flow_id与会话,把连接迁移到该分片所属线程(conn()->Migrate(...)),并惰性初始化该分片的日志环形缓冲(缓冲容量默认 8192 条,受--shard_repl_backlog_len控制)。迁移连接的实现可见SetupFlowConnection(src/server/dflycmd.cc),它会为新流生成 40 个十六进制字符的随机eof_token

随后主库在会话仍处于PREPARATION状态时决定全量还是部分同步:

  1. 故障切换匹配(failover match):本主库自身是由被提升的副本演化而来(执行过REPLICAOF NO ONEREPLTAKEOVER,并记住了旧主库的 id 与自身已追到的每分片 LSN),且请求副本的last_master_id与该记忆中的 id 匹配。此时续传 LSN 取自副本提供的lsn-vec中对应flow_id的值,而非<lsn>参数。源码中的判断为failover_match = my_last_master && replica_last_master && replica_last_master->id == my_last_master->id(src/server/dflycmd.cc)。
  2. 否则,如果发送了裸<lsn>,则以其为候选续传点(重连同一主库/同一流)。

候选 LSN 只有在其仍可被取回时才被采纳:要么等于当前 LSN(什么都没错过),要么落在该分片环形缓冲的保留区间内(介于缓冲最旧与最新条目之间)。若该 LSN 已被缓冲淘汰(副本断连太久或缓冲太小),主库记录日志并静默回退到该流的全量同步——这种特定失败不会发送错误回复。缓冲内判断逻辑为journal::GetLsn() == lsn || journal::IsLSNInBuffer(lsn)(src/server/dflycmd.cc),IsLSNInBuffer的区间判定见 src/server/journal/journal_slice.cc。

主库对DFLY FLOW回复(sync_type, eof_token),其中sync_type"FULL""PARTIAL"eof_token是全新随机的 40 位十六进制字符串,仅在后续全量同步路径中用作 RDB 流的带外结束标记(rb->StartArray(2); rb->SendSimpleString(sync_type); rb->SendSimpleString(eof_token);,src/server/dflycmd.cc)。

关键的不对称性:全量还是部分同步在DFLY FLOW阶段就按流独立决定——早于副本在控制连接上发出DFLY SYNC。每条流的答案在DFLY SYNC执行前就已定死:DFLY SYNC会扇出到各分片,对任何已协商为PARTIAL的流完全跳过启动全量同步 saver,绝不会重新审视DFLY FLOW的决定(src/server/dflycmd.cc 中,若流已带start_partial_sync_at值则直接返回INVALID_VALUE拒绝DFLY SYNC)。不过逐流决定不一定是会话的最终结果:若各流意见不一(有的全量、有的部分),这种不一致会在之后才被发现,并强制整个会话回退到全量重同步(见下文"混合全量/部分")。

3. 全量同步(DFLY SYNC

一旦所有流都回复了DFLY FLOW,副本就在第一条(控制)连接上发送DFLY SYNC <sync_id>。主库要求会话处于PREPARATION状态,并在事务保护(Transaction::Guard,确保没有写事务处于飞行途中)下,为每个尚未解析为PARTIAL的分片启动全量快照,随后将会话转入FULL_SYNC并回复+OK——不等待快照本身完成,RDB 字节会在已打开的流 socket 上异步流出。

DflyCmd::Sync()的实现确认了这一流程(src/server/dflycmd.cc):先校验状态为PREPARATION,然后shard_set->RunBlockingInParallel对每个分片执行StartFullSyncInThread,全部成功后将会话置为FULL_SYNCSendOk()

每个分片启动全量同步时:

  • 创建直接写入流 socket 的 RDB saver。分片 0 还会额外保存"摘要"(Lua 脚本、全局元数据、搜索索引定义)连同自身数据;其余分片只保存自己分片的数据。
  • 开始桶遍历快照。关键点:当请求了日志流式时,快照在开始遍历哈希表之前就先把自身注册为日志消费者——早于遍历 fiber 的启动。任何落在快照游标已越过之后的键/桶上的写入,都会被捕获为日志条目并追加进同一条 RDB 字节流中,与桶数据交错。这正是客户端在全量同步期间可以持续写入的安全保证:快照到日志之间没有交接空窗,因为日志监听器在整个快照期间都处于活跃状态,而不只是快照之后。
  • 所有桶遍历完成后,快照在 RDB 流中写入"全量同步截断标记"(full sync cut marker)。

副本侧,每条流把 socket 喂给 RDB loader,loader 带一个回调:首次观察到截断标记时递减共享计数器。副本会阻塞到每条流都命中截断标记——这样副本就能知道所有分片上的 RDB"照片"部分已同时完成,尽管每个分片是独立、按各自节奏流式的。

截断标记之后主库的快照并未结束:日志变更仍会继续转发进 RDB 流。脱离该阶段由DFLY STARTSTABLE(下一节)单独驱动——结束全量同步会把日志偏移作为 RDB opcode 发送(副本侧读回)、flush,然后才把原始eof_token字节写到 socket。副本的全量同步 worker 在把流的全量同步视为完成前,会从线上读取并校验该eof_token,并把越过 token 读取到的多余字节暂存起来,作为稳定态同步流的起始部分重放(连接足够快时,token 之后可能已经跟着实时日志字节)。

若每条流都协商为PARTIAL,以上全部不会发生:同步类型短路为"partial",副本直接跳到发送DFLY STARTSTABLE。同一会话内各流全量/部分混杂被视为不可恢复("不会做部分同步:部分流必须全量重同步")——该会话的复制报错并从头重连。

副本清空现有数据集只发生一次:在启动全量流之前、且仅当所有流都解析为全量时——纯部分重同步绝不会触碰现有数据。

桶序列化、加锁与并发写入

快照遍历与实时客户端写入在无全局锁的情况下并发作用于同一张哈希表。该机制(全量同步、SAVE/BGSAVE、集群槽位迁移共用)是基于每桶写时复制(per-bucket copy-on-write)的方案:

  • 遍历 fiber 按物理哈希表桶顺序行走。每个桶都有版本号;快照在开始时记住目标版本。桶只有在其版本仍旧于目标版本时才会被序列化,且会在序列化之前打上目标版本戳(因此并发二次访问——无论是遍历继续推进还是写入与之竞争——都保证是空操作)。
  • 写入不会被快照阻塞。数据库层会在每次即将触碰某个桶时、在应用变更之前通知快照。该通知内联在写入者自己的 fiber 上执行相同的"未访问则序列化"逻辑(side-save),捕获变更前的值并标记桶已完成,让遍历稍后跳过。若桶已被序列化,则只是做一次版本检查、零额外工作。无论哪种情况,都是由调用写入的 fiber 自己完成这些工作后继续执行变更;触碰其他桶的其他客户端 fiber 永远不会因此被阻塞,因为每个桶的版本/闩状态相互独立——这条路径上没有任何表级锁。
  • 路径上唯一的真正互斥锁保护的是输出流(共享序列化器的缓冲/socket 写入器),而非桶访问:它阻止遍历 fiber 正在写入的大值,与另一个 fiber 的 side-save 变更在同一输出流上交错、使值被拦腰截断。仅当关闭 tag-chunk 序列化时才真正持有该锁;tag-chunk 线上格式通常使这变得不必要。它从不用于门控桶访问。
  • 对卸载到分层存储(tiered storage)的值,每个桶通过一个小的每桶闩跟踪进行中的异步取回,因此对该特定桶的二次触碰(例如遍历到达时 side-save 仍在等待分层读取完成)只会阻塞到该桶的挂起读取解析——同样是桶级而非全局级。

该机制保证的不变式:对于任何键,副本/快照必须严格在"变更它的日志条目"之前观察到变更前的值,且上述机制做到这一点而无需阻塞无关键的写入。

限速快照

全量同步(以及普通SAVE/BGSAVE)的出口流量按分片线程限速,由--snapshot_egress_limit_bytes(字节/秒;0表示不限制,为默认值)配置。它使用 GCRA(通用信元速率算法)决定遍历 fiber 必须睡眠多久才能把观测字节速率控制在限制之下。该标志在 src/server/server_state.cc 中定义为strings::MemoryBytesFlag,默认值 0(不限制),并通过egress_throttler_.SetLimit(...)生效。

两个调用点协同工作:桶遍历循环每次迭代请求一次限速,若该分片当前超预算则阻塞;实际写入路径在数据推出时记录字节数。限速器区分高、低优先级出口:遍历 fiber 自身推送的常规批量快照数据记为低优先级,而任何其他 fiber 推送的数据(如响应实时写入的内联 side-save)记为高优先级,且在低优先级出口已占用其基线份额之前不会被限速。这保证了大批量快照不会饿死同一条连接上搭载的实时日志流量,反之亦然——饱和的快照出口预算只会拖慢遍历,而不会拖慢普通写入。

同一个每线程限速器实例也被稳定态同步的日志流式器与集群迁移的桶循环使用,因此--snapshot_egress_limit_bytes实际限制的是每个分片线程的总复制/迁移出口带宽,而不只是最初的全量快照照片。

4. 稳定态同步(DFLY STARTSTABLE

副本在每条流上都观察到截断标记后(或全部分支的部分同步会话立即)发送DFLY STARTSTABLE <sync_id>。主库要求会话处于FULL_SYNCPREPARATION状态(后者覆盖从未发送DFLY SYNC的全部分支部分同步情形),且每条流的连接仍然存活。对每个分片:

  • 若该流是全量:结束快照,发送日志偏移与 EOF token,如前所述。
  • 若该流是部分:无需停止任何东西(从未启动 saver)。
  • 无论哪种情况:在流 socket 上启动实时日志流式器。

DflyCmd::StartStable()的实现确认了上述逻辑(src/server/dflycmd.cc):校验状态后并行执行"非部分流则StopFullSyncInThread,然后StartStableSyncInThread",最终将会话置为STABLE_SYNC

启动日志流式器:若这是全量同步流(总是从"现在"开始稳定态同步),立即注册为实时日志消费者。若是起始 LSN 非零的部分同步流,则暂不注册——后台写入者先从请求的 LSN 开始逐条正向遍历环形缓冲,把每条直接写到 socket,追平当前 LSN 后才注册为实时消费者。若重放期间缓冲条目在其脚下被淘汰(正常情况下不该发生,因为淘汰检查在DFLY FLOW时已通过,但缓冲仍在并发推进),它会报告不可恢复错误而非静默重同步。

会话转入STABLE_SYNC,主库回复+OK。副本侧,每条流启动一对读/ACK worker 并阻塞,直到错误/取消将其全部拆除——没有干净退出稳定态同步的方式;唯一出口是取消后的错误。

线上格式与命令分组

每条日志条目携带一个 opcode(SELECTEXPIREDCOMMANDPINGLSN),COMMAND/EXPIRED还附带事务 id、数据库索引与背书的命令参数。副本把原始流逐个转成按事务组织的记录,并在(跟踪 LSN 时)把 LSN opcode 内嵌值与自身运行计数器交叉校验,不匹配时记日志(而非失败)。

跨分片事务(全局命令FLUSHALL/FLUSHDB/DFLYCLUSTER FLUSHSLOTS,以及以共享事务 id 标识的普通跨分片 multi 命令)会在各流 worker 间重新同步:每条流把自己的事务 id 插入共享映射,等待所有参与分片都收到自己的部分,然后一个屏障确保恰好一条流(最先插入的那条)真正执行一次全局命令,同时所有流再次在屏障上等待其完成后再继续。这是必要的,因为每条流 worker 独立读取执行、与其他流并不同步。

PING日志条目(主库用来强制 ACK,见 背压与 ACK 与 接管)会计入副本已执行记录数,但不触碰事务执行器;且若本副本分片自己的日志恰好处于活跃状态,也会被重新记录进其中。

背压与 ACK

每条流运行一个 ACK worker,周期性(--replication_acks_interval,默认 1000ms)或在新增执行记录数>= 1024时立即在同一条流 socket 上向主库发送REPLCONF ACK <executed_count>。主库记录每条流最后 ACK 的 LSN——该写入发生在拥有该分片流连接的线程上,因此绝无跨线程访问。标志定义见 src/server/replica.cc:replication_acks_interval默认 1000ms。

主库侧,日志流式器在挂起写缓冲达到--replication_stream_output_limit(默认 1MB)时阻塞写日志的 fiber(而非副本连接),直到进行中的 socket 写排空,最多等待--replication_timeout(默认 30000ms),超时即放弃并带错误取消整个复制会话。这是主库对慢速或卡死副本的推回机制——它是不对称的:落后的副本会让主库该分片上的所有写者停摆,而不只是一条连接。相关标志定义见 src/server/journal/streamer.cc(replication_timeout默认 30000ms)与同文件 L32 处(replication_stream_output_limit默认 1MB)。

检测卡死的全量同步

独立于 ACK,主库周期性每分片心跳会检查本分片上仍持有活跃全量同步 saver 的每个副本;若该 saver 的最后写入时间早于--replication_timeout,则强制取消该副本的整个会话。这只适用于全量同步阶段(全量同步结束后 saver 引用被清除);稳定态同步依赖日志流式器自身的背压超时。

部分同步缓冲及其边界

部分同步的"近期历史"是每个分片的环形缓冲,容量由--shard_repl_backlog_len控制(默认 8192 条——是条数而非字节数)。它由每次日志写入填充,无论当前是否有副本连接(分片的日志记录在首个副本附着时启动;值得注意的是,当本节点从主库被降级为某人的副本、后来又恢复作为数据源时也会启动)。副本重连时若请求的 LSN 已被淘汰出缓冲,或缓冲被显式清空(由强制所有副本全量同步触发),则受影响流总是回退到全量同步——不存在"部分的部分同步";一条流要么完全可续传,要么必须完全重同步。

故意清空缓冲会把 LSN 计数器推进到越过当前缓冲内容之上,正是为了让一个过期副本持有的 LSN 永远不可能在已清空的缓冲里意外地别名化为未来的 LSN、从而被授予一个基于它其实从未见过的数据的虚假"部分同步"。

值得注意的当前仓库实现细节:shard_repl_backlog_len在 src/server/journal/journal_slice.cc 中已被标记为 legacy 标志,默认值实际为 0;默认路径改用 8192 条容量加上基于时间/字节的淘汰策略:shard_repl_backlog_time_ms(默认 5000ms 保留时长)与shard_repl_backlog_max_bytes(默认 0,此时按maxmemory / shard count / 200推导),见 src/server/journal/journal_slice.cc 与JournalSlice::Init()(同文件 L79-L97)。只有当用户显式配置了非零的--shard_repl_backlog_len且未配置新式标志时,才回到纯条数上限模式并打印弃用警告(同文件 L84-L92)。换言之:在源码层面,"8192 条"是当前默认缓冲容量,而旧文档中的--shard_repl_backlog_len=8192这一表述在现行版本中对应的是这条默认路径。

部分同步:提升与故障切换

当副本被提升为主库时,其他原本跟随旧主库的副本仍可通过部分同步恢复,而不是全量重同步,走的就是流协商中上述故障切换匹配(failover match)的"不同主库"路径。

提升通过REPLICAOF NO ONEREPLTAKEOVER/DFLY TAKEOVER完成。两者最终都会停止本地副本角色——若它已达到稳定态同步,则会返回旧主库的 id 与本节点已执行的每流 LSN 快照,暂存备用。其他跟随旧主库的副本在自己停止/重连路径上也会发送各自记住的last_master_id/lsn-vec;若匹配,它们就能在新主库相同的日志序列中从自己最后见过的 LSN 续传,因为被提升的节点会延续其作为副本时的日志 LSN 编号,而不是重置为 1 或新的 epoch。

该 LSN 连续性来自:重启每个分片的日志时,起始编号为该节点实际为本分片映射流执行的日志记录数(至少 1,因为日志编号从 1 开始)——而不是 0。对应实现为Replica::StartJournalAtOwnLSN()(src/server/replica.cc),它调用journal::StartInThreadAtLsn(rec_executed);每条流的已执行计数由GetRecCountExecutedPerShard计算,且"日志总是从 1 开始"(同文件 L1517-L1519)。这发生在两条提升路径上:

  • REPLICAOF NO ONE,由--replicaof_no_one_start_journal(默认true)门控——"设置后保留REPLICAOF NO ONE之后的日志偏移"。实现见 src/server/server_family.cc 与 L3430 处的调用点。
  • REPLTAKEOVER,无条件执行,且在向旧主库发出接管 RPC之前——这样本节点的日志在接管完成、它成为主库的那一刻就已温热、部分同步就绪。调用点见 src/server/server_family.cc。

5. 接管(REPLTAKEOVER/DFLY TAKEOVER

REPLTAKEOVER <seconds> [SAVE]在副本上发出;它会作为DFLY TAKEOVER <seconds> [SAVE] <sync_id>转发给主库。主库侧,在持有请求会话的共享锁并要求其已处于STABLE_SYNC的前提下:

  1. 原子地翻转主库全局状态ACTIVE -> TAKEN_OVER——这是实际的"封锁";并发的第二次接管尝试在此失败(对应sf_->service().SwitchState(GlobalState::ACTIVE, GlobalState::TAKEN_OVER),src/server/dflycmd.cc)。
  2. 在给定超时内等待所有监听器上飞行中的命令分发全部完成,并禁用键过期,使交接窗口内没有任何状态变更(src/server/dflycmd.cc:DispatchTracker等待 +SetExpireAllowed(false),失败则翻转回ACTIVE并返回Takeover failed!)。
  3. 仅对请求接管的副本发送日志PING(强制每个稳定态同步流立即发送最新 ACK,而非等待正常间隔),并忙等每个分片最后 ACK 的 LSN 等于主库当前 LSN——即副本现已应用了全部数据。实现为WaitReplicaFlowToCatchup(..., with_ping=true)(src/server/dflycmd.cc)。
  4. 若成功,对接管请求回复+OK,然后——尽力而为,不强制 PING(强制 PING 本身会推进 LSN、破坏这些节点的部分同步)——等待所有其他已连接副本也追平,使它们不会错过数据或需要对即将成为新主库的副本做全量重同步(src/server/dflycmd.cc)。
  5. 可选地执行同步SAVE(仅测试用的旋钮),然后关闭进程(非集群模式)或在集群模式下把集群槽位所有权协调到新主库(ReconcileMasterSlots,src/server/dflycmd.cc)。

副本侧交接(把确认的 OK 变成"我现在是主库")会在发出接管 RPC之前把自身日志重启到自身最后执行的 LSN(这样其日志在成为主库的那一刻就已温热、部分同步就绪,src/server/server_family.cc),并且只在主库确认+OK且旧副本角色拆除之后才翻转为主库模式。

6. 取消、断连与清理

任何流上的任何错误(socket 错误、协议错误、超时)都会调用会话的错误处理器(会话创建时安装):它派生出 worker 停止该sync_id的复制。停止一个会话按顺序执行:

  1. 将会话状态翻转为CANCELLED并取消共享执行上下文——任何阻塞等待它的 fiber 都会解除阻塞并开始回卷。
  2. 扇出到每个分片执行该流的清理——全量同步流关闭 socket 并取消 RDB saver;稳定态同步流关闭 socket 并取消日志流式器。对应ReplicaInfo::Cancel()(src/server/dflycmd.cc):置CANCELLEDReportCancelError、并行清理每条流、JoinErrorHandler
  3. 汇入错误处理器 worker,然后擦除会话并重新发布INFO/指标读取者使用的无锁快照(tl_replica_infos每 proactor 缓存,见 src/server/dflycmd.cc)。

副本自身的错误处理类似分层:其主循环在任何阶段任何失败时把所有状态标志重置回仅"enabled",循环等待--master_reconnect_timeout_ms(默认 1000ms,src/server/replica.cc)后从 DNS 解析开始重试整个握手——任何时点的断连都会从问候重启,绝不会中途续接握手。只有"是否完成过一次全量同步"和"最后见过的每流 LSN"(在停止/重连路径顶部捕获)能跨越重连存活,且只有后者使重连后的部分同步成为可能。

可观测性

  • INFO REPLICATION/DFLYCLUSTER系列工具将会话状态报告为preparationfull_syncstable_sync(若开启--info_replication_valkey_compatible则显示online)、cancelled。状态名映射见 src/server/dflycmd.cc;该标志默认值为true(src/server/server_family.cc),相应地在INFO中以slave而非replica报告角色字段(同文件 L3012)。
  • 主库侧每副本摘要(id、地址、端口、状态、lag)由线程本地快照构建,在锁下更新并重新发布到每个 proactor,读方从不加锁(tl_replica_infos+UpdateReplicaInfoCacheLocked,见 src/server/dflycmd.cc)。
  • 复制滞后只对处于STABLE_SYNC的副本有意义(也仅在该状态计算):每个分片为当前 LSN 减去最后 ACK 的 LSN,跨分片取最大值。
  • DFLY REPLICAOFFSET返回主库当前的每分片 LSN 向量——WAIT用它来知道副本必须达到的 LSN(src/server/dflycmd.cc)。
  • 副本侧为INFO/CLIENT LIST暴露自身视角:由状态标志纯粹推导出的人类可读阶段(DISABLED/TCP_CONNECTING/GREETING/FULL_SYNC_IN_PROGRESS/INITIAL_SYNC/STABLE_SYNC),实现在 src/server/replica.cc 的Replica::GetCurrentPhase()系列逻辑中(如STABLE_SYNC分支,见同文件 L1501 附近)。

从 Redis 主库复制

若握手时REPLCONF capa dragonfly的回复是单元素+OK(而非 Dragonfly 主库),副本回退到标准 Redis 复制:发出PSYNC ? -1,解析+FULLRESYNC <replid> <offset>(基于磁盘、带长度前缀的 RDB)或 diskless 的EOF:<40-byte-token>流,通过与 Dragonfly 全量同步相同的 RDB loader 加载(单分片、无分片流——一切都在一条连接上到达),然后解析 Redis 在稳定态同步期间发送的普通 RESP 命令流,批量/压缩命令,并周期发送REPLCONF ACK <offset>。部分重同步(PSYNCCONTINUE)被明确未实现——+CONTINUE回复被当作错误("partial replication not supported yet"),因此对 Redis 主库的每次重连都是全量重同步。Dragonfly 不支持反向方向(Redis 实例从 Dragonfly 主库复制)。相关说明见 src/server/replica.cc 的注释"we currently do not support dragonfly->redis replication"。

配置参数速查

以下参数在复制路径中的角色与默认值,均以当前仓库源码为准:

参数默认值作用源码位置
--replica_announce_ip握手时附加REPLCONF ip-address,主库不支持仅记警告src/server/replica.cc
--break_replication_on_master_restartfalsemaster_replid变化时直接中止复制,防止静默清空数据src/server/replica.cc
--replica_partial_synctrue是否允许对同主库断连做部分同步src/server/replica.cc
--shard_repl_backlog_len0(legacy,显式配置时生效)每分片部分同步缓冲条数上限;默认走 8192 条 + 时间/字节淘汰src/server/journal/journal_slice.cc
--shard_repl_backlog_time_ms5000部分同步缓冲条目保留时长(毫秒)src/server/journal/journal_slice.cc
--shard_repl_backlog_max_bytes0(按maxmemory / shard 数 / 200推导)每分片部分同步缓冲字节上限src/server/journal/journal_slice.cc
--snapshot_egress_limit_bytes0(不限速)每分片线程快照/复制/迁移出口带宽限速(GCRA)src/server/server_state.cc
--replication_stream_output_limit1MB日志流式器挂起写缓冲阈值,触发写 fiber 背压src/server/journal/streamer.cc
--replication_timeout30000ms流式器背压最大等待与全量同步卡死检测阈值src/server/journal/streamer.cc
--replication_acks_interval1000ms副本 ACK 周期src/server/replica.cc
--master_reconnect_timeout_ms1000ms副本失败后重试握手前的等待src/server/replica.cc
--replicaof_no_one_start_journaltrueREPLICAOF NO ONE后保留日志偏移以支持部分同步src/server/server_family.cc
--info_replication_valkey_compatibletrueINFO/状态名使用 valkey 兼容措辞(online/slavesrc/server/server_family.cc

需要强调的是:--replication_stream_output_limit--replication_timeout--replica_partial_sync等标志还注册在 src/server/main_service.cc 的配置注册表中(RegisterMutable),意味着它们可在运行时通过CONFIG SET动态调整,例如CONFIG SET snapshot_egress_limit_bytes <limit>(src/server/rdb_test.cc 的集成测试即演示了该用法)。

总结

Dragonfly 的复制设计以"每分片一条流 + 无全局锁的写时复制快照"为核心,把全量同步的安全性与实时写入的并发性统一在同一套机制里;部分同步依赖每分片 LSN 与环形缓冲的严格边界,并借助REPLICAOF NO ONE/REPLTAKEOVER时"日志从自身已执行 LSN 继续编号"的连续性,让故障切换后的副本也能无缝续传。理解这套状态机与参数体系,是在生产环境中正确配置副本规模、诊断同步滞后与调优带宽的关键。

【免费下载链接】dragonflyA modern replacement for Redis and Memcached项目地址: https://gitcode.com/GitHub_Trending/dr/dragonfly

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 12:55:51

论文的免费大纲怎么分析?按章节信息量拆解

拿到一份免费生成的论文大纲&#xff0c;多数人的反应是照着写&#xff0c;写到一半才发现有的章挤成一团、有的章两三页就收尾&#xff0c;结构头重脚轻。大纲本身不会告诉你问题出在哪&#xff0c;需要你自己按章节信息量拆一遍&#xff1a;每章要讲什么、给多少篇幅、章与章…

作者头像 李华
网站建设 2026/9/10 12:54:45

从入门到进阶:一份完整的 3 阶段免费数学学习资源指南

从入门到进阶&#xff1a;一份完整的 3 阶段免费数学学习资源指南 【免费下载链接】awesome-math A curated list of awesome mathematics resources 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-math 打开浏览器搜"从哪里学数学"&#xff0c;…

作者头像 李华