Linux 路由緩存的前世今生

route cache

3.6版本一定算得上是Linux網絡子系統中一個特別的版本, 這個版本(補丁patch)移除了查找FIB之前的緩存查找。本文就來談談路由緩存的前世今生。

幾個基本概念

爲了讓本文的閱讀曲線更加平緩我決定還是將本文涉及的一些術語作個說明。

路由:將skb按照規則送到該去的地方,這個地方可能是本機,也可能是局域網中的其他主機,或者更遠的主機。從這個角度來說,它一個動詞。那麼路由發生在哪個時候呢? 我們知道路由是網絡層(L3)的概念,接收方向,它需要決定收到的skb是應該上送本機還是轉發,發送方向,它需要決定skb從哪個網絡接口發出。下圖原本是描述Netfilter在內核中的鉤子位置的,但我覺得用來說明路由的位置也是比較合適的。

forward

與此同時,路由也可以特指上面所說的規則,這是名詞的用法。路由從哪來? 一般來說有三個來源:1. 用戶主動配置;2.內核生成; 3. 其他一些路由協議進程(OSPFBGP)生成。普通主機上可能沒有最後一種,所以,爲了理解方便,你可以將路由就理解爲你用route命令看到的內容。

[root@tristan]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
192.168.99.0    0.0.0.0         255.255.255.0   U     0      0        0 eth0
192.168.98.42   192.168.99.1    255.255.255.255 UGH   0      0        0 eth0
127.0.0.0       0.0.0.0         255.0.0.0       U     0      0        0 lo
0.0.0.0         192.168.99.254  0.0.0.0         UG    0      0        0 eth0

FIB:全稱是(Forwarding Information Base),翻譯過來就是轉發信息表FIB是內核skb路由過程的數據庫,或者說內核會將路由翻譯成FIB中的表項。我們習慣說的查詢路由,對於內核來說,應該叫查詢FIB

3.6版本以前的路由緩存

緩存無處不在。現代計算機系統中,CacheCPU與內存間存在一種容量較小但速度很高的存儲器,用來存放CPU剛使用過或最近使用的數據。路由緩存就是基於這種思想的軟件實現。內核查詢FIB前,固定先查詢cache中的記錄,如果cache命中(hit),那就直接用就好了,不必查詢FIB。如果沒有命中(miss), 就回過頭來查詢FIB,最終將結果保存到cache,以便下次不再需要需要查詢FIB

緩存是精確匹配的, 每一條緩存表項記錄了匹配的源地址和目的地址、接收發送的dev,以及與內核鄰居系統(L2層)的聯繫(negghbour)
FIB中存儲的也就是路由信息,它常常是範圍匹配的,比如像ip route 1.2.3.0/24 dev eth0這樣的網段路由。

下圖是3.6版本以前的本機發送skb的路由過程…

fiblookup

看上去的確可能能提高性能! 只要cache命中率足夠高。要獲得高的cache命中率有以下兩個途徑:1. 存儲更多的表項; 2.存儲更容易命中的表項

緩存中存放的表項越多,那麼目標報文與表項匹配的可能性越大。但是cache又不能無限制地增大,cache本身佔用內存是一回事,更重要的是越多的表項會導致查詢cache本身變慢。使用cache的目的是爲了加速,如果不能加速,那要這勞什子有有什麼用呢?

前面說了,cache的特點決定了它只能做精確匹配。也就是說,只有目標數據報文與cache中的表項完全一致,纔算匹配成功。最簡單的cache查找過程應該是下面這樣:遍歷cache中的所有表項,直到遇到匹配的表項跳出循環。

foreach entry in cache:
then
    if entry match skb
    then
        /* 條件匹配,將緩存表項中記錄的結果設置到skb上 */
        skb->dst <= entry->dst
        return
    endif
end

顯然,cache表項的數目越多,那麼查找的過程就越長! 當然,內核不會這麼蠢地將所有cache拉成一個線,而是使用hash桶,看上去應該是這麼一個結構。

cachebucket

內核首先根據目標報文的一些特徵計算hash,找到對應的hash衝突鏈表。在鏈表上一個一個地進行比較遍歷。

爲了避免cache表項過多,內核還會在一定時機下清除過期的表項。有兩個這樣的時機,其一是添加新的表項時,如果衝突鏈的表項過多,就刪除一條已有的表項;其二是內核會啓動一個專門的定時器週期性地老化一些表項.

獲得更高的cache命中率的第二個途徑是存儲更容易命中的表項,什麼是更容易命中的呢? 那就是真正有效的報文。遺憾的是,內核一點也不聰明:只要輸入路由系統的報文不來離譜,它就會生成新的緩存表項。壞人正好可以利用這一點,不停地向主機發送垃圾報文,內核因此會不停地刷新cache。這樣每個skb都會先在cache表中進行搜索,再查詢FIB表,最後再創建新的cache表項,插入到cache表。這個過程中還會涉及爲每一個新創建的cache表項綁定鄰居,這又要查詢一次ARP表。

要知道,一臺主機上的路由表項可能有很多,特別是對於網絡交換設備,由OSPF**BGP等路由協議動態下發的表項有上萬條是很正常的事。而鄰居節點卻不可能達到這個數量。對於轉發或者本機發送的skb來說,路由系統能幫它們找到下一跳鄰居**就足夠了。

總結起來就是,3.6版本以前的這種路由緩存在skb地址穩定時的確可能提高性能。但這種根據skb內容決定的性能卻是不可預測和不穩定的。

3.6版本以後的下一跳緩存

正如前面所說,3.6版本移除了FIB查找前的路由緩存。這意味着每一個接收發送的skb現在都必須要進行FIB查找了。這樣的好處是現在查找路由的代價變得**穩定(consistent)**了。

路由緩存完全消失了嗎? 並沒有!在3.6以後的版本, 你還可以在內核代碼中看到dst_entry。這是因爲,3.6版本實際上是將FIB查找緩存到了下一跳(fib_nh)結構上,也就是下一跳緩存

爲什麼需要緩存下一跳呢? 我們可以先來看下沒有下一跳緩存的情況。以轉發過程爲例,相關的僞代碼如下:

FORWARD:

fib_result = fib_lookup(skb)
dst_entry  = alloc_dst_entry(fib_result)
skb->dst = dst_entry;

skb->dst.output(skb)   
nexthop = rt_nexthop(skb->dst, ip_hdr(skb)->daddr)
neigh = ipv4_neigh_lookup(dev, nexthop)
dst_neigh_output(neigh,skb)
release_dst_entry(skb->dst)

內核利用FIB查詢的結果申請dst_entry, 並設置到skb上,然後在發送過程中找到下一跳地址,繼而查找到鄰居結構(查詢ARP),然後鄰居系統將報文發送出去,最後釋放dst_entry

下一跳緩存的作用就是儘量減少最初和最後的申請釋放dst_entry,它將dst_entry緩存在下一跳結構(fib_nh)上。這和之前的路由緩存有什麼區別嗎? 很大的區別!之前的路由緩存是以源IP和目的IP爲KEY,有千萬種可能性,而現在是和下一跳綁定在一起,一臺設備沒有那麼多下一跳的可能。這就是下一跳緩存的意義!

early demux

early demux是在skb接收方向的加速方案。如前面所說,在取消了FIB查詢前的路由緩存後,每個skb應該都需要查詢FIB。而early demux是基於一種思想:如果一個skb是本機某個應用程序的套接字需要的,那麼我們可以將路由的結果緩存在內核套接字結構上,這樣下次同樣的報文(四元組)到達後,我們可以在FIB查詢前就將報文提交給上層,也就是提前分流(early demux)

圖 early demux

總結

3.6版本將FIB查詢之前的路由緩存移除了,取而代之的是下一跳緩存。

REF

Route cache removed
IPV4 route cache removed from >= 3.6 linux kernel
remove routing cache
Linux3.5內核以後的路由下一跳緩存

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章