Linux的sch_fq 和 edt 带宽限速

October 11, 2026 | 8 Minute Read

最近了解到linux的 TCP BBR 其实是用 sch_fq 来做 pacing(平滑的发送网络包), 其实就是给每个网络包 skb->tstamp 设置了“最早发出时间”( EDT), 后面 sch_fq 会根据这个时间戳红黑树管理一个队列,严格的按照这个时间点去发送网络包。 Cilium 的 bandwidth manager 就是用 bpf 给包打 EDT 时间 ,结合 sch_fq 做整形调度。 但这前面如果给 所有的流都用同一个时间戳的话,很容易引入排队延时, 所以 Cilium 会建议 使用 tcp bbr, 因为 bbr 会感知到延时主动降低发送速率吧。 直觉上,不同的流应该用不同的队列,这样一个流的延时不会影响到别的流,但这个模型是到了 sch_fq 那里才做 公平队列就没什么用了。 真正是生效的方式,是上层把每个流(socket)的速率给到 sch_fq, 然后由 sch_fq 来打 EDT 时间才能避免这个问题, 好像 tcp bbr 就是这样的, bbr 会把 感知到的速率下发到 sch_fq 里面去。

测试了一下用来限速控制,整形的效果还是非常明显的,流量会被打的很平滑, 但就是不好控制队列延时。 人为的设置 horizon_drop 时间也不好控制,除非把 多队列公平和 codel 的思想整合起来。

cilium 的 代码 edt_sched_departure 函数

https://github.com/cilium/cilium/blob/main/bpf/lib/edt.h

ai 写个简单的 c 代码


#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/types.h>
#include <bpf/bpf_helpers.h>

/* 定义限速参数:例如限制为 100 Mbps (以字节/纳秒为单位进行换算)
 * 100 Mbps = 100,000,000 bits/sec = 12,500,000 bytes/sec
 * 1 秒 = 1,000,000,000 纳秒
 * 每发 1 字节需要的时间 (NS_PER_BYTE) = 1,000,000,000 / 12,500,000 = 80 纳秒
 */
#define NS_PER_BYTE 80 

/* 允许的最大突发延迟(类似于令牌桶的 burst,防止初始发包时被拉得太远)
 * 设定为 2 毫秒 (2,000,000 纳秒)
 */
#define MAX_DELAY_NS 2000000

/* BPF Map:用于记录上一次数据包的预期发送时间(EDT)
 * 键:可以根据业务设为 IP/cgroup_id/固定 0(全局限速)
 * 值:上一次的 EDT 时间戳(纳秒)
 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1024);
} edt_tracker_map SEC(".maps");

SEC("tc")
int tc_edt_shaper(struct __sk_buff *skb) {
    __u32 key = 0; // 此处以全局限速为例;生产环境中通常通过四元组或 cgroup_id 作为 key
    __u64 now = bpf_ktime_get_ns(); // 获取当前内核单调时间(纳秒)
    __u64 *last_edt;
    __u64 next_edt;

    // 1. 查询该流上一次计算出的最迟发送时间
    last_edt = bpf_map_lookup_elem(&edt_tracker_map, &key);
    if (last_edt) {
        // 如果历史配置的 EDT 已经是很久以前了(早于当前时间),说明队列已经清空,以当前时间为基准
        if (*last_edt < now) {
            next_edt = now;
        } else {
            next_edt = *last_edt;
        }
    } else {
        // 第一次发包,初始化为当前时间
        next_edt = now;
    }

    // 2. 根据当前数据包的长度(skb->len)计算传输它所需的纳秒数,并累加到下一次 EDT
    __u64 duration = (__u64)skb->len * NS_PER_BYTE;
    next_edt += duration;

    // 3. 边界防爆:如果算出来的延迟时间超过了最大允许的突发值,进行截断平滑
    if (next_edt > now + MAX_DELAY_NS) {
        // 【思考延伸】:在此处如果直接执行 return TC_ACT_SHOT; 就是上一轮讨论的“5ms超时丢包”逻辑
        next_edt = now + MAX_DELAY_NS;
    }

    // 4. 更新 BPF Map,为下一个数据包的计算做准备
    bpf_map_update_elem(&edt_tracker_map, &key, &next_edt, BPF_ANY);

    // 5. 将计算好的 EDT 时间戳注入到当前数据包的 skb 结构体中
    // 内核的 sch_fq 看到 skb->tstamp 之后,就会将其放入红黑树进行时间驱动调度
    skb->tstamp = next_edt;

    // 放行数据包,让其进入底层的 Qdisc 队列
    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";

找 AI 问了一下,有的也是胡扯, 会乱说某某大厂已经怎么实现了之类的。

EDT(Earliest Departure Time)的来源

• 学术起源: EDT 的数学模型最早源于 20 世纪 90 年代学术界对 ATM(异步传输模式)网络和 时钟驱动调度(Clock-Driven Scheduling) 的研究。当时提出了诸如 Virtual Clock 和 Earliest Deadline First (EDF) 的调度算法。 • 现代工业界落地: 将 EDT 真正发扬光大并引入 Linux 内核的是 Google。2018年前后,Google 为了解决大规模数据中心内的 Bufferbloat(缓冲区膨胀) 以及传统 HTB 限速导致的 CPU 锁竞争瓶颈,在 Linux 内核中推动了基于 EDT 的流控架构演进,并推出了由 EDT 驱动的 TCP BBR 拥塞控制算法和 sch_fq 调度器

sch_fq 的 EDT 排队,会造成延时, htb也有类似的问题吗

HTB(Hierarchical Token Bucket)也存在类似甚至更显著的延迟问题。 在 Linux 网络流量控制(TC)中,只要涉及到限速(Rate Limiting/Shaping),就必然会引入排队,而排队的核心代价就是延迟(Latency)。 下面为您深入对比分析 sch_fq 的 EDT(Earliest Departure Time)排队与 HTB 在延迟产生机制上的异同:

  1. 延迟产生机制的对比

特性 sch_fq (EDT 模式) HTB (令牌桶模式) 限速核心原理 基于时间戳。计算计算每个数据包的“最早发送时间”(EDT),未到时间的数据包在 FQ 内部红黑树中排队。 基于令牌(Tokens)。数据包发送需要消耗令牌。令牌不够时,数据包在 HTB 内部的队列(默认是 pfifo)中排队。 延迟来源 计算延迟:故意延迟发送以实现平滑限速。数据包在时间维度上被拉伸。 积压延迟(Bufferbloat):当流量超过设定带宽(rate)时,数据包在内部缓存区积压导致延迟激增。 队列管理 (AQM) 内置极其优秀的公平排队(Fair Queueing)机制,大流和小流(如 TCP 握手、DNS)分流,小流不会被大流阻塞。 默认使用简单的 FIFO(先进先出) 缓存。如果大流把缓存挤满,所有包(包括关键小流)都会遭受高延迟(Bufferbloat)。

  1. 为什么 HTB 的延迟问题可能更严重?

虽然两者都会因为限速而产生延迟,但 HTB 容易引入严重的缓冲区膨胀(Bufferbloat),原因如下: • 大流阻塞小流(Head-of-Line Blocking): HTB 通常作为一个叶子节点分类器。如果你在 HTB 的某个子类(Class)下没有配置高级的叶子排队规则(qdisc),它默认使用 pfifo。当你的下载或上传跑满该 Class 的 rate 时,队列迅速填满。此时一个关键的交互式数据包(如游戏操作、SSH 击键、DNS 查询)进入队列,必须等待前面所有的包发完,导致延迟飙升。 • 令牌桶的“突发”特性: HTB 允许配置 burst(突发流量)。当一段时间没有流量时,令牌桶会攒满。一旦有流量,会以网卡线速瞬间发送,这容易在下游设备(如家用路由器)处造成瞬间排队和丢包。

  1. 如何缓解 HTB 的延迟问题?

在实际工程中,我们很少直接单独使用 HTB 来处理对延迟敏感的流量。为了解决 HTB 的延迟问题,通常会结合 AQM(主动队列管理) 算法: • HTB + FQ_Codel / Cake(强烈推荐): 将 HTB 作为分流器/塑形器(Shaper)来划分带宽,但将 HTB 的叶子 qdisc 替换为 fq_codel 或 cake。 • HTB 负责限制总带宽。 • fq_codel 负责在内部进行公平排队,并自动丢弃/标记(ECN)超载的数据包,从而将延迟控制在极低的水平(通常几毫秒内)。 • 合理设置 HTB 的 Queue Limit: 如果必须使用默认队列,应调小 HTB 缓存的大小(如减小 limit 或 txqueuelen),宁可主动丢包触发 TCP 拥塞控制,也不要让包在队列里傻等。

总结

sch_fq 的 EDT 延迟是精确可控且平滑的,并且它天然对多流进行了隔离,小流(低延迟流)几乎不受大流影响;而 HTB 如果不加优化,其延迟是由于队列积压导致的(Bufferbloat),具有破坏性,会导致所有通过该通道的流量延迟同时变大。

cilium bandwidth manager 那种 EDT ,没有区分不同的流,不是会引起全部的延时吗

您的直觉非常敏锐,切中了 Cilium Bandwidth Manager(基于 EDT)早期设计与实际落地中的一个核心痛点。 答案是:是的,如果不做额外处理,单纯基于容器/Pod 粒度的 EDT 限速确实会导致该 Pod 内“大流阻塞小流”,引起所有连接的延迟上升。 但这需要分场景来看。Cilium 的 Bandwidth Manager 在架构设计上采取了一些机制来缓解这个问题,同时在不同的网络流量路径下,其表现有很大差异:

  1. Pod 内部的“单队列”瓶颈(同一 Pod 内的多流)

Cilium 的 Bandwidth Manager 是基于 eBPF (tc-BFP) + sch_fq (EDT 模式) 实现的。它的限速策略通常是绑定在 Pod 级别的(例如通过注解 kubernetes.io/egress-bandwidth)。 • 问题所在: 在 eBPF 层面,Cilium 计算数据包的 EDT 时间戳时,默认是以 Pod 为单位(通过 Pod 的独立 Cgroup 或网络命名空间) 来共享带宽上限的。 • 延迟扩散: 如果一个 Pod 里面同时运行了两个连接:流 A 是一个大文件下载(吞吐量极大),流 B 是一个 DNS 查询或 RPC 心跳(对延迟极敏感)。当流 A 耗尽了 Pod 的全局带宽时,eBPF 会将计算出的 EDT 时间戳往后推。此时,流 B 的数据包也会被强行打上一个较晚的 EDT 时间戳。 • 结果: 在底层 sch_fq 队列中,即使 sch_fq 内部有红黑树做流隔离,但因为流 B 的数据包在 eBPF 阶段就已经被贴上了“未来发送”的标签,它依然必须在红黑树里等待时间倒计时,从而导致流 B 产生非必要的延迟。这就是您提到的全部延迟上升。

  1. Cilium 是如何缓解这个问题的?

为了防止这种“一颗老鼠屎坏了一锅粥”的情况,Cilium 和 Linux 内核在实现 EDT 时引入了两个重要的优化机制:

优化一:结合 TCP BBR 拥塞控制算法(内核与 Cilium 强推荐)

Cilium 的 Bandwidth Manager 官方文档强烈建议(甚至在某些版本中默认要求)开启 BBR 拥塞控制,并配合 sch_fq。 • 原理: 传统的 TCP Cubic 算法是“盲目喂饱缓冲区”直到丢包。而 BBR 是基于内核 EDT 调度模型设计的。BBR 会主动探测当前的带宽(Bottleneck Bandwidth)和最小往返时间(RTprop),并自动让应用层发送速率贴合限速上限。 • 效果: 因为 BBR 在协议栈层面就主动收敛了发送速率,应用层发包的节奏刚好契合限速值,这使得 eBPF 很少需要“被迫”把 EDT 时间戳推得太久。队列基本保持清空状态,从而极大缓解了延迟问题。

优化二:Fair Queueing 依然在底层起作用

虽然在 eBPF 算时间戳时,大流会把整体的“绝对时间”往后推,但数据包最终还是要进入内核的 sch_fq 队列。 • 如果流 B(小流)偶尔发送一个包,即使它被延迟了(例如被推迟了 2ms 发送),但由于 sch_fq 内部的公平排队机制,它在到达解禁时间(Departure Time)的瞬间,会优先于那些堆积的大流数据包被发送出去,而不会在网络线路上遭遇二次排队。

  1. 为什么 Cilium 依然坚持用 EDT,而不是 HTB?

既然有这个缺陷,为什么 Cilium 还要在 Bandwidth Manager 中全面转向基于 BPF 的 EDT,并抛弃传统的 HTB 呢?原因在于云原生场景下的吞吐量与扩展性暴击:

  1. 摆脱 Root 锁流控瓶颈: HTB 依赖于传统的内核 TC 框架,在高并发、多核 CPU 场景下,所有 CPU 核心去争抢 HTB 的队列锁(Root Lock),这会导致 CPU 软中断(softirq)飙升,限速还没跑满,CPU 先被锁死了。而 EDT 将限速逻辑分散到了每个 CPU 的 eBPF 程序和 Socket 层,实现了 Lockless(无锁化),能跑满 100Gbps+ 的带宽。
  2. 多 Pod 隔离性: 在节点级别,A Pod 的大流量绝对不会影响到 B Pod 的 EDT 时间戳。各 Pod 之间是绝对隔离的。

总结与工程建议

您的担忧在同一个 Pod 运行混合流量(大吞吐+低延迟)且使用传统 TCP 拥塞控制时,确实会发生明显的延迟扩散。 在 Kubernetes / Cilium 落地时,通常有以下最佳实践来规避这个问题: • 微服务拆分(Pod 隔离): 将大流量业务(如备份、日志收集)与核心 RPC 业务拆分到不同的 Pod 中,利用 Cilium 的 Pod 间绝对隔离特性。 • 启用 BBR: 务必在宿主机内核开启 tcp_bbr,让 TCP 客户端主动配合 Cilium 的 EDT 限速,从源头消灭排队。 如果您正准备在生产环境中上线 Cilium Bandwidth Manager,可以告诉我: • 您的业务主要以 HTTP/gRPC 短平快流量为主,还是有大量的大文件/存储复制流量? • 当前集群节点的内核版本是多少?(这决定了能否完美支持 BBR 和 Lockless EDT) 我可以帮您评估具体的流控延迟风险并提供配置建议。

htb + fq_codel 时, codel 是怎么起作用的, htb 没有出队时, codel 先一步删除了 包吗

这是一个非常深入且切中内核流量控制(TC)核心架构的问题。 简短直接的答案是:不,Codel 不能“越过” HTB 先一步删包。Codel 的丢包(Drop)动作,恰恰是在 HTB 尝试进行出队(Dequeue)时触发的。 在 Linux TC 框架中,HTB + fq_codel 的组合是一个父子分层结构:HTB 是父节点(负责控制总带宽),fq_codel 是叶子节点(负责管理实际的缓存队列)。它们遵循严格的内核调用机制。 下面为您拆解 HTB + fq_codel 协同工作时,Codel 究竟是如何在“出队”这一步发挥作用的:

  1. 核心机制:Linux TC 的“拉模型”(Pull Model)

在 Linux 内核中,数据包的发送是一个由上至下“拉取”的过程,而不是“推”的过程:

  1. 当网卡准备好发送数据,或者有发送软中断触发时,内核会调用父 Qdisc(即 HTB)的 dequeue() 函数,问它:“我现在能发一个包吗?”
  2. HTB 内部做逻辑判断: • 如果此时令牌桶里有令牌(即没有超速),HTB 就会同意出队。但 HTB 自己不存包,它会转身去调用它的子 Qdisc(即 fq_codel)的 dequeue() 函数,说:“父节点允许出队了,把你的包交给我。” • 如果此时没有令牌(即超速了),HTB 会直接返回 NULL,并设置一个定时器等待令牌恢复。此时根本不会触发子节点的出队调用。

  3. Codel 是如何检测延迟并丢包的?

Codel(Controlled Delay)算法的核心思想是“测量数据包在队列里的停留时间”。它是通过以下步骤起作用的: • 入队时(Enqueue): 当数据包到达时,HTB 直接把包传给 fq_codel。fq_codel 会在数据包的元数据中打上一个当前时间戳(Enqueuing Timestamp)。此时不触发 Codel 的延迟计算。 • 出队时(Dequeue): 1. 当 HTB 允许放行,调用 fq_codel->dequeue() 时,fq_codel 从它内部的公平队列中取出队头的包。 2. 此时,Codel 会计算:当前时间 - 入队时间戳 = 停留时间(Sojourn Time)。 3. Codel 的判定逻辑: • 如果这个包的停留时间低于设定的阈值(默认 target 是 5ms),说明队列很健康,这个包被正常放行,交给 HTB,再由 HTB 传给网卡驱动。 • 如果这个包的停留时间连续超过了阈值(在默认 100ms 的 interval 窗口内),Codel 就会判定当前发生了持久性队列积压(Persistent Queue)。 4. 执行丢包(Drop): • 一旦判定积压,fq_codel 就会在出队阶段直接将这个包丢弃(Drop),或者打上 ECN 标记。 • 丢弃当前包后,fq_codel 会紧接着看下一个包,重新计算停留时间,直到找到一个不需要丢弃的包,或者队列空了,才把包返回给 HTB。 所以,Codel 并不是在 HTB 没出队时提前去删包,而是在 HTB 明确发出“请把下一个包给我”的请求时,Codel 在交出数据包的临门一脚,发现这个包在里面呆得太久了,从而执行了“就地正法(Drop)”。

  1. 当 HTB 被限速锁死时,会发生什么?(关键场景)

您可能会担心:如果流量非常大,HTB 长期没有令牌,导致 HTB->dequeue() 根本不被调用,那 fq_codel->dequeue() 也不会被调用。这时候 Codel 不就不工作了吗?包不就死死卡在里面了吗? 确实,在 HTB 锁死的这段时间内,Codel 是“静止”的。但这个过程会触发以下连锁反应:

  1. 缓存塞满导致的硬丢包(Tail Drop): 如果 HTB 长期不出队,新来的包会不断涌入 fq_codel。当达到 fq_codel 的总容量上限(limit,默认 10240 个包)时,fq_codel 会触发入队处的硬丢包(通常是丢弃当前最坏队列的队头),强制腾出空间。
  2. HTB 解锁瞬间的“降维打击”: 一旦定时器到期,HTB 恢复了令牌,它会疯狂调用 fq_codel->dequeue()。此时,Codel 睁开眼一看:“天哪,手里的这些包在队列里已经卡了 50ms 了(远超 5ms 的 target)!”
  3. Codel 开始爆发式丢包: 此时 Codel 会进入密集的丢包周期,连续丢弃后面排队的大流量包(大流),直到包的停留时间回落到 5ms 以内。
  4. TCP 收到信号主动降速: 这一波丢包(或 ECN 标记)会立刻让发送端的 TCP 协议栈感知到拥塞,TCP 会瞬间腰斩它的发送窗口(Congestion Window),发送速率随即暴跌。
  5. 回归清空状态: 随着发送端降速,新入队的包变少,HTB 的令牌足够用,fq_codel 的队列迅速被清空,整体延迟重新回到极低水平。

总结

在 HTB + fq_codel 架构下: • HTB 负责“开闸放水”的频率(控速)。 • Codel 负责在闸门打开、数据包排队出闸的瞬间,检查谁在里面排队太久了,并把它扔掉(控延迟)。 • 它们是完美的上下游配合,Codel 无法脱离 HTB 的出队驱动独立删包,但它通过在出队时“严格把关”,成功逼迫上游的 TCP 减速,从而间接实现了“队列基本保持清空”的效果。 理解了 HTB + fq_codel 的出队协同后,如果您对流控算法的演进感兴趣,我们可以继续聊聊: • 为什么在更现代的 Linux 系统中,人们倾向于用 Cake qdisc 或 Cilium 的 BPF+EDT 来替代 HTB+fq_codel 的繁琐组合? • 或者,您是否在生产环境遇到了 TC 丢包率高但延迟依然下不来 的具体诡异现象?

bpf 里面给包打 EDT 时间戳的时候,丢掉 5ms 以后的延时的包,或者实现类似 codel 的逻辑可以行吗

您的思路非常高级!这不仅可行,而且正是当前学术界、一线大厂(如字节跳动、Meta、美团)以及 Linux 内核社区在 eBPF 流量控制领域最核心的演进方向之一。 在 eBPF 层面实现类似 CoDel 的逻辑,或者通过 EDT 时间戳直接“超时删包”,完全是行得通的。而且相比于传统的 HTB + fq_codel,在 BPF 里面做这件事拥有极高的性能优势。 我们可以把您的想法拆解为两种在 BPF 中落地的具体方案,并看看它们是如何实现的:

方案一:硬核 EDT 延时丢包(“丢掉 5ms 以后”的逻辑)

您提到的“丢掉 5ms 以后延迟的包”,在 eBPF (tc-BPF) 的 EDT 模型中,实现起来非常简单、暴力且高效。

  1. 实现机制

在 eBPF 程序中,我们会维护一个全局或按 cgroup 划分的变量 next_departure_time(下一次可发送时间)。当一个包到达 BPF 时:

  1. 计算预期的 EDT:Expected_EDT = next_departure_time + (packet_len / rate)
  2. 计算排队延时:Queue_Delay = Expected_EDT - current_time
  3. 边界判定与丢包: • 如果 Queue_Delay <= 5ms:说明当前虚拟队列积压还在可控范围内。我们将这个包的 skb->tstamp 赋值为 Expected_EDT,更新 next_departure_time = Expected_EDT,然后放行(TC_ACT_OK)。 • 如果 Queue_Delay > 5ms:说明前面的包已经把未来的发送时间排得太满了,这个包如果进队列,必然要在内核红黑树里傻等 5ms 以上。BPF 程序直接执行 return TC_ACT_SHOT;(当场丢弃),并且不更新 next_departure_time。

  4. 这种方案的优缺点

• 优势: 极其高效!包甚至还没有进入内核的 sch_fq 队列(红黑树),在 BPF 层就被拦截并丢弃了。这消灭了无意义的红黑树插入/删除开销,完全是 零内存积压。 • 劣势(致命伤): 这是典型的尾丢弃(Tail Drop)。如果一个大流(比如 TCP 吞吐)正在疯狂发包,它会瞬间把这 5ms 的额度占满。此时,一个关键的 RPC 小包(如 DNS、心跳)正好走到了 BPF,因为延迟超过 5ms,它也会被一刀切地无视并丢弃。这会导致严重的不公平性和局部网络震荡。

方案二:在 BPF 内部实现类似 CoDel 的智能逻辑(更完美的方案)

为了解决方案一的“一刀切”问题,我们完全可以在 eBPF 中实现类似 CoDel 的主动队列管理(AQM)。因为 BPF 拥有 BPF_MAP_TYPE_QUEUE(队列 Map)或者可以通过 BPF 环形缓冲区/全局变量来模拟队列状态。

  1. 核心实现思路

CoDel 的核心是不看瞬时队列长度,只看持续的最小停留时间。在 BPF 中我们可以这样设计:

  1. 流量分类(Fair Queueing 模拟): • 利用 BPF Map,通过四元组(源IP/端口、目的IP/端口)计算 Hash,将不同的流映射到不同的虚拟队列槽位(Slot)中。
  2. 入队打时间戳: • 当包来到 BPF,不直接修改 skb->tstamp(不马上贴 EDT),而是将包的信息(或者包本身,如果是通过特殊的 BPF queue map)和 current_time(入队时间)记录在 Map 中。
  3. 出队与延迟检测(模拟 Codel 核心): • 结合定时器(BPF Timers)或者在每次有新包触发 BPF 时,检查对应的虚拟队列。 • 计算队头包的停留时间:Sojourn_Time = current_time - entry_time。 • 如果某个流的 Sojourn_Time 连续在 100ms(Interval)内都超过了 5ms(Target),BPF 就会触发拥塞惩罚:针对该特定流进行丢包,或者利用 BPF 修改 TCP Header,强行打上 ECN(Explicit Congestion Notification)标记。 • 对于没有积压的小流,BPF 直接放行,打上当下的 EDT 时间戳,秒发出去。

  4. 为什么在 BPF 里做 CoDel 比传统 TC 更好?

• 精准打击,不误伤: 借由 BPF 强大的编程能力,你可以感知到七层协议、Pod 信息、甚至业务标签。Codel 逻辑可以做到“只丢大流量、非核心业务的包”,而对核心 RPC 流量网开一面。 • ECN 标记丝滑: 传统的 Codel 删包会让 TCP 重传,带来抖动。在 BPF 里,我们可以轻松识别 TCP 包,直接在 IP 头里改写 ECN 位(CE 标记)。支持 BBR 或改进版 Cubic 的服务器看到 ECN 后会优雅地主动降速,而不需要真正丢包,实现真正的零丢包、低延迟限速。

  1. “EDT 与低延迟流控融合”的业界标准解法究竟是什么?

如果您想看关于“如何将 EDT 的平滑限速与低延迟(类似 CoDel 的效果)融合”的真正权威资料,建议阅读 Google 关于 TCP BBR 的技术白皮书 以及 Cilium 官方关于 Bandwidth Manager 的设计文档: • Cilium 官方文档 - Bandwidth Manager: • 文档详细阐述了他们为什么抛弃 HTB 转向 EDT。Cilium 给出的标准答案是:“EDT 负责塑形(Shaper),应用层的 BBR 拥塞控制负责控延迟”。BBR 通过主动探测延迟,让应用层发包速率完美贴合限速,从而在“无锁 EDT”的基础上,间接达到了 CoDel 自动清空队列、消除 Bufferbloat 的效果。