我重點(diǎn)分享如下兩個(gè)問(wèn)題:
實(shí)現層面主要是三方面優(yōu)化,主要聚焦在應用、框架、內核。
硬件限制可能有的同學(xué)也都聽(tīng)過(guò),把網(wǎng)卡調到萬(wàn)兆、10G 或者 40G 是最好的,磁盤(pán)會(huì )根據成本的預算和應用場(chǎng)景來(lái)選擇固態(tài)硬盤(pán)或者機械式硬盤(pán),關(guān)注 IOPS 或者 BPS。
CPU 是我們重點(diǎn)看的一個(gè)指標。實(shí)際上它是把操作系統的切換代價(jià)換到了進(jìn)程內部,所以它從一個(gè)連接器到另外一個(gè)連接器的切換成本非常低,它性能很好,協(xié)程 Openresty 其實(shí)是一樣的。
資源的高效使用,降低內存是對我們增大并發(fā)性有幫助的,減少 RTT、提升容量。
Reuseport 都是圍繞著(zhù)提升 CPU 的機核性。還有 Fast Socket,因為我之前在阿里云的時(shí)候還做過(guò)阿里云的網(wǎng)絡(luò ),所以它能夠帶來(lái)很大的性能提升,但是問(wèn)題也很明顯,就是把內核本身的那套東西繞過(guò)去了。
下面我首先會(huì )去聊一下怎么看“請求”,了解完這個(gè)以后再去看怎么優(yōu)化就會(huì )很清楚了。
說(shuō)這個(gè)之前必須再說(shuō)一下 Nginx 的模塊結構,像 Nginx 以外,任何一個(gè)外部框架都有個(gè)特點(diǎn),如果想形成整個(gè)生態(tài)必須允許第三方的代碼接進(jìn)來(lái),構成一個(gè)序列,讓一個(gè)請求挨個(gè)被模塊共同處理。
那 Nginx 也一樣,這些模塊會(huì )串成一個(gè)序列,一個(gè)請求會(huì )被挨個(gè)的處理。在核心模塊里有兩個(gè),分別是 Steam 和 NGX。
一個(gè)連接開(kāi)始剛剛建立請求到來(lái)的時(shí)候會(huì )發(fā)生什么事情?先是操作系統內核中有一個(gè)隊列,等著(zhù)我們的進(jìn)程去系統調用,這時(shí)候因為有很多工作進(jìn)程,誰(shuí)會(huì )去調用呢,這有個(gè)負載均衡策略。
現在有一個(gè)事件模塊,調用了 Epoll Wait 這樣的接口,Accept 建立好一個(gè)新連接,這時(shí)會(huì )分配到連接內存池,這個(gè)內存池不同于所有的內存池,它在連接剛創(chuàng )建的時(shí)候會(huì )分配,什么時(shí)候會(huì )釋放呢?
只有這個(gè)連接關(guān)閉的時(shí)候才會(huì )去釋放。接下來(lái)就到了 NGX 模塊,這時(shí)候會(huì )加一個(gè) 60 秒的定時(shí)器。
就是在建立好連接以后 60 秒之內沒(méi)有接到客戶(hù)端發(fā)來(lái)的請求就自動(dòng)關(guān)閉,如果 60 秒過(guò)來(lái)之后會(huì )去分配內存,讀緩沖區。什么意思呢?
現在操作系統內核已經(jīng)收到這個(gè)請求了,但是我的應用程序處理不了,因為沒(méi)有給它讀到用戶(hù)態(tài)的內存里去,所以這時(shí)候要分配內存。
從連接內存池這里分配,那要分配多大呢?會(huì )擴到 1K。
當收到請求以后,接收 Url 和 Header,分配請求內存池,這時(shí)候 Request Pool Size 是 4K,大家發(fā)現是不是和剛才的有一個(gè) 8 倍的差距,這是因為利用態(tài)的內存是非常消耗資源的。
再看為什么會(huì )消耗資源,首先會(huì )用狀態(tài)機解去形容,所謂狀態(tài)機解就是把它當做一個(gè)序列,一個(gè)支節一個(gè)支節往下解,如果發(fā)現換行了那就是請求行解完了。
但如果這個(gè)請求特別長(cháng)的時(shí)候,就會(huì )去再分配更大的,剛剛 1K 不夠用了,為什么是 4 乘 8K 呢?
就是因為當 1K 不夠了不會(huì )一次性分配 32K,而是一次性分配 8K。如果 8K 以后還沒(méi)有解析到剛才的標識符,就會(huì )分配第二個(gè) 8K。
我之前收到的所有東西都不會(huì )釋放,只是放一個(gè)指針,指到 Url 或者指到那個(gè)協(xié)議,標識它有多長(cháng)就可以了。
接下來(lái)解決 Header,這個(gè)流程一模一樣的沒(méi)有什么區別,這時(shí)候還會(huì )有一個(gè)不夠用的情況,當我接收完所有的 Header 以后,會(huì )把剛剛的定時(shí)器給移除,移除后接下來(lái)做 11 個(gè)階段的處理。
也就是說(shuō)剛剛所有的外部服務(wù)器都是通過(guò)很多的模塊串成在一起處理一個(gè)請求的。
像剛剛兩頁(yè) PPT 都在說(shuō)藍色的區域,那么請求接下來(lái) 11 個(gè)階段是什么意思呢?這個(gè)黃色的、綠色的,還有右邊這個(gè)都是在 11 階段之中。
這 11 個(gè)階段大家也不用記,非常簡(jiǎn)單,只要掌握三個(gè)關(guān)鍵詞就可以。
剛剛讀完 Header 要做處理,所以這時(shí)候第一階段是 Post-Read。接下來(lái)會(huì )有 Rewrite,還有 Access 和 Preaccess。
先看左手邊,當我們下載完 Nginx 源碼編以后會(huì )有一個(gè) Referer,所有的第三方數據都會(huì )在這里呈現有序排列。
這些序列中并不是簡(jiǎn)單的一個(gè)請求給它再給它,先是分為 11 個(gè)階段,每個(gè)階段之內大家是有序一個(gè)個(gè)往后來(lái)的,但在 11 個(gè)階段中是按階段來(lái)的。
我把它分解一下,第一個(gè) Referer 這階段有很多模塊,后面這是有序的。
這個(gè)圖比剛剛的圖多了兩個(gè)關(guān)鍵點(diǎn):
請求的反向代理,反向代理這塊是我們 Nginx 的重點(diǎn)應用場(chǎng)景,因為 Nginx 會(huì )考慮一種場(chǎng)景,客戶(hù)端走的是公網(wǎng),所以網(wǎng)絡(luò )環(huán)境非常差,網(wǎng)速非常慢。
如果簡(jiǎn)單用一個(gè)緩沖區從客戶(hù)端收一點(diǎn)發(fā)給上游服務(wù)器,那上游服務(wù)器的壓力會(huì )很大,因為上游服務(wù)器往往它的效率高,所以都是一個(gè)請求被處理完之前不會(huì )再處理下一個(gè)請求。
Nginx 考慮到這個(gè)場(chǎng)景,它會(huì )先把整個(gè)請求全部收完以后,再向上游服務(wù)器建立連接,所以是默認第一個(gè)配置,就是 Proxy Request Buffering On,存放包體至文件,默認 Size 是 8K。
那建立上游連接的時(shí)候會(huì )放 Time Out,60 秒,添加超時(shí)定時(shí)器,也是 60 秒的。
發(fā)出請求(讀取文體包件),如果向上游傳一個(gè)很大的包體的話(huà),那 Sizk 就是 8K。
默認 Proxy Limit Rate 是打開(kāi)的,我們會(huì )先把這個(gè)請求全部緩存到端來(lái),所以這時(shí)候有個(gè) 8×8K,如果關(guān)掉的話(huà),也就是從上游發(fā)一點(diǎn)就往下游發(fā)一點(diǎn)。
知道這個(gè)流程以后,再說(shuō)這里的話(huà)大家可以感覺(jué)到這里的內存消耗還是蠻大的。
返回響應,這里面其實(shí)內容蠻多的,我給大家簡(jiǎn)化一下,還是剛剛官方的那個(gè)包,這也是有順序的從下往上看,如果有大量第三方模塊進(jìn)來(lái)的話(huà),數量會(huì )非常高。
第一個(gè)關(guān)鍵點(diǎn)是上面的 Header Filter,上面是 Write Filter,下面是 Postpone Filter,這里還有一個(gè) Copy Filter,它又分為兩類(lèi),一類(lèi)是需要處理,一類(lèi)是不需要處理的。
OpenResty 的指令,第一代碼是在哪里執行的,第二個(gè)是 SDK。
做應用層的優(yōu)化我們會(huì )先看協(xié)議層有沒(méi)有什么優(yōu)化,比如說(shuō)編碼方式、Header 每次都去傳用 Nginx 的架構,以至于浪費了很多的流量。我們可以改善 Http2,有很多這樣的協(xié)議會(huì )大幅度提升它的性能。
當然如果你改善 Http2 了,會(huì )帶來(lái)其他的問(wèn)題,比如說(shuō) Http2 必須走這條路線(xiàn)。
這條路線(xiàn)又是一個(gè)很大的話(huà)題,它涉及到安全性和性能,是互相沖突的東西。
我們希望“商”越大越好,壓縮這里會(huì )有一個(gè)重點(diǎn)提出來(lái)的動(dòng)態(tài)和靜態(tài),比如說(shuō)我們用了拷貝,比如說(shuō)可以從磁盤(pán)中直接由內核來(lái)發(fā)網(wǎng)卡,但一旦做壓縮的話(huà)就不得不先把這個(gè)文件讀到 Nginx,交給后面的極內核去做一下處理。
Keepalive 長(cháng)連接也是一樣的,它也涉及到很多東西,簡(jiǎn)單來(lái)看這也就是附用連接。
因為連接有一個(gè)慢啟動(dòng)的過(guò)程,一開(kāi)始它的窗口是比較小,一次可能只傳送很小的 1K 的,但后面可能會(huì )傳送幾十K,所以你每次新建連接它都會(huì )重新開(kāi)始,這是很慢的。
當然這里還涉及到一個(gè)問(wèn)題,因為 Nginx 內核它默認打開(kāi)了一個(gè)連接空閑的時(shí)候,長(cháng)連接產(chǎn)生的作用也會(huì )下降。
剛剛在說(shuō)具體的請求處理過(guò)程中已經(jīng)比較詳細的把這問(wèn)題說(shuō)清楚了,這里再總結一下,在我看來(lái)有一個(gè)角度,Nginx 對下游只是必須要有的這些模塊,Client Header、Buffer Size:1K,上游網(wǎng)絡(luò ) Http 包頭和包體。
CPU 通過(guò)緩存去取儲存上東西的時(shí)候,它是一批一批取的,每一批目前是 64 字節,所以默認的是 8K。
如果你配了 32 它會(huì )給你上升到 64;如果你配了 65 會(huì )升到 128,因為它是一個(gè)一個(gè)序列化重組的。
所以了解這個(gè)東西以后自己再配的時(shí)候就不會(huì )再犯問(wèn)題。紅黑樹(shù)這里用的非常多,因為是和具體的模塊相關(guān)。
大部分我們在做分公司流控的時(shí)候,主要在限什么呢?主要限 Nginx 向客戶(hù)端發(fā)送響應的速度。
這東西非常好用,因為可以和 Nginx 定量連接在一起。這不是限上游發(fā)請求的速度,而是在限從上游接響應的速度。
當時(shí)我在用 0.6 版本的時(shí)候那時(shí)候都在默認用這個(gè),這個(gè)“鎖”它是在用進(jìn)程間同步方式去實(shí)現負載均衡,這個(gè)負載均衡怎么實(shí)現呢?
就是保證所有的 Worker 進(jìn)程,同一時(shí)刻只有一個(gè) Worker 進(jìn)程在處理距離,這里就會(huì )有好幾個(gè)問(wèn)題,綠色的框代表它的吞吐量,吞吐量不高,所以會(huì )導致第二個(gè)問(wèn)題 Requests,也是比較長(cháng)的,這個(gè)方差就非常的大。
如果把這個(gè)“鎖”關(guān)掉以后,可以看到吞吐量是上升的,方差也在下降,但是它的時(shí)間在上升,為什么會(huì )出現這樣的情況?
因為會(huì )導致一個(gè) Worker 可能會(huì )非常忙,它的連接數已經(jīng)非常高了,但是還有其他的 Worker 進(jìn)程是很閑的。
如果用了 Requests,它會(huì )在內核層面上做負載均衡。這是一個(gè)專(zhuān)用場(chǎng)景,如果在復雜應用場(chǎng)景下開(kāi) Requests 和不開(kāi)是能看到明顯變化的。
這里我剛剛說(shuō)了好多,它是一個(gè)紅黑樹(shù)在實(shí)現的。唯一要說(shuō)的也就是這里,Nginx 現在做四層的反向代理也很成熟了。
像 UTP 協(xié)議是可以做反向代理的,但要把有問(wèn)題的連接迅速踢掉的話(huà),要遵循這個(gè)原則,一個(gè)請求對一個(gè)響應。
只要想提升性能必須要在緩存上下工夫。比如說(shuō)我以前在阿里云做云盤(pán),云盤(pán)緩存的時(shí)候就會(huì )有個(gè)概念叫空間維度緩存,在讀一塊內容的時(shí)候可能會(huì )把這內容周邊的其他內容也讀到緩存中。
大家如果熟悉優(yōu)化的話(huà)也會(huì )知道有分支預測先把代碼讀到那空間中,這個(gè)用的比較少,基于時(shí)間維度用的比較多了。
其實(shí)要做的事也非常多,優(yōu)化讀取,Sendfile 零拷貝、內存盤(pán)、SSD 盤(pán)。減少寫(xiě)入,AIO,磁盤(pán)是遠大于內存的,當它把你內存消化完的時(shí)候還會(huì )退化成一個(gè)調用。
像 Thread Pool 只用讀文件,當退化成這種模式變多線(xiàn)程可以防止它的主進(jìn)程被阻塞住,這時(shí)候官方的博客上說(shuō)是有 9 倍的性能提升。
我們建連接的時(shí)候也有,還有些向客戶(hù)端發(fā)起連接的時(shí)候會(huì )有一個(gè)端口范圍,還有一些像對于網(wǎng)卡設備的。
CPU緩存的親和性,這看情況了,現在用 L3 緩存差不多也 20 兆的規模,CPU 緩存的親和性是一個(gè)非常大的話(huà)題,這里就不再展開(kāi)了。
把內存分成兩部分,一部分是靠近這個(gè)核,一部分靠近那個(gè)核,如果訪(fǎng)問(wèn)本核的話(huà)就會(huì )很快,靠近另一邊大概會(huì )耗費三倍的損耗。對于多核 CPU 的使用對性能提升很大的話(huà)就不要在意這個(gè)事情。
因為 TCP 的連接最麻煩的是在建立連接和關(guān)閉連接,這里有很多參數都是在調,每個(gè)地方重發(fā),多長(cháng)時(shí)間重發(fā),重發(fā)多少次。
這里給大家展示的是快啟動(dòng),有好幾個(gè)概念在里面,第一個(gè)概念在快速啟動(dòng)的時(shí)候是以?xún)杀兜乃俣取?/p>
因為網(wǎng)的帶寬是有限的,當你超出網(wǎng)絡(luò )帶寬的時(shí)候其中的網(wǎng)絡(luò )設備是會(huì )被丟包的,也就是控制量在往下降,那再恢復就會(huì )比較慢。
TCP 協(xié)議優(yōu)化,本來(lái)可能差不多要四個(gè)來(lái)回才能達到每次的傳輸在網(wǎng)絡(luò )中有幾十 K,那么提前配好的話(huà)用增大初始窗口讓它一開(kāi)始就達到最大流量。
提高資源效率,這一頁(yè)東西就挺多了,比如說(shuō)先從 CPU 看,TCP Defer Accept,如果有這個(gè)的話(huà),實(shí)際上會(huì )犧牲一些即時(shí)性,但帶來(lái)的好處是第一次建立好連接沒(méi)有內容過(guò)來(lái)的時(shí)候是不會(huì )激活 Nginx 做切換的。
內存在說(shuō)的時(shí)候是系統態(tài)的內存,在內存大和小的時(shí)候操作系統做了一次優(yōu)化,在壓力模式和非壓力模式下為每一個(gè)連接分配的系統內存可以動(dòng)態(tài)調整。
網(wǎng)絡(luò )設備的核心只解決一個(gè)問(wèn)題,變單個(gè)處理為批量處理,批量處理后吞吐量一定是會(huì )上升的。
因為消耗的資源變少了,切換次數變少了,它們的邏輯是一樣的,就這些邏輯和我一直在說(shuō)的邏輯都是同一個(gè)邏輯,只是應用在不同層面會(huì )產(chǎn)生不同的效果。
端口復用,像 Reals 是很好用的,因為它可以把端口用在上游服務(wù)連接的層面上,沒(méi)有帶來(lái)隱患。
提升多 CPU 使用效率,上面很多東西都說(shuō)到了,重點(diǎn)就兩個(gè),一是 CPU 綁定,綁定以后緩存更有效。多隊列網(wǎng)卡,從硬件層面上已經(jīng)能夠做到了。
BDP,帶寬肯定是知道的,帶寬和時(shí)延就決定了帶寬時(shí)延積,那吞吐量等于窗口或者時(shí)延。
內存分配速度也是我們關(guān)注的重點(diǎn),當并發(fā)量很大的時(shí)候內存的分配是比較糟糕的,大家可以看到有很多它的競品。
PCRE 的優(yōu)化,這用最新的版本就好。
原文轉自:https://mp.weixin.qq.com/s/gM48S7y6PR99Heh_g-NdCw