tc 的锁问题

1. TC 的基本原理

流量控制 Traffic Control (TC),表示网络设备接收和发送数据包的排队机制。比如,数据包的接收速率、发送速率、多个数据包的发送顺序等。

Linux 实现了流量控制子系统,它包括两部分:

  • 内核部分的 traffic control 框架
  • 用户态的规则配置工具:iproute2 软件包中的 tc 程序

它们有些类似于内核态的 netfilter 框架和用户态的 iptables 程序。但相较于 netfilter, 关于 tc 的资料非常少,并且也较为古老,彻底理解它的机制还是需要对照源码。

Traffic Control 的作用包括以下几种:

  • 调整(Shaping): 通过推迟数据包发送来控制发送速率,只用于网络出方向(egress)

  • 时序(Scheduling):调度不同类型数据包发送顺序,比如在交互流量和批量下载类型数据包之间进行发送顺序的调整。只用于网络出方向(egress)

  • 监督(Policing): 根据到达速率决策接收还是丢弃数据包,用于网络入方向(ingress)

  • 丢弃(Dropping): 根据带宽丢弃数据包,可以用于出入两个方向

要实现对数据包接收和发送的这些控制行为,需要使用队列结构来缓存数据包

在Linux实现中,把这种包括数据结构和算法实现的控制机制抽象为结构队列规程 : Queuing discipline,简称为 qdisc

qdisc 对外暴露两个回调接口:

  • enqueue
  • dequeue

分别用于数据包入队和数据包出队。

而具体的排队算法实现则在 qdisc 内部隐藏

不同的 qdisc 实现在 Linux内核中实现为不同的内核模块,在系统的内核模块目录里可以查看前缀为 sch_ 的模块:

bash 复制代码
▶ ls -l /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_*
-rw-r--r-- 1 root root 63299 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_cake.ko
-rw-r--r-- 1 root root 30171 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_cbs.ko
-rw-r--r-- 1 root root 21411 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_choke.ko
-rw-r--r-- 1 root root 21507 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_codel.ko
-rw-r--r-- 1 root root 32027 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_drr.ko
-rw-r--r-- 1 root root 23347 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_etf.ko
-rw-r--r-- 1 root root 38411 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_ets.ko
-rw-r--r-- 1 root root 33627 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_fq.ko
-rw-r--r-- 1 root root 27499 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_fq_pie.ko
-rw-r--r-- 1 root root 37099 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_gred.ko
-rw-r--r-- 1 root root 48363 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_hfsc.ko
-rw-r--r-- 1 root root 22787 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_hhf.ko
-rw-r--r-- 1 root root 65883 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_htb.ko
-rw-r--r-- 1 root root 17739 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_ingress.ko
-rw-r--r-- 1 root root 24963 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_mqprio.ko
-rw-r--r-- 1 root root 24443 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_multiq.ko
-rw-r--r-- 1 root root 37707 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_netem.ko
-rw-r--r-- 1 root root 26211 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_pie.ko
-rw-r--r-- 1 root root 11627 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_plug.ko
-rw-r--r-- 1 root root 24915 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_prio.ko
-rw-r--r-- 1 root root 50491 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_qfq.ko
-rw-r--r-- 1 root root 31867 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_red.ko
-rw-r--r-- 1 root root 27227 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_sfb.ko
-rw-r--r-- 1 root root 31531 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_sfq.ko
-rw-r--r-- 1 root root 14955 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_skbprio.ko
-rw-r--r-- 1 root root 62299 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_taprio.ko
-rw-r--r-- 1 root root 28755 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_tbf.ko
-rw-r--r-- 1 root root 23555 Aug  2 21:13 /usr/lib/modules/6.1.0-38-amd64/kernel/net/sched/sch_teql.ko

简单来说

qdisc 的实现可以非常简单,比如只包含单个队列,数据包先进先出,如: pfifo, 代码位于 net/sched/sch_fifo.c。

复杂的情况

也可以实现相当复杂的调度逻辑。比如,可以根据数据包的属性进行过滤分类,而针对不同的分类:

  • class 采用不同的算法来进行处理。
  • class 可以理解为 qdisc 的载体,它还可以包含子类与 qdisc。
  • 用来实现过滤逻辑的组件叫做 filter,也叫做分类器 classfier, 它需要挂载在 qdisc 或者 class 上。

基于 qdisc, class 和 filter 种三元素可以构建出非常复杂的树形 qdisc 结构,极大扩展流量控制的能力。

对于树形结构的 qdisc, 当数据包流程最顶层 qdisc 时,会层层向下递归进行调用:

  • 比如,父对象(qdisc/class)的 enqueue 回调接口被调用 时,其上所挂载的所有 filter 依次被调用,直到一个 filter 匹配成功
  • 然后将数据包入队到 filter 所指向的 class,具体实现则是调用 class 所配置的 Qdisc 的 enqueue 函数 。没有成功匹配 filter 的数据包分类到默认的 class 中。

大约在 2015 年的时候,TC 框架实现中又加入了 Classifier-Action 机制。上文提到的 filter 实际作用就是 classifier,当数据包匹配到特定的 filter 之后,可以执行该 filter 所挂载的 actions 对数据包进行处理。

filter 和 action 也可以实现为独立的内核模块,tc 框架的扩展性非常灵活。

一个网络接口有两个默认的 qdisc 锚点。

  • 入方向的锚点叫做 ingress,
  • 出方向叫做 root。

入方向的 ingress 功能比较有限,不能挂载其他的 class,只是做为Classifier-Action 机制的挂载点。

2. TC 的锁竞争问题

Linux 内核 tc(流量控制)的锁竞争,本质是多个执行单元(进程/软中断)同时申请操作共享资源(如 qdisc、filter 结构)的锁,导致彼此阻塞等待的现象。

其具体原理可拆解为以下3个核心环节:

1. 共享资源的必然存在

tc 操作的核心数据结构(如队列规则 qdisc、过滤器 filter、流量分类 class)是全局或设备级别的共享资源。例如:

  • 用户通过 tc 命令行工具(用户态进程)修改网卡的 qdisc 配置。
  • 内核软中断(如 NET_RX_SOFTIRQ)在处理数据包时,会实时访问该 qdisc 进行流量调度。 这些不同执行单元的操作目标是同一份数据,必须通过锁来保证操作的原子性(避免数据错乱)。

2. 锁的"排他性"引发等待

为保护共享资源,tc 模块会使用内核锁(如 spinlock_t 自旋锁)。这类锁的核心特性是排他性

  • 当执行单元 A 成功获取锁后,会持有锁并操作共享资源。
  • 若此时执行单元 B 也尝试获取同一把锁,会因锁已被占用而进入等待状态(自旋锁会循环等待,互斥锁会睡眠)。 这种"一人占用、他人等待"的机制,就是锁竞争的直接表现。

3. 高并发场景放大竞争

锁竞争的严重程度与资源访问频率正相关,以下场景会显著加剧竞争:

  • 高数据包吞吐量:软中断频繁访问 qdisc 进行封包/调度,持续占用锁。
  • 频繁配置变更 :用户态进程反复执行 tc add/del 命令,频繁申请锁以修改配置。
  • 多 CPU 核心并行:不同核心上的软中断/进程同时发起对同一锁的请求,冲突概率大幅提升。

例如,在 10G 网卡高流量场景下,软中断每秒可能上万次访问 qdisc 锁;若此时管理员频繁执行 tc 配置命令,用户态进程会与软中断激烈争夺同一把锁,导致软中断等待锁的时间变长,最终表现为网络延迟增加、吞吐量下降。

3. 如何解决该锁竞争

解决 Linux 内核 tc 的锁竞争问题,核心思路是减少锁的持有时间、降低锁的竞争频率、优化锁的粒度,具体可通过以下四类方案实现:

一、优化锁本身的设计与使用

这是从内核底层机制层面的优化,需修改 tc 模块源码,核心是让锁"更高效"。

  1. 缩小锁粒度 :将原有的全局/设备级大锁,拆分为针对单个 qdiscclassfilter 的细粒度锁。例如,不同网卡的 qdisc 用独立锁保护,避免一个网卡的操作阻塞所有网卡。
  2. 替换锁类型 :根据场景选择更合适的锁。例如,将长时间持有(如配置修改)的 spinlock(自旋锁,忙等)替换为 mutex(互斥锁,睡眠),避免软中断长时间自旋占用 CPU;对只读操作,使用 rwlock(读写锁),允许多个读操作并发,仅阻塞写操作。
  3. 缩短锁持有时间 :将锁保护范围内的非核心操作(如统计数据计算、打印调试信息)移到锁外执行,仅在修改共享数据结构(如 qdisc 的队列长度、filter 的匹配规则)时持有锁。

二、优化 tc 配置与使用策略

这是用户/管理员可直接操作的层面,通过规避高竞争场景减少冲突。

  1. 减少配置变更频率 :避免在高流量场景下频繁执行 tc add/del/change 等命令。例如,提前规划好流量控制策略,一次性配置完成,而非动态频繁调整。
  2. 简化 tc 规则复杂度
    • 避免使用过多层级的 class(如嵌套多层 HTB/CBQ),减少锁的嵌套持有和访问路径。
    • 用更高效的过滤器(如 clsact 结合 bpf)替代复杂的 u32 多级匹配,降低每次数据包处理时的锁访问次数。
  3. 分散资源竞争 :将高流量的应用流量分散到不同的网卡或 qdisc 实例中。例如,通过网卡绑定(RSS)将不同端口的流量分配到不同 CPU 核心,配合 mq(多队列)qdisc,让每个核心的软中断仅操作本地队列的锁,减少跨核心竞争。

三、利用 BPF 技术实现无锁/低锁逻辑

eBPF(扩展 Berkeley 包过滤器)是近年解决内核网络性能问题的关键技术,可绕过传统 tc 的锁竞争瓶颈。

  • 核心原理 :将流量分类、限速等逻辑通过 BPF 程序直接加载到内核的数据包处理路径(如 clsact 钩子),BPF 程序运行在无锁或低锁环境中,无需频繁访问 tc 的共享数据结构。
  • 优势 :BPF 程序执行高效且隔离性强,不同程序间无共享资源竞争;同时支持动态加载,兼顾灵活性与性能。例如,用 BPF 实现的 htbfq_codel 变体,可大幅降低锁的使用频率。

四、升级内核版本与使用优化补丁

Linux 内核社区持续优化 tc 模块的锁机制,新内核版本常包含性能改进。

  • 升级到最新稳定版 :例如,5.10+ 内核对 qdisc 的锁粒度、软中断调度做了多项优化;5.15+ 进一步增强了 BPF 与 tc 的整合,减少锁依赖。
  • 应用针对性补丁 :对于特定场景(如高并发 tc 配置),可关注内核邮件列表(LKML)或第三方厂商(如 Intel、Red Hat)发布的优化补丁,解决特定锁竞争问题。

总结

实际优化中,通常优先从配置层面(方案二) 快速规避问题;若性能仍不满足,再考虑BPF 改造(方案三) ;对于内核开发者或深度优化场景,才需要涉及锁机制重构(方案一)。升级内核(方案四)则是低成本获取社区优化成果的基础手段。

参考:

just4coding.com/2022/08/05/...

相关推荐
间彧4 分钟前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧5 分钟前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧6 分钟前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧10 分钟前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧16 分钟前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang1 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
草明1 小时前
Go 的 IO 多路复用
开发语言·后端·golang
蓝-萧2 小时前
Plugin ‘mysql_native_password‘ is not loaded`
java·后端
故事不长丨2 小时前
【Java SpringBoot+Vue 实现视频文件上传与存储】
java·javascript·spring boot·vscode·后端·vue·intellij-idea
9ilk2 小时前
【仿RabbitMQ的发布订阅式消息队列】--- 前置技术
分布式·后端·中间件·rabbitmq