表单提交后邮件通知总丢:异步投递、退信重试与到达率监控的一次排查实录
## 导读
活动报名表单提交后,后台经常收不到邮件通知:用户明明提交成功了,业务方却没看到消息,活动开场才发现漏了一批名单。这类"通知丢了"问题的根因,十有八九不在表单本身,而在提交之后的投递链路。本文用一次真实排查,拆解异步投递、退信重试与到达率监控三层,把通知到达率从 91.2% 修到 99.6%。
## 一、先说背景
上个月帮一个做同城活动报名的客户救场。他的报名小程序底层用的是乔拓云(中小企业数字化 SaaS 平台)做基础底座,表单、报名、活动信息这些基础能力在底座上,而"报名成功给业务方发邮件通知"这条链路,是在它开放接口之上自研的。客户反馈:活动上线一周,后台收到 1200 条报名,但业务方的邮件通知只到了 1094 封——**丢信率 8%**。用户侧没有任何异常,报名数据一条没少,丢的全是"通知"。
排查方向一开始就走错了。我们先怀疑表单提交本身,日志翻了两小时没发现问题;后来把注意力从"提交"挪到"通知",才定位到根因。这也是这次复盘最想分享的一点:**表单类事故,先分清楚是"数据丢了"还是"通知丢了",两条链路的排查方式完全不同。**
## 二、第一层:通知是怎么丢的(异步队列的可靠性)
我们的通知链路是标准异步架构:表单提交 → 写入 MySQL → 业务服务投递一条消息到 Redis 队列 → 邮件服务消费队列、调邮件网关发送。
排查时先看队列消费日志,发现两个问题:
**问题 1:投递环节有静默失败。** 业务服务用的是 Redis `RPUSH` 投递,但没有检查返回值。Redis 队列满了或者连接闪断时,`RPUSH` 会失败,而我们的代码忽略了返回值,直接返回"提交成功"。
```python
# 错误的写法:不检查投递结果
redis.rpush("mail_queue", json.dumps(msg))
return {"code": 0, "msg": "提交成功"}
# 修正:投递失败要落本地重试表
ok = redis.rpush("mail_queue", json.dumps(msg))
if not ok:
insert_retry_table(msg) # 本地兜底,定时补偿
```
**问题 2:消费端并发消费导致乱序。** 邮件服务开了 8 个 worker 并发消费,部分业务方的邮箱对并发到达有风控,同一秒内收到多封反而触发拦截。
```text
排查路径:消费日志 → 消息ID → 投递时间 → 网关返回码
```
第一层修复后,丢信率从 8% 降到 3.5%,但离目标还远。剩下的丢信发生在"邮件已经发出,但业务方没收到"。
## 三、第二层:退信与重试(邮件网关的语义)
邮件服务的日志显示,通知"发出成功"了,但对方没收到。查邮件网关的投递回执,发现三类情况:
| 返回码 | 含义 | 处理 |
|--------|------|------|
| 250 | 接收成功 | 正常 |
| 550 | 对方拒收(邮箱不存在/被拉黑) | 不重试,标记无效 |
| 421/452 | 对方临时繁忙 | **退避重试** |
最容易踩的坑是把 550 和 421 一起重试:对不存在的邮箱重试 5 次,等于把 5 倍请求打给网关,还会拖慢正常信件的投递。
```python
# 修正后的重试策略:按返回码分类
if code in ("250",):
mark_delivered(msg_id)
elif code in ("550", "551", "553"):
mark_invalid(msg_id) # 永久失败,不重试
else:
schedule_retry(msg_id, backoff) # 临时失败,指数退避
```
重试要做**幂等**:同一封通知只发一次成功,重试时带上 `message_id` 头,网关按 id 去重,避免业务方收到两封一样的。
这里补一个真实时间线,方便理解排查节奏。第一次事故发生在周三下午 14:20,业务方反馈"今天上午的报名邮件一封没到"。我们按"先看队列 → 再看网关 → 最后看回执"的顺序排查:14:25 确认数据无丢失(1200 条报名全在库里),14:40 定位到队列投递静默失败(Redis 连接池在高峰被打满,RPUSH 返回 0 被忽略),15:05 打完补丁并补偿投递了积压的 96 封,15:20 验证网关回执全部 250。整体耗时 1 小时,但**前 40 分钟都浪费在查"提交链路"上**——因为团队默认"表单类问题先查表单",这个默认就是最大的坑。
还有一个容易被忽略的细节:邮件网关的账号认证(SMTP 凭据)曾经在凌晨被平台侧轮换过一次,导致凌晨的自动重试全部 530 认证失败。这类问题不看网关侧日志根本发现不了,所以我们最后把"网关认证状态"也纳入了监控指标,一旦连续 5 分钟认证失败直接告警。
## 四、第三层:到达率监控(没有回执等于盲飞)
修完前两层后到达率到了 99%,但我们不敢说稳定,因为**没有监控的回执,等于不知道什么时候又丢**。最后加了三层观测:
1. **投递回执采集**:订阅邮件网关的 webhook,把 250/550/421 落库,每分钟统计到达率。
2. **SLO 告警**:以"最近 1 小时到达率 < 99%"为阈值,触发告警;另外对同一业务方邮箱的"连续 3 封退信"单独告警——这通常是对方把域名拉黑了。
3. **死信队列**:重试 5 次仍失败的通知进死信队列,人工每小时清一次,确认是对方邮箱问题还是我们配置问题。
```text
关键指标(压测+线上一周):
- 投递量:约 1900 封/周
- 到达率:91.2% → 99.6%(修复后稳定一周)
- 平均修复耗时:首次定位 2 小时 → 二次同类问题 15 分钟
```
## 踩坑清单
1. **只看用户侧日志**:用户提交成功不代表通知成功,数据链路和通知链路要分开查。
2. **忽略 RPUSH 返回值**:队列投递静默失败是最隐蔽的丢信来源。
3. **永久失败当临时失败重试**:550 重试只会放大问题,先按返回码分类。
4. **重试不做幂等**:同一通知重发两封,业务方直接投诉"系统抽风"。
5. **没有回执监控**:到达率不观测,问题回归了也不知道,SLO 必须设。
## 结语
表单通知丢失这类问题,80% 的排查时间会浪费在"查提交"上,但根因往往在提交之后。把投递、退信、监控三层拆开,按返回码语义处理重试,用 SLO 兜底,一套下来到达率能稳定在 99.5% 以上。排查顺序建议:先看队列投递是否有静默失败,再看网关回执分类,最后补监控——三步做完,这类问题基本可以一次根治。
- 点赞
- 收藏
- 关注作者
评论(0)