《Linux 高級路由與流量控制手冊(2012)》第九章

更多奇技淫巧歡迎訂閱博客:https://fuckcloudnative.io
譯者序
本文內(nèi)容來自 Linux Advanced Routing & Traffic Control HOWTO[1] (2012) , 這是一份在線文檔(小書),直譯為《Linux 高級路由與流量控制手冊》。本文翻譯第九章 Chapter 9. Queueing Disciplines for Bandwidth Management[2]。
這份文檔年代略久,但 qdisc 部分整體并未過時,并且是我目前看過的內(nèi)容最詳實、可讀 性最好的 tc qdisc 教程。
tc/qdisc 是 Cilium/eBPF 依賴的最重要的網(wǎng)絡(luò)基礎(chǔ)設(shè)施之一。
由于譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。
以下是譯文。
初次發(fā)現(xiàn) Linux 的這些功能時,我感到無比震驚。Linux 的帶寬管理能力足以媲美許多 高端、專用的帶寬管理系統(tǒng)(high-end dedicated bandwidth management systems)。
1. 隊列(Queues)和排隊規(guī)則(Queueing Disciplines)
通過對包進行排隊(queuing),我們可以決定數(shù)據(jù)的發(fā)送方式(the way in which data is SENT)。但理解下面這一點非常重要:我們只能對發(fā)送(transmit)的數(shù) 據(jù)進行整形(shape the data)。
互聯(lián)網(wǎng)的工作機制決定了接收端無法直接控制發(fā)送端的行為。這就像你家的 (實體?。┼]箱一樣:除非能聯(lián)系到所有人(告訴他們未經(jīng)同意不要寄信給你),否則 你無法控制別人寄多少東西過來。
但與實際生活不同的是,互聯(lián)網(wǎng)基于 TCP/IP 協(xié)議棧,這多少會帶來一些幫助。TCP/IP 無法提前知道兩臺主機之間的網(wǎng)絡(luò)帶寬,因此開始時它會以越來越快的速度發(fā)送數(shù)據(jù)(慢啟 動),直到開始出現(xiàn)丟包,這時它知道已經(jīng)沒有可用空間來存儲這些待發(fā)送的包了,因此就會 降低發(fā)送速度。TCP/IP 的實際工作過程比這個更智能一點,我們后面會再討論。
這就好比你留下一半的信件在實體郵箱里不取,期望別人知道這個狀況后會停止給你寄新的信件。但不幸的是,這種方式只對互聯(lián)網(wǎng)管用,對你的實體郵箱無效 :-)
如果內(nèi)網(wǎng)有一臺路由器,你希望限制某幾臺主機的下載速度,那你應(yīng)該找到發(fā)送數(shù)據(jù)到 這些主機的路由器內(nèi)部接口(inner interface of your router),然后在這些 路由器內(nèi)部接口上做 整流(traffic shaping,流量整形)。
此外,還要確保鏈路瓶頸(bottleneck of the link)也在你的控制范圍內(nèi)。例如,如果網(wǎng) 卡是 100Mbps,但路由器的鏈路帶寬是 256Kbps,那首先應(yīng)該確保不要發(fā)送過多數(shù)據(jù)給路由 器,因為它可能扛不住。否則,鏈路控制和帶寬整形的決定權(quán)就不在主 機側(cè)而到路由器側(cè)了。要達(dá)到限速目的,我們需要對 “發(fā)送隊列” 有完全的把控(”own the queue”),這里的 “發(fā)送隊列” 也就是整條鏈路上最慢的一段(slowest link in the chain)。幸運的是,大多數(shù)情況下這個條件都是能滿足的。
2. Simple, classless qdisc(簡單、不分類排隊規(guī)則)
如前所述,排隊規(guī)則(queueing disciplines)改變了數(shù)據(jù)的發(fā)送方式。
不分類(或稱無類別)排隊規(guī)則(classless queueing disciplines)可以對某個網(wǎng)絡(luò) 接口(interface)上的所有流量進行無差別整形。包括對數(shù)據(jù)進行:
重新調(diào)度(reschedule) 增加延遲(delay) 丟棄(drop)
與 classless qdisc 對應(yīng)的是 classful qdisc,即有類別(或稱分類別)排隊規(guī)則,后者是一個排隊規(guī)則中又包含其他 排隊規(guī)則(qdisc-containing-qdiscs)!先理解了 classless qdisc,才能理解 classful qdisc。
目前最常用的 classless qdisc 是 pfifo_fast qdisc,這也是默認(rèn)排隊規(guī)則。這也解釋了為什么這些高級功能如此健壯:本質(zhì)上來說,它們只不過是 “另一個隊列” 而 已(nothing more than ‘just another queue’)。
每種隊列都有自己的優(yōu)缺點。其中一些可能測試的并不全面。
2.1 pfifo_fast(先入先出隊列)
如名字所示,這是一個先入先出隊列(First In, First Out),因此對所有包都一視同仁。
pfifo_fast 有三個所謂的 “band”(可理解為三個隊列),編號分別為 0、1、2:
每個 band 上分別執(zhí)行 FIFO 規(guī)則。 但是,如果 band 0 有數(shù)據(jù),就不會處理 band 1;同理,band 1 有數(shù)據(jù)時, 不會去處理 band 2。 內(nèi)核會檢查數(shù)據(jù)包的 TOS 字段,將 “最小延遲” 的包放到 band 0。
不要將 pfifo_fast qdisc 與后面介紹的 PRIO qdisc 混淆,后者是 classful 的!雖然二者行為類似,但 pfifo_fast 是無類別的,這意味你無法通過 tc 命令向 pfifo_fast 內(nèi)添加另一個 qdisc。
2.1.1 參數(shù)與用法
pfifo_fast qdisc 默認(rèn)配置是寫死的(the hardwired default),因此無法更改。
下面介紹這份寫死的配置是什么樣的。
priomap
priomap 決定了如何將內(nèi)核設(shè)置的 packet priority 映射到 band。priority 位于包的 TOS 字段:
??????0?????1?????2?????3?????4?????5?????6?????7
???+-----+-----+-----+-----+-----+-----+-----+-----+
??|?????????????????|???????????????????????|?????|
??|???PRECEDENCE????|??????????TOS??????????|?MBZ?|
??|?????????????????|???????????????????????|?????|
???+-----+-----+-----+-----+-----+-----+-----+-----+
TOS 字段占用 4 個比特,各 bit 含義如下:
Binary?Decimcal??Meaning
-----------------------------------------
1000???8?????????Minimize?delay?(md)
0100???4?????????Maximize?throughput?(mt)
0010???2?????????Maximize?reliability?(mr)
0001???1?????????Minimize?monetary?cost?(mmc)
0000???0?????????Normal?Service
tcpdump -vv 會打印包的 TOS 字段,其中的 TOS 值對應(yīng)下面的第一列:
TOS?????Bits??Means????????????????????Linux?Priority????Band
------------------------------------------------------------
0x0?????0?????Normal?Service???????????0?Best?Effort?????1
0x2?????1?????Minimize?Monetary?Cost???1?Filler??????????2
0x4?????2?????Maximize?Reliability?????0?Best?Effort?????1
0x6?????3?????mmc+mr???????????????????0?Best?Effort?????1
0x8?????4?????Maximize?Throughput??????2?Bulk????????????2
0xa?????5?????mmc+mt???????????????????2?Bulk????????????2
0xc?????6?????mr+mt????????????????????2?Bulk????????????2
0xe?????7?????mmc+mr+mt????????????????2?Bulk????????????2
0x10????8?????Minimize?Delay???????????6?Interactive?????0
0x12????9?????mmc+md???????????????????6?Interactive?????0
0x14????10????mr+md????????????????????6?Interactive?????0
0x16????11????mmc+mr+md????????????????6?Interactive?????0
0x18????12????mt+md????????????????????4?Int.?Bulk???????1
0x1a????13????mmc+mt+md????????????????4?Int.?Bulk???????1
0x1c????14????mr+mt+md?????????????????4?Int.?Bulk???????1
0x1e????15????mmc+mr+mt+md?????????????4?Int.?Bulk???????1
第二列是對應(yīng)的十進制表示,第三列是對應(yīng)的含義。例如,15 表示這個包期望 Minimal Monetary Cost + Maximum Reliability + Maximum Throughput + Minimum Delay。我把這樣的包稱為 “荷蘭包”(a ‘Dutch Packet’。荷蘭人比較 節(jié)儉 / 摳門,譯注)。第四列是對應(yīng)到 Linux 內(nèi)核的優(yōu)先級;最后一列是 映射到的 band,從命令行輸出看,形式為:
1,?2,?2,?2,?1,?2,?0,?0?,?1,?1,?1,?1,?1,?1,?1,?1
例如,priority 4 會映射到 band 1。priomap 還能列出 priority > 7 的那些 不是由 TOS 映射、而是由其他方式設(shè)置的優(yōu)先級。例如,下表列出了應(yīng) 用(application)是如何設(shè)置它們的 TOS 字段的,來自 RFC 1349(更多信息可閱 讀全文),
TELNET???????????????????1000???????????(minimize?delay)
FTP?????Control??????????1000???????????(minimize?delay)
????????Data?????????????0100???????????(maximize?throughput)
TFTP?????????????????????1000???????????(minimize?delay)
SMTP????Command?phase????1000???????????(minimize?delay)
????????DATA?phase???????0100???????????(maximize?throughput)
DNS?????UDP?Query????????1000???????????(minimize?delay)
????????TCP?Query????????0000
????????Zone?Transfer????0100???????????(maximize?throughput)
NNTP?????????????????????0001???????????(minimize?monetary?cost)
ICMP????Errors???????????0000
????????Requests?????????0000?(mostly)
????????Responses?????????(mostly)
txqueuelen發(fā)送隊列長度,是一個網(wǎng)絡(luò)接口(interface)參數(shù),可以用
ifconfig命令設(shè)置。例 如,ifconfig eth0 txqueuelen 10。tc命令無法修改這個值。
2.2 TBF(Token Bucket Filter,令牌桶過濾器)
TBF 是一個簡單 qdisc,對于沒有超過預(yù)設(shè)速率的流量直接透傳,但也能容忍超過預(yù) 設(shè)速率的短時抖動(short bursts in excess of this rate)。
TBF 非常簡潔,對網(wǎng)絡(luò)和處理器都很友好(network- and processor friendly)。如果只是想實現(xiàn)接口限速,那 TBF 是第一選擇。
TBF 實現(xiàn)包括幾部分:
A buffer (bucket):bucket 最重要的參數(shù)是它的大小,即能容納的 token 數(shù)量。 Tokens:token 會以特定的速率(specific rate)填充 bucket 緩沖區(qū)。
當(dāng)一個包到來時,會從 bucket 中拿到一個 token,然后收集這個包的信息,最后從 bucket 中刪除這個 token。這個算法和 token flow、data flow 結(jié)合起來,會產(chǎn)生三種可能的場景:
數(shù)據(jù)速率 == token 速率:每個包都能找到一個對應(yīng)的 token,然后直接從隊列出去,沒有延時(delay)。數(shù)據(jù)速率 < token 速率:正常到來的數(shù)據(jù)都能及時發(fā)送出去,然后刪除一個 token。由于 token 速率大于數(shù)據(jù)速率,會產(chǎn)生 bucket 積壓,極端情況會將 bucket 占滿。如果數(shù)據(jù)速率突然高于 token 速率,就可以消耗這些積壓的 token 。因此積壓的 token 有一個額外好處:能夠容忍短時數(shù)據(jù)速率抖動(burst)。數(shù)據(jù)速率 > token 速率:token 很快就會用完,然后 TBF 會關(guān)閉(throttle )一會。這種 情況稱為 overlimit(超過限制)。如果包還是源源不斷地到來,就會產(chǎn)生丟包。
第三種非常重要,因為它使我們能夠對數(shù)據(jù)可用的帶寬進行整形(administratively shape the bandwidth)。
積壓的 token 使得超過限速的短時抖動數(shù)據(jù)仍然能發(fā)送,不會丟包,但持續(xù)的 overload 會導(dǎo)致數(shù)據(jù)不斷被 delay,然后被丟棄。
注意:在實際的實現(xiàn)中,token 是基于字節(jié)數(shù),而不是包數(shù)。
2.2.1 參數(shù)與用法
雖然通常情況下并不需要修改 TBF 配置參數(shù),但我們還是可以看一下有哪些。
首先,永遠(yuǎn)可用的(always available)參數(shù):
limit or latency
limit:因等待可用 token 而被放入隊列的字節(jié)數(shù)。 latency:每個包在 TBF 中停留的最長時間。隨后會基于 latency、bucket size、rate 和 peakrate(如果設(shè)置了)來計算 limit。 burst/buffer/maxburst
bucket 的大小,單位是字節(jié)。這是累積可用的 token 所支持的最大字節(jié)數(shù)( maximum amount of bytes that tokens can be available for instantaneously)???體來說,越大的整流速率(shaping rates)需要越大的緩沖區(qū)。要在 Intel 網(wǎng)卡 上實現(xiàn)
10Mbps整流,你至少需要10KB緩沖區(qū)。如果緩沖區(qū)太小,可能會丟包,因為 token 到來太快導(dǎo)致無法放入 bucket 中。
mpu
“零長度” 的包占用的并不是零帶寬(A zero-sized packet does not use zero bandwidth)。例如對于以太網(wǎng),任何一個包的字節(jié)數(shù)不會少于
64。Minimum Packet Unit(最小包單元)決定了一個包所使用的最小 token 量(the minimal token usage for a packet)。rate
速度旋鈕(speedknob)。
如果當(dāng)前 bucket 中有 token,并且沒有禁止 bucket 的 token 刪除動作,那默認(rèn)情況下 ,它會全速刪除。如果不期望這種行為,那可以設(shè)置下面幾個參數(shù):
peakrate
如前所述,默認(rèn)情況下,包到了之后只要有 token 就會被立即發(fā)送。這可能不是你期 望的,尤其當(dāng) bucket 很大的時候。
peakrate可指定 bucket 發(fā)送數(shù)據(jù)的最快速度。通常來說,這需要做的 就是:放行一個包 - 等待恰當(dāng)?shù)臅r長 - 放行下一個包。通過計算等待時長,最終實現(xiàn) 了 peakrate 效果。但實際中,由于 Unix 默認(rèn)的 10ms 定時器精讀限制,如果平均每個包
10K bits, 我們只能做到1Mbpspeakrate?。?code style="font-size: 14px;padding: 2px 5px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;background: rgb(249, 242, 244);border-radius: 4px;color: rgb(199, 37, 78);">10Kb/10ms = 1000Kbps = 1Mbps,譯注)。mtu/minburst
1Mbit/s的 peakrate 通常并不是很有用,因為實際中的帶寬要遠(yuǎn)大于此。實現(xiàn)更高 peakrate 的一種方式是:每個 timer tick 發(fā)送多個包,在效果上就好像我們創(chuàng)建 了第二個 bucket!這第二個 bucket 默認(rèn)只有一個包(defaults to a single packet),完全算不上一個 bucket。
計算最大可能的 peakrate 時,用 MTU 乘以 100(更準(zhǔn)確地說,乘以 HZ 數(shù),例如 Intel 上是 100,Alpha 上是 1024)。
2.2.2 示例配置
一個簡單但非常有用的配置:
$?tc?qdisc?add?dev?ppp0?root?tbf?rate?220kbit?latency?50ms?burst?1540
為什么說這個配置很有用呢?如果你有一個 queue 很大的網(wǎng)絡(luò)設(shè)備,例如 DSL modem 或 cable modem,而且用一個快速設(shè)備(例如以太網(wǎng)接口)連接到這個網(wǎng)絡(luò)設(shè)備,那你會發(fā)現(xiàn) 大文件上傳會嚴(yán)重影響實時交互。
這是因為上傳的數(shù)據(jù)會被緩存到 modem 的 queue 里,而且緩存的數(shù)據(jù)量很大(以提升吞吐) 。但這并不是期望的,你希望的是 queue 不要太大,這樣能保證交換式數(shù)據(jù)的實時性,因 此能在上傳數(shù)據(jù)過程中同時做其他事情。
上面的配置將發(fā)送速率降低到了 modem 不會對數(shù)據(jù)進行排隊緩存(queuing)的水平 —— 此時 queue 前移到了 Linux 中,而我們可以將它控制在一個合理的范圍內(nèi)。
這里的 220kbit 是上行鏈路的真實帶寬乘以一個系數(shù),如果你的 modem 足 夠快,可以將 burst 調(diào)大一些。
2.3 SFQ(Stochastic Fairness Queueing,隨機公平排隊)
隨機公平排隊(SFQ)是公平排隊算法族的一個簡單實現(xiàn)。相比其他算法,SFQ 精準(zhǔn)性要差 一些,但它所需的計算量也更少,而結(jié)果幾乎是完全公平的(almost perfectly fair)。
SFQ 中的核心是 conversion(會話)或 flow(流),大部分情況下都對應(yīng)一個 TCP session 或 UDP stream。每個 conversion 對應(yīng)一個 FIFO queue,然后將流量分到不 同 queue。發(fā)送數(shù)據(jù)時,按照 round robin 方式,每個 session 輪流發(fā)送。
這種機制會產(chǎn)生非常公平的結(jié)果,不會因為單個 conversion 太大而把其他 conversion 的帶寬都 擠占掉。SFQ 被稱為 “隨機的”(stochastic)是因為它其實并沒有為每個 session 分配一個 queue,而是用算法將流量哈希到了一組有限的 queue。
但這里會出現(xiàn)另一個問題:多個 session 會可能會哈希到同一個 bucket(哈希槽), 進而導(dǎo)致每個 session 的 quota 變小,達(dá)不到預(yù)期的整流帶寬(或速度)。為避免這個 問題過于明顯,SFQ 會不斷變換它使用的哈希算法,最終任何兩個會話沖突的持續(xù)時間 都不會很長,只會有幾秒鐘。
SFQ 只有在實際出向帶寬已經(jīng)非常飽和的情況下才有效,這一點非常重要!否則, Linux 機器上就不存在 queue,因此也就沒用效果。稍后會看到如何將 SFQ 與其他 qdisc 相結(jié)合來實現(xiàn)一般情況下的公平排隊。
說的更明確一點:沒用配套的整流配置的話,單純在(連接 modem 的)以太網(wǎng)接口上配 置 SFQ 是毫無意義的。
2.3.1 參數(shù)與用法
SFQ 大部分情況下默認(rèn)參數(shù)就夠了,
perturb
每隔多少
秒就重新配置哈希算法。如果這個參數(shù)沒設(shè),哈希算法就永遠(yuǎn)不會重新配置。建議顯式設(shè)置這個參數(shù),不要為空。10s可能是個不錯的選擇。quantum
在輪到下一個 queue 發(fā)送之前,當(dāng)前 queue 允許出隊(dequeue)的最大字節(jié)數(shù)。默認(rèn)是 一個 MTU。不建議設(shè)置為小于 MTU 的值。
limit
SFQ 能緩存的最大包數(shù)(超過這個閾值將導(dǎo)致丟包)。
2.3.2 示例配置
如果你有一個帶寬已經(jīng)飽和的網(wǎng)絡(luò)設(shè)備,例如一個電話調(diào)制解調(diào)器(phone modem),那下 面的配置有助于提高公平性:
$?tc?qdisc?add?dev?ppp0?root?sfq?perturb?10
$?tc?-s?-d?qdisc?ls
qdisc?sfq?800c:?dev?ppp0?quantum?1514b?limit?128p?flows?128/1024?perturb?10sec
?Sent?4812?bytes?62?pkts?(dropped?0,?overlimits?0)
解釋:
800c::自動分配的 handle number(句柄編號)limit 128p:最大緩存 128 個包flows 128/1024:這個 sfq 有 1024 個哈希槽(hash buckets),其中 128 個當(dāng)前有 數(shù)據(jù)待發(fā)送。perturb 10sec:每隔 10s 換一次哈希算法。
3. 使用建議:何時選擇哪種隊列?
總結(jié)起來,上面幾種都是簡單的 qdisc,通過重排序(reordering)、降速(slowing)或 丟包(dropping)來實現(xiàn)流量管理。
選擇使用哪種 qdisc 時,下面幾點可供參考。其中提到了幾種在第 14 章才會介紹到的 qdisc。
單純對出向流量限速(slow down outgoing traffic),推薦使用 TBF。如果是 針對大帶寬進行限速,需要將 bucket 調(diào)大。 如果帶寬已經(jīng)打滿,想確保帶寬沒有被任何單個 session 占據(jù),推薦使用 SFQ。 If you have a big backbone and know what you are doing, consider Random Early Drop (see Advanced chapter). 對(不再轉(zhuǎn)發(fā)的)入向流量整形,使用 Ingress Policer。順便說一句,入向整形稱為 ‘policing’,而不是 ‘shaping’。 對需要本機轉(zhuǎn)發(fā)的流量整形, 如果目的端是單個設(shè)備,那在目的端設(shè)備上使用 TBF。 如果目的端是多個設(shè)備(同一個入向設(shè)備分流到多個目的設(shè)備),使用 Ingress Policer。 如果你不需要整形,只是想看看網(wǎng)絡(luò)接口(interface)是否過載(so loaded that it has to queue), 使用 pfifoqueue(注意不是pfifo_fast)。pfifo內(nèi)部沒有 bands,但會記錄 backlog 的大小。最后 —— 你還可以嘗試 “社會學(xué)整形”(”social shaping”)。有時候一些問題是無法單純 用技術(shù)解決的。用戶會對技術(shù)限制充滿敵意。和氣地對別人說幾句好話,也許你需要的 帶寬就解決了。
4. 術(shù)語
為方便理解接下來更復(fù)雜的配置,我們需要先引入一些概念。由于這項技術(shù)本身比較復(fù)雜, 發(fā)展也還處在較為早期的階段,因此大家可能會用不同的術(shù)語描述同一樣?xùn)|西。
下列術(shù)語大體上來自 draft-ietf-diffserv-model-06.txt, An Informal Management Model for Diffserv Routers[3]。想進一步了解一些術(shù)語的定義,可參考這份文檔。
我們接下來會用到下列術(shù)語:
Queueing Discipline (qdisc,排隊規(guī)則)
管理設(shè)備隊列(queues of devices)的算法,可以是管理入向(incoing/ingress )隊列,也可以是管理出向隊列(outgoing/egress)。
root qdisc(根排隊規(guī)則)
attach 到網(wǎng)絡(luò)設(shè)備的那個 qdisc。
Classless qdisc(無類別排隊規(guī)則)
對所有包一視同仁,同等對待。
Classful qdisc(有類別排隊規(guī)則)
一個 classful qdisc 會包含多個類別(classes)。每個類別(class)可以進一步包 含其他 qdisc,可以是 classful qdisc,也可以是 classless qdisc。
嚴(yán)格按定義來說,
pfifo_fast屬于有類別排隊規(guī)則(classful),因為它內(nèi)部包 含了三個 band,而這些 band 實際上是 class。但從用戶配置的視角來說,它是 classless 的,因為這三個內(nèi)部 class 用戶是無法通過 tc 命令配置的。Classes(類別)
每個 classful qdisc 可能會包含幾個 class,這些都是 qdisc 內(nèi)部可見的。對于每 個 class,也是可以再向其添加其他 class 的。因此,一個 class 的 parent 可以 是一個 qdisc,也可以是另一個 class。
Leaf class 是沒有 child class 的 class。這種 class 中 attach 了一個 qdisc ,負(fù)責(zé)該 class 的數(shù)據(jù)發(fā)送。
創(chuàng)建一個 class 時會自動 attach 一個 fifo qdisc。而當(dāng)向這個 class 添加 child class 時,這個 fifo qdisc 會被自動刪除。對于 leaf class,可以用一個更合適的 qdisc 來替換掉這個 fifo qdisc。你甚至能用一個 classful qdisc 來替換這個 fifo qdisc,這樣就可以添加其他 class 了。
Classifier(分類器)
每個 classful qdisc 需要判斷每個包應(yīng)該放到哪個 class。這是通過分類器完成的。
Filter(過濾器)
分類過程(Classification)可以通過過濾器(filters)完成。過濾器包含許多的判 斷條件,匹配到條件之后就算 filter 匹配成功了。
Scheduling(調(diào)度)
在分類器的協(xié)助下,一個 qdisc 可以判斷某些包是不是要先于其他包發(fā)送出去,這 個過程稱為調(diào)度,可以通過例如前面提到的
pfifo_fastqdisc 完成。調(diào)度也被 稱為重排序(reordering),但后者容易引起混淆。Shaping(整形)
在包發(fā)送出去之前進行延遲處理,以達(dá)到預(yù)設(shè)的最大發(fā)送速率的過程。整形是在 egress 做的(前面提到了,ingress 方向的不叫 shaping,叫 policing,譯者注)。不嚴(yán)格地說,丟棄包來降低流量的過程有時也稱為整形。
Policing(執(zhí)行策略,決定是否丟棄包)
延遲或丟棄(delaying or dropping)包來達(dá)到預(yù)設(shè)帶寬的過程。在 Linux 上, policing 只能對包進行丟棄,不能延遲 —— 沒有 “入向隊列”(”ingress queue”)。
Work-Conserving qdisc(隨到隨發(fā) qdisc)
work-conserving qdisc 只要有包可發(fā)送就立即發(fā)送。換句話說,只要網(wǎng)卡處于可 發(fā)送狀態(tài)(對于 egress qdisc 來說),它永遠(yuǎn)不會延遲包的發(fā)送。
non-Work-Conserving qdisc(非隨到隨發(fā) qdisc)
某些 qdisc,例如 TBF,可能會延遲一段時間再將一個包發(fā)送出去,以達(dá)到期望的帶寬 。這意味著它們有時即使有能力發(fā)送,也不會發(fā)送。
有了以上概念,我們來看它們都是在哪里用到的。
????????????????Userspace?programs
?????????????????????^
?????????????????????|
?????+---------------+-----------------------------------------+
?????|???????????????Y?????????????????????????????????????????|
?????|????------->?IP?Stack????????????????????????????????????|
?????|???|??????????????|??????????????????????????????????????|
?????|???|??????????????Y??????????????????????????????????????|
?????|???|??????????????Y??????????????????????????????????????|
?????|???^??????????????|??????????????????????????????????????|
?????|???|??/?---------->?Forwarding?->????????????????????????|
?????|???^?/???????????????????????????|???????????????????????|
?????|???|/????????????????????????????Y???????????????????????|
?????|???|?????????????????????????????|???????????????????????|
?????|???^?????????????????????????????Y??????????/-qdisc1-\???|
?????|???|????????????????????????????Egress?????/--qdisc2--\??|
??--->->Ingress???????????????????????Classifier?---qdisc3----?|?->
?????|???Qdisc???????????????????????????????????\__qdisc4__/??|
?????|????????????????????????????????????????????\-qdiscN_/???|
?????|?????????????????????????????????????????????????????????|
?????+----------------------------------------------------------+
Thanks?to?Jamal?Hadi?Salim?for?this?ASCII?representation.
上圖中的框代表 Linux 內(nèi)核。最左側(cè)的箭頭表示流量從外部網(wǎng)絡(luò)進入主機。然后進入 Ingress Qdisc,這里會對包進行過濾(apply Filters),根據(jù)結(jié)果決定是否要丟棄這個 包。這個過程稱為 “Policing”。這個過程發(fā)生在內(nèi)核處理的很早階段,在穿過大部 分內(nèi)核基礎(chǔ)設(shè)施之前。因此在這里丟棄包是很高效的,不會消耗大量 CPU。
如果判斷允許這個包通過,那它的目的端可能是本機上的應(yīng)用(local application),這 種情況下它會進入內(nèi)核 IP 協(xié)議棧進行進一步處理,最后交給相應(yīng)的用戶態(tài)程序。另外,這 個包的目的地也可能是其他主機上的應(yīng)用,這種情況下就需要通過這臺機器 Egress Classifier 再發(fā)送出去。主機程序也可能會發(fā)送數(shù)據(jù),這種情況下也會通過 Egress Classifier 發(fā)送。
Egress Classifier 中會用到很多 qdisc。默認(rèn)情況下只有一個:pfifo_fast qdisc ,它永遠(yuǎn)會接收包,這稱為 “入隊”(”enqueueing”)。
此時包位于 qdisc 中了,等待內(nèi)核召喚,然后通過網(wǎng)絡(luò)接口(network interface)發(fā)送出去。這稱為 “出隊”(”dequeueing”)。
以上畫的是單網(wǎng)卡的情況。在多網(wǎng)卡的情況下,每個網(wǎng)卡都有自己的 ingress 和 egress hooks。
5. Classful qdisc(分類別排隊規(guī)則)
如果想對不同類型的流量做不同處理,那 classful qdisc 非常有用。其中一種是 CBQ( Class Based Queueing,基于類別的排隊),由于這種類型的 qdisc 使用太廣泛了,導(dǎo)致 大家將廣義上基于類別的排隊等同于 CBQ(identify queueing with classes solely with CBQ),但實際并非如此。
CBQ 只是其中最古老 —— 也是最復(fù)雜 —— 的一種。它的行為有時可能在你的意料之外。那些鐘愛 “sendmail effect” 的人可能感到震驚。
sendmail effect:對于任何復(fù)雜的技術(shù),沒有文檔的實現(xiàn)一定是最好的實現(xiàn)。
Any complex technology which doesn’t come with documentation must be the best available.
接下來介紹更多關(guān)于 CBQ 及其類似 qdisc 的信息。
5.1 Classful qdisc & class 中的 flow
當(dāng)流量進入一個 classful qdisc 時,該 qdisc 需要將其發(fā)送到內(nèi)部的某個 class —— 即 需要對這個包進行 “分類”。而要這個判斷過程,,實際上是查詢所謂的 “過濾器”( ‘filters’)。過濾器是在 qdisc 中被調(diào)用的,而不是其他地方,理解一點非常重要!
過濾器返回一個判決結(jié)果給 qdisc,qdisc 據(jù)此將包 enqueue 到合適的 class。每個 subclass 可能會進一步執(zhí)行其他 filters,以判斷是否需要進一步處理。如果沒有 其他過濾器,這個 class 將把包 enqueue 到它自帶的 qdisc。
除了能包含其他 qdisc,大部分 classful qdisc 還會執(zhí)行流量整形。這對包調(diào) 度(packet scheduling,例如,基于 SFQ)和速率控制(rate control)都非常有用。當(dāng)高速設(shè)備(例如,以太網(wǎng))連接到一個低速設(shè)備(例如一個調(diào)制解調(diào)器)時,會用到這個 功能。
如果只運行 SFQ,那將什么事情都不會發(fā)生,因為包會無延遲地進入和離開你的路由 器:網(wǎng)卡的發(fā)送速度要遠(yuǎn)大于真實的鏈路速度。瓶頸不在主機中,就無法用 “隊列”(queue )來調(diào)度這些流量。
5.2 qdisc 大家庭:roots, handles, siblings and parents
每個接口都有一個 egress “root qdisc”。默認(rèn)情況下,這個 root qdisc 就是前 面提到的 classless pfifo_fastqdisc。每個 qdisc 和 class 都會分配一個相應(yīng)的 handle(句柄),可以指定 handle 對 qdisc 進行配置。 每個接口可能還會有一個 ingress qdisc,用來對入向流量執(zhí)行策略(which polices traffic coming in)。
關(guān)于 handle:
每個 handle 由兩部分組成, 。: 按照慣例,root qdisc 的 handle 為 1:,這是1:0的簡寫。每個 qdisc 的 minor number 永遠(yuǎn)是 0。
關(guān)于 class:
每個 class 的 major number 必須與其 parent 一致。 major number 在一個 egress 或 ingress 內(nèi)必須唯一。 minor number 在一個 qdisc 或 class 內(nèi)必須唯一。
上面的解釋有點模糊,可對照 tc(8) man page[4] 的解釋:
所有 qdiscs、classes 和 filters 都有 ID,這些 ID 可以是指定的,也可以是自動分的。
ID 格式
major:minor,major和minor都是 16 進制數(shù)字,不超過 2 字節(jié)。兩個特殊值:
root的major和minor初始化全 1。省略未指定的部分將為全 0。 下面分別介紹以上三者的 ID 規(guī)范。
qdisc:qdisc 可能會有 children。
major部分:稱為handle,表示的 qdisc 的唯一性。minor部分:留給 class 的 namespace。class:class 依托在 qdisc 內(nèi),
major部分:繼承 class 所在的 qdisc 的major。minor部分:稱為 classid,在所在的 qdisc 內(nèi)唯一就行。filter:由三部分構(gòu)成,只有在使用 hashed filter hierarchy 時才會用到。
譯者注。
5.2.1 如何用過濾器(filters )對流量進行分類
綜上,一個典型的 handle 層級如下:
?????????????????????1:???root?qdisc
??????????????????????|
?????????????????????1:1????child?class
???????????????????/??|??\
??????????????????/???|???\
?????????????????/????|????\
?????????????????/????|????\
??????????????1:10??1:11??1:12???child?classes
???????????????|??????|?????|
???????????????|?????11:????|????leaf?class
???????????????|????????????|
???????????????10:?????????12:???qdisc
??????????????/???\???????/???\
???????????10:1??10:2???12:1??12:2???leaf?classes
但不要被這棵樹迷惑!不要以為內(nèi)核位于樹的頂點,網(wǎng)絡(luò)位于下面。包只會通過 root qdisc 入隊或出隊(get enqueued and dequeued),這也是內(nèi)核唯一與之交互的部分( the only thing the kernel talks to)。
一個包可能會被鏈?zhǔn)降胤诸惾缦拢╣et classified in a chain):
1:?->?1:1?->?1:12?->?12:?->?12:2
最后到達(dá) attach 到 class 12:2 的 qdisc 的隊列。在這個例子中,樹的每個 “節(jié)點”( node)上都 attach 了一個 filter,每個 filter 都會給出一個判斷結(jié)果,根據(jù)判斷結(jié)果 選擇一個合適的分支將包發(fā)送過去。這是常規(guī)的流程。但下面這種流程也是有可能的:
在這種情況下,attach 到 root qdisc 的 filter 決定直接將包發(fā)給 12:2。
9.5.2.2 包是如何從 qdisc 出隊(dequeue)然后交給硬件的
當(dāng)內(nèi)核決定從 qdisc dequeue packet 交給接口(interface)發(fā)送時,它會
向 root qdisc 1:發(fā)送一個 dequeue request1:會將這個請求轉(zhuǎn)發(fā)給1:1,后者會進一步向下傳遞,轉(zhuǎn)發(fā)給10:、11:、12:每個 qdisc 會查詢它們的 siblings,并嘗試在上面執(zhí)行 dequeue()方法。
在這個例子中,內(nèi)核需要遍歷整棵樹,因為只有 12:2 中有數(shù)據(jù)包。
簡單來說,嵌套類(nested classes)只會和它們的 parent qdiscs 通信,而永遠(yuǎn)不會直 接和接口交互。內(nèi)核只會調(diào)用 root qdisc 的 dequeue() 方法!
最終結(jié)果是,classes dequeue 的速度永遠(yuǎn)不會超過它們的 parents 允許的速度。而這正 是我們所期望的:這樣就能在內(nèi)層使用一個 SFQ 做純調(diào)度,它不用做任何整形的工作 ;然后在外層使用一個整形 qdisc 專門負(fù)責(zé)整形。
5.3 PRIO qdisc(優(yōu)先級排隊規(guī)則)
PRIO qdisc 實際上不會整形,只會根據(jù)設(shè)置的過濾器對流量進行分類。
可以將 PRIO qdisc 理解為 pfifo_fast qdisc 的升級版,它也有多個 band,但 每個 band 都是一個獨立的 class,而不是簡單的 FIFO。
當(dāng)一個包 enqueue 到 PRIO qdisc 之后,它會根據(jù)設(shè)置的 filters 選擇一個 class ,并將包送到這個 class。默認(rèn)情況下會創(chuàng)建三個 class。每個 class 默認(rèn)情況下都包含一 個純 FIFO qdisc,沒有其他內(nèi)部結(jié)構(gòu),但你可以用其他類型的 qdisc 替換掉 FIFO。
當(dāng)從 PRIO qdisc 取出(dequeue)一個包時,會先嘗試 :1。只有 lower bands/classes 沒有數(shù)據(jù)包可取時,才會嘗試 higher classes。
如果想基于 tc filters 而不僅僅是 TOS flags 做流量優(yōu)先級分類時,這個 qdisc 會非常 有用。還可以向這三個預(yù)置的 classes 添加額外的 qdisc,畢竟 pfifo_fast 只能提供簡 單的 FIFO qdisc。
由于 PRIO 沒有流量整形功能,因此針對 SFQ 的忠告也適用于這里:
如果你的物理鏈路已經(jīng)打滿了,可以用 PRIOqdisc (對流量進行分類),或者在外層嵌套一個 classful qdisc,后者負(fù)責(zé)流量整形。
用正式的術(shù)語來說,PRIO qdisc 是一個 work-conserving 調(diào)度器(隨到隨發(fā))。
5.3.1 參數(shù)與用法
下面幾個參數(shù)能被 tc 識別:
bands需要創(chuàng)建的 band 數(shù)量。這個每個 band 實際上都是一個 class。如果改變這個配置, 還需要同時修改
priomap參數(shù)。priomap如果沒有提供 tc filters 來指導(dǎo)如何對流量分類,那 PRIO qdisc 將依據(jù)
TC_PRIO優(yōu)先級來決定優(yōu)先級。這里的工作方式與pfifo_fastqdisc 是類似的, 更多細(xì)節(jié)可以參考前面的 `pfifo_fast` 小節(jié)[5]。
PRIO qdisc 里面的 band 都是 class,默認(rèn)情況下名字分別為 major:1、 major:2、 major:3, 因此如果你的 PRIO qdisc 是 12:,那 tc filter 送到 12:1 的流量就有更高的優(yōu)先級。
重復(fù)一遍:band 0 對應(yīng)的 minor number 是 1! band 1 對應(yīng)的 minor number 是 2 ,以此類推。
5.3.2 示例配置
我們將創(chuàng)建一棵如下所示的樹:
1:?->?12:2
高吞吐流量(Bulk traffic)將送到 30:,交互式流量(interactive traffic)將送到 20: 或 10:。
命令行:
??????????1:???root?qdisc
?????????/?|?\
????????/??|??\
???????/???|???\
?????1:1??1:2??1:3????classes
??????|????|????|
?????10:??20:??30:????qdiscs????qdiscs
?????sfq??tbf??sfq
band??0????1????2
然后查看創(chuàng)建出來的 qdisc:
$?tc?qdisc?add?dev?eth0?root?handle?1:?prio?#?This?*instantly*?creates?classes?1:1,?1:2,?1:3
$?tc?qdisc?add?dev?eth0?parent?1:1?handle?10:?sfq
$?tc?qdisc?add?dev?eth0?parent?1:2?handle?20:?tbf?rate?20kbit?buffer?1600?limit?3000
$?tc?qdisc?add?dev?eth0?parent?1:3?handle?30:?sfq
可以看到,band 0 已經(jīng)有了一些流量,而且在執(zhí)行這條命令的過程中,剛好又發(fā)送了一個 包!
現(xiàn)在我們來用 scp 命令傳輸一些數(shù)據(jù),它會自動設(shè)置 TOS flags:
#?tc?-s?qdisc?ls?dev?eth0
qdisc?sfq?30:?quantum?1514b
?Sent?0?bytes?0?pkts?(dropped?0,?overlimits?0)
?qdisc?tbf?20:?rate?20Kbit?burst?1599b?lat?667.6ms
?Sent?0?bytes?0?pkts?(dropped?0,?overlimits?0)
?qdisc?sfq?10:?quantum?1514b
?Sent?132?bytes?2?pkts?(dropped?0,?overlimits?0)
?qdisc?prio?1:?bands?3?priomap??1?2?2?2?1?2?0?0?1?1?1?1?1?1?1?1
?Sent?174?bytes?3?pkts?(dropped?0,?overlimits?0)
可以看到,所有的流量都進入了優(yōu)先級最低的 handle 30:,這正是我們期望的。為了驗 證交互式流量會進入優(yōu)先級更高的 bands,我們可以生成一些交互式流量。然后再來查看統(tǒng)計:
[email protected]:./
[email protected]'s?password:
tc???????????????????100%?|*****************************|???353?KB????00:00
$?tc?-s?qdisc?ls?dev?eth0
qdisc?sfq?30:?quantum?1514b
?Sent?384228?bytes?274?pkts?(dropped?0,?overlimits?0)
?qdisc?tbf?20:?rate?20Kbit?burst?1599b?lat?667.6ms
?Sent?2640?bytes?20?pkts?(dropped?0,?overlimits?0)
?qdisc?sfq?10:?quantum?1514b
?Sent?2230?bytes?31?pkts?(dropped?0,?overlimits?0)
?qdisc?prio?1:?bands?3?priomap??1?2?2?2?1?2?0?0?1?1?1?1?1?1?1?1
?Sent?389140?bytes?326?pkts?(dropped?0,?overlimits?0)
正如預(yù)期 —— 所有額外流量都進入了 10:,這是我們優(yōu)先級最高的 qdisc。handle 30: 的流量這次沒有增長,而剛才它吸收了所有的 scp 流量。
5.4 著名的 CBQ(Class Based Queueing)qdisc
前面提到,CBQ(Class Based Queueing,基于類的排隊) 是最復(fù)雜、最花哨、最少被理 解、也可能是最難用對的 qdisc。這并非因為它的發(fā)明者都是魔鬼或者能力不夠,而是 因為 CBQ 算法經(jīng)常不夠精確,而這是由于它與 Linux 的工作方式不是太匹配造成的。
除了是 classful qdisc 之外,CBQ 還是一個整流器(shaper),作為一個整流器來說, 其實它工作地并不是非常理想。理想的工作方式應(yīng)該是這樣的:如果想將一個 10Mbps 的連 接整形為 1Mbps,那這條鏈路應(yīng)該有 90% 的時間是空閑的。否則,我們就需要 throttle 來確保鏈路 90% 的時間是空閑的。
但空閑時間是很難測量的,CBQ 的方式是:用硬件層連續(xù)兩次請求數(shù)據(jù)的時間間隔( 毫秒)來推算。這可以用來近似估計鏈路的空閑狀態(tài)(how full or empty the link is)。
這種測量方式是非常間接的,因此結(jié)果并不總是很準(zhǔn)確。例如,接口的物理帶寬是 100Mbps ,但它可能永遠(yuǎn)打不到 100Mbps,而原因可能是網(wǎng)卡驅(qū)動寫的太爛。另一個例子,PCMCIA 網(wǎng) 卡永遠(yuǎn)打不到 100Mbps,這是由于其總線設(shè)計導(dǎo)致的 —— 因此,又回到那個問題:應(yīng)該 如何計算空閑時間?
當(dāng)考慮到非純物理網(wǎng)絡(luò)設(shè)備(not-quite-real network devices)時,例如 PPP over Ethernet 或 PPTP over TCP/IP,情況會更加糟糕。在這些場景中,有效帶 寬可能是由到用戶空間的管道(pipe)效率決定的 —— 這個值可能很高。
真正測量過的人會發(fā)現(xiàn),CBQ 并不是永遠(yuǎn)很精確,有時甚至完全偏離了真實值。
但在某些場景下,CBQ 能很好地滿足需求?;诒疚牡慕榻B,你應(yīng)該能恰當(dāng)?shù)嘏渲?CBQ,使 其在大部分情況下都工作良好。
5.4.1 CBQ shaping 詳解
如前所述,CBQ 的工作原理是:在發(fā)送包之前等待足夠長的時間,以將帶寬控制到期望 的閾值。為實現(xiàn)這個目標(biāo),它需要計算包之間的等待間隔。
系統(tǒng)在運行過程中會計算一個有效空閑時間(effective idletime):用指數(shù)加權(quán)移動平均( exponential weighted moving average,EWMA)來計算,這個算法假設(shè)包的優(yōu)先級大小 是指數(shù)變化的,越近的包(recent packets)優(yōu)先級越高。UNIX 的 loadaverage 指標(biāo) 就是用的這個算法。
平均空閑時間(avgidle)的定義:avgidle = 有效空閑時間(EWMA)- 計算出的空閑時間,
理想的未過載鏈路(loaded link): avgidle = 0,每經(jīng)過精確地計算出的時間間隔,就有一個數(shù)據(jù) 包到來(packets arrive exactly once every calculated interval)。過載鏈路(overloaded link): avgidle < 0,如果這個負(fù)值變得太大,CBQ 會關(guān)閉一 會,表示超出限制了(overlimit)。空閑鏈路(idle link): avgidle < 0,而且這個值可能會非常大,這可能會導(dǎo)致 累積幾個小時之后,算法允許無限大的帶寬(infinite bandwidths after a few hours of silence)。為防止這種情況發(fā)生,avgidle會設(shè)置一個上限(maxidle)。
如果發(fā)生 overlimit,理論上 CBQ 會嚴(yán)格等待 calculated_idletime,然后才發(fā)生下一個 包,然后再次 throttle 自己。但此時也要注意 minburst 參數(shù),見下面。
下面是整形(shaping)相關(guān)的配置參數(shù):
avpkt
平均包長,單位是字節(jié)。計算
maxidle時會用到。bandwidth
設(shè)備的物理帶寬,計算 idle time 時會用到。
cell
包長的增長步長。設(shè)備發(fā)送不同長度的包時,耗時可能是不一樣的,與包長有關(guān)。例如,一個 800Byte 和一個 806Byte 的包所花的發(fā)送時間可能是一樣的。默認(rèn)值通常是
8,必須是2的冪次。maxburst計算 maxidle 時用到,單位:包數(shù)(number of packets)。
當(dāng)
avgidle == maxidle時,可以并發(fā)發(fā)送maxburst個包,直到avgidle == 0。注意maxidle是無法直接設(shè)置的,只能通過這個參數(shù)間接設(shè)置。minburst
前面提到,overlimit 情況下 CBQ 要執(zhí)行 throttle。理想情況下是精確 throttle
calculated idel time,然后發(fā)送一個包。但對 Unix 內(nèi)核來說,通常很難調(diào)度10ms以下精度的事件,因此最好的方式就是 throttle 更長一段時間,然后一次發(fā) 送minburst個包,然后再睡眠minburst倍的時間。The time to wait is called the offtime。從較長時間跨度看,更大的
minburst會使得整形更加精確,但會導(dǎo)致在毫秒級別有更大的波動性。minidle
如果
avgidle < 0,那說明 overlimits,需要等到avgidle足夠大才能發(fā)送下一個包。為防止突然的 burst 打爆鏈路帶寬,當(dāng) avgidle 降到一個非常小的值之后,會 reset 到minidle。minidle的單位是負(fù)微秒(negative microseconds),因此10就表示 idle time 下限是-10us。mpu
最小包長(Minimum packet size)—— 需要這個參數(shù)是因為,即使是零字節(jié)的包在以太 網(wǎng)上傳輸時也會被填充到 64 字節(jié),因此總會有一個發(fā)送耗時。CBQ 需要這個參數(shù)來精確計算 idle time。
rate
期望的離開這個 qdisc 的流量速率(rate of traffic)—— 這就是 “速度旋鈕”(speed knob)!
在內(nèi)部,CBQ 有很多優(yōu)化。例如,在 dequeue 包時,已經(jīng)明確知道沒有數(shù)據(jù)的 class 都會跳過。Overlimit 的 class 會通過降低其有效優(yōu)先級(effective priority)的方式進行懲罰。所有這些都是很智能也很復(fù)雜的。
5.4.2 CBQ classful behaviour
除了整形之外,基于前面提到的 idletime 近似,CBQ 也能完成類似 PRIO queue 的功能 ,因為 class 可以有不同優(yōu)先級,優(yōu)先級高的總是限于優(yōu)先級低的被 poll。
每次硬件層請求一個數(shù)據(jù)包來發(fā)送時,都會開啟一個 weighted round robin (WRR)過程, 從優(yōu)先級最高的 class 開始(注意,優(yōu)先級越高對應(yīng)的 priority number 越?。?。
優(yōu)先級相同的 class 會作為一組,依次判斷它們是否有數(shù)據(jù)要發(fā)送。
下列參數(shù)控制 WRR 過程:
allot
當(dāng)外層 CBQ 收到網(wǎng)卡要發(fā)送一個數(shù)據(jù)包的請求后,它會按照
prio參數(shù)指定的 優(yōu)先級,嘗試依次 classes 內(nèi) attach 的所有內(nèi)部 qdiscs。每個 class 被輪到時, 它只能發(fā)送有限的一些數(shù)據(jù)。alloct就是這個數(shù)據(jù)量的一個基本單位。更多信息參見weight參數(shù)。prio
CBQ 也能執(zhí)行與
PRIO設(shè)備一樣的行為。內(nèi)部 classes 都有一個優(yōu)先級prio,高 優(yōu)先級的會先于低優(yōu)先級的被 poll。weight
這個參數(shù)用于 WRR 過程。每個 class 都有機會發(fā)送數(shù)據(jù)。如果要指定某個 class 使 用更大的帶寬,就調(diào)大其
weight。CBQ 會將一個 class 內(nèi)的所有權(quán)重歸一化,因此指定用整數(shù)還是小數(shù)都沒關(guān)系:重要 的是比例。大家的經(jīng)驗值是 “rate/10”,這個值看上去工作良好。歸一化后的
weight乘以allot,決定了每次能發(fā)送的數(shù)據(jù)量。
注意:CBQ 層級內(nèi)的所有 class 要共享同一個 major number!
5.4.3 決定 link sharing & borrowing 的 CBQ 參數(shù)
除了限制特定類型的流量,還能指定哪些 class 能從另外哪些 class 借容量(borrow capacity)或者說,借帶寬(對前一種 class 來說是借入,對后一種 class 來說就是借出)。
isolated/sharing配置了
isolated的 class 不會向 sibling classes 借出帶寬。如果多個應(yīng)用 之間在鏈路利用上是競爭或互斥的,彼此不想給對方帶寬,那可以用這個配置。tc工具還有一個sharing配置,作用于isolated相反。bounded/borrow也可以配置 class 為
bounded,這表示它不會向其他 siblings 借帶寬。tc工具還支持一個borrow選項,作用于bounded相反。
一個典型場景可能是:同一個鏈路上有兩個應(yīng)用,二者都是 isolated + bounded ,這表示二者都只會限制在它們各自分配的速率內(nèi),不會互相借帶寬。
有了這樣的 agency class(代理類),可能還會有其他允許交換帶寬的 class。
5.4.4 示例配置
???????????????1:???????????root?qdisc
???????????????|
??????????????1:1???????????child?class
?????????????/???\
????????????/?????\
??????????1:3?????1:4???????leaf?classes
???????????|???????|
??????????30:?????40:???????qdiscs
?????????(sfq)???(sfq)
這個例子將
webserver 限制為 5Mbps。SMTP 流量限制到 3Mbps。webserver + SMTP 總共不超過 6Mbps。物理網(wǎng)卡是 100Mbps。每個 class 之間可以互借帶寬。
命令:
$?tc?qdisc?add?dev?eth0?root?handle?1:0?cbq?bandwidth?100Mbit?????????\
??avpkt?1000?cell?8
$?tc?class?add?dev?eth0?parent?1:0?classid?1:1?cbq?bandwidth?100Mbit??\
??rate?6Mbit?weight?0.6Mbit?prio?8?allot?1514?cell?8?maxburst?20??????\
??avpkt?1000?bounded
上面兩條命令創(chuàng)建了 root qdisc 和相應(yīng)的 1:1 class。這個 1:1 class 是 bounded 類型,因此總帶寬不會超過設(shè)置的 6Mbps 限制。如前所述,CBQ 需要很多 速度選項(knobs,旋鈕式開關(guān))。但用到的參數(shù)前面都介紹過了。如果 HTB 來實現(xiàn)這個 功能,就會簡單很多。
$?tc?class?add?dev?eth0?parent?1:1?classid?1:3?cbq?bandwidth?100Mbit??\
??rate?5Mbit?weight?0.5Mbit?prio?5?allot?1514?cell?8?maxburst?20??????\
??avpkt?1000
$?tc?class?add?dev?eth0?parent?1:1?classid?1:4?cbq?bandwidth?100Mbit??\
??rate?3Mbit?weight?0.3Mbit?prio?5?allot?1514?cell?8?maxburst?20??????\
??avpkt?1000
上面兩個創(chuàng)建的是葉子節(jié)點(leaf classes)。注意其中是如何配置速率的。兩個 class 都沒有配置 bounded 參數(shù),但它們都連著到了 1:1 class,后者是有限速不超 過6Mbps 的。因此這兩個 leaf class 的總帶寬不會超過 6Mbps。另外需要注意, classid 中的 major number 必須要和 parent qdisc 中的 major number 一樣!
$?tc?qdisc?add?dev?eth0?parent?1:3?handle?30:?sfq
$?tc?qdisc?add?dev?eth0?parent?1:4?handle?40:?sfq
每個 class 默認(rèn)都有一個 FIFO qdisc。但我們將其替換成了 SFQ 這樣每條 flow 都能被 獨立、平等對待了。
$?tc?filter?add?dev?eth0?parent?1:0?protocol?ip?prio?1?u32?match?ip?\
??sport?80?0xffff?flowid?1:3
$?tc?filter?add?dev?eth0?parent?1:0?protocol?ip?prio?1?u32?match?ip?\
??sport?25?0xffff?flowid?1:4
這些過濾規(guī)則直接作用在 root qdisc 上,作用是將流量分類到下面正確的 qdisc。
注意其中是先用 tc class add 命令往 qdisc 內(nèi)添加 class,然后又用 tc qdisc add命令向 class 內(nèi)添加 qdisc。
你可能會好奇:**沒有匹配到以上兩條規(guī)則的流量怎么辦?**在本例中,它們會進入 1:0 接受處理,而這里是沒有限速的。
如果 SMTP+web 的總帶寬超過 6Mbps,那總帶寬將根據(jù)給定的權(quán)重參數(shù)分為兩部分, 5/8 給 webserver,3/8 給郵件服務(wù)。也可以說,在這個配置下,webserver 流量在 任何時候至少能獲得 5/8 * 6Mbps = 3.75Mbps 帶寬。
5.4.5 CBQ 其他參數(shù):split & defmap
如前所述,classful qdisc 需要調(diào)用過濾器(filters)來判斷應(yīng)該將包送到那個 class 里面。
除了調(diào)用過濾器,CBQ 還提供了其他選項:defmap 和 split。這一塊非常復(fù)雜,很難 理解,而且并不是非常重要。但考慮到這是目前已知的關(guān)于 defmap & split 最完善的文 檔,我將盡我可能來介紹一下。
As you will often want to filter on the Type of Service field only, a special syntax is provided. Whenever the CBQ needs to figure out where a packet needs to be enqueued, it checks if this node is a ‘split node’. If so, one of the sub-qdiscs has indicated that it wishes to receive all packets with a certain configured priority, as might be derived from the TOS field, or socket options set by applications.
The packets’ priority bits are and-ed with the defmap field to see if a match exists. In other words, this is a short-hand way of creating a very fast filter, which only matches certain priorities. A defmap of ff (hex) will match everything, a map of 0 nothing. A sample configuration may help make things clearer:
$?tc?qdisc?add?dev?eth1?root?handle?1:?cbq?bandwidth?10Mbit?allot?1514?\
??cell?8?avpkt?1000?mpu?64
$?tc?class?add?dev?eth1?parent?1:0?classid?1:1?cbq?bandwidth?10Mbit????\
??rate?10Mbit?allot?1514?cell?8?weight?1Mbit?prio?8?maxburst?20????????\
??avpkt?1000
Standard CBQ preamble. I never get used to the sheer amount of numbers required!
defmap 會用到 TC_PRIO bits,后者定義如下:
TC_PRIO..??????????Num??Corresponds?to?TOS
-------------------------------------------------
BESTEFFORT?????????0????Maximize?Reliablity
FILLER?????????????1????Minimize?Cost
BULK???????????????2????Maximize?Throughput?(0x8)
INTERACTIVE_BULK???4
INTERACTIVE????????6????Minimize?Delay?(0x10)
CONTROL????????????7
關(guān)于 TOS bits 如何映射到 priorities,參考 pfifo_fast[6] 小結(jié)。
現(xiàn)在看交互式和批量 classes:
$?tc?class?add?dev?eth1?parent?1:1?classid?1:2?cbq?bandwidth?10Mbit?????\
??rate?1Mbit?allot?1514?cell?8?weight?100Kbit?prio?3?maxburst?20????????\
??avpkt?1000?split?1:0?defmap?c0
$?tc?class?add?dev?eth1?parent?1:1?classid?1:3?cbq?bandwidth?10Mbit?????\
??rate?8Mbit?allot?1514?cell?8?weight?800Kbit?prio?7?maxburst?20????????\
??avpkt?1000?split?1:0?defmap?3f
“split qdisc” 是 1:0,表示在 1:0 進行判斷。C0 是 11000000 的二進制表示, 3F 是 00111111,因此這二者足以匹配任何東西。第一個 class 匹配第 6 & 7 位,因 此對應(yīng)的是 INTERACTIVE 和 CONTROL 流量。第二個 class 匹配的是其他所有流量。
節(jié)點 1:0 此時有一個如下的映射表:
priority??send?to
0?????????1:3
1?????????1:3
2?????????1:3
3?????????1:3
4?????????1:3
5?????????1:3
6?????????1:2
7?????????1:2
如果對此有進一步興趣,還可以通過 tc class change 命令傳遞一個 “change mask” 參 數(shù),精確地指定你期望的優(yōu)先級映射關(guān)系。例如,要將 best effort 流量轉(zhuǎn)到 1:2,執(zhí) 行命令:
$?tc?class?change?dev?eth1?classid?1:2?cbq?defmap?01/01
此時 1:0 處的 priority map 將變成下面這樣:
priority??send?to
0?????????1:2
1?????????1:3
2?????????1:3
3?????????1:3
4?????????1:3
5?????????1:3
6?????????1:2
7?????????1:2
FIXME: did not test ‘tc class change’, only looked at the source.
5.5 HTB(Hierarchical Token Bucket,層級令牌桶)
Martin Devera (devik) 意識到 CBQ 太復(fù)雜了,并且沒有針對很多典型的場景進 行優(yōu)化。因此他設(shè)計了 HTB,這種層級化的方式對下面這些場景很適用:
有一個固定總帶寬,想將其分割成幾個部分,分別用作不同目的 每個部分的帶寬是有保證的(guaranteed bandwidth) 還可以指定每個部分向其他部分借帶寬
HTB 的工作方式與 CBQ 類似,但不是借助于計算空閑時間(idle time)來實現(xiàn)整形。在內(nèi)部,它其實是一個 classful TBF(令牌桶過濾器)—— 這也是它叫層級令牌桶(HTB) 的原因。HTB 的參數(shù)并不多,在它的網(wǎng)站[7]文檔 里都已經(jīng)寫的很明確了。
即使發(fā)現(xiàn)你的 HTB 配置越來越復(fù)雜,這些配置還是能比較好地擴展(scales well)。而使 用 CBQ 的話,即使在簡單場景下配置就很復(fù)雜了!HTB3(HTB 的不同版本參見其官方文檔[8])現(xiàn)在 已經(jīng)并入正式內(nèi)核了(from 2.4.20-pre1 and 2.5.31 onwards)。但你可能還是要應(yīng)用一 個 tc 的 patch:HTB 內(nèi)核和用戶空間模塊的主版本號必須相同,否則 tc HTB 無法正 常工作。
如果使用的內(nèi)核版本已經(jīng)支持 HTB,那非常建議用用看。
5.5.1 示例配置
功能幾乎與 前面的 CBQ 示例配置[9] 一樣的 HTB 配置:
$?tc?qdisc?add?dev?eth0?root?handle?1:?htb?default?30
$?tc?class?add?dev?eth0?parent?1:?classid?1:1?htb?rate?6mbit?burst?15k
$?tc?class?add?dev?eth0?parent?1:1?classid?1:10?htb?rate?5mbit?burst?15k
$?tc?class?add?dev?eth0?parent?1:1?classid?1:20?htb?rate?3mbit?ceil?6mbit?burst?15k
$?tc?class?add?dev?eth0?parent?1:1?classid?1:30?htb?rate?1kbit?ceil?6mbit?burst?15k
HTB 作者推薦在這些 class 內(nèi)部使用 SFQ:
$?tc?qdisc?add?dev?eth0?parent?1:10?handle?10:?sfq?perturb?10
$?tc?qdisc?add?dev?eth0?parent?1:20?handle?20:?sfq?perturb?10
$?tc?qdisc?add?dev?eth0?parent?1:30?handle?30:?sfq?perturb?10
最后,將流量導(dǎo)向這些 class 的過濾器(filters):
$?U32="tc?filter?add?dev?eth0?protocol?ip?parent?1:0?prio?1?u32"
$?$U32?match?ip?dport?80?0xffff?flowid?1:10
$?$U32?match?ip?sport?25?0xffff?flowid?1:20
這就是 HTB 的配置了 —— 沒有看上去不知道是什么意思的數(shù)字(unsightly unexplained numbers),沒有查文檔都查不到的參數(shù)。HTB 顯然看上去非常棒 —— 如果 10: 和 20: 都獲得了保證的帶寬(guaranteed bandwidth),并且總帶寬中還有很多剩余,它們還可以 5:3 的比例借用額外帶寬,正如 我們所期望的。
未分類的流量(unclassified traffic)會進入 30:,這個 band 只有很小的帶寬,但能 夠從剩余的可用帶寬中借帶寬來用。由于我們用了的 SFQ(隨機公平調(diào)度),我們還獲得了 公平調(diào)度而沒有增加額外成本!
6. 用過濾器對流量進行分類
每次要判斷將包送到哪個 class 進行處理時,都會調(diào)用所謂的 “classifier chain”(分類 器鏈)。這個 chain 由 attach 到 classful qdisc 的所有 filter 構(gòu)成。
還是前面那個例子(包最終到 12:2):
????????????????????root?1:
??????????????????????|
????????????????????_1:1_
???????????????????/??|??\
??????????????????/???|???\
?????????????????/????|????\
???????????????10:???11:???12:
??????????????/???\???????/???\
???????????10:1??10:2???12:1??12:2
當(dāng) enqueue 一個包時,在每一個分叉的地方都需要查詢相關(guān)的過濾規(guī)則。
一種典型的配置是:
在 1:1配置一個 filter,將包送到12:。在 12:配置一個 filter,將包送到12:2。
另外一種配置:將兩個 filters 都配置在 1:1,但將更精確的 filter 下放到更下面 的位置有助于提升性能。
需要注意的是,包是無法向上過濾的(filter a packet ‘upwards’)。另外,使用 HTB 時,所有的 filters 必須 attach 到 root!
包只能向下 enqueue!當(dāng) dequeue 時,它們會重新上來,到達(dá)要發(fā)送它的網(wǎng)絡(luò)接口。包并不是一路向下,最后從葉子節(jié)點到達(dá)網(wǎng)卡的!
6.1 一些簡單的流量過濾(filtering)示例
正如在 Classifier 章節(jié)中介紹的,匹配語法非常復(fù)雜,但功能強大,可以對幾乎任 何東西進行匹配。
這里先從簡單的開始。假設(shè)有一個名為 10: 的 PRIO qdisc,其中包含了三個 class,我們想將所有端口 22 的流量都導(dǎo)向優(yōu)先級最高的 band,那 filters 將如下:
$?tc?filter?add?dev?eth0?protocol?ip?parent?10:?prio?1?u32?match?\
??ip?dport?22?0xffff?flowid?10:1
$?tc?filter?add?dev?eth0?protocol?ip?parent?10:?prio?1?u32?match?\
??ip?sport?80?0xffff?flowid?10:1
$?tc?filter?add?dev?eth0?protocol?ip?parent?10:?prio?2?flowid?10:2
這幾行命令是什么意思?第一條命令:
tc filter add dev eth0:attach 到 eth0 設(shè)備。parent 10::父設(shè)備是10:。prio 1:優(yōu)先級為 1(數(shù)字越小,優(yōu)先級越高)。u32 match ip dport 22 0xfffffilter:精確匹配 dst port 22,并將匹配的包發(fā)送到 band10:1。
第二條命令與第一條類似,不過匹配的源端口 80。第三條命令表示所有未匹配到上面的包 ,都發(fā)送到優(yōu)先級次高的 band 10:2。
上面的命令中需要指定網(wǎng)絡(luò)接口(interface),因為每個接口都有自己獨立的 handle 空間。
要精確匹配單個 IP 地址,使用下面的命令:
$?tc?filter?add?dev?eth0?parent?10:0?protocol?ip?prio?1?u32?\
??match?ip?dst?4.3.2.1/32?flowid?10:1
$?tc?filter?add?dev?eth0?parent?10:0?protocol?ip?prio?1?u32?\
??match?ip?src?1.2.3.4/32?flowid?10:1
$?tc?filter?add?dev?eth0?protocol?ip?parent?10:?prio?2??????\
??flowid?10:2
這會將 dst_ip=4.3.2.1 或 src_ip=1.2.3.4 的流量送到優(yōu)先級最高的隊列,其他流量 送到優(yōu)先級次高的隊列。
還可以將多個 match 級聯(lián)起來,同時匹配源 IP 和 port:
$?tc?filter?add?dev?eth0?parent?10:0?protocol?ip?prio?1?u32?match?ip?src?4.3.2.1/32?\
??match?ip?sport?80?0xffff?flowid?10:1
6.2 常用 filtering 命令
大部分整形的命令都會以這樣的命令開頭:
$?tc?filter?add?dev?eth0?parent?1:0?protocol?ip?prio?1?u32?..
這種是所謂的 u32 匹配,特點是能匹配包的任何部分:
匹配源 / 目的 IP 地址
match ip src 1.2.3.0/24match ip dst 4.3.2.0/24匹配單個 IP:指定掩碼 /32,或者直接省略掩碼部分匹配源 / 目的端口,任何 IP 協(xié)議
match ip sport 80 0xffffmatch ip dport 80 0xffff匹配 ip protocol(tcp, udp, icmp, gre, ipsec)
使用
/etc/protocols里面的協(xié)議號,例如,ICMP 是1:match ip protocol 1 0xff。匹配 fwmark
可以用
ipchains/iptables等工具對包打標(biāo)(mark),這些 mark 在不同接口 之間路由時是不會丟失的(survive routing across interfaces)。這非常有用,例 如,實現(xiàn) “只對從 eth0 進入 eth1 的流量進行整形” 的功能。語法:$?tc?filter?add?dev?eth1?protocol?ip?parent?1:0?prio?1?handle?6?fw?flowid?1:1注意這里用的已經(jīng)不是
u32匹配了!對包打標(biāo)(mark):
$?iptables?-A?PREROUTING?-t?mangle?-i?eth0?-j?MARK?--set-mark?6上面的
6只是本例隨便設(shè)置的一個數(shù)字,可以是任意值。如果不想理解完整的 tc filter 語法,那可以選擇用 iptables 來打標(biāo),根據(jù) fwmark 完成分類功能。
iptables 還可以打印統(tǒng)計信息,有助于判斷你設(shè)置的規(guī)則是否生效。下面的命令會打 印
mangle表內(nèi)所有的 mark 規(guī)則,已經(jīng)每個規(guī)則已經(jīng)匹配到多少包和字節(jié)數(shù):$?iptables?-L?-t?mangle?-n?-v匹配 TOS 字段
選擇交互式、最小延遲(interactive, minimum delay)流量:
$?tc?filter?add?dev?ppp0?parent?1:0?protocol?ip?prio?10?u32?\
??match?ip?tos?0x10?0xff?flowid?1:4高吞吐流量(bulk traffic)對應(yīng)的過濾條件是
0x08 0xff。
更多過濾相關(guān)的命令(filtering commands),見 Advanced Filters 章節(jié)。
7. IMQ(Intermediate queueing device,中轉(zhuǎn)排隊設(shè)備)
IMQ 并不是一種 qdisc,但其使用是與 qdisc 緊密關(guān)聯(lián)的。
在 Linux 中,所有 qdisc 都是 attach 到網(wǎng)絡(luò)設(shè)備上的,所有 enqueue 到設(shè)備的東西都 是先 enqueue 到設(shè)備 qdisc 上。從概念上來說,這會存在兩個限制:
只有出方向(egress)能做整形:入方向的 qdisc 實際上也是有的,但與 classful qdiscs 相比,其發(fā)揮空間非常有限。 任何一個 qdisc 只能看到一個接口(interface)的流量,沒有全局限流功能(global limitations can’t be placed)。
IMQ 就是用來解決以上兩點限制的。簡單來說,你可以將選中的任何東西放到 qdisc 里面。打了標(biāo)的包在經(jīng)過 netfilter NF_IP_PRE_ROUTING 和 NF_IP_POST_ROUTING hook 點時會被捕獲,送到 IMQ 設(shè)備上 attach 的 qdisc。
因此對外部進來的包先打上標(biāo)記(mark),就能實現(xiàn)入向整型(ingress shaping), ;將接口們作為 classes(treat interfaces as classes),就能設(shè)置全局限速。
你還可以做很多其他事情,例如將 http 流量放到一個 qdisc,將新連接請求放到另一個 qdisc,等等。
7.1 示例配置
首先能想到的例子就是用入向整形(ingress shaping)給自己一個受保證的高帶寬 ;)
配置如下:
$?tc?qdisc?add?dev?imq0?root?handle?1:?htb?default?20
$?tc?class?add?dev?imq0?parent?1:?classid?1:1?htb?rate?2mbit?burst?15k
$?tc?class?add?dev?imq0?parent?1:1?classid?1:10?htb?rate?1mbit
$?tc?class?add?dev?imq0?parent?1:1?classid?1:20?htb?rate?1mbit
$?tc?qdisc?add?dev?imq0?parent?1:10?handle?10:?pfifo
$?tc?qdisc?add?dev?imq0?parent?1:20?handle?20:?sfq
$?tc?filter?add?dev?imq0?parent?10:0?protocol?ip?prio?1?u32?match?\
??ip?dst?10.0.0.230/32?flowid?1:10
這個例子用的是 u32 做分類,用其他分類器也行。
接下來,需要選中流量,給它們打上標(biāo)記,以便能被正確送到 imq0 設(shè)備:
$?iptables?-t?mangle?-A?PREROUTING?-i?eth0?-j?IMQ?--todev?0
$?ip?link?set?imq0?up
在 mangle 表內(nèi)的 PREROUTING 和 POSTROUTING chain,IMQ 都是有效的 target。語法:
IMQ?[?--todev?n?]?n?:?number?of?imq?device
注意,流量并不是在命中 target 的時候放入 imq 隊列的,而是在更后面一點(not enqueued when the target is hit but afterwards)。流量進入 imq 設(shè)備的精確位置與 流量方向(in/out)有關(guān)。下面這些是預(yù)定義的 netfilter hooks,iptables 會用到它們:
enum?nf_ip_hook_priorities?{
????????NF_IP_PRI_FIRST?=?INT_MIN,
????????NF_IP_PRI_CONNTRACK?=?-200,
????????NF_IP_PRI_MANGLE?=?-150,
????????NF_IP_PRI_NAT_DST?=?-100,
????????NF_IP_PRI_FILTER?=?0,
????????NF_IP_PRI_NAT_SRC?=?100,
????????NF_IP_PRI_LAST?=?INT_MAX,
};
對于 ingress 流量,imq 會將自己注冊為 NF_IP_PRI_MANGLE + 1優(yōu)先級,這意味包 經(jīng)過 PREROUTING chain 之后就會直接進入 imq 設(shè)備后。對于 egress 流量,imq 使用 NF_IP_PRI_LAST,which honours the fact that packets dropped by the filter table won’t occupy bandwidth.
IMQ patch 及其更多信息見 ~IMQ 網(wǎng)站~[10](原始 鏈接已失效,可移步參考這篇[11],譯者注)。
參考資料
Linux Advanced Routing & Traffic Control HOWTO: https://lartc.org/howto/index.html
[2]Chapter 9. Queueing Disciplines for Bandwidth Management: https://lartc.org/howto/lartc.qdisc.html
[3]draft-ietf-diffserv-model-06.txt, An Informal Management Model for Diffserv Routers: http://www.ietf.org/internet-drafts/draft-ietf-diffserv-model-06.txt
[4]tc(8) man page: https://man7.org/linux/man-pages/man8/tc.8.html
[5]pfifo_fast 小節(jié): #pfifo_fast
pfifo_fast: #pfifo_fast
[7]網(wǎng)站: http://luxik.cdi.cz/~devik/qos/htb/
[8]官方文檔: http://luxik.cdi.cz/~devik/qos/htb/
[9]前面的 CBQ 示例配置: #cbq_sample_config
[10]~IMQ 網(wǎng)站~: http://luxik.cdi.cz/~patrick/imq/
[11]這篇: https://github.com/imq/linuximq
原文鏈接:http://arthurchiao.art/blog/lartc-qdisc-zh/


你可能還喜歡
點擊下方圖片即可閱讀

云原生是一種信仰??

掃碼關(guān)注公眾號
后臺回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點擊?"閱讀原文"?獲取更好的閱讀體驗!
??給個「在看」,是對我最大的支持??

