导读:本期聚焦于徐致远创作的《Fedora 中如何实现限流算法?从内核令牌桶到应用层方案》,敬请观看详情。Fedora 的流量整形能力主要来自 Linux 内核的排队规则子系统,其中令牌桶和分层令牌桶是两种应用最广的限流算法。内核通过 qdisc 挂载在网卡队列上,对出方向数据包进行速率限制。本文先梳理令牌桶与漏桶的差异,再演示在 Fedora 中用 tc 配置 TBF 和 HTB 实现端口限速与带宽分层,随后给出用户态实现滑动窗口和漏桶的 Go 代码,并讨论结合 eBPF 在内核态做高精度限流的思路。通过这些方式可以应对服务器出口带宽管理、API 限流和网关流量整形等场景。需要说明的是,内核限流更偏重出方向,入方向需要配合策略路由或 ingress 钩子。掌握这些算法与配置方法后,可以在 Fedora 上搭建稳定可控的限流体系。

在 Fedora 服务器或网关上,限流需求通常来自两个层面:一是控制出方向带宽,避免单个服务占满出口;二是在应用层对请求速率进行约束。Fedora 依托 Linux 内核的流量控制框架,可以使用 tc 工具配置令牌桶过滤器、分层令牌桶等排队规则,也可以借助 eBPF 编写更灵活的限流逻辑。本文以 Fedora 为操作环境,分析限流算法的实现思路和配置方法。

Fedora 中如何实现限流算法?从内核令牌桶到应用层方案

限流算法基础:令牌桶、漏桶与内核排队规则

令牌桶算法维护一个容量固定的令牌池,系统以恒定速率向桶中放入令牌,每个数据包出队必须消耗一个令牌。当桶满时多余令牌会被丢弃。这种机制允许流量在桶容量范围内产生突发,适合 TCP 流量特征,因为 TCP 本身需要一定的突发能力来快速启动窗口。Fedora 内核中的 tbf 排队规则正是令牌桶思想的一种直接实现,它允许用户指定速率、突发量和延迟参数,对出方向流量进行限制。

漏桶算法则强制数据包以固定速率离开,无论突发到达多少,输出速率始终不变。漏桶更适合需要严格平滑输出的场景,例如语音或视频流,但它的缺点是无法有效地利用空闲带宽,可能在流量突发时造成不必要的丢包。与令牌桶相比,漏桶不保存可累积的突发额度,而是通过队列缓存超出的数据包,当队列满时再丢弃。两者的核心区别可以概括为:令牌桶限制的是平均速率并允许突发,漏桶限制的是输出速率并强制平滑。

在 Fedora 中,这些算法通过排队规则挂载在网卡队列上。Linux 内核的流量控制框架使用 qdisc 表示一个排队规则,通常挂在出方向路径上。最常用的无类别 qdisc 包括 pfifo_fast 和 tbf,而有类别 qdisc 如 htb 可以对多个流量类别分别施加不同的限速策略。理解这些基础概念是进一步配置内核限流的前提。

内核级限流实践:TBF 与 HTB 配置详解

TBF 是最直接的令牌桶实现,适合对某个网卡整体出口做简单限速。在 Fedora 上使用 tc 命令可以快速添加一个 TBF 规则。例如把 eth0 的出方向速率限制为 100Mbit/s,允许 10KB 的突发,延迟参数设为 50ms,命令如下:

# 查看当前网卡队列规则
tc qdisc show dev eth0

# 添加 TBF 限速:100Mbit/s,突发 10KB
tc qdisc add dev eth0 root handle 1: tbf rate 100mbit burst 10kb latency 50ms

# 删除现有规则
tc qdisc del dev eth0 root

上面的规则直接把 eth0 的 root qdisc 替换为 TBF,所有经过该网卡的出方向流量都会受到 100Mbit/s 的速率约束。其中 rate 参数指定长期平均速率,burst 参数决定桶容量,latency 参数影响排队时延。TBF 的优点是配置简单、内核开销低,但缺点是无法对不同类型的流量做差异化处理。

当需要对不同来源、不同目的或不同业务流量进行分层限速时,HTB 更加合适。HTB 基于令牌桶,但引入了一个树形结构,可以在父类和子类之间分配带宽。例如先创建一个根类,带宽上限定为 200Mbit/s,再创建两个子类,分别限制为 50Mbit/s 和 100Mbit/s。配置命令如下:

tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 200mbit ceil 200mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 100mbit ceil 200mbit
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dst 192.168.1.0/24 flowid 1:10

在这个配置中,根类 1:1 的总带宽上限为 200Mbit/s,子类 1:10 和 1:20 分别拥有 50Mbit/s 和 100Mbit/s 的保证速率,同时可以通过 ceil 参数借用剩余带宽。filter 规则根据目的 IP 地址将匹配流量分配给对应的子类。这样既能保证关键业务的带宽,又能让其他流量在空闲时使用更多带宽。

如果希望通过防火墙标记来分配流量类别,可以先用 iptables 给数据包打上标记,再用 fw 过滤器进行匹配。下面的例子把发往特定网段的流量打上标记 10,然后交给对应的 HTB 子类处理:

iptables -t mangle -A POSTROUTING -d 192.168.1.0/24 -j MARK --set-mark 10
tc filter add dev eth0 parent 1: protocol ip prio 1 handle 10 fw flowid 1:10

使用 fw 标记的好处是可以在 iptables 中组合更复杂的匹配条件,例如按端口、协议、连接状态等进行区分,大大提高了流量分类的灵活性。需要注意的是,iptables 的 mangle 表操作要发生在数据包离开主机之前,因此通常放在 POSTROUTING 链中。

用户态实现:Go 语言编写滑动窗口与漏桶限流器

内核级限流适合对整机出口做带宽控制,但在微服务环境中,应用层限流往往更贴近业务场景。例如一个 HTTP API 需要限制每个客户端的请求频率,这时可以在服务进程内实现限流逻辑,而不必依赖内核模块。Go 语言在 Fedora 上具有原生支持,编译部署都很方便,适合编写这类中间件。

先看一个简单的漏桶实现。漏桶的核心思想是维护一个水位值,每次请求前根据时间流逝漏掉一部分水,如果水位低于容量就允许请求并增加水位,否则拒绝。下面的 Go 代码演示了这个过程:

package main

import (
    "fmt"
    "time"
)

type LeakyBucket struct {
    rate     float64 // 每秒允许请求数
    capacity int     // 桶容量
    water    float64
    lastLeak time.Time
}

func NewLeakyBucket(rate float64, capacity int) *LeakyBucket {
    return &LeakyBucket{rate: rate, capacity: capacity, lastLeak: time.Now()}
}

func (b *LeakyBucket) Allow() bool {
    now := time.Now()
    elapsed := now.Sub(b.lastLeak).Seconds()
    b.water -= elapsed * b.rate
    if b.water < 0 {
        b.water = 0
    }
    b.lastLeak = now
    if b.water < float64(b.capacity) {
        b.water++
        return true
    }
    return false
}

func main() {
    lb := NewLeakyBucket(2, 5)
    for i := 0; i < 20; i++ {
        fmt.Println(i, lb.Allow())
        time.Sleep(100 * time.Millisecond)
    }
}

这个漏桶实现的时间复杂度为 O(1),每次 Allow 调用只做常数次浮点运算。它通过容量限制允许的突发大小,超出容量的请求会被直接拒绝。漏桶适合需要严格限制平均速率的场景,但在实际 API 限流中可能显得过于僵硬,因为它无法允许短时间内的小幅突发。

滑动窗口算法则更加灵活,它在时间窗口内统计请求数量,窗口随时间滑动,可以更准确地反映最近一段时间的请求强度。下面是 Go 实现的滑动窗口限流器:

package main

import (
    "sync"
    "time"
)

type SlidingWindow struct {
    mu       sync.Mutex
    window   time.Duration
    limit    int
    requests []time.Time
}

func (s *SlidingWindow) Allow() bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    now := time.Now()
    cutoff := now.Add(-s.window)
    idx := 0
    for i, t := range s.requests {
        if t.After(cutoff) {
            idx = i
            break
        }
    }
    s.requests = s.requests[idx:]
    if len(s.requests) < s.limit {
        s.requests = append(s.requests, now)
        return true
    }
    return false
}

滑动窗口的优势在于统计更精确,不会像固定窗口那样在窗口边界出现双倍请求的问题。它的代价是需要维护一个请求时间列表,每次请求都要清理过期记录,因此在高并发下可能产生一定的内存和 CPU 开销。对于大多数中小规模 API,这种开销可以忽略不计。可以根据业务需求在漏桶和滑动窗口之间做选择,如果强调平滑输出就用漏桶,如果强调精确的近期窗口限制就用滑动窗口。

eBPF 内核态限流:降低开销的高精度方案

用户态限流虽然灵活,但每次请求都需要穿越用户态和内核态的边界,在高吞吐场景下会带来明显的上下文切换开销。eBPF 允许开发者把限流逻辑直接挂载到内核的 XDP 或 TC 路径上,在数据包到达网卡驱动层时立即做出放行或丢弃的决定。这种方式可以达到线速处理,并且不占用额外的系统调用消耗,非常适合网关或高流量边缘节点。

下面是一个运行在 XDP 层的令牌桶限流示例,它使用 BPF map 保存时间戳和当前令牌数,每次收到数据包时根据纳秒级的时间差补充令牌,如果有可用令牌就放行,否则丢弃:

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

#define BURST 1000ULL
#define RATE_PER_NS 1ULL

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __array(values, __u64, 2);
} token_map SEC(".maps");

SEC("xdp")
int xdp_rate_limit(struct xdp_md *ctx) {
    __u32 key = 0;
    __u64 *tokens = bpf_map_lookup_elem(&token_map, &key);
    if (!tokens) {
        __u64 init[2] = {0, BURST};
        bpf_map_update_elem(&token_map, &key, init, BPF_ANY);
        tokens = bpf_map_lookup_elem(&token_map, &key);
    }
    __u64 now = bpf_ktime_get_ns();
    __u64 elapsed = now - tokens[0];
    tokens[0] = now;
    __u64 new_tokens = tokens[1] + elapsed * RATE_PER_NS;
    if (new_tokens > BURST) new_tokens = BURST;
    tokens[1] = new_tokens;
    if (new_tokens >= 1) {
        tokens[1]--;
        return XDP_PASS;
    }
    return XDP_DROP;
}

这段代码演示了最核心的令牌补充和消耗逻辑,实际部署时还需要根据业务需求调整 RATE_PER_NS 和 BURST 的值,并且要处理多 CPU 并发访问 map 的原子性问题。BPF map 默认使用自旋锁保护,但在极端高并发下可能需要使用 per-CPU map 来减少争用。

在 Fedora 上开发 eBPF 程序需要安装 clang、libbpf 和 bpftool 等工具。编译后的对象文件可以通过 bpftool 或 C 语言加载器挂载到网络接口上。由于 eBPF 运行在内核中,代码必须通过验证器检查,不能包含循环和未限制的内存访问。这些限制使得 eBPF 限流代码相对简单小巧,但也意味着复杂的业务限流逻辑更适合放在用户态,而把最核心的令牌桶或计数逻辑下沉到内核。

综合来看,Fedora 上的限流算法实现可以根据场景灵活选择。内核 TBF 和 HTB 适合做整机带宽管理,用户态 Go 实现适合做应用层 API 限流,而 eBPF 则在需要极高吞吐和低延迟时提供了一种将限流逻辑前置到网卡驱动层的方案。理解这些不同层次的特点,能够帮助开发者在 Fedora 上搭建更加稳定和高效的限流体系。

Fedora限流令牌桶算法流量控制修改时间:2026-09-29 14:36:32

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0929/63437.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。