跨数据中心部署Riak时,Multi-Datacenter Replication(下文简称MDC复制)承担着把主集群的写入同步到远端机房的任务。这条链路一旦出现延迟,业务侧最先感知到的现象通常是:本机房写入毫秒级返回,切到备用机房读取时却查不到最新数据,或者复制队列一路上涨、本地写入延迟也跟着被拖高。延迟本身不可怕,可怕的是不知道它卡在网络、队列、full-sync还是目标集群的磁盘上。这篇文章把这条链路拆开讲清楚,并给出一套从排查到调优的完整做法。

MDC复制的两条链路:延迟到底从哪来
Riak的MDC复制是Riak KV Enterprise提供的特性,模型上分为主集群和备集群:写入只落在主集群,再由复制组件单向推送到备集群。它有两条独立的工作链路:realtime链路负责增量数据,full-sync链路负责周期性的全量比对。realtime链路的路径是:写入先经过主集群的vnode落盘,复制钩子把这次写入捕获后送入realtime队列,再由专门的client进程通过TCP长连接发送到备集群,备集群收到后按正常写入流程落盘。也就是说,一次跨机房同步的总延迟,等于队列排队时间加上网络传输时间再加上备集群的写入时间,三段里任何一段出问题都会放大整体延迟。
网络往返时延是延迟的物理下限。同城两个机房RTT一般在2毫秒以内,跨省几十毫秒,跨洋链路动辄150毫秒以上,这个底数改不掉,能做的是不让它被其他因素放大。真正常见的放大器是队列积压:当主集群的写入吞吐超过TCP链路和备集群的吸收能力时,realtime队列会持续增长。Riak对此设计了背压机制,队列长度超过queue_max或字节数超过rtq_max_bytes后,vnode的写入会被阻塞,直到队列回落到queue_min以下。这个机制保护了节点内存,代价是本地写入延迟也会被连带拉高,很多所谓Riak变慢了的故障,根因其实是复制链路堵了。
full-sync是另一个容易被忽视的干扰源。它按配置的周期对两个集群做全量比对,找出差异并补齐,无论是keylist策略还是aaeb策略,都会在备集群上产生大量读请求,同时占用跨机房带宽。如果full-sync恰好和业务高峰重叠,realtime队列几乎必然积压。此外还有连接层的问题:跨机房的TCP长连接可能被中间的防火墙、NAT设备按空闲超时掐断,MTU不一致造成的黑洞会让大对象传输反复重传,这些都会表现为间歇性的同步停顿,日志里能看到连接重连的痕迹。
排查:用数据把问题定位到具体环节
排查的第一站是riak-admin repl status,这个命令输出主集群侧的复制状态快照。重点看几个字段:realtime队列的当前字节数和上限,两者接近说明链路已经堵住;overflow、drop类计数如果持续增长,说明队列曾经溢出甚至丢弃过数据;连接相关字段能看出client是否处于连接状态。不同版本的Riak字段名略有差异,下面是一份示意输出:
riak-admin repl status # 输出示意(不同版本字段名略有差异) cluster_name: beijing_dc connected_cluster_nodes: ['shanghai_dc@10.10.2.21'] realtime_enabled: true realtime_running: true realtime_queue_bytes: 89753088 realtime_queue_max_bytes: 104857600 realtime_queue_overflows: 37 realtime_errors: 12 fullsync_enabled: true fullsync_running: false fullsync_interval: 6
单次快照说明不了趋势,延迟排查最需要的是连续采样。可以写个简单的脚本把关键字段定时落盘,或者接入监控系统做时序存储,然后观察队列字节数的曲线形态:如果它长期贴着上限走,说明整体吞吐能力不足;如果平时很低、每天固定时段抬升,多半是full-sync或定时任务在抢资源;如果曲线呈锯齿状且伴随连接断开日志,优先查网络中间设备。下面是一个采样脚本的示例:
#!/bin/bash
# 每10秒采样一次复制队列与连接状态,便于事后分析趋势
while true; do
ts=$(date '+%F %T')
riak-admin repl status | \
grep -E 'realtime_queue_bytes|realtime_queue_overflows|realtime_errors|fullsync_running' | \
sed "s/^/[$ts] /" >> /var/log/repl_watch.log
sleep 10
done
命令行之外,还要看网络层和备集群。网络层用ping确认RTT基线,用mtr看路径上有没有丢包和异常跳数,用tcpdump在复制端口上确认流量是否持续平稳,MDC复制的连接默认走备集群的protocol buffers监听端口(默认8087,以实际配置为准)。备集群侧则要看put延迟和磁盘IO:如果备集群本身写入就要几十毫秒,复制吞吐的天花板就被它压死了,调网络参数也无济于事。另外记得翻一下sasl日志,riak_repl的连接重试、队列溢出都会在这里留下记录,时间点往往能和监控曲线对上。
调优:队列参数、full-sync调度与TCP缓冲区
队列参数的核心是权衡。rtq_max_bytes和queue_max调大,能容纳更深的积压、减少vnode被阻塞的频率,本地写入延迟更平稳,但内存占用更高,极端情况下故障恢复后要追赶的数据也更多;调小则相反。一般建议先保证队列能扛住一次正常的流量抖动,比如把上限设为高峰期5到10秒的写入量,再通过监控确认它平时维持在低水位。full-sync侧的调度更关键:full_sync_on_start建议设为false,避免集群重启后立刻发起全量同步把链路打满;full_sync_interval安排在业务低峰时段;并发上限用max_fssource_node、max_fssink_node等参数压住,别让full-sync把备集群和带宽同时打满。下面是advanced.config的参考写法:
[
{riak_repl, [
{rtq_max_bytes, 524288000},
{queue_max, 20480},
{queue_min, 1024},
{full_sync_on_start, false},
{full_sync_interval, 12},
{full_sync_strategy, aaeb},
{max_fssource_cluster, 2},
{max_fssource_node, 1},
{max_fssink_cluster, 2},
{max_fssink_node, 1}
]}
].
跨机房或跨洋链路还涉及一个经典问题:长肥网络。带宽延迟积等于带宽乘以RTT,一条1Gbps、RTT为150毫秒的链路要跑满带宽,需要约18MB的在途数据窗口,而操作系统默认的TCP缓冲区远远不够,结果就是吞吐被窗口限制卡在很低的水平,复制队列自然追不平。把收发缓冲区调大、开启窗口缩放,往往能让复制吞吐提升数倍:
# /etc/sysctl.conf,针对长肥网络的TCP调优 net.core.rmem_max = 33554432 net.core.wmem_max = 33554432 net.ipv4.tcp_rmem = 4096 87380 33554432 net.ipv4.tcp_wmem = 4096 65536 33554432 net.ipv4.tcp_window_scaling = 1 net.ipv4.tcp_mtu_probing = 1 # 修改后执行 sysctl -p 生效
连接稳定性同样值得处理。如果确认中间设备会掐空闲连接,可以让复制流量保持足够密度(队列有数据时连接本来就不会空闲),或者和网络团队协商调整空闲超时;遇到大对象同步反复失败,开启tcp_mtu_probing排查MTU黑洞。这些改动都要配合监控曲线验证效果,调优不是一次性动作,改完之后至少观察一个完整的业务周期再下结论。
架构层面:让业务尽量少等复制
MDC复制本质上是单向的最终一致性,主备集群之间的数据差异永远存在一个时间窗口。如果应用把写请求发到主集群、紧接着又到备集群读同一条数据,读到的就可能是旧值,这不是复制故障,而是读路由的问题。常见做法有三种:一是读写都走主集群,备集群只做容灾,业务对延迟的感知为零;二是会话粘性,同一用户的读写落在同一侧;三是业务上接受短暂不一致,用版本号或时间戳做兜底展示。选哪种取决于业务对一致性的要求,而不是技术偏好。
容量规划上,备集群的写入能力应与主集群的高峰写入吞吐匹配,包括磁盘IO和vnode处理能力,否则无论怎么调网络参数,队列都会长期积压。跨机房链路建议走专线或至少有QoS保障的通道,把复制流量和业务流量隔离,避免高峰期互相挤占。定期演练也很重要:人为制造一次队列积压,观察背压行为和恢复速度,比故障发生时才第一次见到这类曲线要从容得多。
总的来说,MDC复制延迟的治理思路可以概括为一句话:先用监控把延迟拆解到网络、队列、full-sync、备集群四个环节,再针对具体环节做参数和架构调整,并且始终用队列水位这类指标验证效果。把队列字节数、连接状态、full-sync进度纳入日常监控之后,绝大多数延迟问题都能在影响业务之前被发现和处理。

