异步任务与消息队列选型:下单接口发邮件从24毫秒拖到924毫秒的实测判据
本文目录
- 异步任务改造前,为什么先测响应时间再做队列选型?
- 同步调用第三方接口会让响应时间增加多少毫秒?
- 串行、并发与消息队列异步化,用户等待时间差多少?
- 不引入消息队列,异步任务还有哪两条退路?
- 退路一:用定时任务轮询异步任务
- 退路二:用数据库表充当任务队列
- 退路三:引入独立的消息队列组件
- 同样5000条任务入队,为什么换存储只快10倍、改成批量提交却快29倍?
- 什么样的异步任务才真正需要进消息队列?
- 任务耗时低于多少时,消息队列就不划算了?
- 队列消息体里该放完整数据还是只放标识?
- 引入消息队列后会多出哪些新的运维问题?
- 为什么单看队列长度无法判断消费是否正常?
- 队列消费者空轮询和阻塞等待的开销差多少?
- 异步任务架构应该怎样分三个阶段演进?
- 第一阶段的任务表至少需要哪几列?
- 独立站把第三方调用移出请求路径,能拿到哪些收益?
- 如何找出藏在请求路径里的同步外部调用?
- 常见问题解答
- 权威参考资料
摘要:下单成功后要给用户发一封确认邮件,这行代码放在哪里,决定用户要等多久。保哥在自己那台服务器上实测了一遍:本站响应的首字节中位数是23.9毫秒,一次真实第三方接口调用的中位数在437到900毫秒之间,还有一个境外接口整整15秒才超时返回。把这类调用内联进请求路径,响应时间会变成原来的19倍到629倍。同一轮实测还得到一个反直觉的结果:把存储从数据库换成内存库,吞吐只提升了10倍;把逐条调用换成批量调用,同一个存储上提升了29倍。决定快慢的主要是往返次数,用什么存储是次要的。本文不讲队列教程,给的是一张判据表:什么时候还不需要队列,以及三个替代方案各自能撑到哪一步。
市面上讲消息队列的文章,大多从生产者、代理、消费者这三个名词讲起,再接一张选型对比表。这些内容有用,但默认了一个前提:你已经决定要上队列了。
实际情况往往相反。一个日均几百单的独立站,后台跑着几个定时脚本,有人提出这里应该上个队列,团队里没人说得清为什么。结果要么硬上,多了一个会挂的组件;要么一直不上,让用户在页面上等第三方接口返回。
所以这次先不谈方案,先量数字。下面的数据全部来自本站那台单机,没有引用别处的基准测试,测试期间站点正常对外服务。
异步任务改造前,为什么先测响应时间再做队列选型?
那台机器上跑着这个站,热态响应很稳。用命令行连打5次,首字节分别是23.8、23.9、23.9、24.0毫秒,四次几乎一致。
第5次是486.8毫秒。
同一个地址、同一分钟、同一台机器,最慢的一次是最快的20.5倍。因此后面所有对照都取中位数,不取平均值,平均值会被这种偶发尖刺严重抬高。服务器负载排查那篇里也遇到过同样的问题,一次异常就能把平均值抬高好几倍。
基线确定为23.9毫秒。接下来往请求里加一个外部依赖。
同步调用第三方接口会让响应时间增加多少毫秒?
保哥挑了5个真实端点,每个连打5次,取最快、中位、最慢三个值。请求全部从那台服务器发出,不走本地网络:
| 被调用的对象 | 最快 | 中位 | 最慢 | 返回码 |
|---|---|---|---|---|
| 某代码托管平台的接口 | 296.5 | 436.8 | 981.4 | 403 |
| 某CDN厂商官网 | 642.3 | 721.9 | 918.9 | 200 |
| 某接口调试服务 | 876.6 | 899.7 | 1082.2 | 503 |
| 某搜索引擎首页 | 15000.5 | 15001.1 | 15001.2 | 0 |
| 本站自己(对照组) | 24.1 | 24.4 | 681.9 | 200 |
单位都是毫秒。最后一列的0表示没有拿到响应:到了15秒的超时上限,连接被主动断开。这台机器访问不了那个域名,三次读数分别是15000.5、15001.1、15001.2,误差不到千分之一,反映的是超时计时器本身的精度,与网络抖动无关。
另一个细节:返回码403和503那两行,耗时并不比返回200的那行短。失败不比成功快,很多人做容量估算时会漏掉这一点,出错的调用不一定很快返回。
把这三档耗时加到基线上:
- 内联一个437毫秒的调用:23.9变成460.9毫秒,慢19.3倍
- 内联一个900毫秒的调用:23.9变成923.9毫秒,慢38.7倍
- 内联那个访问不通的接口:23.9变成15023.9毫秒,慢628.6倍
第三行最需要关注。这个接口平时可能一直正常,某天对方机房故障,或者某条国际线路不稳定,你的下单接口就从24毫秒变成15秒。用户不知道是第三方的问题,只看到点击提交后页面转了半分钟。
判断依据很简单:只要调用对象不在你的机房里,它的最坏耗时就等于你配置的超时时长。配15秒,最坏就是15秒;不配超时,最坏是无穷。
串行、并发与消息队列异步化,用户等待时间差多少?
假设下单后要做三件事:发确认邮件、通知仓储、同步数据看板。按上面实测的耗时取437、722、900毫秒,三种做法的结果如下:
| 做法 | 用户等待 | 说明 |
|---|---|---|
| 三件事串着做 | 2059毫秒 | 三段时间直接相加 |
| 三件事并发做 | 900毫秒 | 等于最慢那件事 |
| 三件事丢进队列 | 0.4毫秒 | 只付了三次写入的钱 |
最后一行是实测值,往内存库推三条消息实际耗时0.4毫秒,和串行的2059毫秒相差5148倍。
中间那行最容易被忽略。很多团队的第一反应是改成并发调用,耗时确实从2059降到了900。但用户仍然在等,而且同一个请求里挂着三个可能失败的调用,任何一个超时都会把响应时间拉回15秒。本站写过进程池被慢请求占满这件事,并发调用只把占用时间从串行的总和压到了最大值,调用仍然留在请求路径里。
还有一个常见误解:队列并没有让这三件事完成得更快。发邮件原本要花437毫秒,进了队列还是437毫秒,只是这437毫秒发生在用户看到成功页面之后。端到端总时长甚至可能更长,因为多了入队和出队两跳。队列换来的是用户不用等,任务本身并没有变快。
不引入消息队列,异步任务还有哪两条退路?
把任务移出请求路径,队列只是其中一种办法。按复杂度从低到高,可用的方案有三种。
退路一:用定时任务轮询异步任务
把待办任务写进一张表或一个文件,由定时任务定期扫描。这是成本最低的方案,基本不引入新组件。
代价是延迟。定时任务的最小粒度是一分钟,手册页里那句"cron(8) examines cron entries every minute"写得很明确:守护进程每分钟唤醒一次去比对表达式,无法做到每10秒执行一次。延迟分布如下:
| 跑的频率 | 最好情况 | 最坏情况 | 平均等待 |
|---|---|---|---|
| 每1分钟 | 0秒 | 60秒 | 30秒 |
| 每5分钟 | 0秒 | 300秒 | 150秒 |
| 每15分钟 | 0秒 | 900秒 | 450秒 |
| 每60分钟 | 0秒 | 3600秒 | 1800秒 |
那台机器的任务表里有一条每5分钟触发一次的站点计划任务。依赖这条任务的动作,平均要等两分半钟。下单后发确认邮件这类场景,两分半钟已经超出用户的耐心,很多人这时已经在翻垃圾邮件箱了。
定时任务适合备份、生成站点地图、续证书、清缓存这类没人等结果的事,本站的运维自动化那套就是这样跑的。表达式怎么写本站也有专门一篇。
退路二:用数据库表充当任务队列
建一张任务表,写入时插一行,消费时锁一行、改状态、执行任务。这是电商系统里最常见的做法,不引入新组件,而且自带持久化。
在那台机器上实际跑了5000条任务,用最标准的写法:在事务里先加锁读取一行,再更新状态。
| 做法 | 入队耗时 | 出队耗时 | 入队速率 | 出队速率 |
|---|---|---|---|---|
| 数据库表,逐条 | 5731.0毫秒 | 10092.3毫秒 | 872条/秒 | 495条/秒 |
| 内存库列表,逐条 | 560.7毫秒 | 549.2毫秒 | 8917条/秒 | 9104条/秒 |
出队慢有明确原因。关于加锁读取,官方文档里的原话是它会设置与搜索型更新语句相同的锁,其他事务要更新这些行、或者在某些隔离级别下要读取这些行,都会被阻塞。一条任务的完整流程是开事务、加锁读、更新、提交,共四次往返,实测每条2.02毫秒。
顺便测了体积:5000条任务的表连同索引占464KB,折合每条95字节。十万条积压任务大约占9MB,对磁盘来说可以忽略。用数据库表当队列,瓶颈在锁,不在空间。
495条每秒意味着什么?一天86400秒,理论上能处理4200万条,日均几百单的站完全够用。所以不必急着嫌它慢:它慢在单位吞吐,绝对处理能力并不差。
退路三:引入独立的消息队列组件
到这一步才引入独立的消息中间件。它带来的不只是吞吐,更关键的是投递保证、重试、死信这些机制。这部分内容另写一篇,本文只回答要不要走到这一步。
同样5000条任务入队,为什么换存储只快10倍、改成批量提交却快29倍?
这是本轮实测里最反直觉的一组数字,单独列出来。
同样是5000条,四种做法的入队速率:
| 做法 | 耗时 | 速率 | 相对最慢 |
|---|---|---|---|
| 数据库表,一条一条插 | 5731.0毫秒 | 872条/秒 | 1倍 |
| 内存库,一条一条推 | 560.7毫秒 | 8917条/秒 | 10.2倍 |
| 数据库表,每500条一句 | 215.9毫秒 | 23159条/秒 | 26.6倍 |
| 内存库,管道批量推 | 19.1毫秒 | 261780条/秒 | 300倍 |
对比第二行和第三行:批量写数据库,比逐条写内存库还快2.6倍。如果你的判断依据是内存比磁盘快,这一行正好是反例。
差距来自往返次数。单次连接开销实测0.095毫秒,1000次心跳探测共95.4毫秒。单次开销很小,乘以5000就是475毫秒,接近逐条推送总耗时560.7毫秒的85%。把命令打包一次发出去之后,5000条只需19.1毫秒。
可以记成一句话:换存储换来一个量级,换调用方式换来两个量级。先降低往返次数,再考虑要不要换组件。
这个规律在别处同样成立。批量推送搜索引擎收录接口、批量写日志、批量更新库存,只要接口支持一次提交多条,收益都比换一个更快的后端来得直接。本站在对象缓存那篇里测过同类差距,结论方向一致。
什么样的异步任务才真正需要进消息队列?
汇总前面的数字,判据只有四条,全部满足才值得上队列:
| 判据 | 怎么验证 | 不满足时的替代 |
|---|---|---|
| 调用方不关心结果 | 问一句失败了要不要当场告诉用户 | 要告诉,就必须同步做 |
| 耗时进了请求路径 | 量一下它占响应时间的比例 | 占比不到5%,不值得动 |
| 触发时机不确定 | 是事件驱动还是按点跑 | 按点跑,定时任务就够 |
| 生产快过消费 | 比较两端的实测速率 | 消费更快,队列永远是空的 |
第四条最容易被忽略。本轮实测内存库的出队速率是9104条每秒,如果业务每秒只产生几十条任务,队列里根本积压不起来,搭好的削峰机制一次都不会触发。削峰只在峰值真实存在时才有意义。
反过来,如果入队5000条每秒、消费只有495条每秒,每秒净积压4505条,十分钟就是270万条。这时队列起的作用是为扩容争取时间,谈不上削峰。
继续往下算:积压270万条之后,即使流量瞬间降到零,按495条每秒消化完也要5454秒,一个半小时。这一个半小时里,排在后面的用户都收不到确认邮件。削峰没有消除问题,只是把一个五分钟的尖峰摊成一个半小时的长尾,你需要确认业务能接受这种摊法。
解决办法是让消费端能横向扩展。同一个队列挂四个消费者,理论上消费速率乘以四,积压消化时间除以四。这是队列相对定时任务的核心优势:定时任务加机器会重复执行,队列加机器会分摊任务。前提是消费端本身无状态,这一点要在动手前想清楚。
任务耗时低于多少时,消息队列就不划算了?
这条判据很少有人给出具体数字,这里做了实测。单条入队实测0.107毫秒,单条出队0.108毫秒,合计0.215毫秒是队列的固定开销,与任务内容无关。
| 任务本身耗时 | 队列开销占比 | 结论 |
|---|---|---|
| 0.5毫秒 | 30.1% | 纯粹的浪费 |
| 5毫秒 | 4.1% | 勉强可接受 |
| 50毫秒 | 0.4% | 完全可忽略 |
| 500毫秒 | 0.0% | 该进队列 |
由此得到一条实用的粗判据:任务本身跑不到5毫秒,就别放进队列。写一条日志、加一个计数器这类动作,直接执行比排队快,还省掉一个组件。有人把每次访问的计数更新放进队列,结果队列本身的开销比那次更新还高,相当于为了少走两步路,先花十分钟叫了辆车。
队列消息体里该放完整数据还是只放标识?
这一点也实测了。同样5000条,只改消息体大小:
| 消息体大小 | 入队耗时 | 入队速率 | 内存放大 |
|---|---|---|---|
| 60字节 | 19.1毫秒 | 261780条/秒 | 1.05倍 |
| 600字节 | 43.1毫秒 | 116009条/秒 | 1.06倍 |
| 6000字节 | 281.8毫秒 | 17743条/秒 | 1.03倍 |
消息体增大100倍,吞吐下降14.8倍。内存一列的放大系数稳定在1.03到1.06之间,说明存储本身几乎没有额外开销,成本主要在传输。
结论:消息里只放标识,不放数据。传一个订单号,让消费者自己去库里查详情,比把整个订单对象序列化进消息体划算得多。这样还能避开另一个问题:消息里的数据在排队期间可能已经被修改,消费者拿到的是快照,去库里查拿到的才是当前值。
引入消息队列后会多出哪些新的运维问题?
引入一项技术,只了解优点不够。同一台机器上那个内存库的持久化配置,查下来很有代表性:
- 追加写日志:关闭
- 快照策略:空的,等于不做快照
- 距离上次落盘:530938秒,也就是6.14天
- 这期间累计的未落盘变更:108431次
- 内存打满时的淘汰策略:
allkeys-lru
这意味着,这台机器上的内存库六天没有向磁盘写过一个字节,十万多次变更全部只存在内存里。官方持久化文档对几种模式的取舍讲得很清楚,这台机器用的是最宽松的一档。
如果在这个配置下把它当队列用,进程一重启,所有未消费的消息都会丢失。最后那条淘汰策略的风险更隐蔽:内存打满时它按最近最少使用删除键,排队等待处理的消息,在它眼里和普通缓存键没有区别。这就像把待办清单写在一块随时可能被擦掉的白板上,而且路过的人都可能顺手擦掉。
除了丢消息,还会多出三个原来没有的问题:
| 新问题 | 具体表现 | 要额外做的事 |
|---|---|---|
| 可用性下降 | 多一个组件就多一个故障点 | 监控队列长度和消费者存活 |
| 顺序不保证 | 多个消费者并发处理,先进的不一定先完成 | 需要顺序就得按键分区 |
| 重复投递 | 确认丢失会导致同一条被处理多次 | 消费端做幂等 |
第三条属于行业默认行为,与实现质量无关。某云厂商的标准队列文档写得很直接:"Standard queues ensure at-least-once message delivery, but due to the highly distributed architecture, more than one copy of a message might be delivered, and messages may occasionally arrive out of order."即至少一次投递,可能重复,可能乱序。这是设计上的取舍,不算缺陷。
为什么单看队列长度无法判断消费是否正常?
上了队列之后,多数团队做的第一项监控是队列长度,超过某个条数就告警。这个指标没错,但单独看几乎没有信息量。
队列长度是瞬时值,同时受生产端和消费端影响。长度为100条,可能是生产端刚推了100条、消费端还没来得及处理,一秒后就清空;也可能是消费端已经挂了半小时,这100条只是最近积压的一小部分。同一个数字,可能是正常波动,也可能是重大事故。
更有判断价值的是下面三个数,都能从任务表或队列元信息里直接算出来:
| 指标 | 怎么算 | 它回答什么 |
|---|---|---|
| 最老一条的等待时长 | 当前时间减最早那条的创建时间 | 最坏情况下用户等了多久 |
| 队列长度的变化率 | 两次采样的差除以间隔 | 在积压还是在消化 |
| 单条处理耗时的分位数 | 取九十分位而不是平均 | 消费端自己有没有变慢 |
第一个数最应该放在监控面板最显眼的位置。队列长度1000条看着吓人,但如果最老那条只等了2秒,说明消费端跟得上,只是刚好遇到一波流量;反过来,长度只有5条而最老那条已经等了20分钟,基本可以确定消费者卡住了,连5条都处理不完。
第三个数用九十分位而不用平均值,理由和文章开头23.9对486.8那组数据相同:平均值会把一次卡死稀释到看不出来。相关排查思路在负载排查那篇里有展开,同样适用于队列。
队列消费者空轮询和阻塞等待的开销差多少?
这里很容易写错。消费者取不到消息时有两种处理方式,成本差距极大。
用空队列测了3秒:不停询问有没有新消息,3秒内发出29690次请求,折合每秒9897次,全部落空。换成阻塞式等待,同样3秒只发出1次请求,一直挂起到超时返回。
两者之比是29690比1。前一种写法在业务空闲时反而最耗资源:半夜没有订单,消费者却以每秒近万次的频率访问存储。需要记住的是:轮询的成本和业务量成反比,越空闲越浪费。
异步任务架构应该怎样分三个阶段演进?
根据前面的数字,一个处于增长期的独立站可以这样安排:
第一阶段:先把任务移出请求路径。建一张任务表,定时任务每分钟扫描一次。用户不用等,平均延迟30秒。这一步只需半天,能覆盖八成的场景。
这个阶段最关键的工作是把任务表的字段设计对,代码量反而不大:至少要有状态、重试次数、最后一次尝试时间、错误信息这四列,后续所有排查都依赖它们。
第一阶段的任务表至少需要哪几列?
这一步做扎实,后面两个阶段都会轻松。保哥的经验是最少七列,缺任何一列,都会在某次排查时后悔:
| 列 | 作用 | 缺了会怎样 |
|---|---|---|
| 类型 | 区分是发邮件还是同步库存 | 只能一种任务一张表,越建越多 |
| 载荷 | 放订单号这类标识 | 只能靠表名硬编码,改不动 |
| 状态 | 待处理、处理中、已完成、已放弃 | 并发消费时同一条被多人抢 |
| 重试次数 | 失败了第几次 | 坏任务无限重试,占死消费能力 |
| 下次可执行时间 | 实现退避,也实现延迟任务 | 失败就立刻重试,把下游打得更惨 |
| 最后错误 | 失败原因原文 | 只知道失败,不知道为什么 |
| 创建时间 | 算积压时长 | 说不清一条任务等了多久 |
最容易被省掉的是下次可执行时间这一列,而它恰恰最有用。有了它,取任务的条件就从“状态等于待处理”变成“状态等于待处理且下次可执行时间已到”。一个字段同时实现了三件事:失败退避、延迟任务和定时发送。想让一封提醒邮件三天后发出,写入对应时间即可,不需要额外搭建调度器。
状态列一定要建索引。前面提过一次,这里再强调,是因为它是最常见的故障点:表里只有几万行时感觉不到,涨到几百万行后,每次取任务都变成全表扫描,消费速率会一路掉到个位数。
第二阶段:把轮询改成常驻消费。仍然用那张表,改由一个常驻进程持续运行,处理完一条立即取下一条。延迟从30秒降到毫秒级,吞吐达到实测的495条每秒。进程守护交给系统服务管理器即可,写成开机自启服务本站有现成做法。
这一步要避免常驻进程空转:用带超时的阻塞读,不要在死循环里反复查表,否则就会重现前面那个29690比1的情况。
第三阶段:换成专门的队列组件。触发条件是前面四条判据全部满足,并且第二阶段的消费速率已经追不上生产速率。到这时再上,你至少清楚自己要解决哪个具体问题。主流框架自带的队列抽象允许在不改业务代码的前提下,把驱动从数据库换成内存库或专门的中间件,这个切换能力值得在第一阶段就预留好。
本站在邮件群发队列那篇里写过一个反面例子:把队列当成万能药,发送节流配置错误,邮件一次性全部推出,进垃圾箱的比例反而更高。技术选对了,参数配错了,效果同样是负的。
独立站把第三方调用移出请求路径,能拿到哪些收益?
回到搜索这一侧。响应时间除了影响用户体验,也影响抓取。
本站热态首字节23.9毫秒,靠的是长期把动态渲染放在缓存后面。如果这条链路里内联一个900毫秒的第三方调用,首字节就变成923.9毫秒:访客能感觉到卡顿,爬虫在同样的时间预算里能抓取的页面也会少很多。
不稳定的影响更大。那个境外接口的15秒超时如果恰好发生在爬虫抓取时,爬虫拿到的就是超时或错误页。偶发几次问题不大,成为常态就会影响索引。这类问题在监控上也很难发现:正常访客可能因为地理位置不同,不会触发那条慢路径,只有爬虫碰巧遇上。
具体到独立站,哪些动作应该移出渲染路径,可以参考下面这份清单:
| 动作 | 典型耗时 | 该放哪 |
|---|---|---|
| 发交易类邮件 | 数百毫秒 | 队列 |
| 推送收录接口 | 数百毫秒 | 队列,且批量提交 |
| 同步库存到第三方 | 数百毫秒到数秒 | 队列,带重试 |
| 生成缩略图 | 数百毫秒 | 队列,或者上传时一次做完 |
| 写访问统计 | 亚毫秒 | 直接做,进队列反而亏 |
| 刷新缓存标记 | 亚毫秒 | 直接做 |
如何找出藏在请求路径里的同步外部调用?
上面的表格很清楚,但真实项目里最难的是第一步:你并不知道下单流程里到底有几个外部调用。代码是几年里好几个人陆续加上的,没人能说全。
有个不用读代码的办法,实测很有效:把同一个接口在两种条件下各请求十次,比较耗时分布。一种是正常状态,另一种是切断出网流量,或者把第三方域名解析到一个不通的地址。如果两组数字差距很大,说明请求路径里确实挂着外部依赖;如果几乎没变,说明这些调用已经是异步的,或者根本没有被触发。
再细一点,可以看响应时间的分布形状。纯本地处理的接口,耗时会集中在很窄的区间里,比如本文开头那四次23.8到24.0毫秒。一旦分布出现长尾,大部分请求很快,偶尔冒出几百毫秒甚至几秒,这条长尾几乎一定来自网络调用。本地计算不会突然慢十倍,网络会。
以本站那五次实测为例:四次23.9,一次486.8。如果这是你的下单接口,第五次就是需要排查的对象,不能当作噪声忽略。做性能优化时常见的错误是只盯平均值,而用户投诉总是来自长尾那一端。
某个做户外装备的独立站在旺季促销时就遇到过这种情况:平时下单接口的平均耗时一直正常,促销一开始就有用户反馈点击提交没反应。排查后发现库存同步接口挂在下单流程里,对方平时响应很快,促销期间自身也被打满,响应从两百毫秒涨到十几秒。问题出在对方,受影响的却是自己的转化率。把这个调用移进队列后,下单接口的耗时曲线立刻平稳了。
所以对独立站来说,把第三方调用移出请求路径有两方面收益:访客等待更少,爬虫也抓得动。判断标准可以简化为:能在页面渲染完成后再做的事,就不该出现在渲染路径上。这和用工作流工具把重复动作产线化的思路一致:先让人和请求不再等待,再考虑提速。
常见问题解答
问:几百单的小站到底要不要上队列?
按第四条判据,大概率不需要。日均几百单意味着每秒不到一条任务,而一张普通数据库表实测能出队495条每秒,两者相差四个数量级。先做第一阶段,把任务写进表、由定时任务扫描,通常就够了。等到发现表里积压的行数持续增长,再考虑往上走。判断标准是积压曲线的斜率,订单量的绝对值参考意义不大。
问:数据库表当队列会不会把主库拖垮?
会,原因在锁,与吞吐无关。加锁读取会阻塞其他事务对这些行的更新,如果消费者持有锁去做一件耗时几百毫秒的事,锁会一直不释放。正确写法是先在事务里快速把状态标记为处理中并提交,再在事务外执行任务。本轮实测的10092.3毫秒就是标准写法的成本,全部是锁和事务的开销,与实际业务无关。另外要给状态列建索引,否则每次取任务都是全表扫描。
问:为什么并发调用不算数?
因为并发调用没有把任务移出请求路径,只是把总耗时从三段相加压到了最慢的那一段。实测从2059毫秒降到900毫秒,看起来省了一半多,但用户仍然在等,而且同一个请求上挂着三个可能超时的调用。任何一个遇到15秒超时,整个请求仍然是15秒。并发属于优化,异步属于解耦,两者解决的是不同问题。
问:内存库当队列到底靠不靠谱?
取决于配置。本轮在那台机器上查到的配置是:追加写关闭、快照策略为空、六天没有落盘、内存打满时按最近最少使用淘汰。这种配置下当队列用,重启会丢消息,内存紧张时也会丢。要把它当队列,至少要打开追加写,并把队列的键放在一个不做淘汰的实例上,不要和缓存共用同一个实例。共用的最大风险是:缓存打满触发淘汰,被删掉的可能恰好是待处理任务,而且没有任何提示。
问:批量提交是不是万能的?
不是。批量提交降低了往返次数,代价是延迟增加、失败粒度变粗。攒够500条再发,第一条要多等后面499条的时间;一批里有一条失败,还要决定是整批重试还是单独挑出来处理。对实时性要求高的场景,常见的折中是按条数和时间双阈值触发:攒够100条发一次,或者攒满50毫秒发一次,哪个先到按哪个。
问:怎么判断现在的任务表已经撑不住了?
看三个数:待处理行数是否在持续增长而非波动,最老一条待处理任务的等待时长是否在变长,消费一条的平均耗时是否在变长。前两个说明生产快过消费,第三个说明单条处理本身在变慢。三个里出现两个,就该考虑升级一级了。这三个数都能用一条查询算出来,建议直接挂到监控面板上,不要等用户投诉才去看。
问:消息体大小有没有硬上限?
各家组件的上限不同,但按本轮实测的趋势,还远没到上限,吞吐就已经下降到难以接受:6000字节消息体的入队速率只有60字节时的十四分之一。实际操作中,消息体控制在几百字节以内比较安全,超过一两千字节就该考虑只传标识。确实需要传大对象时,标准做法是把对象存到别处,消息里只放一个取回地址。
权威参考资料
本文标题:《异步任务与消息队列选型:下单接口发邮件从24毫秒拖到924毫秒的实测判据》
本文链接:https://zhangwenbao.com/async-task-when-to-use-message-queue.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0