深入理解高併發服務器性能優化
我們現在已經搞定了 C10K 併發連接問題 ,升級一下,如何支持千萬級的併發連接?你可能說,這不可能。你說錯了,現在的系統可以支持千萬級的併發連接,只不過所使用的那些激進的技術,並不爲人所熟悉。
要了解這是如何做到的,我們得求助於 Errata Security 的 CEO Robert Graham,看一下他在 Shmoocon 2013 的絕對奇思妙想的演講,題目是 C10M Defending The Internet At Scale。
Robert 以一種我以前從來沒有聽說過的才華橫溢的方式來搭建處理這個問題的架構。他的開場是一些歷史,關於 Unix 最初爲什麼不是設計成一個通用的服務器的 OS,而是爲電話網絡的控制系統設計的。真正傳輸數據的是電話網絡,因而控制層和數據層有非常清晰的區分。問題是,我們現在用的 Unix 服務器還是數據層的一部分,雖然並不應當是這樣的。如果一臺服務器只有一個應用程序,爲這樣的系統設計內核,與設計一個多用戶系統的內核的區別是非常大的。
關鍵問題:
- **不要讓內核去做所有繁重的處理。**把數據包處理,內存管理以及處理器調度從內核移到可以讓他更高效執行的應用程序中去。讓 Linux 去處理控制層,數據層由應用程序來處理。
結果就是成爲一個用 200 個時鐘週期處理數據包,14 萬個時鐘週期來處理應用程序邏輯,可以處理 1000 萬併發連接的系統。而作爲重要的內存訪問花費 300 個時鐘週期,這是儘可能減少編碼和緩存的設計方法的關鍵。
用一個面向數據層的系統你可以每秒處理 1000 萬個數據包。用一個面向控制層的系統每秒你只能獲得 1 百萬個數據包。
這貌似有點極端,你不能侷限於操作系的性能,你必須自己去實現。
現在,讓我們學習 Robert 是怎樣創作一個能處理 1000 萬併發連接的系統……
C10K 的問題——過去十年
十年前,工程師在處理 C10K 可擴展性問題時,都儘可能的避免服務器處理超過 10,000 個的併發連接。通過修正操作系統內核以及用事件驅動型服務器(如 Nginx 和 Node)替代線程式的服務器(如 Apache)這個問題已經解決。從 Apache 轉移到可擴展的服務器上,人們用了十年的時間。在過去的幾年中,(我們看到)可擴展服務器的採用率在大幅增長。
Apache 的問題
Apache 的問題是,(併發)連接數越多它的性能會越低下。
關鍵問題:(服務器的)性能和可擴展性並不是一碼事。它們指的不是同一件事情。當人們談論規模的時候往往也會談起性能的事情,但是規模和性能是不可同日而語的。比如 Apache。
在僅持續幾秒的短時連接時,比如快速事務處理,如果每秒要處理 1,000 個事務,那麼大約有 1,000 個併發連接到服務器。如果事務增加到 10 秒,要保持每秒處理 1,000 個事務就必須要開啓 10K(10,000 個)的併發連接。這時 Apache 的性能就會陡降,即使拋開 DDos 攻擊。僅僅是大量的下載就會使 Apache 宕掉。
如果每秒需要處理的併發請求從 5,000 增加到 10,000,你會怎麼做? 假使你把升級硬件把處理器速度提升爲原來的兩倍。會是什麼情況? 你得到了兩倍的性能,但是卻沒有得到兩倍的處理規模。處理事務的規模或許僅僅提高到了每秒 6,000 個(即每秒 6,000 個併發請求)。繼續提高處理器速度,還是無濟於事。甚至當性能提升到 16 倍時,併發連接數還不能達到 10,000 個。由此,性能和規模並不是一回事。
問題在於 Apache 總是創建了一個進程然後又把它關閉了,這並不是可擴展的。爲什麼? 因爲內核採用的 O(n^2) 算法導致服務器不能處理 10,000 個併發連接。
-
內核中的兩個基本問題:
-
**連接數 **= 線程數 / 進程數。當一個包(數據包)來臨時,它(內核)會遍歷所有的 10,000 個進程以決定由哪個進程處理這個包。
-
連接數 = 選擇數 / 輪詢次數(單線程情況下)。同樣的擴展性問題。每個包不得不遍歷一遍列表中的 socket。
-
解決方法:修正內核在規定的時間內進行查找
-
不管有多少線程,線程切換的時間都是恆定的。
-
使用一個新的可擴展的 epoll()/IOCompletionPort 在規定的時間內做 socket 查詢。
由於線程調度依然沒有被擴展,因此服務器對 socket 大規模的採用 epoll,導致需要使用異步編程模式,然而這正是 Node 和 Nginx 所採用的方式。這種軟件遷移會得到(和原來)不一樣的表現(指從 apache 遷移到 ngix 等)。即使在一臺很慢(配置較低)的服務器上增加連接數性能也不會陡降。介於此,在開啓 10K 併發連接時,一臺筆記本電腦(運行 ngix)的速度甚至超越了一臺 16 核的服務器(運行 Apache)。
異步編程模型可以參考:深入理解重要的編程模型
C10M 問題 —— 下一個十年
在不久的將來,服務器將需要處理數百萬的併發連接。由於 IPV6 普及,連接到每一個服務器的潛在可能連接數目將達到數百萬,所以我們需要進入下一個可擴張性階段。
示例應用程序將會用到這類可擴張性方案:IDS/IPS,因爲他們是連接到一臺服務器的主幹。另一個例子:DNS 根服務器、TOR 節點、Nmap 互聯網絡、視頻流、銀行業務、NAT 載體、網絡語音電話業務 PBX、負載均衡器、web 緩存、防火牆、郵件接收、垃圾郵件過濾。
通常人們認爲互聯網規模問題是個人計算機而不是服務器,因爲他們銷售的是硬件 + 軟件。你買的設備連接到你的數據中心。這些設備可能包含英特爾主板或網絡處理器和用於加密的芯片、數據包檢測,等等。
2013 年 2 月 40gpbs、32 核、256gigs RAM X86 在新蛋的售價爲 $5000。這種配置的服務器能夠處理 10K 以上的連接。如果不能,這不是底層的硬件問題,那是因爲你選錯了軟件。這樣的硬件能夠輕而易舉的支持千萬的併發連接。
10,000,000 個併發連接挑戰意味着什麼
-
10,000,000 個併發連接
-
每秒 1,000,000 個連接——每個連接大約持續 10 秒
-
10 千兆比特 / 每秒——快速連接到互聯網。
-
10,000,000 包 / 每秒——預期當前服務器處理 50,000 包 / 每秒,這將導致更高的級別。服務器能夠用來處理每秒 100,000 箇中斷和每個包引發的中斷。
-
10 微秒延遲——可擴張的服務器也許能夠處理這樣的增長,但是延遲將會很突出。
-
10 微秒上下跳動——限制最大延遲
-
10 個一致的 CPU 內核——軟件應該擴張到更多內核。典型的軟件只是簡單的擴張到四個內核。服務器能夠擴張到更多的內核,所以軟件需要被重寫以支持在擁有更多內核的機器上運行。
我們學的是 Unix 而不是網絡編程(Network Programming)
-
一代代的程序員通過 W. Richard Stevens 所著的《Unix 網絡編程》(Unix Networking Programming)學習網絡編程技術。問題是,這本書是關於 Unix 的,並不是網絡編程。它講述的是,你僅需要寫一個很小的輕量級的服務器就可以讓 Unix 做一切繁複的工作。然而內核並不是規模的(規模不足)。解決方案是,將這些繁複的工作轉移到內核之外,自已處理。
-
一個頗具影響的例子,就是在考慮到 Apache 的線程每個連接模型(is to consider Apache’s thread per connection model)。這就意味着線程調度器根據到來的數據(on which data arrives)決定調用哪一個(不同的)read() 函數(方法)。把線程調度系統當做(數據)包調度系統來使用(我非常喜歡這一點,之前從來沒聽說過類似的觀點)。
-
Nginx 宣稱,它並不把線程調度當作(數據)包調度來用使用,它自已做(進行)包調度。使用 select 來查找 socket,我們知道數據來了,於是就可以立即讀取並處理它,數據也不會堵塞。
-
經驗:讓 Unix/Linux 處理網絡堆棧,之後的事情就由你自已來處理。
你怎麼編寫軟件使其可伸縮?
你怎麼改變你的軟件使其可伸縮?有大量的經驗規則都是假設硬件能處理多少。我們需要真實的執行性能。
要進入下一個等級,我們需要解決的問題是:
-
包的可擴展性
-
多核的可擴展性
-
內存的可擴展性
精簡包 - 編寫自己的定製驅動來繞過內核堆棧
-
數據包的存在的問題是它們要通過 Unix 的內核。網絡堆棧複雜又慢。你的應用程序需要的數據包的路徑要更加直接。不要讓操作系統來處理數據包。
-
做到這一點的方法是編寫自己的驅動程序。所有驅動程序要做到是發送數據包到你的應用程序而不是通過內核協議棧。你可以找得到驅動有:PF_RING,Netmap,Interl DPDK(數據層開發套件)。
-
有多快呢?Inter 有一個基準是在一個輕量級的服務器上每秒可以處理 8000 萬的數據包(每個數據包 200 個時鐘週期)。這也是通過用戶模式。數據包通過用戶模式後再向下傳遞。當 Linux 獲得 UDP 數據包後通過用戶模式在向下傳遞時,它每秒處理的數據包不會超過 100 萬個。客戶驅動對 Linux 來說性能比是 80:1。
-
如果用 200 個時間週期來每秒獲得 1000 萬個數據包,那麼可以剩下 1400 個時鐘週期來實現一個類似 DNS/IDS 的功能。
-
用 PF_RING/DPDK 來獲得原始的數據包的話,你必須自己去做 TCP 協議棧。人們正在做用戶模式的堆棧。對於 Inter 來講已有一個提供真正可擴展性能的可用的 TCP 堆棧。
多核的可擴展性
多核的可擴展性和多線程可擴展性是不一樣的。我們熟知的 idea 處理器不在漸漸變快,但是我們卻擁有越來越多的 idea 處理器。
大多數代碼並不能擴展到 4 核。當我們添加更多的核心時並不是性能不變,而是我們添加更多的核心時越來越慢。因爲我們編寫的代碼不好。我們期望軟件和核心成線性的關係。我們想要的是添加更多的核心就更快。
多線程編程不是多核編程
多線程:
-
每個 CPU 有多個線程
-
鎖來協調線程(通過系統調用)
-
每個線程有不同的任務
多核:
-
每個 CPU 核心一個線程
-
當兩個核心中的兩個不同線程訪問同一數據時,它們不用停止來相互等待
-
所有線程是同一任務的一部分
我們的問題是如何讓一個程序能擴展到多個核心。Unix 中的鎖是在內核中實現的,在 4 核心上使用鎖會發生什麼? 大多數軟件會等待其他線程釋放一個鎖,這樣的以來你有更多的 CPU 核心內核就會耗掉更多的性能。
我們需要的是一個像高速公路的架構而不是一個像靠紅綠燈控制的十字路口的架構。我們想用盡可能少的小的開銷來讓每個人在自己的節奏上而沒有等待。
解決方案:
-
保持每一個核心的數據結構,然後聚集起來讀取所有的組件。
-
原子性. CPU 支持的指令集可以被 C 調用。保證原子性且沒有衝突是非常昂貴的,所以不要期望所有的事情都使用指令。
-
無鎖的數據結構。線程間訪問不用相互等待。不要自己來做,在不同架構上來實現這個是一個非常複雜的工作。
-
線程模型。線性線程模型與輔助線程模型。問題不僅僅是同步。而是怎麼架構你的線程。
-
處理器族。告訴操作系統使用前兩個核心。之後設置你的線程運行在那個核心上。你也可以使用中斷來做同樣的事兒。所以你有多核心的 CPU,但這不關 Linux 事。
內存的可擴展性
假設你有 20G 內存(RAM),第個連接佔用 2K,假如你只有 20M 三級緩存(L3 cache),緩存中沒有數據。從緩存轉移到主存上消耗 300 個時鐘週期,此時 CPU 處於空閒狀態。想象一下,(處理)每個包要 1400 個時鐘週期。切記還有 200 時鐘週期 / 每包的開銷(應該指等待包的開銷)。每個包有 4 次高速緩存的缺失,這是個問題。
-
**提高緩存效率,**不要使用指針在整個內存中隨便亂放數據。每次你跟蹤一個指針都會造成一次高速緩存缺失:[hash pointer] -> [Task Control Block] -> [Socket] -> [App]。這造成了 4 次高速緩存缺失。將所有的數據保持在一個內存塊中:[TCB | Socket | App]. 爲每個內存塊預分配內存。這樣會將高速緩存缺失從 4 降低到 1。
-
**分頁,**32G 的數據需要佔用 64M 的分頁表,不適合都放在高速緩存上。所以造成 2 個高速緩存缺失,一個是分頁表另一個是它指向的數據。這些細節在開發可擴展軟件時是不可忽略的。解決:壓縮數據,使用有很多內存訪問的高速架構,而不是二叉搜索樹。NUMA 加倍了主內存的訪問時間。內存有可能不在本地,而在其它地方
-
內存池 , 在啓動時立即分配所有的內存。在對象(object)、線程(thread)和 socket 的基礎上分配(內存)。
-
**超線程,**提高 CPU 使用率,減少延遲,比如當在內存訪問中一個線程等待另一個全速線程,這種情況,超線程 CPU 可以並行執行,不用等待。
-
**大內存頁, **減小頁表的大小。從一開始就預留內存,並且讓應用程序管理內存。
選擇合適的語言
go 語言這種天生爲併發而生的語言,完美的發揮了服務器多核優勢,很多可以併發處理的任務都可以使用併發來解決,比如 go 處理 http 請求時每個請求都會在一個 goroutine 中執行,C 和 C++ 語言當然也可以實現高併發系統,總之: 怎樣合理的壓榨 CPU, 讓其發揮出應有的價值,是優化一直需要探索學習的方向。
推薦開源項目學習:
F-Stack 開發框架
F-Stack 是一款兼顧高性能、易用性和通用性的網絡開發框架,傳統上 DPDK 大多用於 SDN、NFV、DNS 等簡單的應用場景下,對於複雜的 TCP 協議棧上的七層應用很少,市面上已出現了部分用戶態協議棧,如 mTCP、Mirage、lwIP、NUSE 等,也有用戶態的編程框架,如 SeaStar 等,但統一的特點是應用程序接入門檻較高,不易於使用。
F-Stack 使用純 C 實現,充當膠水粘合了 DPDK、FreeBSD 用戶態協議棧、Posix API、微線程框架和上層應用(Nginx、Redis),使絕大部分的網絡應用可以通過直接修改配置或替換系統的網絡接口即可接入 F-Stack,從而獲得更高的網絡性能。
F-Stack 架構
F-Stack 總體架構如上圖所示,具有以下特點:
-
使用多進程無共享架構。
-
各進程綁定獨立的網卡隊列和 CPU,請求通過設置網卡 RSS 散落到各進程進行處理。
-
各進程擁有獨立的協議棧、PCB 表等資源。
-
每個 NUMA 節點使用獨立的內存池。
-
進程間通信通過無鎖環形隊列(rte_ring)進行。
-
使用 DPDK 作爲網絡 I/O 模塊,將數據包從網卡直接接收到用戶態。
-
移植 FreeBSD Release 11.0.1 協議棧到用戶態並與 DPDK 對接。
http://www.f-stack.org
Nginx :
一個高性能的 HTTP 和反向代理 web 服務器,同時也提供了 IMAP/POP3/SMTP 服務。
http://nginx.org/
**Redis : **
一個開源的使用 ANSI C 語言編寫、遵守 BSD 協議、支持網絡、可基於內存亦可持久化的日誌型。
https://redis.io/
Fasthttp :
Fasthttp 是一個高性能的 web server 框架。Golang 官方的 net/http 性能相比 fasthttp 遜色很多。根據測試,fasthttp 的性能可以達到 net/http 的 10 倍。所以,在一些高併發的項目中,我們經常用 fasthttp 來代替 net/http。
https://github.com/savsgio/atreugo
Oat++:
oat++ 是一個輕量級高性能 Web 服務開發框架,採用純 C++ 編寫而成。
https://gitee.com/mirrors/oatpp
Undertow,jetty,Tomcat 等:
java 語言的高併發框架。
https://undertow.io/
https://gitee.com/mirrors/jetty
https://tomcat.apache.org/
性能測試對比(排行榜):
https://www.techempower.com/benchmarks/
有興趣同學可以試一試你的極限優化,讓你們的程序上榜!這個絕對是簡歷和項目推薦裏面的最有力的說明。
參考
翻譯:DYOS, 裴寶亮, dexterman, zicode
https://www.oschina.net/translate/the-secret-to-10-million-concurrent-connections-the-kernel?lang=chs
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yEDVk_1tezBzgOvReqXFjg