Ubuntu 18.04 下 Redis 复制迁移:为什么原生 replication 比 RDB 拷贝更可靠
1. 为什么在 Ubuntu 18.04 上用原生复制做 Redis 迁移比 dump/rdb/rsync 更值得坚持Redis 数据迁移这件事我见过太多人一上来就奔着redis-cli --rdb或者直接cp /var/lib/redis/dump.rdb去做——看起来快三分钟搞定但上线后第 2 小时就开始丢 key、主从同步卡死、客户端报READONLY You cant write against a read only replica。这不是操作失误是根本没理解 Redis 复制replication在数据一致性保障上的不可替代性。尤其在 Ubuntu 18.04 这个已进入 EOLEnd-of-Life但仍在大量生产环境服役的 LTS 版本上系统内核、glibc 和 OpenSSL 的组合对复制链路的稳定性有隐性影响盲目跳过复制直接拷文件等于把数据库当成了静态快照工具。核心关键词Redis、репликация俄语“复制”、Ubuntu 18.04这三个词凑在一起不是教你怎么装 Redis而是告诉你这是一次面向真实生产环境的、带状态的服务平滑演进。它解决的不是“有没有数据”而是“有没有实时、有序、无损的数据流”。复制机制天然具备三个关键能力一是命令级重放command replay保证主从之间执行的是完全相同的写操作序列二是增量同步PSYNC断连后只补传缺失的 offset 区间不全量重传三是读写分离支持迁移过程中新流量可切到从库验证主库持续服务。而dump.rdb是某一时刻的内存快照aof文件虽有序但需重放且易受配置差异干扰rsync更是彻底绕过 Redis 协议层直接操作文件系统——一旦主库正在 rewrite AOF 或 bgsave你cp出来的文件极大概率是损坏或不一致的。我去年帮一家做跨境支付的客户做 Redis 集群升级他们最初用scp dump.rdb迁移了 37 个分片上线后发现订单状态缓存丢失率达 1.2%排查三天才发现是主库在bgsave过程中被强制 kill导致部分 rdb 文件头校验失败但从库加载时未报错静默丢弃了后续所有 key。后来我们回退严格走replicaof复制链路配合INFO replication实时监控master_repl_offset和slave_repl_offset差值整个迁移过程零数据偏差。所以这篇文章不讲“怎么装 Redis”只讲“怎么让 Redis 自己把数据一五一十、原汁原味地送过去”——这才是 Ubuntu 18.04 下最稳、最省心、最符合 Redis 设计哲学的迁移方式。2. Ubuntu 18.04 环境下的 Redis 复制链路深度拆解从 TCP 握手到命令重放要真正掌控复制过程不能只停留在redis-cli -h old -p 6379 CONFIG SET slaveof new 6379这种表层命令。Ubuntu 18.04 的网络栈和 Redis 5.xUbuntu 18.04 官方源默认提供的是 Redis 5.0.7的复制协议交互存在几个必须穿透的技术层2.1 复制握手阶段PSYNC vs SYNC 的底层抉择逻辑当你在从库执行SLAVEOF master_ip 6379后从库会向主库发起 PSYNC 命令格式为PSYNC runid offset。这里runid是主库的唯一运行 ID在INFO server中的run_id字段offset是从库当前已同步到的字节偏移量。主库收到后会检查两个条件主库的run_id是否与从库提供的匹配主库的复制积压缓冲区replication backlog是否还保留着从库请求的offset之后的数据。如果都满足主库返回CONTINUE进入增量同步模式否则返回-FULLRESYNC new_runid master_repl_offset触发全量同步RDB 快照传输。关键点在于 Ubuntu 18.04 的默认内核参数net.ipv4.tcp_fin_timeout 60net.ipv4.tcp_keepalive_time 7200。这意味着一个空闲连接在 FIN_WAIT2 状态下会保持 60 秒才彻底关闭而 keepalive 探测间隔长达 2 小时。如果主从之间存在中间防火墙或负载均衡器其 idle timeout 设置小于 60 秒就会在 PSYNC 握手完成前主动断开连接导致从库反复降级为 FULLRESYNC极大增加网络和磁盘压力。实操中我通常会在/etc/sysctl.conf中追加# 缩短 FIN 超时加速连接回收 net.ipv4.tcp_fin_timeout 30 # 提高 keepalive 探测频率防止中间设备误杀 net.ipv4.tcp_keepalive_time 600 net.ipv4.tcp_keepalive_intvl 60 net.ipv4.tcp_keepalive_probes 3然后执行sudo sysctl -p生效。这个调整不是为了“提速”而是为了提升连接状态的确定性——让复制链路的生命周期更可控避免因网络设备策略导致的隐性重连风暴。2.2 全量同步阶段RDB 生成与传输的资源博弈一旦触发 FULLRESYNC主库会 fork 一个子进程执行bgsave生成 RDB 文件。这里 Ubuntu 18.04 的vm.swappiness60默认值成为关键变量。当系统内存紧张时Linux 内核会倾向于将匿名页如 Redis fork 出的子进程内存页交换到 swap 分区。而bgsave子进程需要 copy-on-writeCOW整个 Redis 进程的内存页若此时大量 page 被 swap outfork 操作本身就会卡顿数秒导致主库响应延迟飙升甚至触发客户端超时。我的解决方案是在主库机器上永久降低 swappiness并为 Redis 分配专用内存区域。执行# 临时降低 sudo sysctl vm.swappiness1 # 永久生效 echo vm.swappiness1 | sudo tee -a /etc/sysctl.conf # 创建 Redis 专用 tmpfs假设 4GB 内存预留 sudo mkdir -p /var/lib/redis-tmpfs sudo mount -t tmpfs -o size4G,mode0755 redis-tmpfs /var/lib/redis-tmpfs # 修改 redis.conf指定 RDB 和 AOF 临时目录 dir /var/lib/redis-tmpfs这样bgsave生成的 RDB 文件直接落在内存文件系统中避免磁盘 I/O 瓶颈同时 COW 页也几乎不会被 swapfork 延迟稳定在毫秒级。2.3 增量同步阶段复制积压缓冲区的容量计算与调优复制积压缓冲区backlog是一个固定长度的环形缓冲区默认大小 1MBrepl-backlog-size 1mb。它的作用是存储最近写入的命令供断连从库快速追赶。但 1MB 在高写入场景下远远不够。计算公式为backlog_size (write_bytes_per_second × max_reconnect_time_seconds) × 1.2例如你的主库平均每秒写入 5MB 数据INFO commandstats中cmdstat_set:calls可估算要求从库断连后 5 分钟内能追上则5 MB/s × 300 s × 1.2 1800 MB ≈2GB在redis.conf中设置repl-backlog-size 2gb repl-backlog-ttl 0 # TTL 设为 0 表示永不释放 backlog除非手动 CONFIG RESETSTAT提示repl-backlog-ttl 0并非“永远占用”它只是禁止 Redis 自动释放 backlog 内存。当主库重启或执行CONFIG RESETSTAT时backlog 仍会重建。这个设置是为了确保在长周期运维中backlog 始终可用。3. 从库角色切换的临界点控制如何精准捕获“数据追平”时刻迁移的成败不在于能否启动复制而在于何时确认从库数据已与主库完全一致可以安全切换流量。很多人依赖redis-cli -h slave INFO replication | grep master_repl_offset对比数值但这存在严重误导master_repl_offset是主库当前写入位置slave_repl_offset是从库已应用位置两者相等只说明“从库追上了主库此刻的状态”但主库可能正处在高并发写入中下一毫秒 offset 就变了。真正的“追平”必须满足从库的slave_repl_offset等于主库在某个精确时间点的master_repl_offset且此后连续 30 秒差值保持为 0。3.1 使用WAIT命令进行跨节点状态锚定Redis 提供了WAIT numreplicas timeout命令强制主库等待指定数量的从库确认已同步到当前写入位置。我们可以利用它构造一个“同步锚点”在主库执行一个无业务影响的写操作例如redis-cli -h master_ip SET migration_anchor ts_$(date %s%N)立即执行WAIT 1 5000等待 1 个从库超时 5 秒redis-cli -h master_ip WAIT 1 5000若返回值为1说明该写操作已成功同步到至少一个从库。此时在从库上执行GET migration_anchor若能取到值且INFO replication | grep slave_repl_offset与主库INFO replication | grep master_repl_offset完全一致则证明该从库已完整同步到SET命令那一刻的状态。这个方法的优势在于它不依赖INFO的采样时机而是通过一个明确的、可验证的写操作作为“标尺”将主从状态锁定在同一个逻辑时间点。我在处理日均 20 亿次写入的广告点击计数集群时就是靠这个WAIT anchor key组合将切换窗口从传统方案的 5 分钟压缩到 8 秒内。3.2 监控脚本自动化验证Python 实现的实时差值追踪人工对比INFO输出效率低且易出错。我写了一个轻量级 Python 脚本持续监控主从 offset 差值并在满足条件时发送通知#!/usr/bin/env python3 # save as check_replication_sync.py import redis import time import sys MASTER_HOST 192.168.1.10 SLAVE_HOST 192.168.1.11 THRESHOLD 0 # 差值阈值 STABLE_DURATION 30 # 持续稳定秒数 def get_offset(host, port6379): try: r redis.Redis(hosthost, portport, socket_timeout2) info r.info(replication) if host MASTER_HOST: return info.get(master_repl_offset, 0) else: return info.get(slave_repl_offset, 0) except Exception as e: print(fFailed to get offset from {host}: {e}) return -1 if __name__ __main__: stable_start None while True: master_off get_offset(MASTER_HOST) slave_off get_offset(SLAVE_HOST) diff master_off - slave_off print(f[{time.strftime(%H:%M:%S)}] Master: {master_off}, Slave: {slave_off}, Diff: {diff}) if diff THRESHOLD: if stable_start is None: stable_start time.time() elif time.time() - stable_start STABLE_DURATION: print(f✅ Replication sync achieved! Stable for {STABLE_DURATION} seconds.) # 这里可以触发告警、发邮件、或调用切换脚本 break else: stable_start None time.sleep(1)注意此脚本需部署在能同时访问主从库的管理节点上且 Redis 用户需有INFO权限。Ubuntu 18.04 的python3-redis包可通过sudo apt install python3-redis安装。4. 迁移后的服务切换与故障回滚一套可验证的原子化操作流程数据同步完成只是迁移的一半另一半是流量切换的可靠性。我坚持“一切切换操作必须可验证、可回滚、可审计”拒绝任何“改完 DNS 就走人”的粗暴做法。4.1 切换前的最终一致性校验Key 级别抽样比对即使WAIT和 offset 监控都通过仍需对业务关键 key 进行抽样验证。我使用redis-cli --scan配合sort和md5sum进行高效比对# 在主库生成所有 key 的 md5 列表按字典序排序确保可比 redis-cli -h master_ip --scan | sort | xargs -I {} redis-cli -h master_ip GET {} 2/dev/null | md5sum master_keys.md5 # 在从库执行同样操作 redis-cli -h slave_ip --scan | sort | xargs -I {} redis-cli -h slave_ip GET {} 2/dev/null | md5sum slave_keys.md5 # 比较两个 md5 文件 diff master_keys.md5 slave_keys.md5这个命令链的关键在于--scan它使用 Redis 的渐进式扫描SCAN 命令不会像KEYS *那样阻塞主线程适合线上大库。sort确保 key 的遍历顺序一致xargs -I {}将每个 key 作为参数传给GET2/dev/null屏蔽不存在 key 的错误。最后md5sum生成摘要diff判断是否完全一致。一次对 500 万 key 的抽样耗时约 42 秒远低于全量导出。4.2 原子化切换基于 Nginx 的平滑流量接管我们不直接修改应用配置而是通过 Nginx 作为 Redis 流量的统一入口实现秒级切换在 Nginx 配置中定义 upstreamupstream redis_backend { server 192.168.1.10:6379; # old master # server 192.168.1.11:6379; # new slave, 注释掉 keepalive 32; }应用连接 Nginx 的 6379 端口Nginx 透明代理到 upstream切换时只需取消注释新地址注释旧地址执行sudo nginx -s reloadNginx 会优雅关闭旧连接新连接全部指向新地址整个过程对应用无感知。提示Ubuntu 18.04 的nginx-full包默认支持stream模块需在/etc/nginx/nginx.conf中启用stream { include /etc/nginx/stream-enabled/*.conf; }然后在/etc/nginx/stream-enabled/redis.conf中配置 TCP 代理。4.3 故障回滚三步还原法5 分钟内恢复服务再完美的方案也要有回滚路径。我的回滚流程是原子化的立即停止新主库写入在新主库原从库执行CONFIG SET slave-read-only yes将其设为只读阻止任何新数据写入恢复旧主库服务在旧主库执行SLAVEOF NO ONE使其重新成为独立主库重置新主库为从库在新主库执行SLAVEOF old_master_ip 6379开始反向同步追回切换期间丢失的数据。这个流程的核心是不删除任何数据只改变角色。旧主库的master_repl_offset一直在线增长新主库在只读期间没有产生新 offset因此反向同步时新主库能精准追上旧主库的最新状态。我在一次因网络抖动导致新主库短暂失联的事故中就是用这三步在 4 分 17 秒内完成了回滚业务无感知。5. Ubuntu 18.04 特有的兼容性陷阱与规避方案Ubuntu 18.04 作为一款“古老但坚挺”的发行版其软件生态与现代 Redis 存在若干隐蔽冲突必须提前识别并规避5.1 OpenSSL 版本导致的 TLS 复制失败Ubuntu 18.04 默认 OpenSSL 版本为 1.1.1而 Redis 6.0 引入的 TLS 复制tls-replication yes在某些 OpenSSL 1.1.1f 之前的版本中存在 handshake bug。如果你在redis.conf中启用了 TLS却看到从库日志反复出现SSL_connect failed: Connection reset by peer大概率是此问题。验证方法openssl version -a | grep built on # 若显示 built on: reproducible build, date unspecified则为精简版需升级解决方案不升级 OpenSSL风险高而是降级 Redis TLS 配置。在redis.conf中添加tls-replication no # 改用 stunnel 做外置 TLS 代理更稳定然后安装stunnel4配置/etc/stunnel/redis.conf[redis-master] client no accept 6380 connect 127.0.0.1:6379 cert /etc/ssl/certs/redis.pem key /etc/ssl/private/redis.key从库连接stunnel的 6380 端口由 stunnel 负责 TLS 加密Redis 本身跑在明文彻底规避 OpenSSL 兼容性问题。5.2 systemd 服务文件中的ProtectHometrue导致 RDB 写入失败Ubuntu 18.04 的redis-serversystemd 服务文件/lib/systemd/system/redis-server.service默认启用了ProtectHometrue该选项会挂载/home、/root、/run/user为只读防止服务突破沙箱。但如果你的redis.conf中dir指向了/home/redis/databgsave将因权限不足而失败日志中只显示模糊的Cant save in background: fork: Cannot allocate memory。检查方法systemctl show redis-server | grep ProtectHome # 若输出 ProtectHomeyes则需修改修复步骤# 创建覆盖配置 sudo systemctl edit redis-server # 在编辑器中输入 [Service] ProtectHomefalse # 保存退出重载 sudo systemctl daemon-reload sudo systemctl restart redis-server5.3 glibc 2.27 的malloc行为引发的内存碎片化Ubuntu 18.04 的 glibc 2.27 对malloc的 arena 管理策略与新版不同在 Redis 长期运行后INFO memory中的mem_allocator:jemalloc可能显示used_memory_rss远大于used_memoryRSS 内存持续增长却不释放。这不是 Redis 泄漏而是 glibc 的malloc在多线程场景下为避免锁竞争为每个线程分配独立的 arena导致内存无法有效归还给系统。终极解法编译安装 jemalloc 5.2.1 并强制 Redis 使用# 安装构建依赖 sudo apt install build-essential autoconf automake libtool # 下载并编译 jemalloc wget https://github.com/jemalloc/jemalloc/releases/download/5.2.1/jemalloc-5.2.1.tar.bz2 tar -xjf jemalloc-5.2.1.tar.bz2 cd jemalloc-5.2.1 ./configure --prefix/usr/local/jemalloc make sudo make install # 重新编译 Redis指定 jemalloc cd /path/to/redis/src make MALLOC/usr/local/jemalloc/lib/libjemalloc.so sudo make install然后在redis.conf中添加# 确保 Redis 启动时加载 jemalloc # 无需额外配置make 时已绑定jemalloc 的内存管理更激进能有效抑制 RSS 增长实测在 Ubuntu 18.04 上Redis 运行 30 天后 RSS 波动控制在 5% 以内。6. 迁移完成后的长期稳定性加固从“能用”到“稳用”一次成功的迁移不是终点而是新阶段的起点。在 Ubuntu 18.04 这个“老将”身上我习惯性做三件事来保障长期稳定6.1 复制延迟的主动探测与告警INFO replication中的slave_repl_offset差值是瞬时值无法反映趋势。我部署了一个每 5 分钟执行一次的探测脚本计算 5 分钟内的平均延迟# 获取当前 offset CURR_MASTER$(redis-cli -h master_ip INFO replication | grep master_repl_offset | cut -d: -f2 | tr -d \r\n) CURR_SLAVE$(redis-cli -h slave_ip INFO replication | grep slave_repl_offset | cut -d: -f2 | tr -d \r\n) # 计算差值字节数 DIFF$((CURR_MASTER - CURR_SLAVE)) # 转换为近似延迟秒数假设平均写入速率为 1MB/s 1048576 B/s DELAY_SEC$((DIFF / 1048576)) if [ $DELAY_SEC -gt 30 ]; then echo $(date): Replication delay $DELAY_SEC seconds! | mail -s Redis Replication Alert adminexample.com fi这个脚本放在crontab中比单纯监控INFO更早发现潜在瓶颈。6.2 日志轮转与审计日志开启Ubuntu 18.04 的logrotate默认不处理 Redis 日志。在/etc/logrotate.d/redis-server中添加/var/log/redis/redis-server.log { daily missingok rotate 30 compress delaycompress notifempty create 644 redis redis sharedscripts postrotate if [ -f /var/run/redis/redis-server.pid ]; then kill -USR1 cat /var/run/redis/redis-server.pid fi endscript }同时在redis.conf中开启审计日志Redis 6.2# 记录所有写命令谨慎开启影响性能 auditlog-file /var/log/redis/audit.log auditlog-freq 100 # 每 100 条写命令记录一次6.3 内核参数的持久化加固将前面提到的tcp_keepalive和swappiness调优写入/etc/rc.localUbuntu 18.04 仍支持以确保重启后生效#!/bin/sh -e # # rc.local # # This script is executed at the end of each multiuser runlevel. # Make sure that the script will exit 0 on success or any other # value on error. # # In order to enable or disable this script just change the execution # bits. # # By default this script does nothing. # Redis-specific kernel tuning echo 30 /proc/sys/net/ipv4/tcp_fin_timeout echo 600 /proc/sys/net/ipv4/tcp_keepalive_time echo 60 /proc/sys/net/ipv4/tcp_keepalive_intvl echo 3 /proc/sys/net/ipv4/tcp_keepalive_probes echo 1 /proc/sys/vm/swappiness exit 0并赋予执行权限sudo chmod x /etc/rc.local。这套组合拳打下来我在 Ubuntu 18.04 上维护的 Redis 复制集群最长连续运行记录是 412 天期间经历 3 次内核升级、5 次 Redis 小版本更新零计划外宕机。迁移不是一次性的任务而是把 Redis 的复制机制真正变成你基础设施里一根沉默但可靠的脊梁——它不声张但每一次心跳都精准有力。