項目中用tc,htb做流控期間,研究了htb(分層令牌桶)算法的實現.覺得這種思想在類似與有消費優先級的生產者消費者場景中也很適用.
該算法過於復雜,礙於嘴拙遂在標題中加了簡析,只介紹核心思想和關鍵代碼的實現.
一個栗子:
tc qdisc add dev eth0 root handle 1: htb tc class add dev eth0 parent 1: classid 1:1 htb rate 100mibps tc class add dev eth0 parent 1:1 classid 1:10 htb rate 30mibps ceil 80mibps prio 0 burst 10kbit cburst 20kbit quantum 30000 tc class add dev eth0 parent 1:1 classid 1:20 htb rate 20mibps ceil 50mibps prio 1 burst 10kbit cburst 20kbit quantum 20000 tc class add dev eth0 parent 1:10 classid 1:101 htb rate 10mibps ceil 80mibps prio 1 burst 10kbit cburst 20kbit quantum 10000 tc class add dev eth0 parent 1:10 classid 1:102 htb rate 5mibps ceil 40mibps prio 0 burst 10kbit cburst 20kbit quantum 5000
圖1
首先創建了一個htb隊列,在隊列中創建了5個類,他們之間的關系可以表示成上圖這樣的一棵樹.一些關鍵參數也標出來了,后面會解釋.
下面用iptables將流量分類,根據目的ip,將相應的流量分類到1:20 1:101 1:102三個類隊列中.這里有兩點要說明:
1.分類流量又很多方式,例如cgroup iptables tc filter等工具.但本文中重心在於流量如何出,所以網絡包如何進入相應的類隊列就省略了.
2.流量只能緩存在樹的葉節點(leaf class),其他類節點(inner class)是不能緩存流量的.但innerclass對於不同子類能共享帶寬起到重要作用.
iptables -t mangle -A OUTPUT -d 192.168.1.2 -j CLASSIFY --set-class 1:20 iptables -t mangle -A OUTPUT -d 192.168.1.3 -j CLASSIFY --set-class 1:101 iptables -t mangle -A OUTPUT -d 192.168.1.4 -j CLASSIFY --set-class 1:10
htb雖然為每個類設定了rate,但並不是說每個類只能以設定的rate出包.當網卡比較空閑時,leafclass是可以以高於rate的速率出包的.但不能高於ceil.一句話來說就是閑時共享,忙時按照比例(這個比例是rate,quantum共同決定的)分配帶寬.網絡包只能入/出leafclass,innerclass對於不同子類能共享帶寬起作用.
圖1中還為每個leafclass標注出了priority屬性.htb對類支持0-7 8個優先級,0優先級最高.優先級越高的類可以優先出流量.
原理介紹
某個時刻每個類可以處於三種狀態中的一種:
CAN_SEND(令牌充足的,發送的網絡包小於rate,例圖中用綠色表示)
MAY_BORROW(沒有令牌,但可借用.發送的網絡包大於rate小於ceil,例圖中用黃色表示)
CANT_SEND(沒有令牌不可借用,發送的網絡包大於ceil,例圖中用紅色表示)
htb是如何決策哪個類出包的?
1.htb算法從類樹的底部開始往上找CAN_SEND狀態的class.如果找到某一層有CAN_SEND狀態的類則停止.
2.如果該層中有多個class處於CAN_SEND狀態則選取優先級最高(priority最小)的class.如果最高優先級還是有多個class,那就在這些類中輪訓處理.每個類每發送自己的quantum個字節后,輪到下一個類發送.
3.上面有講到只有leafclass才可以緩存網絡包,innerclass是沒有網絡包的.如果步驟1,2最終選到了innerclass怎么處理?既然是innerclass,肯定有自己的subclass.innerclass會順着樹往下找,找到一個子孫leafclass.並且該leafclass處於MAY_BORROW狀態,將自己富余的令牌借給該leafclass讓其出包.同樣的道理,可能會有多個子孫leafclass處於MAY_BORROW狀態,這里的處理跟步驟2是一樣的.
多個子類共享父類帶寬也就體現在這里了.假設父類富余了10MB, 子類1的quantum為30000,子類2的quantum為20000.那么父類幫子類1發送30000byte,再幫子類2發送20000byte.依次循環.最終效果就是子類1借到了6MB,子類2借到了4MB.因此上文說,當子類之間共享帶寬時,rate/quantum共同決定它們分得的帶寬.rate處於CAN_SEND狀態時能發送多少字節,quantum決定處於MAY_BORROW狀態時可借用令牌發送多少字節.
場景舉例
1.假設某一時刻,1:101, 1:102都有網絡包堆積,並且都處於CAN_SEND狀態.1:20因為沒有流量因此可視為沒有這個節點.圖2所示:
圖2
按照前面說的,1:101, 1:102這兩個類都屬於同一層,優先級相同.htb輪訓它們出包.1:101每輪發送20000byte,1:102每輪發送5000byte.
某一時刻1:101發送的流量超過了其rate值(10MB),但未超過其ceil值(20MB).因此1:101狀態轉變成MAY_BORROW狀態.如圖3:
圖3
此時最底層只有1:102這個類是CAN_SEND,只能全力出這個類的包了.當1:102發送的流量超過其rate值(5MB)時,其狀態也變為MAY_BORROW.如圖4:
圖4
這時最底層已經沒有CAN_SEND狀態的類了.網上找到1:10.1:10的兩個子類此時都處於MAY_BORROW狀態,因此1:10又輪訓着發送1:101,1:102的包.每輪還是發送它們對應的quantum值. 很快1:101發送的流量達到其ceil值(20MB)了,此時1:101狀態變成CANT_SEND.如圖5:
圖5
此時1:10只能全力發送1:102的包.直到達到其ceil值(30MB).此時1:102變為CANT_SEND,1:101和1:102累計發送了50MB數據,達到了其父類1:10的rate值,因此1:10變為MAY_BORROW.如圖6:
圖6
核心代碼
幾個核心數據結構,這里就不貼了:
struct Qdisc: tc隊列
struct htb_class: htb類
網絡包從協議棧出來經驅動送往網卡之前,會做一個入隊/出隊操作.這也就是tc的入口.
當我們使用htb算法時,出包回調為htb_dequeue,htb_dequeue返回一個skb給網卡發送.
static struct sk_buff *htb_dequeue(struct Qdisc *sch) { ... for(level = 0; level < TC_HTB_MAXDEPTH; level++) { // 逐層找.這里level是反的,0層表示最底層. /* common case optimization - skip event handler quickly */ int m; ... m = ~q->row_mask[level]; while(m !=(int)(-1)) { // 同一層取優先級高的 int prio = ffz(m); m |= 1 << prio; skb = htb_dequeue_tree(q, prio, level); //出包 if(likely(skb != NULL)) { sch->q.qlen--; sch->flags &= ~TCQ_F_THROTTLED; goto fin; } } } sch->qstats.overlimits++; ... fin: return skb; }
htb_dequeue找到對應的層數和優先級之后調用htb_dequeue_tree,只列出htb_dequeue_tree中核心代碼:
static struct sk_buff *htb_dequeue_tree(struct htb_sched *q, int prio, int level) { struct sk_buff *skb = NULL; struct htb_class *cl, *start; ... cl = htb_lookup_leaf(q->row[level] + prio,prio, q->ptr[level] + prio, // 找到該level 該priority下的一個leafclass q->last_ptr_id[level] + prio); skb = cl->un.leaf.q->dequeue(cl->un.leaf.q); // 出包 if(likely(skb != NULL)) { cl->un.leaf.deficit[level] -= qdisc_pkt_len(skb); //deficit[level] 扣掉該包的byte數 if(cl->un.leaf.deficit[level] < 0) { //當deficit[level]<0時說明該類已經發送了quantum.需要發送下一個類了. cl->un.leaf.deficit[level] += cl->quantum; htb_next_rb_node((level ? cl->parent->un.inner.ptr : q-> ptr[0]) + prio); } htb_charge_class(q, cl, level, skb); // 更新令牌. } return skb; }
因為不確定傳進來的level是不是最底層,因此調用htb_lookup_leaf保證得到的class是leafclass.其中參數(q->ptr[level] + prio)記錄該層該優先級當前應該發那個leafclass.
skb = cl->un.leaf.q->dequeue(cl->un.leaf.q) 出包.
當出隊一個數據包時,類對應的deficit[level]扣減包的byte數,當deficit[level]<0時說明該類已經發送了quantum.於是雖然再次給deficit[level]加了quantum,
但是htb_next_rb_node((level ? cl->parent->un.inner.ptr : q->ptr[0]) + prio)已經將該層該優先級的出包類指針指向下一個類了.下發出包,將會出另一個類.
可以對比前面提到的1:101,1:102輪流出包.
htb_charge_class最后更新令牌.看下htb_charge_class:
static void htb_charge_class(struct htb_sched *q, struct htb_class *cl,int level, struct sk_buff *skb) { int bytes = qdisc_pkt_len(skb); enum htb_cmode old_mode; long diff; while(cl) { //這里是一個循環,子類發包,是同時要扣子類和父類的令牌的. diff = psched_tdiff_bounded(q->now, cl->t_c, cl->mbuffer); if(cl->level >= level) { if(cl->level == level) //令牌的生產,扣減在這里. cl->xstats.lends++; htb_accnt_tokens(cl, bytes, diff); } else { cl->xstats.borrows++; cl->tokens += diff; /* we moved t_c; update tokens */ } htb_accnt_ctokens(cl, bytes, diff); cl->t_c = q->now; old_mode = cl->cmode; diff = 0; htb_change_class_mode(q, cl, &diff); //判斷是不是要切換狀態了. if(old_mode != cl->cmode) { if(old_mode != HTB_CAN_SEND) htb_safe_rb_erase(&cl->pq_node, q->wait_pq + cl->level); if(cl->cmode != HTB_CAN_SEND) htb_add_to_wait_tree(q, cl, diff); } cl = cl->parent; } }
子類發包同時要口父類的令牌很好理解,本質上子機就是再用父類的帶寬.但是當父類令牌富余借給子機出包時,只需從父類開始到祖先扣令牌.子機會更新這個周期生產的令牌,但不會扣了,因為是借用父類的令牌發包的.
令牌計算完后,更改class的狀態.大體上就是這樣,不再深入了.
為了描述方便,有些地方寫的跟代碼細節有差別.見諒.