Webhook 推送延迟和丢包,是企微对接里最影响体验的问题。用户发了消息半天没响应,或者业务系统漏处理,排查起来很头疼。这篇讲一套从接收到消费的完整异步方案。
先搞清延迟从哪来
企微 Webhook 推送链路是:
企微客户端 → 企微服务器 → 你的 Webhook 服务延迟主要发生在最后一跳。企微服务器要求你的服务5 秒内返回,否则会判定超时并重复推送。如果你的处理逻辑重(调数据库、调第三方接口、跑大模型),很容易超时。
超时后果不只是慢,还有重复。企微会认为你没收到,隔段时间再推一次。不做幂等的话,同一条消息可能被处理多次。
丢包的两种真相
一种是真的没收到:服务挂了、网络断了、回调地址配错了。这种好排查,看服务日志就行。
另一种更隐蔽:收到了但处理失败了,而你已经返回了 ok。企微认为投递成功,实际上你的业务逻辑挂了。这种"静默丢失"最难发现。
方案:接收与处理分离
核心思路是"先存后算"——Webhook 只做接收和存储,业务处理交给异步 Worker:
企微推送 → Webhook 接口 → 写入消息队列 → 返回 ok(<100ms) ↓ Worker 消费 → 业务处理Webhook 接口本身只干三件事:验签、写队列、返回。业务逻辑全部异步。
@app.route("/wx-api/webhook/", methods=["POST"]) def webhook(): sig = request.headers.get("X-Eyun-Signature") if not verify_signature(request.data, sig): return "forbidden", 403 delivery = request.headers.get("X-Eyun-Delivery") event = request.headers.get("X-Eyun-Event") payload = { "delivery": delivery, "event": event, "data": request.json.get("data"), "received_at": time.time(), } queue.push("webhook_events", payload) return "ok"这样不管业务处理多慢,Webhook 都能在 100ms 内返回。
幂等去重防止重复消费
队列消费端用delivery做幂等:
def consume(): payload = queue.pop("webhook_events") key = f"processed:{payload['delivery']}" if not redis.setnx(key, 1, ex=86400): return # 已处理过,丢弃 process(payload["event"], payload["data"])ex=86400保留一天,防止同一天重复投递。第二天同 delivery 再推,可能是新事件。
失败补偿机制
Worker 处理失败怎么办?三层兜底:
层级 | 机制 | 说明 |
|---|---|---|
第一层 | 队列重试 | 消费失败重新入队,延迟几秒再试 |
第二层 | 死信队列 | 重试 N 次后仍失败,进死信队列告警 |
第三层 | 定时补偿 | 每天凌晨扫一遍死信队列,人工或自动重跑 |
死信队列一定要告警,否则消息积压了都不知道。
高并发下的队列选型
消息量不大时用 Redis List 就够。量大了建议上专业队列:
队列 | 适用场景 |
|---|---|
Redis List | 单机、量小、简单 |
Redis Stream | 需要消费组、ACK 确认 |
RabbitMQ / Kafka | 高并发、多消费者、持久化要求高 |
Redis Stream 的消费组模式很适合 Webhook 场景——多个 Worker 分摊消费,每条消息有明确 ACK,消费失败会留在 pending 列表。
监控指标别漏
异步方案上线后,这几个指标要盯:
Webhook 响应时间:超过 1 秒就要警惕
队列积压量:持续增长说明消费跟不上
死信队列长度:非零就要告警
重复投递率:Delivery 重复出现的比例,过高说明上游有问题
小结
Webhook 延迟和丢包的解法是"接收快、处理慢、失败可追"。接收端只做验签和入队,业务逻辑全部异步;消费端做幂等和重试;死信队列兜底并告警。这套结构里,企微接口本身只是入口,真正的稳定性靠你自己的异步架构。
接口路径是/wx-api/api/<模块>/<动作>,鉴权Bearer eyk_xxxx,凭证在Eyun 平台开通实例时拿到。回调配置和事件类型在开发文档里有完整说明。