news 2026/9/11 7:50:46

OpenSSL QUIC 流控(Flow Control)设计解析:双层信用模型、水位线状态机与窗口自动调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenSSL QUIC 流控(Flow Control)设计解析:双层信用模型、水位线状态机与窗口自动调优

OpenSSL QUIC 流控(Flow Control)设计解析:双层信用模型、水位线状态机与窗口自动调优

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

导读

QUIC 的流控(Flow Control)负责在连接级(connection-level)与流级(stream-level)两个层面约束对端可发送的数据量,是保证接收端缓存不溢出、端到端吞吐得以收敛的关键机制。本文以 OpenSSL 仓库中的 QUIC 流控设计文档 doc/designs/quic-design/quic-fc.md 为主体,结合其核心实现 ssl/quic/quic_fc.c、include/internal/quic_fc.h 及帧处理与测试代码,完整讲解信用(credit)模型、TX/RX 两侧状态机事件、水位线(watermark)术语体系以及 RX 窗口的动态自动调优(auto-tuning)算法。读完本文,你将掌握 OpenSSL QUIC 实现中流控的完整数据流与判定规则,并能将设计与源码一一对应,为深入阅读或二次开发 OpenSSL QUIC 栈打下基础。

QUIC 流控概览:双层级信用模型

QUIC 的流控同时在连接级流级两个层面生效。任何时刻,流数据的发送都可能被连接级流控、流级流控,或两者同时限制。流控采用信用(credit)模型:相关的流控上限被表达为"自流或连接开始以来,允许在单个流上、或跨所有流发送的最大字节数",并且该上限可以被周期性地抬高(bump)。

关于 "credit" 一词:在 OpenSSL 源码中,"credit" 即"当前还可发送的受控字节数",等价于 CWM 与 SWM 之差(见 include/internal/quic_fc.h 中ossl_quic_rxfc_get_credit的实现:cwm - swm)。

受控字节(Controlled Bytes)与重传计数

连接级与流级流控都只与QUIC 流数据(stream data)的传输有关。流级流控统计的是某个流上发送的逻辑字节总数

  • 不统计重传。一个字节如果被发送、丢失、再发送,对流控而言仍只计作一个字节;
  • 因此"某条流上已发送的逻辑字节总数"等价于该流的当前"长度(length)"。

本质上,相关的量是:对于该流上我们曾发送过的所有 STREAM 帧(offset, len),取max(offset + len)

正确确定这一数值至关重要——如果发送端误以为自己的流控信用已耗尽,而对端认为并未耗尽,那么对端可能会无限期等待发送端继续发送数据,之后再授予更多流控信用,从而造成死锁(deadlock)

帧类型与层级对应

  • 连接级流控MAX_DATA帧控制;
  • 流级流控MAX_STREAM_DATA帧控制;
  • RFC 9000 定义的DATA_BLOCKEDSTREAM_DATA_BLOCKED帧实际作用比表面看起来小:对端不允许依赖它们(例如,对端不能等我们发出DATA_BLOCKED才提高连接级信用;一个合规的 QUIC 实现甚至可以完全不生成这两种帧)。它们的两个实际用途是:提升流控性能、作为调试辅助手段。因此其实现并非关键。

CRYPTO 流不受流控、流控与拥塞控制相互独立

由上述定义自然可以推出:CRYPTO 帧的流(CRYPTO stream)不受流控约束。此外,流控与拥塞控制(congestion control)是完全独立的机制——在特定场景下,两者之一或两者同时都可能限制我们发送应用数据的能力。

在 OpenSSL 源码中也可以看到这种独立性:流控实现在 ssl/quic/quic_fc.c,而拥塞控制是另一套独立的模块(如 ssl/quic/cc_newreno.c),二者互不调用。

核心术语:RWM / SWM / CWM 水位线体系

设计文档用一张示意图概括了 RX 侧各水位线与窗口的关系:

RWM SWM SWM' CWM CWM' | | | | | | |<-- credit| -->| | | <-|- threshold -|----->| | -----------------> window size

文档引入的术语体系如下:

术语含义
Controlled bytes(受控字节)任何对流控计数的字节:STREAM 帧负载中的应用数据字节,首次发送时计数(重传不计)。
Retirement(退役,仅 RX 侧)从 QUIC 流中取出一个或多个受控字节并交给应用的过程;退役后我们不再对这些字节负责。RX 流控设计非常依赖退役节奏——我们希望对端不仅以 QUIC 实现处理入站数据的速度发送,还要以应用能处理的速度发送。
Retired Watermark(RWM,退役水位线,仅 RX 侧)自连接或流开始以来退役的受控字节总数。
Spent Watermark(SWM,已消费水位线)我们已发送(TX 侧)或已接收(RX 侧)的受控字节数,代表已消耗的流控预算;它是单调的、永不回退。RX 侧的这些字节不一定已被退役。
Credit Watermark(CWM,信用水位线)目前已被授权可发送的字节数;这是自连接或流开始以来的累计值,因此同样单调。
Credit(信用)始终等于 SWM 与 CWM 之差(CWM - SWM)。
Threshold(阈值,仅 RX 侧)允许 RWM 逼近 CWM 的程度;当 RWM 到达阈值时才选择抬高 CWM 以授予对端更多信用。阈值是相对 CWM 的(从 CWM 中减去)。
Window size(窗口大小,仅 RX 侧)每次到达或超过阈值时,我们(或对端)抬高 CWM 的量。新的 CWM 等于SWM 加窗口大小(注意是加到 SWM 上,而非旧 CWM 上)。

两个必须牢记的不变量:

  • 若可用信用为 0,则 TX 侧因缺乏信用而被阻塞;
  • 若发生任何会导致SWM 超过 CWM的情况,则属于流控协议违规(flow control protocol violation),应终止连接

在源码中,这些水位线被直接实现为结构体字段,见 include/internal/quic_fc.h 中的QUIC_RXFC

uint64_t cwm, swm, rwm, esrwm, hwm, cur_window_size, max_window_size; OSSL_TIME epoch_start;

其中esrwm是当前自动调优 epoch 开始时的 RWM 值,hwm是迄今从 STREAM 帧中见过的最高流长度(offset + payload length),cur_window_sizemax_window_size分别是当前窗口大小与窗口大小上限。

TX 侧流控:简单的状态机

TX 侧流控"异常简单",可建模为以下状态机:

---> event: On TX (numBytes) ---> event: On TX Window Updated (numBytes) <--- event: On TX Blocked Get TX Window() -> numBytes

连接级 TX 状态机事件

  • On TX:每当我们发送一个数据包时传入。numBytes是包中发送的受控字节总数(即非重传的 STREAM 帧负载字节数),累加到 TX 侧 SWM。该值可以为 0(此时无需传事件)。
  • On TX Window Updated:每当我们的 CWM 被提高时传入,即收到MAX_DATA时(或收到initial_max_data传输参数时),参数为该帧中的整数值。该事件表达的是 CWM(自连接开始以来允许发送的受控字节累计数),因此是单调的、永不回退;如果传入的值低于之前任何一次事件的值,说明对端协议错误或本地编程错误。
  • Get TX Window:返回我们的信用值(允许发送的受控字节数)。它被 On TX 事件减少、被 On TX Window Updated 事件增加。实际上它就是"最后一次 On TX Window Updated 的值"与"迄今所有 On TX 事件numBytes之和"的差,仅此而已。
  • On TX Blocked:在 Get TX Window 返回值从非零变为零的任何边沿时刻发出(总是发生在处理某个 On TX 事件期间)。该事件用于辅助决定何时生成DATA_BLOCKED帧。

我们绝不能超出流控上限,否则对端可能以错误终止连接。初始的连接级信用由对端通过initial_max_data传输参数告知,其余信用全部来自MAX_DATA帧。

流级 TX:与连接级完全相同的机制

流级 TX 流控与连接级 TX 完全一致,仅事件来源不同:

  • On TX Window UpdatedMAX_STREAM_DATA帧触发,或基于相应传输参数(initial_max_stream_data_bidi_localinitial_max_stream_data_bidi_remoteinitial_max_stream_data_uni);
  • On TX Blocked事件可用于决定何时生成STREAM_DATA_BLOCKED帧。

关键约束:一条流上可发送的受控字节数同时受连接级与流级流控限制,因此实际可发送的受控字节数为连接级与流级两个状态机Get TX Window返回值中的较小者

源码中的 TX 流控实现

ssl/quic/quic_fc.c 将上述状态机实现为一组直接的函数:

  • ossl_quic_txfc_init:初始化 TXFC;流级 TXFC 通过parent指针挂接连接级 TXFC(conn_txfc != NULL),连接级 TXFC 的 parent 为 NULL;
  • ossl_quic_txfc_bump_cwm:即On TX Window Updated操作;若传入的 CWM 不大于当前值则返回 0(no-op),否则更新并返回 1;
  • ossl_quic_txfc_get_credit:即Get TX Window;对流级 TXFC,还会递归调用连接级 TXFC 并返回两者中较小的信用值——这正是文档所述"取较小者"约束的落地:
    r = ossl_quic_txfc_get_credit_local(txfc, 0); if (txfc->parent != NULL) { conn_r = ossl_quic_txfc_get_credit_local(txfc->parent, consumed); if (conn_r < r) r = conn_r; } return r;
  • ossl_quic_txfc_consume_credit:即On TX操作;若传入字节数超过当前信用,则消耗剩余信用并返回 0,提示调用方存在严重编程错误;流级调用时同步在连接级 TXFC 上消费;
  • ossl_quic_txfc_has_become_blocked:即On TX Blocked标志,供调用方决定是否需要发送DATA_BLOCKEDSTREAM_DATA_BLOCKED帧(帧中应携带ossl_quic_txfc_get_cwm()的返回值)。

在通道层面,ssl/quic/quic_channel.c 解析传输参数时将initial_max_data直接映射为ossl_quic_txfc_bump_cwm(&ch->conn_txfc, v)(见 quic_channel.c 中QUIC_TPARAM_INITIAL_MAX_DATA分支),而每个流的 TXFC 则通过ossl_quic_txfc_init(&qs->txfc, &ch->conn_txfc)与连接级 TXFC 建立父子关系。

RX 侧流控:以退役节奏驱动信用发放

RX 侧连接级流控的状态机如下:

---> event: On RX Controlled Bytes (numBytes) [internal event] ---> event: On Retire Controlled Bytes (numBytes) <--- event: Increase Window (numBytes) <--- event: Flow Control Error

RX 侧连接级流控负责指示何时生成MAX_DATA帧以提高对端的连接级发送信用,比 TX 侧复杂一些。

连接级 RX 状态机事件

  • On RX Controlled Bytes(内部事件):由流级流控控制器在收到任何受控字节时自动生成,调用方不直接传入。numBytes为收到的受控字节数。之所以由流级生成,是因为重传的流数据只能计数一次,流级流控最擅长判断收到了多少"新的、非重传的"流负载字节。
  • Flow Control Error:若收到的受控字节数超过我们授权的量,状态机发出该事件,此时应以协议错误终止连接
  • Increase Window:当状态机认为应授予对端更多流控信用(即应抬高 CWM)时发出。numBytes为新的 CWM 值,且相对之前所有 Increase Window 事件是单调的。
  • On Retire Controlled Bytes:当一个或多个受控字节从任意流出队并交给应用时传入。状态机依据 On Retire 事件的节奏决定何时增大流控窗口,因此该事件必须在受控字节处理完成(即已交给应用)之后才传入。

这里体现了设计文档强调的关键点:RX 流控希望对端的发送速率既不超过 QUIC 实现的处理能力,也不超过应用消费数据的能力——退役节奏正是把应用消费速率纳入流控决策的桥梁。

流级 RX:On RX Stream Frame 事件

流级 RX 流控与连接级 RX 类似,但有几点差异:

  • 没有 On RX Controlled Bytes 事件
  • On Retire Controlled Bytes 事件可以(由实现决定)同时传递给连接级流控控制器——因为这两个事件总是同时发生;
  • 新增一个替代事件:
---> event: On RX Stream Frame (offsetPlusLength, isFin)

该事件在收到 STREAM 帧时传入:offsetPlusLength是 STREAM 帧 offset 字段与帧负载长度的和;isFin指示该帧是否设置了 FIN 标志。该事件既用于向连接级流控控制器生成内部 On RX Controlled Bytes 事件,也用于流级流控判断对端是否违反流控上限。

状态机对offsetPlusLength单调处理:如果之前已有相同或更大的值,则忽略该事件。之所以用这种 API 而非On RX (numBytes)风格,是因为该 API 是单调的、更易使用——调用方无需记忆某个受控字节是否已在之前的 STREAM 帧中计过数(毕竟之前的帧可能重复包含了其中部分字节)。

源码中的 RX 流控实现与调用链

在 ssl/quic/quic_fc.c 中:

  • ossl_quic_rxfc_on_rx_stream_frame(rxfc, end, is_fin)实现On RX Stream Frame:计算delta = end - hwm(仅在end > hwm时),对自身与 parent(连接级 RXFC)分别调用内部on_rx_controlled_bytes;同时处理 FIN 相关的FINAL_SIZE_ERROR——流结束后大小不可改变(end与已记录hwm不符时置错);
  • 内部on_rx_controlled_bytes检查num_bytes > cwm - swm,若超出则置error_code = OSSL_QUIC_ERR_FLOW_CONTROL_ERROR并截断计数——即Flow Control Error事件;
  • ossl_quic_rxfc_on_retire(rxfc, num_bytes, rtt)实现On Retire Controlled Bytes:累加 RWM,并调用rxfc_update_cwm决定是否抬高 CWM(抬高后置has_cwm_changed标志,供上层生成MAX_DATA/MAX_STREAM_DATA帧)。

调用链可以在帧解包路径 ssl/quic/quic_rx_depack.c 中观察到:每收到一个 STREAM 帧或 RESET_STREAM 帧,都会调用ossl_quic_rxfc_on_rx_stream_frame,随后用ossl_quic_rxfc_get_error检查是否发生流控违规,若违规则通过ossl_quic_channel_raise_protocol_errorFLOW_CONTROL_ERROR终止连接(见 quic_rx_depack.c 中 RESET_STREAM 处理段);CRYPTO 帧则使用独立的 standalone RXFC(crypto_rxfc),违规时报CRYPTO_BUFFER_EXCEEDED

值得一提的复用:ossl_quic_rxfc_init_standalone创建的standalone RXFC还被用于流数量(stream count)强制CRYPTO 缓冲区强制(见 quic_channel.c 中max_streams_bidi_rxfcmax_streams_uni_rxfc的初始化),这也是文档之外的一个实现细节。

RX 窗口自动调优(Auto-Tuning)

对于 RX 流控,我们必须确定窗口大小——即每次 RWM 到达阈值时,加到对端 SWM 上以得出新 CWM 的值。窗口大小应根据网络条件动态调整

许多实现采用"只增不减"的窗口增大机制(可以增加窗口大小但不能减少),OpenSSL 采纳了这一简单方案。

算法原理

常见算法是一种"自动调优"(auto-tuning)方法:

  1. 测量窗口的消耗速率(即 CWM 被抬高后 RWM 向 CWM 逼近的速率);
  2. 与测得的连接RTT比较;
  3. 如果消耗完一个窗口大小所需的时间超过 RTT 的固定倍数,则将窗口大小加倍,直到达到实现选择的最大窗口大小。

自动调优按epoch(周期)进行:每个 epoch 结束时决定是否加倍窗口大小,并开启一个新的 epoch。

OpenSSL 的实现细节

在 ssl/quic/quic_fc.c 中:

  • 阈值被定义为窗口大小的 3/4(WINDOW_THRESHOLD_NUM 3WINDOW_THRESHOLD_DEN 4),rxfc_cwm_bump_desiredcwm - rwm <= 3/4 * cur_window_size且流未终结(!is_fin)时判定需要抬高 CWM;对极端大的窗口兜底使用 1/2 作为阈值;
  • epoch 的起点rxfc_start_epoch记录:epoch_start = now()esrwm = rwm
  • 是否加倍rxfc_should_bump_window_size决定,其推导过程即文档所述:
b = rwm - esrwm (本 epoch 内消耗的窗口字节数) dw = b / window_size T_window = dt / dw = (dt * window_size) / b

T_window < 4 * RTTossl_time_compare(t_window, ossl_time_multiply(rtt, 4)) < 0)时判定应加倍窗口大小。源码注释特别说明:把除法b保留在等号左侧,可降低 64 位纳秒表示溢出的风险,且除法后仍留有充足精度;

  • rxfc_adjust_window_size执行加倍(new_window_size *= 2),并夹在min_window_sizemax_window_size之间(max 优先于 min),随后开启新 epoch;
  • 新 CWM 按文档公式计算:new_cwm = rwm + cur_window_size(加到 SWM/RWM 而非旧 CWM 上),只有确实大于当前 CWM 时才更新并置has_cwm_changed(见rxfc_update_cwm)。

窗口大小参数通过ossl_quic_rxfc_initinitial_window_sizemax_window_size传入,也可用ossl_quic_rxfc_set_max_window_size动态调整。

帧生成与测试验证

  • 帧生成:TX 打包器 ssl/quic/quic_txp.c 负责生成MAX_DATA/MAX_STREAM_DATA帧(并遵循 RFC 9000 s. 13.3 的建议:仅在有意义时生成更多MAX_STREAM_DATA帧);连接级与流级的 CWM 变更标志(ossl_quic_rxfc_has_cwm_changed)驱动帧的按需发送。
  • 测试:test/quic_fc_test.c 系统验证了 TXFC/RXFC 的行为——例如初始化后 SWM 为 0、bump CWM 到 2000 后信用为 2000、消费 500 后本地信用降为 1500、流级 TXFC 与连接级 TXFC 联合取小、has_become_blocked边沿标志等;测试入口由 test/recipes/70-test_quic_fc.t 注册。

小结

OpenSSL 的 QUIC 流控设计可以浓缩为几个要点:

  1. 双层信用模型:连接级由MAX_DATAinitial_max_data驱动,流级由MAX_STREAM_DATA与三个initial_max_stream_data_*传输参数驱动;实际可发送字节数取两层信用的较小者。
  2. 三个水位线:SWM(已消费)、CWM(已授权)、RWM(已退役,RX 侧)——信用恒为CWM - SWM,任何导致 SWM 越过 CWM 的情况都是流控违规。
  3. TX 极简、RX 以退役驱动:TX 侧只是加减法状态机;RX 侧由"收到受控字节"与"退役受控字节"两个节奏共同决定何时抬高 CWM,从而让对端发送速率同时匹配 QUIC 实现处理能力与应用消费能力。
  4. 窗口自动调优:以 3/4 窗口为阈值,以T_window < 4 * RTT为判据按 epoch 加倍窗口大小,且只增不减、受最大窗口上限约束。
  5. 边界清晰:CRYPTO 流不受流控;DATA_BLOCKED/STREAM_DATA_BLOCKED仅作性能增强与调试用途;流控与拥塞控制相互独立。

这套设计与实现可以从设计文档 doc/designs/quic-design/quic-fc.md 出发,对照 ssl/quic/quic_fc.c、include/internal/quic_fc.h 与 test/quic_fc_test.c 逐行印证,是理解 OpenSSL QUIC 栈(以及 RFC 9000 流控语义)的一条清晰路径。

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 7:45:36

SmartMediaKit与YOLO融合:实现低延迟视频播放与实时目标检测

做流媒体播放和做视觉分析&#xff0c;这两拨人平时很少坐在一起。但这两年越来越多的项目要求“边播放边看懂画面”&#xff0c;尤其是安防、智慧工厂、零售统计这类场景&#xff0c;不仅是把视频流拉出来给人看&#xff0c;还希望系统能自动识别画面里的目标。最开始我习惯用…

作者头像 李华
网站建设 2026/9/11 7:45:05

医疗知识图谱+BERT双塔:构建可解释临床推理引擎

简介&#xff1a;这是一套面向Python开发者与医疗AI初学者的智能诊断问答系统实战项目&#xff0c;聚焦知识图谱构建与向量检索技术在健康医疗场景的落地应用&#xff0c;帮助用户掌握从医学知识建模、语义向量化到端到端问答服务部署的完整链路。资源包共188个文件&#xff0c…

作者头像 李华
网站建设 2026/9/11 7:45:03

Agent记忆系统实战:从短期窗口到长期向量库

1. Agent 记忆问题&#xff0c;比你想的更像人类的遗忘曲线 到现在还有不少人问我&#xff0c;Agent 不就是“大模型 提示词 工具调用”串起来吗&#xff1f;这句话对了一半。串起来只是让 Agent 有了“动手能力”&#xff0c;但真正决定一个 Agent 是“演示玩具”还是“能持…

作者头像 李华
网站建设 2026/9/11 7:43:34

AST静态分析实战:Agent集群任务调度源码审计与隐患排查

如果你也在维护一个几十上百节点的 agent 集群&#xff0c;大概率体会过任务莫名其妙丢失、节点失联半天、配置改了却完全不生效的无力感。这三周我把 agent-fleet-manager 的源码整体过了一遍&#xff0c;用 AST 静态分析的方式把它的任务采集引擎和集群调度逻辑翻了个底朝天。…

作者头像 李华
网站建设 2026/9/11 7:42:15

Sharge IceMag 3主动散热移动电源深度评测

1. 开箱与第一印象&#xff1a;当移动电源遇上主动散热从快递盒里取出Sharge IceMag 3的那一刻&#xff0c;就能感受到这个移动电源的与众不同。包装盒上醒目的"Active Cooling"标识直接表明了它的核心卖点——这是市面上首款搭载主动散热系统的Qi2磁吸移动电源。整机…

作者头像 李华