ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RabbitMQ Shovel跨机房消息同步:参数、实操与避坑

RabbitMQ Shovel跨机房消息同步:参数、实操与避坑 跨机房同步消息这件事我前前后后踩过不少坑。最早的做法是自己写一个客户端从 A 机房的 RabbitMQ 消费再往 B 机房的 RabbitMQ 投递中间加个 while 循环和一张进度表。这套东西在测试环境跑得挺好一上线就原形毕露消费端进程挂了没人知道ACK 时机没算对导致重启后重复投递几万条扩容还得改代码重新发版。后来我把这套自研搬运工彻底删掉换成了 RabbitMQ 自带的 Shovel 插件配置量不到原来的十分之一稳定性反而高了一个档次。这篇就把 RabbitMQ 扩展组件里的第 9 号角色——Shovel——从场景、参数、实操到排查完整讲一遍。先说清楚它是什么。Shovel 是 RabbitMQ 官方提供的一个扩展插件本质上是运行在 Broker 内部的一个消息搬运工它从一个源可以是队列也可以是交换机拉取消息再投递到另一个目标同样可以是队列或交换机源和目标是两个完全独立的连接可以跨 vhost、跨集群、跨机房甚至跨产品比如从 AMQP 1.0 的中间件搬进 RabbitMQ。它解决的核心问题是消息的可靠跨域搬运包括机房迁移、多活数据汇聚、灰度分流、灾备同步这些典型场景面。这篇文章适合三类人看一是正在做机房迁移或者多活架构、需要把存量消息搬走的运维和后端同学二是已经用上了 Shovel 但被消息怎么少了链路怎么断了折磨过、想搞清楚内部机制的人三是面试里被问到跨集群消息同步怎么做、只知道 Federation 不知道 Shovel 的求职者。文中所有配置我都会给出可直接抄的版本同时把为什么要这么配讲透避免抄完出问题还不知道从哪查。1. 先把场景想清楚Shovel 解决的到底是什么问题1.1 自研搬运进程为什么会翻车绝大多数团队第一次做跨集群同步第一反应都是写个消费者不就行了。这个思路本身没错Shovel 干的也是这件事区别在于它把那些容易出错的细节全部内化了。我列一下自研方案最常见的四个翻车点你对照看看有没有中招。第一是 ACK 时机。很多人写代码是先投递到目标端成功后再 ACK 源端这看起来是对的但目标端的成功到底指什么是basicPublish没抛异常还是 Broker 确认落盘AMQP 0-9-1 的 publish 默认是异步的basicPublish返回时消息可能还在客户端缓冲区里这时候 ACK 掉源端中间进程一崩这条消息就永久丢了。Shovel 的ack-mode参数就是专门解决这个问题的后面会详细拆。第二是进程存活。自研程序是一个独立进程它挂了之后消息会在源队列里堆积但业务方往往几天后才发现。Broker 内部跑的东西至少能跟着 Broker 的日志和监控体系一起被看见。第三是重复投递。源端 ACK 之后、目标端确认之前的窗口如果发生网络分区重启后必然重复。Shovel 同样有这个窗口但它的重连语义是明确的、可预期的你能提前设计幂等而不是靠猜。第四是权限和资源。自研程序需要申请账号、走安全审批、配连接池、管心跳而 Shovel 直接复用 Broker 自身的连接管理。注意Shovel 不是绝对不丢它的可靠性和ack-mode直接挂钩。选错模式它和自研脚本一样会丢消息这一点后面会用表格讲清楚。1.2 Shovel 与 Federation 的选型边界提到 Shovel就绕不开 Federation。两者经常被放在一起比较我给一个我自己的判断标准看你要同步的是一个点还是一堆点。Shovel 的配置单位是一条链路一条 Shovel 对应一个源和一个目标方向明确、语义清晰、调试直观。Federation 的配置单位是一个上游它配在交换机或者队列上一条上游配置可以同时服务同一个 vhost 下多个交换机或队列适合那种我有 50 个队列都要从总部汇聚到中心的场景。对比项ShovelFederation配置粒度一条链路 一个源 一个目标一个上游 可被多个交换机/队列复用连接方向由本地发起连接远端源和远端目标本地主动连上游拉取协议支持AMQP 0-9-1 与 AMQP 1.0 都支持主要面向 AMQP 0-9-1消息顺序单队列内基本有序跨 Shovel 无序单队列内有序典型场景定向迁移、跨协议桥接、灾备、临时搬数多队列汇聚、树状拓扑动态生效支持运行参数形式无需重启支持运行参数形式还有一个我实际很在意的差别Shovel 支持delete-after可以做到搬完就把源队列删掉。这在机房迁移收尾阶段特别有用——数据搬完了源端队列自动清理不用人工守着点删除也不会误删还没搬完的队列。1.3 Shovel 的内部模型一个特殊的消费者理解 Shovel 的工作模型排查问题时你会省一半力气。它是一个双连接结构进程持有一条到源端的连接在这条连接上开一个 channel 做basicConsume同时持有一条到目标端的连接在这个 channel 上做basicPublish。两条连接的地址、认证、TLS 参数都是分开配置的这就是它能跨机房的原因。它的运行状态大致有这么几个starting正在建立连接、声明资源、running正常搬运、terminating收到停止指令正在收尾。在管理界面里你能实时看到状态、已搬运的消息数、最近的错误信息。有几个由此推导出来的结论很实用Shovel 是一个消费者。它和业务消费者在同一条源队列上是竞争关系。如果你已经有一批在线业务在消费同一个队列再挂一条 Shovel消息会被两边瓜分。要做只同步不影响业务正确做法是让业务侧用独立的队列或者靠交换机的 routing key 分流出一份拷贝到专用队列。Shovel 是一条独立的连接。源端和目标端各占一条会消耗连接数配额也会有心跳。网络抖动导致连接断开时它会按reconnect-delay重试不是直接完蛋。Shovel 没有高可用这个概念。它就是某个节点上的一个 Erlang 进程。节点重启、进程崩溃都会让它中断随后重连所以监控必须做不能假设它永远活着。Shovel 不做消息内容转换。它基本是原样搬运消息属性properties会保留headers 也保留。如果你想在搬运过程中改 header 或者打标记可以用add-forward-headers让它带上x-shovel-*前缀的追踪头方便在目标端识别数据来源。2. 核心参数逐条拆解哪些必须懂哪些可以默认2.1 源端与目标端描述协议、URI、队列与交换机Shovel 的描述分源端src-*和目标端dest-*两组结构是对称的。协议上支持amqp091也就是我们最常用的 AMQP 0-9-1和amqp10后者用于和 AMQP 1.0 的中间件对接。绝大多数场景用默认的amqp091就行。URI 的格式遵循标准的 AMQP URI 规范amqp://用户名:密码主机:端口/vhost。这里最容易出错的不是用户名密码而是vhost 的写法。默认 vhost 是斜杠/在 URL 里必须写成%2f写成/会被解析成路径分隔符。我见过太多次因为这一个字符导致 Shovel 一直卡在starting的案例。# 默认 vhost 的正确写法 amqp://sync_user:Passw0rd10.0.1.11:5672/%2f # 自定义 vhost 叫 /order 的写法 amqp://sync_user:Passw0rd10.0.1.11:5672/%2forder源端有两种取数方式二选一不能同时配src-queue直接消费指定队列的所有消息。适合定向迁移、灾备同步。src-exchangesrc-exchange-keyShovel 会在源端自动声明一个临时的、自动删除的队列绑定到指定交换机上用给定的 routing key 收消息。适合按 routing key 分流一份出来同步的场景。目标端同理dest-queue是投递到指定队列dest-exchangedest-exchange-key是投递到交换机并由交换机路由。注意当目标是队列时Shovel 会尝试对目标队列做声明declare。如果你的目标队列是 quorum 队列或者是带特殊参数的镜像队列声明时的参数必须和已存在的队列完全一致否则会报PRECONDITION_FAILED。我的习惯是目标队列提前用脚本创建好属性写死在脚本里不让 Shovel 去声明这样最稳。2.2 ack-mode决定消息会不会丢也决定吞吐ack-mode是整份配置里最需要动脑子的一个参数没有之一。它决定了什么时候算搬运成功、什么时候向源端 ACK。三个取值语义差别很大。取值行为丢消息风险重复风险吞吐no-ack消费到就 ACK 源端不等目标端高低最高on-publish目标端 publish 成功即 ACK 源端中中高on-confirm等目标端 publisher confirm 回来才 ACK 源端低中最低no-ack基本只适合丢几条无所谓的场景比如日志采集类的冗余数据。生产上做业务数据同步我不会选它。on-publish是很多人的默认选择因为快。但你要清楚它的风险publish 成功只代表消息写进了目标端的 TCP 缓冲区或者 Broker 的内存如果目标端 Broker 这时候宕机、且消息还没落盘这条消息就没了而源端已经 ACK。on-confirm是默认值也是我推荐生产使用的模式。它会等目标端返回 publisher confirm也就是 Broker 确认接收之后才 ACK 源端。代价是每条消息多一个确认的等待吞吐会下降。但注意Shovel 内部对这个等待做了流水线处理配合prefetch-count实际吞吐没有想象中那么惨。实操心得如果你的场景是必须一条不丢选on-confirm同时在业务侧做好幂等。因为on-confirm依然不能避免目标端已写入、但 confirm 回包丢失导致的重复投递这是分布式系统的固有问题插件解决不了。2.3 prefetch-count、reconnect-delay、delete-after 的取舍src-prefetch-count控制 Shovel 预先从源队列拉多少条到本地缓冲。默认值是 1000。这个值本质上是在途消息量它直接决定了三件事内存占用、吞吐上限、以及崩溃时的重复量。因为 Shovel 是批量预取再逐条投递的如果进程在搬运过程中崩掉那些已经拉走但还没 ACK 的消息会重新回到源队列被再次消费。也就是说prefetch-count越大崩溃瞬间的重复窗口越大。1000 这个默认值在内存充足的机器上完全没问题单条消息如果平均 10KB缓冲也就 10MB。但如果你的消息是大报文比如单条 1MB 的图片摘要1000 的预取就是 1GB 内存这就必须调小。我的经验值是这样的小消息小于 1KB、追求吞吐src-prefetch-count设 1000 到 5000。中等消息10KB 左右300 到 1000。大消息100KB 以上50 到 200宁可慢一点也别把 Broker 内存打爆。reconnect-delay是断线后的重连间隔单位秒默认 5。这个值的取舍比较微妙设太小比如 1 秒源端整体挂掉时会造成大量无效重连尝试日志刷屏设太大比如 60 秒正常抖动后的恢复时间就变长了。跨公网的链路我一般设 10 到 15 秒同机房内网设 5 秒。src-delete-after只有两个值never默认和queue-length。设成queue-length意味着当源队列被搬空之后Shovel 会删除这个源队列并停止。这是为一次性数据迁移量身定做的功能。我做过一次跨机房搬 3 亿条订单消息就是给每个队列配一条 Shovel配好src-delete-after: queue-length然后睡觉。第二天早上起来源端队列全部清空并自动删除干干净净。注意queue-length的删除动作有误伤风险。如果源队列在生产上还有别的消费者在写被搬空的瞬间被删掉业务就炸了。用之前一定确认这个源队列是专供搬运的。2.4 静态配置与动态参数两种管理方式的差别Shovel 有两种定义方式很多文章混着讲导致读者抄配置的时候一直失败。静态方式写在advanced.config里节点启动时加载。它的位置在不同系统上不一样Linux 一般在/etc/rabbitmq/advanced.configWindows 一般在%APPDATA%\RabbitMQ\advanced.config。静态方式的好处是配置即代码能进版本管理节点重建后配置还在坏处是改配置要重启节点。动态方式通过运行参数runtime parameter定义走管理 HTTP API、管理界面或者命令行工具立即生效不用重启。这是我现在的主力方式因为改一条链路不用动节点风险小得多。这里有个特别容易踩的坑网上大量老文章写的是source/destination嵌套的 proplist 格式那是 3.6 及更早版本的rabbitmq.config写法。新版advanced.config里用的是统一的src-*/dest-*键名。你直接把老配置贴进新文件节点很可能直接起不来或者插件静默不加载。动态参数的键名和静态配置几乎一致只是把下划线换成了短横线风格比如src-uri、src-queue、dest-uri、dest-queue、ack-mode、src-prefetch-count、src-delete-after、reconnect-delay。参数静态键名动态键名默认值源连接地址src-urisrc-uri无必填源队列src-queuesrc-queue无源交换机src-exchangesrc-exchange无源路由键src-exchange-keysrc-exchange-key无目标连接地址dest-uridest-uri无必填目标队列dest-queuedest-queue无预取数量src-prefetch-countsrc-prefetch-count1000确认模式ack-modeack-modeon-confirm搬完是否删源队列src-delete-aftersrc-delete-afternever重连间隔秒reconnect-delayreconnect-delay5附加追踪头add-forward-headersadd-forward-headersfalse3. 从零跑通一条 Shovel 链路3.1 插件启用与前置检查Shovel 的功能由rabbitmq_shovel提供管理界面里的 Shovel 状态页由rabbitmq_shovel_management提供。注意第二个插件是依赖第一个的两个都要开。# 在需要运行 Shovel 的节点上启用插件 rabbitmq-plugins enable rabbitmq_shovel rabbitmq_shovel_management # 确认插件状态 rabbitmq-plugins list -e | grep shovel跑起来之前有几个前置条件必须确认这几条我在生产上一条一条核对过账号权限。Shovel 用的账号需要在源端有read权限消费队列需要读在目标端有write权限投递消息需要写如果让它自动声明队列还需要configure权限。权限不足的表现是 Shovel 卡在starting日志里出现ACCESS_REFUSED。网络连通性。从运行 Shovel 的节点出发要能访问源端和目标端的 5672 端口。注意是从 Broker 节点发起不是从你的办公电脑发起很多人在这里判断错方向。目标资源已存在。目标队列、目标交换机提前建好避免 Shovel 声明时因为属性不一致失败。vhost 要存在。如果动态 Shovel 定义在某个 vhost 下而这个 vhost 在运行节点上不存在参数是无法创建的。实操心得不要在源端和运行端之间搞混。Shovel 的连接是由运行 Shovel 的那个节点发起的。如果源端在 A 机房、目标端在 B 机房你在 C 机房的节点上配 Shovel那 C 节点必须同时能访问 A 和 B。我一般把 Shovel 放在离源端近的一侧减少搬运动作对生产集群的影响。3.2 动态方式用 HTTP API 建一条 Shovel假设场景是这样源端10.0.1.11上有个 vhost/order队列叫order.sync.source目标端10.0.2.22上 vhost/order队列叫order.sync.dest。我们要把源端的消息搬到目标端。用管理 API 创建注意%2f的转义和 URL 里的 vhost 编码curl -u admin:Admin123 -X PUT \ http://10.0.1.11:15672/api/parameters/shovel/%2f/order-sync-01 \ -H Content-Type: application/json \ -d { value: { src-protocol: amqp091, src-uri: amqp://sync_user:Sync%4012310.0.1.11:5672/%2forder, src-queue: order.sync.source, src-prefetch-count: 500, src-delete-after: never, dest-protocol: amqp091, dest-uri: amqp://sync_user:Sync%4012310.0.2.22:5672/%2forder, dest-queue: order.sync.dest, ack-mode: on-confirm, reconnect-delay: 10 } }几处细节值得说一下。URL 路径里的%2f是 vhost 的编码最后一段order-sync-01是这条 Shovel 的名字自己起建议带上环境和用途比如prod-order-to-dr。JSON 里的src-uri密码如果含有、:之类的特殊字符必须做 URL 编码编码成%40否则解析会错位症状同样是卡在starting。创建完立刻查状态curl -s -u admin:Admin123 http://10.0.1.11:15672/api/shovels | python -m json.tool返回里重点看三个字段state应该是runninginfo数组里如果有error关键字就要警觉name和vhost确认是你建的那条。如果state长时间是starting九成是连接或权限问题去看节点的日志。3.3 静态方式写进 advanced.config如果你更偏好配置即代码就用advanced.config。注意这是 Erlang 语法结尾的点和逗号错一个字符整个文件就废掉。[ {rabbitmq_shovel, [{shovels, [{order_sync_01, [{src-protocol, amqp091}, {src-uri, [amqp://sync_user:Sync%4012310.0.1.11:5672/%2forder]}, {src-queue, order.sync.source}, {src-prefetch-count, 500}, {src-delete-after, never}, {dest-protocol, amqp091}, {dest-uri, [amqp://sync_user:Sync%4012310.0.2.22:5672/%2forder]}, {dest-queue, order.sync.dest}, {ack-mode, on-confirm}, {reconnect-delay, 10} ]} ]} ]} ].写完后改文件、重启节点或者用rabbitmqctl eval热加载但生产上我更倾向于找窗口期重启热加载出错不好回滚。启动后同样用 API 查状态静态 Shovel 也会出现在/api/shovels列表里。src-uri和dest-uri的值是列表这一点和动态方式不同。列表的意义是配多个地址做故障转移第一个不可用时会尝试后面的。如果你的源端有多个节点可以把集群里所有节点的地址都列上比只写一个 VIP 更灵活。注意静态 Shovel 的配置是节点级的。在集群里部署时一定要清楚这条配置会被哪些节点加载、实际在哪几个节点上跑起来避免同一个源队列被多条链路重复消费。3.4 验证消息到底过去没有配置成功不等于数据通了一定要做端到端验证。我的验证流程固定三步。第一步看队列计数。分别在源端和目标端查队列深度# 源端 rabbitmqctl -n rabbitnode1 list_queues -p /order name messages messages_ready # 目标端 rabbitmqctl -n rabbitnode2 list_queues -p /order name messages messages_ready第二步投一条带标记的测试消息用管理 API 往源端的默认交换机投递curl -u admin:Admin123 -X POST \ http://10.0.1.11:15672/api/exchanges/%2forder/amq.default/publish \ -H Content-Type: application/json \ -d { properties: {delivery_mode: 2, content_type: application/json}, routing_key: order.sync.source, payload: {\testId\:\shovel-check-001\,\ts\:1700000000}, payload_encoding: string }第三步在目标端把这个shovel-check-001捞出来确认消息体和属性都没变curl -u admin:Admin123 -X POST \ http://10.0.2.22:15672/api/queues/%2forder/order.sync.dest/get \ -H Content-Type: application/json \ -d {count:1,ackmode:ack_requeue_false,encoding:auto}返回里能看到 payload 完全一致、properties.delivery_mode还是 2说明消息属性被正确保留。如果 payload 对但属性丢了检查你源端生产消息的方式是不是本来就没带属性。实操心得验证一定要用带唯一标识的消息不要用线上真实消息。线上的消息可能被其他消费者抢走导致你误判成Shovel 没搬。用order.sync.source这种专供同步的队列最省心。4. 常见问题与排查技巧实录4.1 插件启不来、Shovel 卡在 starting问题一rabbitmq-plugins enable报错说找不到插件。这通常不是插件本身的问题而是 Erlang 版本或者安装包不完整。RabbitMQ 的插件是跟着版本走的3.11 和 3.12 的插件不通用。先确认你的 RabbitMQ 版本再看该版本的插件有没有随包安装。在 Windows 上安装时如果用的是绿色解压包插件目录容易被忽略建议用官方安装器。另外排查时优先看日志文件而不是控制台输出。日志里搜shovel关键字会看到插件启动过程中的完整信息比界面上那个starting有用得多。问题二Shovel 一直卡在 starting日志里有ACCESS_REFUSED。权限问题。到源端给账号配read权限到目标端配write需要声明队列的话再加configurerabbitmqctl set_permissions -p /order sync_user ^order\.sync\..* ^order\.sync\..* ^order\.sync\..*三个参数依次是配置、写、读的正则。别图省事直接用.*权限范围收窄一点出事故时影响面小。问题三卡在 starting日志里是ENOTFOUND或者连接超时。DNS 解析不了或者网络不通。注意 Shovel 是从 Broker 节点发起连接的先在那个节点上telnet 目标IP 5672试一下。还有一种是 URI 里 vhost 写成/而不是%2f解析出来的地址就错了症状和网络不通一模一样很容易误判。问题四报PRECONDITION_FAILED。目标端已存在的队列属性和 Shovel 尝试声明的属性不一致常见于目标端是 quorum 队列或者设置了自定义参数。解决办法是提前把目标队列建好属性写清楚不让 Shovel 声明。4.2 消息丢了、重复了、堆积了这三类问题的排查思路完全不同我整理成一张速查表从现象倒推原因。现象最可能的原因排查动作处理方式源队列空了目标端少了消息ack-mode配成no-ack或on-publish查 Shovel 参数确认 ack-mode改成on-confirm目标端消息数多于源端发出数断线重连导致的重复投递对比源端 ACK 数与目标端写入数业务侧做幂等不做去重目标端数量对不上且 Shovel 状态 running有别的消费者在抢源队列的消息源端查该队列的 consumer 数量把同步队列独立出来源队列持续堆积Shovel 状态 starting链路根本没建立查日志、查权限、查网络按 4.1 排查源队列堆积但状态 running搬运速度跟不上生产速度看 Shovel 的速率指标拆分队列、提高并行度目标端出现同一批消息反复写入目标端拒绝投递Shovel 重试查目标端日志、查队列是否满扩大目标端容量或加限流关于消息丢失我再强调一句Shovel 的丢消息只可能发生在no-ack和on-publish两种模式下。如果你用的是默认的on-confirm并且源端消息本身是持久化的、目标端队列也是持久化的那消息不会因为 Shovel 崩了而丢只会重复。搞不清这一点排查方向就会完全跑偏花大量时间在网络和磁盘上找原因。还有一个隐蔽的坑源端消息不是持久化的。很多人排查半天 Shovel最后发现源端的生产者在发消息时delivery_mode是 1非持久化。Broker 重启后这些消息本来就不在了跟 Shovel 一点关系都没有。看问题要往上游看一步。4.3 断链与监控别等业务方来告诉你Shovel 崩溃或者断链Broker 自己是不会告警的它只会在日志里默默写一行。等你发现的时候可能源队列已经堆积了上千万条消息。所以我强烈建议把 Shovel 纳入监控体系。最直接的方案是定时轮询管理 API把状态和错误信息一起采集出来curl -s -u admin:Admin123 http://10.0.1.11:15672/api/shovels \ | python -c import json,sys data json.load(sys.stdin) for s in data: name s.get(name) vhost s.get(vhost) state s.get(state) print(f{vhost}/{name} state{state}) for item in s.get(info, []): if error in str(item).lower(): print( ERROR:, item) 把这段包进监控脚本状态不是running就发告警。同时监控两个队列的深度差如果源队列深度持续上涨、目标队列深度不涨说明搬运链路已经断了这个信号比状态字段更早暴露问题。另外要监控源队列的消费者数量。如果 Shovel 挂了这个队列的消费者数会从 1 变成 0。这个指标比队列深度更灵敏因为队列深度要堆积到一定程度才触发告警而消费者数变化是即时的。实操心得Shovel 的断链恢复有延迟reconnect-delay设 10 秒意味着最坏情况下断链 10 秒后才开始重连尝试重连再花几秒。如果你的业务对延迟敏感把reconnect-delay调小一点代价是集群抖动时日志会多一些。我个人的平衡点是 5 秒。5. 生产落地的一些取舍经验5.1 吞吐怎么提并行度、prefetch 与 confirm单条 Shovel 的吞吐是有限制的它的结构决定了它是一个单线程的搬运工一条源连接、一条目标连接、一个 channel。实测下来小消息场景单条 Shovel 大概能跑到每秒一两万条具体取决于消息大小、网络延迟和ack-mode。如果这个速度不够有三个思路我按推荐程度排。第一是拆分队列。这是最有效的办法。比如你有 20 个订单队列要搬就配 20 条 Shovel每条搬一个队列天然并行互不干扰出问题也只影响一个队列。缺点是配置文件会变长配错的可能性上升建议用脚本生成配置。第二是调大src-prefetch-count。在内存允许的前提下预取多一些能让连接上的数据流更饱满减少等待往返的时间。这是最简单的一招先调这个把 1000 提到 3000 试试观察吞吐和内存变化。第三是ack-mode降级。从on-confirm降到on-publish能明显提速但代价是可靠性下降。我只在允许丢少量、且目标端是同步刷盘配置的场景下这么干。降级之前一定要评估别为了性能把数据搞丢。反过来如果你发现 Shovel 把源端的 CPU 或者内存吃满了说明并行度太高或者预取太大要往下调。搬运这件事千万别追求极致速度稳比快重要。5.2 什么时候不该用 Shovel用了几年我总结出几个不该用 Shovel的场景避开这些能省很多事。第一要求严格顺序且不能重复的场景。Shovel 在断线重连后会有重复这是机制决定的。如果你的业务对消息顺序极度敏感比如账户流水必须严格按序处理那要靠业务侧的分区有序 幂等来兜底Shovel 只管把消息搬过去。第二消息量极大且要求低延迟的场景。Shovel 是一个独立的搬运环节天然有一层延迟。如果你的场景是下单后 100 毫秒内对端必须看到那应该用双向的业务级同步而不是靠 Shovel 搬消息。第三需要复杂内容转换的场景。Shovel 不做内容转换它只是个搬运工。如果你需要在搬运过程中解析消息体、改字段、然后重新组装那还是得写业务程序Shovel 帮不上忙。第四短期的、一次性的少量数据搬运。如果只有几百条消息要搬手动导一下或者写个临时脚本更快配 Shovel 加验证的时间比手工搬还长。5.3 我踩过的几个坑最后分享几个具体到能直接避开的坑都是我自己或者同事踩过的。坑一add-forward-headers打开后目标端的业务代码收到陌生 header 就报错。这个参数会往消息头里加一批x-shovel-*字段如果目标端用的是严格模式的反序列化比如某些把 header 映射成对象字段的框架可能直接抛异常。开启之前先和目标端确认一下。坑二在镜像队列上做同步两个节点同时跑 Shovel。有一次我把静态配置部署到了集群所有节点结果同一条源队列被多条 Shovel 消费消息被搬了两遍。静态配置是节点级的部署时一定确认清楚在哪几个节点生效。这个坑排查起来特别费劲因为两边的日志看起来都很正常。坑三源端队列设置了消息 TTL搬运速度慢导致消息过期。源队列有 TTL 的话消息在队列里待太久会被丢弃Shovel 还没搬就没了。做迁移之前一定要检查源队列的 TTL、最大长度这些策略必要时先把策略临时调整或者提高搬运速度。坑四把 Shovel 配在了从节点上主节点故障切换后 Shovel 消失了。节点的角色变化会影响负载分布。生产上我会把搬运类的工作放在专用节点上和业务节点做物理或逻辑隔离避免互相影响也避免节点角色变动带来的意外。坑五密码里有特殊字符没转义。前面提过但还是值得单独列一条。密码里的、:、/、#在 AMQP URI 里都是保留字符必须编码。我现在的做法是直接用纯字母数字加少量安全符号生成同步专用密码从源头上避开这个问题比在配置里到处转义省心得多。坑六以为 Shovel 会跟着集群自动漂移。它不会。节点挂了跑在上面的 Shovel 就停了。如果你的业务不能接受这种中断要么在另一个节点上准备一条备用配置注意别同时跑要么在应用层做双写兜底。这个认知比配置技巧更重要。关于这个组件我个人的体会是Shovel 的价值不在于它有多强大而在于它把一个看起来简单、做起来全是细节的事情标准化了。什么时候 ACK、断线怎么重连、搬完怎么清理这些边界条件它都替你考虑过一遍。你要做的其实只有两件事——把ack-mode选对把监控做上。前面那些配置参数大部分场景下默认值就是最优解不用折腾。
返回列表