五年零故障,一天全崩掉。
这不是什么惊天大漏洞,没有黑客入侵,没有服务器宕机。只是一个普通的周三,推送通知开始"丢三落四"——名字没了,数据空了,UI上只剩下一堆null。然后崩溃报告如雪片般飞来。
故事的主角是Firebase Cloud Messaging(FCM),谷歌的推送服务。开发者Subhankar Bag花了五年时间搭建的系统,一直把它当"数据卡车"用。通知不只是响一声,而是扛着完整的访客信息、审批数据、实时状态,直接送到App里解析展示。
这套设计曾经很优雅:后端打包,FCM运输,App开箱即用。没有额外的API调用,没有延迟,用户点开通知就能看到完整信息。五年间,这套系统默默处理了无数访客审批、安全警报、实时通知,零投诉,零事故。
直到某天,数据结构变了。
原本扁平的键值对,突然变成了嵌套JSON字符串:"update_unit": "{ \"id\":2230, \"visitor\": { ... } }"。App用Gson解析时期待的是一个对象,拿到的是一串字符串。报错信息很直白:Expected BEGIN_OBJECT but was STRING。
但这不是根因。真正的杀手藏在FCM的硬限制里:4KB。
4KB是什么概念?大约4096个英文字符,或者更少的Unicode字符。当后端把JSON对象序列化成字符串塞进payload时,体积膨胀了30%到40%。原本紧巴巴的4KB预算,现在更吃紧了。
更麻烦的是,FCM不会跟你商量。超限时它不报错、不警告,直接丢弃消息。开发者可能在后台看到发送成功,但用户的手机永远收不到。
这个系统的崩溃路径堪称经典:首先,业务数据自然增长,访客信息越来越丰富;然后,某人决定把JSON嵌套进字符串,可能是为了兼容某个旧接口;接着,payload在4KB边缘反复试探;最后,某个大客户的访客记录成为压垮骆驼的最后一根稻草——消息消失,App解析失败,UI显示null,用户困惑,崩溃报告堆积。
整个过程没有一声警报。就像一个人慢慢缺氧,直到晕倒才被人发现。
问题的本质在于设计认知的错位。FCM的官方定位是"信令机制"(signaling mechanism)——轻量、快速、可靠地告诉用户"有件事发生了"。而这套系统把它当成了"数据交付机制"(data delivery mechanism),试图用推送通道替代API调用。
这种错位在初期完全可行。4KB对简单通知绰绰有余,JSON字符串的膨胀也不明显。但随着业务复杂化,技术债以指数级速度累积。字符串嵌套JSON的设计决策,把线性增长的数据变成了超线性增长的payload。
修复方案不是修剪几个字段就能解决的。团队需要彻底转变思路:FCM只发信号,数据走专用通道。
新模式下,推送变得极简:{ "type": "ALERT_OBJECT", "ref_id": "2230" }。App收到后,再用ref_id去调API取完整数据。多了一次网络请求,但换来了可靠性——API可以分页、可以重试、可以缓存,而FCM只有4KB和沉默的丢弃。
这个案例的教训很朴素,但容易被忽视。
第一,任何"硬限制"都值得被监控。4KB不是建议值,是物理边界。系统应该在接近阈值时报警,而不是等到消息消失。
第二,序列化格式有成本。JSON字符串比JSON对象多一层转义字符,在边缘场景下可能是致命的。性能优化不能只测平均值,要测最坏情况。
第三,外部服务的语义很重要。FCM的文档明确说它是"signaling",但开发者常因便利而误用。当服务被用于设计之外的场景时,需要额外的防御层。
第四,沉默的失败是最危险的失败。FCM丢弃消息时不通知发送方,这种"成功幻觉"让问题难以定位。关键路径上,任何外部调用都应该有对账机制——发送了100条,确认收到了多少条?
五年稳定运行反而成了陷阱。它让团队相信这套设计是可靠的,忽视了渐进式的风险积累。技术债不会突然爆发,它像慢性病一样,在某个平凡的下午让你倒下。
对于正在用FCM扛数据的后端和移动开发者,这个故事的价值在于提前审视:你的payload现在多大?增长曲线是什么?有没有字符串套JSON的"小聪明"?4KB的预算还剩多少余量?
别等到null填满屏幕,才想起检查数据边界。
热门跟贴