凌晨一点的告警
那天监控告警是在凌晨 01:17 推送到我手机上的。P95 响应时间从 230ms 突然爬到了 580ms,而我们的 SLA 承诺是 300ms 以内。我打开 APM 面板,第一眼就看出不是数据库——查询时间正常。问题在链路的最后一环:前后端之间的 HTTP 传输。
我把问题缩小到了一个首页的聚合接口。这个接口要把用户的个人信息、推荐内容列表、未读消息数、活动 Banner 等七类数据打包成一个 JSON 一次性返回给前端。逻辑上很合理——减少请求数嘛。但没人注意到,随着业务迭代,这个 JSON 在过去三个月里悄悄从 12KB 胖到了 38KB。
38KB 的 JSON,在快速网络下根本感觉不到。但一旦用移动端 4G 网络,或者碰上运营商晚高峰,580ms 就是真实存在的痛苦。
第一轮手术:砍字段
我做的第一件事不是压缩,而是拿着 JSON 结构找产品经理:"这 38 个字段里,首屏渲染真正需要哪几个?" 这个问题让所有人沉默了约三秒。
结果:我们砍掉了 11 个"以后可能要用"但当前页面根本没有渲染的字段。这一刀,JSON 体积降到了约 26KB,但同时产生了一个新问题——某些字段的字符串值里含有 HTML 标签和反斜杠,在被放入 JSON 字符串时没有被正确转义。一部分低版本 Android WebView 在解析时直接崩溃。
这就是很多人忽视的"压缩"的另一面:转义。
JSON 转义:比你想象的更重要
我来还原一下当时的 Bug 场景。我们的内容字段里有一段这样的值:
// 原始字符串(错误示例)
{
"description": "点击"立即报名"按钮
查看详情"
}
// 这在 JSON 里是非法的!引号需要转义,如下才对:
{
"description": "点击\"立即报名\"按钮 \n 查看详情"
}
理论上,任何标准的 JSON 序列化库都会处理这个问题。但我们的一个遗留服务用的是一个上古时代的自研工具(别问,问就是"历史遗留"),它对中文引号(“”)不做处理,直接输出。标准 JSON 解析器在遇到这种字符时会抛出异常,而老旧的 Android WebView 在异常处理上更是惨不忍睹,直接无声崩溃。
修复方案很简单:在统一的 API 网关层加一个输出过滤器,强制对所有字符串值做标准的 JSON 转义处理。再加上了一条统一规则:
"→\"\→\\- 换行符 →
\n - 制表符 →
\t - 控制字符(Unicode 0x00-0x1F)→
\uXXXX格式
第二轮手术:真正的压缩
字段瘦身 + 转义修复之后,JSON 体积在 26KB。我决定再压一刀:去除所有格式化空白字符。
很多人不知道,一个"格式化漂亮"的 JSON 和一个"压缩成一行"的 JSON,在数据内容完全相同的情况下,体积差异往往有 15% - 30%。来看一个对比:
// 格式化后:62 字节
{
"userId": 1001,
"name": "张三",
"active": true
}
// 压缩后:43 字节(节省 31%)
{"userId":1001,"name":"张三","active":true}
原理很简单:JSON 格式化使用的缩进空格、换行符(\n)本身都是需要传输的字节。对于嵌套层数多的 JSON,每深一层就多两个空格(或一个 Tab),乘以字段数量,积累起来非常可观。
我们的接口 JSON 嵌套了 4-5 层,字段多达 27 个,压缩后从 26KB 降到了 21.8KB——最终比优化前的 38KB 少了 42.6%。
一场关于"可读性"的内部争论
这里插播一个有趣的插曲。当我提出"在生产环境接口里去掉格式化空白"时,团队里一个后端同学提出了反对意见:
"那调试的时候怎么看?直接 curl 出来的都是一坨,眼睛要瞎。"
这个问题本质上是把"传输格式"和"调试体验"混为一谈了。一个好的解决思路是:
- 生产环境:API 永远输出压缩 JSON,传输效率优先
- 调试环节:把压缩的 JSON 粘贴进格式化工具(比如 daima.life),一键展开,带高亮,方便阅读
- 开发环境:可以开启
pretty-print模式,让开发工具自动为你格式化
传输层的 JSON 不需要让人直接阅读。它需要的是小、快、准。
专业提示:哪些场景要特别注意转义?
经历了这次事故,我总结了几个最容易被忽视的 JSON 转义雷区:
- 富文本内容:用户生成内容(UGC)里可能含有任意字符,必须过滤控制字符
- JSON 套 JSON:把一个 JSON 字符串作为另一个 JSON 的某个字段值时,内层 JSON 整个需要被转义(
"变\",\变\\),这是双重转义陷阱,稍不留意就崩 - SQL 注入载体:如果你的接口会把 JSON 字段值拼接进 SQL,转义不彻底可能直接产生注入漏洞
- 日志记录:把 JSON 写入单行日志系统时,换行符必须转义,否则日志收集系统会把一条记录切成多行,导致 Kibana/Datadog 解析乱序
写在最后
那次优化最终让 P95 响应时间从 580ms 回到了 185ms,比原先的基准还低了 45ms。不是因为我做了什么高深的算法,而是因为我认真对待了"数据传输"本身——包括它的体积、它的格式、以及它字符级别的安全性。
JSON 压缩和转义,看起来是两件微不足道的小事。但在高并发、高频接口、移动端弱网这三个场景叠加时,它能成为系统稳定性的决定性因素。
如果你手边有一段需要压缩或者需要检查转义的 JSON,不妨现在就打开 daima.life 的 JSON 压缩转义工具——粘进去,一秒出结果,本地处理不上传,调试生产数据放心用。