1. 从一个真实场景说起:为什么需要HFSC
家里或者公司网络一到晚上就卡,打游戏延迟飙红,视频会议断断续续,但一看带宽占用,下载任务也没跑满。这种情况我遇到过太多次了,排查下来往往不是带宽不够,而是流量调度没做好。所有数据包挤在一个队列里,谁抢到谁先走,结果就是关键业务被后台下载拖垮。
HFSC,全称Hierarchical Fair Service Curve,中文一般叫分层公平服务曲线。它是Linux内核里tc子系统支持的一种队列规则(qdisc),专门用来解决多业务共存时的带宽分配和延迟保障问题。和常见的HTB、CBQ相比,HFSC最大的特点是能同时保证带宽分配和延迟上限,而且对突发流量更友好。
这篇文章适合谁看?如果你正在做软路由、家庭网络优化、企业出口带宽管理,或者单纯想搞清楚Linux流量控制里HFSC到底怎么用、参数怎么算,那这篇内容应该能帮到你。我会从设计思路讲到实操配置,再到踩过的坑,尽量把每个参数背后的逻辑说清楚。
2. HFSC的核心设计思路拆解
2.1 它到底解决了什么问题
传统队列规则大致分两类。一类是公平队列,比如FQ、SFQ,它们把带宽尽量平均分给各个流,但不区分优先级,语音包和下载包一视同仁。另一类是优先级队列,比如PRIO,高优先级先走,但低优先级可能被饿死。
HTB和CBQ属于分层令牌桶,能分配带宽上限和保证带宽,但对延迟的保障是间接的——你只能通过限制带宽来间接控制排队延迟,没法直接说“这个类的包延迟不能超过20ms”。
HFSC的切入点就在这里。它引入了一个叫服务曲线的数学工具,允许你为每个类定义两条曲线:一条是实时曲线,用来保证延迟和即时带宽;另一条是链路共享曲线,用来做带宽的公平分配。两条曲线分开控制,互不干扰。
提示:HFSC的“曲线”不是让你画图,而是用几个参数描述一个分段线性的服务速率模型。理解这一点,后面参数就好懂了。
2.2 服务曲线到底怎么理解
我用一个生活化的类比来解释。假设你开了一家餐厅,有两类客人:一类是预约了时间的商务宴请,到点必须上菜;另一类是散客,随到随吃,但希望别等太久。
- 实时曲线对应商务宴请:我承诺你到店后m1毫秒内开始上菜,之后以d1的速率持续上菜,保证你d毫秒内吃完。
- 链路共享曲线对应散客:我不承诺具体时间,但保证在忙的时候,你至少能分到某个比例的出餐速度,不至于被饿死。
HFSC用sc参数描述实时曲线,用ls参数描述链路共享曲线。一个类可以只定义其中一条,也可以两条都定义。两条都定义时,实时曲线优先满足,剩余带宽再按链路共享曲线分配。
2.3 为什么选HFSC而不是HTB
我最初用的是HTB,配置简单,文档多。但遇到两个问题:一是延迟抖动,即使给语音类留了带宽,突发流量一来,排队延迟还是会涨;二是带宽利用率,为了保证延迟,不得不预留过多带宽,闲时浪费。
HFSC的实时曲线直接对延迟建模,只要参数算对,延迟是有上界的。而且它的链路共享曲线在实时曲线不忙时,可以把带宽借给其他类,利用率更高。
当然HFSC也不是没缺点。它的参数计算比HTB复杂,内核文档也偏学术,很多人第一次看sc和ls的参数直接懵。下面我就把参数计算拆开讲。
3. 核心参数解析与计算方法
3.1 sc实时曲线的三个参数
sc后面跟三个值:m1 d m2。含义是:
- m1:即时带宽,单位是字节/秒。表示数据包到达后,立即以这个速率发送。
- d:延迟上限,单位是毫秒。表示从数据包入队到开始发送,最长等待时间。
- m2:持续带宽,单位是字节/秒。表示在m1发送完之后,以这个速率继续发送。
这三个参数描述的服务曲线是:先以m1速率发送,持续d时间,然后切换到m2速率。如果m1足够大,数据包在d时间内就发完了,实际延迟就小于d。
计算m1有个常用公式:
m1 = 最大包长 / (d / 1000)比如最大包长1500字节,希望延迟不超过10ms,那m1 = 1500 / 0.01 = 150000字节/秒,约1.2Mbps。这个m1保证了一个最大包能在d时间内发完。
m2通常设成你希望这个类长期占用的带宽。比如语音类,m2可以设成实际码率的1.2倍左右。
注意:m1设得过大,会导致这个类在短时间内抢占大量带宽,影响其他类。m1设得过小,延迟就保证不了。一般按最大包长和延迟上限反推。
3.2 ls链路共享曲线的参数
ls后面跟一个值:m2。表示这个类在链路共享层面的服务速率。多个类竞争时,HFSC会按照各自的ls速率比例分配带宽。
如果只写ls m2,不写sc,那这个类就只有带宽分配,没有延迟保证。适合那些不敏感但需要公平分配的后台流量。
如果sc和ls都写,实时曲线先满足,剩余带宽再按ls分配。这里有个细节:实时曲线发送的数据量会从链路共享的配额里扣除,避免一个类两头占便宜。
3.3 上层类和叶子类的区别
HFSC是分层的,你可以建一棵树。上层类通常只定义ls,用来做带宽分组;叶子类定义sc和ls,挂实际的qdisc和filter。
举个例子,你可以先分三个上层类:语音、视频、后台。语音和视频上层类给较高的ls,后台给较低的ls。然后在语音下面建叶子类,给每个语音流定义sc保证延迟。
这种分层的好处是,你可以先按业务分组分配带宽,再在组内做精细控制。上层类的ls决定了组与组之间的带宽比例,叶子类的sc决定了组内每个流的延迟。
3.4 参数计算实例
假设出口带宽100Mbps,约12500000字节/秒。我要保障一路VoIP,码率100kbps,包长200字节,希望延迟不超过20ms。
计算m1:200 / 0.02 = 10000字节/秒,约80kbps。这个m1只保证一个包在20ms内发完,很小。
m2设成码率的1.5倍,150kbps,约18750字节/秒。
ls设成和m2一样,18750字节/秒。
这样配置后,VoIP类的包到达后,最多等20ms就开始发送,发送速率至少m2。实际测试下来,延迟稳定在5ms以内,因为m1虽然小,但包也小,很快就发完了。
4. 实操配置全流程
4.1 环境准备和工具确认
先确认内核支持HFSC。大部分发行版默认编译了sch_hfsc模块。用modprobe sch_hfsc加载,然后tc qdisc add dev eth0 root handle 1: hfsc default 30试试,不报错就说明支持。
工具方面,tc是iproute2包里的,版本最好新一点。老版本对HFSC的sc和ls参数解析可能有bug。我用的是iproute2 5.x,没遇到问题。
网卡要确认支持多队列的话,HFSC挂在root或者某个子队列上都行。如果是多队列网卡,建议先关掉多队列或者用mq qdisc包一层,避免HFSC和硬件队列打架。
4.2 建立根队列和上层类
先删掉现有规则,避免冲突:
tc qdisc del dev eth0 root 2>/dev/null然后建根HFSC:
tc qdisc add dev eth0 root handle 1: hfsc default 30default 30表示未分类的流量走classid 1:30。
接着建三个上层类:
tc class add dev eth0 parent 1: classid 1:1 hfsc ls rate 50mbit tc class add dev eth0 parent 1: classid 1:2 hfsc ls rate 30mbit tc class add dev eth0 parent 1: classid 1:3 hfsc ls rate 20mbit这里1:1给语音,1:2给视频,1:3给后台。ls速率加起来100Mbps,正好是出口带宽。实际可以留一点余量,比如加起来95Mbps,避免队列一直满。
4.3 建立叶子类和实时曲线
在语音类下面建叶子类,给VoIP流:
tc class add dev eth0 parent 1:1 classid 1:10 hfsc sc rate 150kbit umax 200b dmax 20ms ls rate 150kbit这里sc rate 150kbit是m2,umax 200b是最大包长,dmax 20ms是延迟上限。tc会自动计算m1。也可以手动写sc m1 10000 d 20 m2 18750,效果一样。
视频类下面建一个叶子类:
tc class add dev eth0 parent 1:2 classid 1:20 hfsc sc rate 2mbit umax 1500b dmax 50ms ls rate 2mbit后台类下面建一个叶子类,只给ls:
tc class add dev eth0 parent 1:3 classid 1:30 hfsc ls rate 20mbit4.4 挂载qdisc和filter
叶子类下面挂fq_codel或者sfq,做流级别的公平:
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel然后加filter,把流量分到对应类。假设VoIP走UDP 5060端口:
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 5060 0xffff flowid 1:10视频走TCP 1935端口:
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 1935 0xffff flowid 1:20剩下的走默认类1:30。
4.5 验证配置和观察效果
用tc -s class show dev eth0看每个类的统计。重点看sc和ls的发送字节数,以及是否有dropped。
用tc -s qdisc show dev eth0看队列的backlog和延迟。如果backlog一直很高,说明带宽不够或者参数太紧。
实际测试时,我一边跑iperf打满后台,一边用ping测VoIP类的延迟。配置前延迟在100ms以上波动,配置后稳定在3-5ms。效果还是很明显的。
提示:测试时最好用真实业务流量,不要只用ping。ping包太小,体现不出m1和m2的差异。
5. 常见问题与排查技巧
5.1 配置后延迟没改善
先检查filter有没有生效。用tc -s filter show dev eth0看匹配计数。如果计数为0,说明流量没进对类。
再检查sc参数。如果m1算出来太小,比如小于一个包的发送时间,延迟就保证不了。用tc class show dev eth0看tc实际用的m1是多少。
还有一种可能是上层类的ls太小,叶子类虽然sc很大,但被上层卡住了。HFSC是分层的,上层带宽不够,下层再大也没用。
5.2 带宽利用率上不去
HFSC默认比较保守,实时曲线预留的带宽即使空闲,也不一定借给其他类。可以调整ls的分配,让空闲类的带宽能被借用。
另外检查default类是不是设得太小。未分类流量都走default,如果default的ls很小,大部分流量会被限速。
5.3 某些类被饿死
如果某个类的ls设得很小,而其他类一直有流量,这个类可能长时间得不到发送机会。解决办法是给这个类也加一个小的sc,保证最低延迟和带宽。
或者调整上层类的ls比例,让每个类都有保底。
5.4 内核报错或tc命令失败
常见错误是RTNETLINK answers: Invalid argument,一般是参数格式不对。HFSC的sc和ls参数顺序有要求,sc必须写全m1 d m2或者用rate/umax/dmax简写。
还有可能是classid冲突,检查有没有重复的classid。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 延迟没改善 | filter未生效 | tc filter show看计数 | 检查filter优先级和匹配规则 |
| 延迟没改善 | m1太小 | tc class show看实际m1 | 按最大包长和dmax重算m1 |
| 带宽上不去 | 上层ls太小 | tc class show看上层速率 | 调整上层ls分配 |
| 某类被饿死 | 该类ls太小且无sc | 观察该类发送字节数 | 加sc或调大ls |
| 命令报错 | 参数格式错误 | 看tc报错信息 | 用rate/umax/dmax简写 |
| 统计不更新 | qdisc未挂载 | tc qdisc show | 检查parent是否正确 |
5.6 我踩过的几个坑
第一个坑是umax设错。我一开始把umax设成MTU 1500,但实际VoIP包只有200字节,结果m1算出来偏小,延迟反而大了。后来改成实际最大包长,就正常了。
第二个坑是上层类没给sc。我以为上层只做分组,不需要sc。但实际上如果上层只有ls,叶子类的sc在竞争时会被上层的ls限制。后来给上层也加了sc,虽然参数很宽松,但延迟就稳了。
第三个坑是fq_codel和HFSC的配合。fq_codel自己也有队列管理,如果它的limit设得太小,会在HFSC之前丢包。我把fq_codel的limit调大,让HFSC来做主要调度,效果更好。
6. 进阶技巧与场景扩展
6.1 动态调整参数
HFSC的参数是静态的,但你可以用脚本根据流量情况动态调整。比如晚上下载多,就把后台类的ls调小;白天办公多,就把视频类的ls调大。
用tc class change可以修改现有类的参数,不用重建整个树。但注意,修改sc参数可能会导致短暂的延迟抖动,最好在低峰期操作。
6.2 多网卡场景
如果出口有多个网卡,比如双WAN,HFSC可以挂在每个网卡上独立调度。也可以用IFB做入向流量控制,把入向流量重定向到IFB设备,再在IFB上挂HFSC。
入向控制比出向复杂,因为入向流量在到达时已经占用了带宽。IFB相当于一个虚拟网卡,把入向包重新注入,再排队发送。效果不如出向直接,但比不控强。
6.3 和CAKE的对比
CAKE是另一种流行的qdisc,集成了流公平、ACK过滤、带宽整形等功能,配置比HFSC简单很多。但CAKE的延迟保证是全局的,没法像HFSC那样给每个类单独定义延迟曲线。
如果你的需求是“整体延迟低”,CAKE更省事。如果需要“不同业务不同延迟保证”,HFSC更灵活。我一般是出口用HFSC做分层,叶子类挂CAKE或者fq_codel做流公平。
6.4 监控和告警
HFSC的统计可以通过tc -s看,但不够直观。我一般用vnstat或者iftop看整体流量,用tc -s class看每个类的发送量,再配合Prometheus的node_exporter采集tc统计,做趋势图。
如果某个类的dropped持续增长,说明带宽不够或者参数太紧,需要调整。如果backlog持续很高,说明队列积压,延迟会变大。
7. 我个人在实际操作中的体会
HFSC刚上手时确实容易被sc和ls的参数绕晕,但一旦理解了服务曲线的含义,配置起来其实很直接。我的经验是,先把业务分类,每类想清楚是要延迟保证还是带宽公平,然后分别用sc和ls去描述。
参数计算不用追求精确,留20%余量就行。实际网络流量是波动的,算得太紧反而容易出问题。测试时一定要用真实业务流量,ping只能测连通性,测不出调度效果。
最后分享一个小技巧:如果不想手算m1,可以直接用sc rate XXX umax YYY dmax ZZZ的简写,tc会自动算。umax填实际最大包长,dmax填你能接受的延迟上限,rate填长期带宽。这样配置起来快很多,效果也够用。
这个内容后续还可以这样扩展:结合eBPF做动态流量分类,或者用Python脚本自动根据流量类型调整HFSC参数。不过那是另一个话题了,先把基础配置跑通再说。