别再猜源站要哪种 TLS:把握手重试率当成可测量对象

一、写死假设的代价不在报错里,在重试里
反向代理回源时的 TLS 配置,多数团队是这么来的:翻一遍协议规范,挑一组看着最标准的参数组合,全局生效。这么做有三个理由——实现简单、每次握手不必多绕一步、规范文本给人一种“对端应当兼容”的错觉。问题出在第三个理由上。规范写的是“允许实现什么”,线上跑着的是“各家实际实现了什么”。嵌入式网关、老虚拟机上的加密库、自研的反向代理、被中间设备改写过的握手请求,这些构成了一条长尾。长尾对端不会因为我们的假设不符合规范就明确失败,它会拒绝、会断连、会在另一组参数上成功。于是代理层的逻辑自然变成“先按假设试,不行退一档再试”。功能上看着完全正常。
这条兜底路径的坏处在于它不进错误率。重试成功了,请求就是成功的,监控上一片绿。成本只是换了个形式存在:多出来的一两次往返计进了握手耗时,计进了首字节时间,最后由终端用户承担。据公开报道,有厂商把回源 TLS 握手里的静态假设换成针对每个源站的测量值之后,握手重试率从约五成降到不足百分之四。这个数字只能说明“重试率是可以被大幅压下来的”,它不是我们下面这些判断的依据,也不代表任何站点都能拿到同样的幅度。我们要说的是另一件事:重试率比失败率更早暴露配置错配,而它通常根本没人看。
二、把对端偏好当成可测量对象
测什么,就三条线。版本支持集合是第一条:源站到底接受哪个 TLS 版本,是否只吃特定的版本组合。密码套件与曲线偏好是第二条:同一版本之下,套件列表和椭圆曲线的支持差异是重试的主要来源。证书链与会话恢复行为是第三条:链是否完整下发、签名算法是否被接受、是否支持会话复用、复用票据有没有长度限制。第三条最容易被忽略,却常常决定了首屏之后那一批请求的耗时形状。
| 测量对象 | 探测方式 | 得到的结论形态 | 典型误判 |
|---|---|---|---|
| 版本支持集合 | 按版本逐个发起最小握手,记录成败 | 支持版本列表与优先序 | 把限流导致的失败读成不支持 |
| 套件与曲线偏好 | 固定版本,变换套件与曲线做组合探测 | 可用组合集合与首个成功项 | 只试一组就下结论,漏掉长尾组合 |
| 证书链形态 | 读取对端下发的链,查中间证书与签名算法 | 链完整性与算法兼容性 | 用自家信任库判定对方“配置错了” |
| 会话恢复行为 | 连续两次握手,检查是否复用成功 | 支持的复用类型与失效条件 | 票据被中间设备改写后误判为不支持 |
测量结果与静态假设的根本差别是:前者带着来源、时效与置信度,后者只是一份意图。把意图当事实用,故障就发生在意图与现实不一致的那批站点上。
三、探测本身不能变成对新源站的攻击
这一节最容易做过头。我们对外部源站做主动探测,本质是在别人的机器上发连接,纪律必须写死在代码里,而不是靠自觉。
- 限频:单站并发探测连接数设上限,两次探测之间留退避间隔,探测预算按站点分配,不允许全局抢占
- 缓存:结果按源站标识缓存并带有效期,未过期不再打;同一站点的重复探测应当被计数器拦掉
- 人工失效:提供按单站清除的入口,对方换完证书后运维能立刻让新结果生效,而不是等有效期到
- 范围收敛:只做握手层探测,不发业务请求、不枚举路径、不试探口令、不构造畸形报文
- 自动刹车:对端返回限流信号、连接被重置或收到告警,立即停手并把该站标记为“仅用保守集合”
- 全程留痕:每条记录保留探测时刻、探测节点所在区域、参数集合、返回结果、耗时,供事后复核与对客户解释
配套的字段表可以直接当作实现依据:
| 字段 | 含义 | 约束 |
|---|---|---|
| origin_id | 源站标识 | 主机名与回源地址分开记,避免同 IP 多站串味 |
| probe_window | 探测窗口 | 首次接入与长期巡检分开设预算 |
| max_concurrency | 单站并发上限 | 全局与单站两级,单站不得高于全局 |
| backoff_step | 退避档位 | 失败后按档拉长间隔,成功若干轮后回落 |
| result_ttl | 结果有效期 | 版本与套件偏好用较短有效期,证书链用事件触发刷新 |
| confidence | 置信度 | 单次成功与多节点多次一致要区分标注 |
| fallback_used | 是否走了回退 | 用于事后统计“测量失效”的比例 |
四、从测量结果到线上配置的落地路径
三段式,每段都要留回退点。
第一段,冷启动用保守集合。新源站接入时还没有测量数据,此时的选择必须是“任何对端都能谈成”的那一组参数,宁可放弃一部分性能,也不要在第一天就产生大量重试。第二段,探测完成后按站点定制。测量结果落到站点级的参数模板上,只覆盖该站,不动全局默认。这里有个纪律:模板内容由测量结果生成,不允许运维手写成千上百份“感觉能行”的特例,否则半年后没人知道哪条对应哪个站。第三段,异常时回退。某个站的探测结果突然整体劣化、或者对方证书变更后连续失败,系统要能自动退回保守集合并告警,而不是停在坏结果上反复撞。
灰度与回滚按这个顺序走:
- 按源站分桶,先取一小批流量结构简单的站点启用测量结果,观察一轮完整周期
- 每批只改一个维度,先版本、再套件、最后会话恢复;一次全改就没办法归因
- 看单站延迟分布与重试率的联合变化,不看全局均值,均值会把长尾站点淹掉
- 扩批次时保留“同站点静态假设对照组”,出问题能立刻判断是新参数还是对方变更
- 回退开关按站点和全局各留一个,能退回静态假设这条路径不能删——它是唯一的兜底
五、观测面板的最小字段集
不加面板这件事就停留在“我们改好了”的自我感觉上。四类字段是下限:重试率(按站点、按维度分别算,重试成功与最终失败要分开)、握手耗时分布(分位数而不是均值,重点看尾部)、首字节时间(重试的成本最终显形在这里)、错误分类(把“对端拒绝”“超时”“证书问题”“被中间设备干预”分开,混成一类就没法判断该改配置还是该联系对方)。在这四类之上再加一条派生指标:测量结果与保守集合的偏离度,用来回答“这批站点值不值得单独维护模板”。偏离度长期接近零的站点,配置就该合并回默认,减少维护面。
六、同一套做法能迁移到哪里
把静态假设换成对端测量,这个思路不止用于 TLS。HTTP/2 与 HTTP/3 的支持与否常被写成全局开关,实际上大量源站只支持其中一个,或者在特定路径上降级;压缩算法与分块传输的协商同样存在长尾,写着支持但行为不一致的对端并不罕见;SNI 与证书选择在多宿主回源时是隐式约定,选错就得到名字不匹配的告警;超时值更是常被全局写死,而不同源站的响应时间分布差别很大,一个全局值必然对某些站点太短、对另一些太宽松。这些地方的共同点是:配置项看起来只有一个,真实分布却有一百个。
国内网络环境还要多算一层变量。客户端到代理、代理到源站是两段链路,中间设备对握手报文的处理差异会使同一站点在不同节点上测出不同结果;运营商链路的差别会让“某个城市能谈成、另一个城市不行”变成常态。结论是探测必须按节点区域分别记录,不能只从少数几个点取样就当作站点属性。洹拓在给客户做交付时,习惯把这一段列进验收项:同一站点在不同区域节点上的握手一致性,比单点测出来的参数表更能说明问题。
兼容性问题的正确处理顺序不是“等失败日志堆够再加兜底”,而是先承认自己对端一无所知,把分布测出来,再决定发什么参数。这一步做完,重试率会降,但更重要的收益是配置从“意图”变成了“有来源、有时效、能失效、可回滚”的对象——这是它能不能被长期维护的分界线。