負載均衡是指對後端服務進行流量分發的服務。通過負載均衡入口,後端服務可以水平擴充套件來提公升對外服務能力,可以消除單點故障以提公升應用系統可用性。我們以web server舉例,如圖1所示,web server可以在負載均衡後面透明伸縮,當其中一部分web server例項掛掉後,剩餘例項仍然能夠提供服務。
圖 1:典型負載均衡使用示例
負載均衡又分為四層和七層負載均衡,所謂四層和七層就是指對後端服務進行負載均衡時,依據傳輸層還是應用層的資訊來進行流量**。這兩面包含兩個問題,一是我們怎麼來區分負載均衡**流量,二是我們對**流量進行怎樣的負載均衡。
當進行四層負載均衡時,依據的是網路發布的ip加上傳輸層的埠號來區分**流量,而對**流量選擇怎樣的負載均衡時,可能選擇使用者ip,使用者埠,協議號,目標ip和目標埠的5元組hash的方式,也可能選擇普通的round robin輪轉,這個依據使用場景而定,典型四層負載均衡產品如lvs,f5。而七層負載均衡則依據應用層資訊區分**流量,典型如網域名稱資訊來區分**流量,通過url hash等方式進行後端服務的負載均衡,典型產品如nginx,haproxy,簡單對比如圖2所示。
圖 2:四層與七層負載均衡的差別
顯然七層負載均衡相比四層負載均衡對**效率要求更高,且需要支援多樣化的應用層協議,而不僅僅是http協議等,而四層負載均衡雖然支援tcp/udp至上的任何協議,但是無法像七層那樣對應用進行定製化**,比如根據url hash等。
總結來說業界通常綜合二者優點來進行使用,很多網際網路公司的接入架構是採用lvs+nginx之類的四層+七層方案。介紹完本基礎,我們接下來闡述唯品會高效能四層負載均衡vgw的產生背景及高效能秘密,希望能對大家有所借鑑。
vgw高效能法寶
cpu多核的親和性
一般cpu有多層cache,每級cache訪問時間相差約10倍,如l1的時間為0.5ns,l2的訪問時間為7ns,容量也隨之增大。因此如果乙個程序或執行緒可能被排程到不同cpu核上,可能會導致cpu上的多級cache miss,一次cpu的cache miss導致的訪問記憶體開銷大約是l1 cache訪問開銷的幾百倍。因此我們在vgw中將各個執行緒都固定跑在各自的核上,避免排程到不同核導致的cpu cache miss。另外我們也將vgw工作執行緒跑的核單獨隔離,避免被作業系統排程的任務擠占cache。
我們的核分配結構如圖3所示:
圖 3:vgw多核分配結構
大頁記憶體
相對於傳統虛擬記憶體,大頁記憶體不受虛擬記憶體影響,不會被替換出記憶體。另外linux預設頁大小為4kb,而大頁記憶體可以支援2m或1g頁大小,相同記憶體容量下能夠減少頁表條目數,使得tlb的miss數大大降低。以8g記憶體為例,如果採用4k頁大小,則需要2m的頁表項來存虛擬記憶體到物理頁面對映關係,而以大頁記憶體1g的話,只需要8條頁表項,因此能夠使得tlb衝突開銷大大降低。
vgw中採用2m頁大小,劃分8g記憶體獨佔,這個在環境初始化指令碼中設定,由於機器是numa結構,故需要給親和的numa node建立大頁。
1.echo pages> /sys/devices/system/node/node-num hugepages/hugepages-pagesize/nr_hugepages
2.mkdir –p /mnt/huge
3.mount -t hugetlbfs nodev /mnt/huge
num表示numa號,pages和pagesize分別表示頁數和頁大小。
無鎖化
典型的互斥鎖的時間開銷是100 ns級,而對於vgw使用的dpdk來說開銷過於昂貴,dpdk為此提供了fifo固定大小的環形無鎖佇列。它通過name為標誌區分,支援單寫單讀,多寫單讀,單寫多讀,多寫多讀。本質上來說該無鎖佇列底層實現是通過cas等cpu支援的原子操作指令和忙等待來實現。
在vgw中無鎖佇列主要有兩個用途,乙個是用於記憶體池中的記憶體分配,另外乙個是用於接收執行緒和傳送執行緒,接收執行緒和kni執行緒的通訊中,控制線程和**執行緒中控制資訊互動。具體可參考圖3。
另外vgw中也將讀多寫少情況進行rcu化,避免採用讀寫鎖,rcu機制通過少量的記憶體開銷來換取無鎖化, ip黑名單是典型的讀多寫少情形,啟用該模組後,資料報都會去讀取黑名單來判斷自身的源ip是否匹配,只有少數情況下才會更新這個黑名單,因此我們在ip黑名單模組的實現中應用了rcu,極大簡化了該功能模組的實現,避免通過無鎖佇列出現的更新通知冗餘及維護成本。
使用者態輪詢驅動
傳統linux系統以中斷的方式通知cpu來收發資料報。在每秒到來的資料報量小的時候沒有問題,一旦大量資料報蜂擁,中斷就佔了大量的開銷,擠占了處理資料報的時間。為避免中斷開銷,vgw使用dpdk的pmd輪詢驅動直接在使用者態獲取資料報,簡單來說就是通過輪詢的方式收發包,避免大流量下的中斷開銷,同時使用者態驅動避免包拷貝, 如圖4所示。
圖4:輪詢驅動
其他優化措施
多核訪問資料per-lcore化及cache line對齊
在vgw程式中盡量不在多核間分配全域性變數,而是在各個lcore上分配,這樣避免讀寫時加鎖衝突,同時也使得各個lcore的訪問都是它最近的記憶體。例如vgw的統計資訊,arp表,連線表等結構都是per-lcore化。
路由查詢優化
vgw**資料報需要知道該資料報的下一跳ip,相當於需要維護路由資訊,因此怎樣快速的根據目的位址來查詢到下一跳位址也是關鍵問題。最開始我們直接通過linux ioctl api獲取系統路由,但在資料**的邏輯中使用系統呼叫不是乙個好方法,系統呼叫的開銷很可能影響極限**效能,後面通過利用dpdk的lpm(longest prefix match)模組來加快每個資料報的路由資訊查詢,lpm維護了字首24位和後8位的兩張表,字首24位表匹配概率較大,採取預先分配,大部分情況下只需要一次訪問就能匹配到,而字尾8位表則訪問概率較小,採取按需分配,需要兩次訪問命中,從而達到時間和空間的折中。
合適的批量值及預取
在我們vgw的收包中,一次從網絡卡批量收包的數目也需要權衡優化,太低會導致網絡卡硬體快取區滿,因為存在多次獲取的開銷,太高則可能導致處理延遲增加,也使得網絡卡快取區滿,我們通過測試調優選取了乙個比較合理的值。
預取能夠使得cpu將待處理的資料提前從記憶體載入到cpu cache中,除了底層驅動使用預取來加速收發包外,vgw應用程式也在收到每個包後,將包體內容預取到cache中,加速後續包處理的過程。如rte_prefetch0(rte_pktmbuf_mtod(m, void *));
分支**
cpu通過流水線重疊連續指令增加處理吞吐量,要充分利用流水線,必須知道執行指令的序列和先後順序,而cpu在執行分支判斷語句時,無法知道後續該執行哪個分支,故無法將後續指令填充進入流水線。這個時候就可以通過分支**顯示告訴cpu大概率執行的分支,加速流水線指令處理。
比如vgw的收包判斷邏輯中,對於收到的每個資料報,都有兩種流向,一種是被**到後端真實伺服器,另外一種是轉化成skb_buff結構交給vgw的本機作業系統的協議棧處理,而正常情況下大部分的流量應該是**流,故我們在收包執行緒的判斷邏輯中加入likely函式來指示cpu最可能執行的分支為**邏輯。
if(likely(packet to real server)) {
process(packet);
numa親和
由於我們的伺服器是numa結構,numa親和包括記憶體與cpu,裝置與cpu兩個方面,vgw在分配記憶體時,通過rte_pktmbuf_pool_create函式的socket_id引數來分配離指定cpu所在numa的記憶體。vgw在初始化網絡卡收發佇列時,通過函式rte_eth_rx_queue_setup的socket_id引數來控制傳入的numa id,保證網絡卡和對應numa上的cpu親和性。
vgw保證最開始只關注了記憶體和cpu的numa親和,忽略了網絡卡裝置與cpu numa的親和,效能測試沒達到預期後,才開始關注修復,目前接收和傳送執行緒的資料報使用的記憶體和網絡卡裝置都處於同一numa node,保證了效能最優化。
Nginx Tomcat搭建高效能負載均衡集群
nginx 1.8.0 apache tomcat 6.0.33 實現高效能負載均衡的tomcat集群 2 然後解壓兩個tomcat,分別命名為apache tomcat 6.0.33 1和apache tomcat 6.0.33 2 3 然後修改這兩個tomcat的啟動埠,分別為18080和280...
Nginx Tomcat搭建高效能負載均衡集群
nginx 1.8.0 apache tomcat 6.0.33 實現高效能負載均衡的tomcat集群 2 然後解壓兩個tomcat,分別命名為apache tomcat 6.0.33 1和apache tomcat 6.0.33 2 3 然後修改這兩個tomcat的啟動埠,分別為18080和280...
Nginx Tomcat搭建高效能負載均衡集群
原創2015年08月19日 11 45 23 nginx 1.8.0 apache tomcat 6.0.33 實現高效能負載均衡的tomcat集群 2 然後解壓兩個tomcat,分別命名為apache tomcat 6.0.33 1和apache tomcat 6.0.33 2 3 然後修改這兩個...