提示

返回博客列表

那家 API 挂了四十分钟

14:20 左右有人在群里问,说客服那边怎么没有 AI 回复了。

我去翻日志,没有异常堆栈,只有一行接一行的 AI 客服回复生成失败,本次将降级为留言。generate_reply 里那个 except Exception 把超时全吞了,返回 None,chat_send 照常把用户消息存好、返回成功。从用户那边看,就是"发了条留言",完全正常。

这也是我们当时设计里最在意的一条:AI 挂了不能影响送达。这点确实做到了,四十分钟里没有一条留言丢。

但另一面是,我们连"上游挂了"都是靠人肉发现。

该切的时候没切

配置里只有一个地址:

AI_CHAT_BASE_URL = 'https://api.deepseek.com'
AI_CHAT_MODEL = 'deepseek-chat'
AI_CHAT_TIMEOUT = 8

兼容协议让换服务商变得很便宜——改两个字符串的事。可"能换"和"会自动换"是两码事。那天下午我们手上没有健康检查,没有备用地址,也没有熔断,只有一个超时 8 秒。

14:23 确认不是自己网络的问题,14:31 在对方状态页看到公告,15:02 恢复。中间那四十分钟,每一次离线留言都要先白等 8 秒才降级。

更要命的是重试。有人提过"超时就重试一次",听着很自然,真算一下不对:8 秒超时重试三次就是 24 秒,而这 24 秒里 gunicorn 的那个 sync worker 一直被占着。我们跑的是 20 个 sync worker,worker_class='gevent' 是注释状态,一个进程同时只能处理一个请求。上游一慢,排队等 AI 的请求会把 worker 一点点吃掉,最后拖累的就不只是客服了。

所以超时这件事不能靠重试解决,得靠"别再打了"。

补的三处

第一处是熔断。连续失败到一定次数,就直接跳过调用,不用每次都等那 8 秒:

FAIL_KEY = 'chat:ai_fail_streak'
CIRCUIT_TTL = 60          # 熔断 60 秒后放一次探针请求

def circuit_open():
    return (cache.get(FAIL_KEY) or 0) >= 5

def note_failure():
    try:
        cache.incr(FAIL_KEY)
    except ValueError:
        cache.set(FAIL_KEY, 1, CIRCUIT_TTL)

连续 5 次失败就断,60 秒后放一个请求过去探一探,成功就清零。写得很糙,incr 遇上 key 过期会抛 ValueError,那层 try 是被线上报错逼出来的。

第二处是健康检查不能只 ping 域名。API 域名活着、TCP 通、TLS 握手成功,都不代表 chat/completions 能正常回。我们改成定时发一个最小请求,max_tokens=1,看两件事:有没有在 3 秒内回来,以及 choices 里有没有内容。这个探针本身也花钱,一分钟一次,一个月下来不到一块钱。

第三处才是备用地址。既然兼容协议把接口长得一样这件事解决了,切换就只是把 base_url 和 model 换成另一组值。我们加了个 AI_CHAT_FALLBACK_*,主地址熔断后自动用备用的,参数单独配——因为两家对 max_tokens 的叫法、模型名规则、返回里 content 会不会是 None,都不完全一样。

顺带踩了个小坑:切换之后每日限额的 key 是按身份加日期算的,跟服务商无关,所以不用重置;但提示词缓存是跟着服务商走的,切过去之后头几次请求全是冷启动,延迟会突然高一截。这个在压测里没出现,是灰度那天才看见的。

灰度没敢一次全切

备用地址接好之后没直接全量。先按会话 id 取模放了 10% 流量过去,观察两天拒答率和 p95 延迟,再放到 50%,最后才全量。

这一步靠的是前面那套离线回归集——两家模型对边界条款的遵守程度不一样,同一个提示词在备用模型上,"能开发票吗"那条会多答两句。跑一遍三十条就能看出来,不用等用户投诉。

没做完的还有不少。备用地址的健康状态现在是独立的,主地址恢复之后怎么平滑切回来,写了一半;两家单价不同,故障期间的成本没有单独统计;熔断计数放在缓存里,多进程之间倒是共享了,但重启就没了,谈不上多可靠。

熔断那个探针请求也踩过坑。60 秒后放一个请求过去探活,如果还是失败,note_failure() 又把计数加回去了,于是每次探针都在给熔断续命,日志上看起来像"上游一直没好"。改成探针失败不动计数、只刷新 TTL 才对。这个逻辑我是看见每 60 秒准时一条失败记录才反应过来的。

多进程那块也不严谨。计数放在 Redis 里,20 个 worker 共享,这点没问题;但缓存一重启,熔断状态就没了,所有请求又开始一起等那 8 秒。真要严实得把状态落到库里,我们没做,因为一年也遇不上几次。

切换之后还有个隐性成本没算过:两家的提示词缓存互相不认,切过去头几百次请求全是冷启动,那半天的单价明显高一点。金额不大,可当时看曲线以为备用商更贵,还差点去问人家是不是计价错了。

VidDown 上换服务商那次没这么惊险,主要是提前留了能改的口子。

那份事后记录我写了三段就搁下了,文件名还叫 ai-outage.md,连日期都没填。倒是熔断那段代码第二天就合了,人总是先修能修的那部分。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部