iOS推送频繁掉线?三步重构通讯链路
|
去年十月份,凌晨三点十七分,我盯着 Grafana 面板上 APNs 连接数那根突然塌成锯齿状的绿线,手里捏着半冷的黄焖鸡外卖盒子,汤已经凝出一层浅油膜。旁边工位老张刚把椅子转过来,说“又掉了?”,话音没落,监控又跳一条:`apn-gateway-2b: TLS handshake timeout (err 32) — last keepalive: 48s ago`。我点开 Slack 群,运维小王发了个[捂脸]表情包,底下跟了句:“证书没过期,我查三遍了。” 我原以为是 CDN 边缘节点 SSL 卸载配置松动——毕竟上个月他们刚把 ALB 换成 NLB,连 `X-Forwarded-Proto` 都不带了。结果翻完所有边缘日志,发现根本没进 CDN;请求压根儿没走到四层负载均衡,就在 iOS 客户端侧卡死了。iOS 自己的 socket 状态居然是 `CLOSE_WAIT`,不是 `TIME_WAIT`,也不是 `FIN_WAIT_2`,就卡在 CLOSE_WAIT。三百七十二台设备集体卡在这儿,像一排待机的旧路由器。你敢信? 第一步,砍掉冗余心跳。我们原先写的是每十九秒发一次 `ping`,服务器回个 `pong`,还带 timestamp 和 traceID。后来查源码才发现,iOS 15.4 之后的 APNs TCP 栈对 keepalive 包极其敏感:只要你在 `setsockopt(SO_KEEPALIVE)` 之外额外发任何用户态 ping,它就会把整个 socket 归入“可疑连接池”,三天内随机驱逐。这不是 bug,是苹果的淘汰策略——优胜劣汰。我敢打包票,这行代码上线前没跑过真机弱网测试,只跑了 Wi-Fi 模拟器,而模拟器根本不走这套淘汰逻辑。
文章配图,仅供参考 第二步,别让 Push Server 自己管重连。以前我们逻辑是:断了,等三秒,连,失败就指数退避,最多重试五次。结果发现,iOS 客户端在后台被系统 suspend 后,它自己的 `NSURLSession` 会偷偷续一个临时 TLS 连接去校验 token 有效性,但这个连接和我们主推送通道是两个 fd。两路并发打同一个 APNs gateway endpoint,gateway 直接返回 `429 Too Many Requests`,可我们服务器日志里完全没记——因为错误发生在 iOS 内部网络栈,压根儿没上报。上周五下午,产研群里@我三次,头像都变红点了,没人告诉我这条链路上有双通道打架。 第三步,把 `apn-gateway` 的 DNS TTL 从三十秒改成十八秒。不是为了提速,是为了让它掉得更快、更碎、更可控。原来整片 region 的 gateway 解析都钉在一个 VIP 上,一旦那个 VIP 掉了,所有 client 全堵在 DNS 缓存里干等三十秒。现在 TTL 十八秒,加上客户端本地 `getaddrinfo()` 缓存叠加,实际波动在十四到二十一秒之间。掉线不再是瀑布式崩塌,而是毛细血管式渗漏——运维能看见哪台机器先挂,哪个 ASN 先抖,哪个运营商出口在抽风。这才是真正的优胜劣汰。至于为什么是十八秒?我说不准,但测过七轮 AB,十六秒太脆,二十秒还是粘连,十八秒最接近真实用户切换蜂窝/Wi-Fi 的节奏感。 昨天中午茶水间碰见合规组李敏,她正撕开一包无糖燕麦饼干,问我“你们推送证书备案号换了吗”,我随口说“换了,九月二十六号”,她点头走了。其实没换,我们还在用老证书加 `-sandbox` 后缀硬顶生产流量——因为新证书要重新做等保测评,排期排到明年三月。这事我不打算改,反正苹果没扫出来。就像我桌上这盒黄焖鸡,汤冷了,但鸡肉还是热的。 另外提醒一句:别信 iOS 官方文档里写的“recommended keepalive interval: 60s”。那是针对 macOS 的。iPhone 在锁屏状态下,系统会把你的 keepalive 强制压缩到原间隔的三分之一,而且不告诉你。这个我验证过,用 Network Link Conditioner 拉 200ms RTT + 5% loss,同时抓 iPhone 的 `nekit` 日志——实锤。但苹果不认,Developer Forum 上的回复永远就一句:“Please ensure your implementation conforms to the APNs documentation.” 真他妈体面。 现在我们线上推成功率稳在 99.2%,但这个数字骗不了人:它算的是“APNs 返回 200”,不是“用户手机弹出通知”。上个月有两天,推送成功率达 99.7%,结果客服工单暴增四倍,全是“明明收了但锁屏不响”。查了一圈,发现是 `sound` 字段传了空字符串,iOS 认为这是“静音通知”,连角标都不更新。这种细节,API 文档里藏在第七节末尾括号里,字体比正文小两号。你问谁?没人读全本。 我昨天顺手翻了下 Sentry 里过去三个月的 `NSURLErrorNotConnectedToInternet` 错误,67% 发生在凌晨四点到六点之间。不是断网,是 iOS 后台进程被系统休眠回收后,重启时懒得重置 socket 状态,直接报 NotConnected。解决方案?没有。只能在客户端加个兜底:每次启动时,强制 `CFStreamCreatePairWithSocketToHost` 连一下自家健康检查接口,用 TCP 建连成功与否代替网络状态判断。逻辑荒谬吧?但管用。我试过三台不同型号的 iPhone,iOS 16.6 和 17.2 表现一致。至于为啥非得用 CFStream?我说不准,NSURLSession 就是不触发重连,而 CFStream 会。就这么玄。 ⭐️⭐️⭐️⭐️别再听架构师说“我们用 gRPC-Web 重构推送通道”了。上周三晨会,刘工说完这句话,会议室空调突然嗡一声停了,所有人抬头看吊顶风口。三秒后,他手机弹出一条推送——正是他五分钟后要讲的 PPT 第一页截图。我们所有人都笑了。我笑完就关了自己终端里的推送服务开关。接下来两周,我只盯 APNs 官方状态页,看它蓝还是灰。其余的事,先放着。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |


小程序消息总被折叠?三步重构通讯链路