一个生产环境的Python Telegram机器人,每140分钟准时崩溃。日志像雪崩——先是API限流警告,接着超时、数据库锁定、47个会话泄漏,最后被系统OOM杀掉重启。然后循环再来。
这种连环故障最危险的不是技术本身,是诊断时的误判冲动。"SQLite扛不住并发,换Postgres""裸asyncio太底层,加消息队列""干脆用Go重写"。这些想法我都闪过,但都没做。最终修复只用了18行代码,机器人已稳定运行数周。
技术栈与规模
Python 3.12,aiogram 3.x框架,SQLite存用户状态,全asyncio架构。日活4000条消息,绝对不算高并发。但崩溃间隔精准到140分钟,说明问题在累积而非饱和。
错误日志的链条很唬人:TelegramRetryAfter(限流)→ asyncio.TimeoutError(超时)→ sqlite3.OperationalError: database is locked(数据库锁死)→ 47个活跃会话未关闭 → OOM被杀 → systemd重启。表面看是数据库扛不住,但细想不对——SQLite每秒能处理数万写入,4000条/天的流量连它的零头都够不上。
两个被排除的假设
第一个假设"SQLite是瓶颈",30分钟就被证伪。如果是数据库饱和,应该在持续负载下暴露,而非固定间隔爆发。第二个假设"触发Telegram API限流"也不成立——日均4000条意味着平均每20秒1条消息,而Telegram单机器人限流是每秒30条,差了两个数量级。
结论很明确:系统在把稳定的入站流量,转化成突发的出站API调用。
根因:重试风暴
问题藏在aiogram的默认重试逻辑里。当Telegram返回Retry-After(比如28秒后重试),aiogram会乖乖等待,然后重发。但如果同一时刻积压了大量待发送消息,它们会在28秒后同时苏醒,形成请求洪峰。
更糟的是,这些重试没有全局协调。每个待处理更新各自重试,瞬间制造出数十倍的API调用量。Telegram立刻再次限流,且惩罚更重。循环加速:更多消息积压→更密集的重试→更严厉的限流→数据库连接池耗尽→会话泄漏→内存暴涨→OOM。
140分钟的周期由此而来:这是内存泄漏到临界值的时间。
18行修复:指数退避+抖动+全局锁
核心思路很简单:别让所有重试同时发生。
第一,指数退避。第一次重试等28秒,第二次等56秒,第三次112秒,而非固定间隔。第二,加抖动。在退避时间上随机增减0-30%,打散重试集群。第三,全局锁。同一时刻只允许一个重试请求在飞,其余排队。
代码就三段:一个asyncio.Semaphore(1)做并发控制,一个带random.uniform的退避计算器,一个包装send_message的middleware。没有换数据库,没有加队列,没有换语言。
关键认知
连环故障的日志最会骗人。TelegramRetryAfter、数据库锁定、会话泄漏、OOM,看起来是四个独立问题,实则是同一个根因的连锁反应。诊断时找到第一个失败的点,后面的全是噪音。
4000条/天的流量不需要分布式架构。很多时候"扛不住"不是工具的问题,是默认配置在极端场景下的行为失控。修复成本可以很低,前提是你不被日志的表象带偏。
热门跟贴