一个凌晨三点的电话
去年做的一个网关类项目,硬件是通用多核处理器,软件跑的是自研转发框架。功能测试全过,压力测试也扛住了,偏偏在现网割接后的第二天凌晨出了状况。
值班同事打电话来说,业务监控面板上一条曲线突然翘起来了——不是流量涨了,是新建连接的成功率掉了两成,同时已有长连接的吞吐量出现周期性抖动,抖动周期大概几十秒一次。更诡异的是,重启业务进程能缓解一阵,过几小时又复现。
当时第一反应是资源泄漏。抓了内存和句柄的曲线,平稳得很。第二反应是锁竞争。上了性能采样工具,发现几个转发线程确实有等锁,但等待时间不足以解释吞吐抖动。
折腾到天亮,才把问题定位到一个很隐蔽的地方:加密模块的启动时机。
问题现场长什么样
先描述一下这个系统的结构,不涉及任何具体厂商和型号,全部用泛化名词。
系统里有一个数据面,负责处理网络报文。数据面在启动时,会预先创建一批会话表项,用于快速转发。这些表项在建立时,如果策略要求加密,就会走一个“加密上下文初始化”的流程。
我们的加密上下文初始化,是在会话首次创建时同步做的。也就是说,第一个报文触发建流,建流过程中调用加密初始化,初始化完成后再把报文发出去。
问题出在:加密初始化这个动作,不是瞬间完成的。它内部会去申请硬件加速资源、加载密钥、做一轮自检。整个过程在冷启动时可能耗时几十毫秒,热路径上如果资源池耗尽,也会阻塞。
而我们的会话表项,在加密初始化完成之前,已经挂到了转发哈希表里。这意味着什么呢?意味着后续属于同一个流的报文,会命中这个表项,然后发现加密上下文还没准备好,就进入等待。等待本身没问题,问题是等待的队列没有做背压,新报文不断进来,老报文还没出去,队列越堆越长,最终触发流控,把整条链路的吞吐拉下来了。
新建连接成功率下降,是因为加密初始化的耗时被算进了建流时间,超时阈值一卡,部分建流直接失败。
为什么启动时机这么要命
这里面的核心矛盾,公开资料里很少展开讲。
加密流程的启动,有两个天然的时间点:一个是“会话创建时”,一个是“会话首次需要加密时”。听起来差不多,实际上差别很大。
如果在会话创建时就启动加密,好处是逻辑简单,建流成功即代表加密就绪,后续报文直接走加密封装,路径清晰。坏处是建流时间被拉长,而且加密资源被提前占用。如果建流失败,加密资源还要回滚释放,回滚路径容易出 bug。
如果在首次需要加密时才启动,好处是建流快,资源按需分配。坏处是第一个需要加密的报文要承担初始化开销,这个报文如果走在转发快路径上,就会把快路径堵住。更麻烦的是,如果这时加密资源不够,报文是丢还是等?丢了业务受损,等了队列膨胀。
我们当时选的是第一种,但犯了一个错误:加密初始化和会话挂表没有做原子性保证。会话先挂表,再初始化加密,中间有一个窗口期。窗口期内命中的报文,看到的是一个“半成品”会话。
现场怎么观察和判断
如果你怀疑遇到了类似问题,可以按下面这个顺序去查。
第一,看抖动是否与新建连接速率相关。把新建连接速率压到很低,如果抖动消失,说明问题在新建路径上。如果抖动还在,那可能是别的原因。
第二,抓一次抖动发生前后的会话表项状态。重点看有没有表项处于“已创建但加密未就绪”的中间态。这个状态如果有计数器,直接看计数器增长曲线;如果没有计数器,可以在代码里临时加一个原子计数,统计进入等待队列的报文数。
第三,观察加密初始化的耗时分布。不要只看平均值,要看 P99 和最大值。冷启动和资源竞争时的耗时,往往是平均值的几十倍。如果 P99 耗时和抖动周期对得上,基本可以确认。
第四,检查等待队列有没有上限。很多框架的等待队列是无界的,或者上限设得非常大,导致问题被掩盖到很晚才暴露。把队列上限调小,如果抖动变成丢包,说明队列确实是膨胀源。
几种可行的处理方式与取舍
定位到问题之后,我们有几种选择,每种都有代价。
第一种,把加密初始化提前到会话创建之前,先初始化再挂表。这样窗口期消失了,但建流时间进一步拉长,新建连接成功率可能更难看。适合对建流时延不敏感、但对已有连接稳定性要求极高的场景。
第二种,保持先挂表再初始化,但在窗口期内对命中的报文做特殊处理。比如把报文暂存到一个旁路队列,等加密就绪后再注入。这个方案对转发路径改动小,但旁路队列本身需要做流控,否则只是把膨胀点从主队列挪到了旁路队列。
第三种,预分配加密资源池,会话创建时直接从池里取一个已经初始化好的上下文。这样建流快,也没有窗口期。代价是资源池大小要提前规划,池子空了之后新建连接会失败,而且池子的回收策略要非常小心,回收错了会导致上下文串流。
第四种,把加密初始化做成异步,会话创建时只挂一个“待初始化”标记,后续报文命中时如果标记还在,就触发一次异步初始化,同时把报文缓存有限个数。这个方案最复杂,但兼顾了建流速度和已有连接的稳定性。难点在于异步初始化的完成通知和缓存报文的顺序保证。
我们最后选的是第三种加第二种的混合:预分配一个小资源池应对突发,池子空了之后走旁路队列,旁路队列有严格上限,超限就丢弃并告警。这样正常场景下建流快,极端场景下不会拖垮已有连接,只是新建连接会失败,但失败是可见的、可控的。
一个容易被忽略的细节
加密上下文在初始化时,通常会做一些自检,比如用已知向量验证加解密是否正确。这个自检如果放在会话创建路径上,会进一步拉长建流时间。我们后来的做法是,把自检挪到系统启动阶段,会话创建时只做密钥加载和资源绑定。自检失败就在启动阶段报出来,不要等到业务跑起来才发现。
还有一个细节是,加密上下文的释放。如果会话老化删除时,加密上下文释放是同步的,而且释放本身可能阻塞,那老化路径也会成为抖动源。我们后来把释放也改成了异步,用一个独立的回收线程去处理,老化路径只做标记。
怎么在设计阶段就避开这个坑
如果你正在设计类似系统,有几个判断可以做在前面。
先问自己:加密初始化的最坏耗时是多少?这个数字不能拍脑袋,要实际测。测的时候要模拟资源竞争、模拟冷启动、模拟密钥加载失败重试。
再问自己:会话表项在加密未就绪时被命中,会发生什么?这个问题的答案,决定了你的系统在极端场景下是优雅降级还是雪崩。
最后问自己:新建连接失败和已有连接抖动,哪个更不能接受?这个问题没有标准答案,取决于业务。但你必须选一个,不能两个都要。
我们当时的教训是,把加密初始化当成一个“瞬间完成”的动作来设计,忽略了它的耗时和资源依赖。实际上,任何涉及硬件加速、密钥管理、外部自检的流程,都不应该被当作瞬间完成。它要么提前做,要么异步做,就是不能同步做在快路径上。
关键词
核心关键词:P99、绑定、超时、丢包
精准长尾关键词:加密流程启动时机对已有连接的影响、问题现场长什么样、现场怎么观察和判断、蓝牙BLE开发实战、BLE协议栈详解、低功耗蓝牙调试、BLE连接参数优化、蓝牙抓包分析、BLE广播数据解析、蓝牙连接稳定性优化、低功耗蓝牙功耗优化、BLE实战教程