C10K和C10M

          計(jì)算機(jī)領(lǐng)域的很多技術(shù)都是需求推動的,上世紀(jì)90年代,由于互聯(lián)網(wǎng)的飛速發(fā)展,網(wǎng)絡(luò)服務(wù)器無法支撐快速增長的用戶規(guī)模。1999年,Dan
          Kegel提出了著名的C10問題:一臺服務(wù)器上同時(shí)處理10000個(gè)客戶網(wǎng)絡(luò)連接。10000個(gè)網(wǎng)絡(luò)連接并不會發(fā)送請求到服務(wù)器,有些連接并不活躍,同一時(shí)刻,只有極少的部分連接發(fā)送請求。不同的服務(wù)類型,每個(gè)連接發(fā)送請求的頻率也不相同,游戲服務(wù)器的連接會頻繁的發(fā)送請求,而Web服務(wù)器的連接發(fā)送請求的頻率就低很多。無論如何,根據(jù)經(jīng)驗(yàn)法則,對于特定的服務(wù)類型,連接越多,同一時(shí)刻發(fā)送請求的連接也越多。


          時(shí)至今日,C10K問題當(dāng)然早已解決,不僅如此,一臺機(jī)器能支撐的連接越來越多,后來提出了C10M問題,在一臺機(jī)器上支撐1000萬的連接,2015年,MigratoryData在單機(jī)承載12M的連接,解決了C10M問題。

          本文先回顧C(jī)10問題的解決方案,再探討如何構(gòu)建支撐C10M的應(yīng)用程序,聊聊其中涉及的各種技術(shù)。

          C10K問題的解決

          時(shí)間退回到1999年,當(dāng)時(shí)要實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)服務(wù)器,大概有這樣幾種模式

          簡單進(jìn)程/線程模型


          這是一種非常簡單的模式,服務(wù)器啟動后監(jiān)聽端口,阻塞在accept上,當(dāng)新網(wǎng)絡(luò)連接建立后,accept返回新連接,服務(wù)器啟動一個(gè)新的進(jìn)程/線程專門負(fù)責(zé)這個(gè)連接。從性能和伸縮性來說,這種模式是非常糟糕的,原因在于

          *
          進(jìn)程/線程創(chuàng)建和銷毀的時(shí)間,操作系統(tǒng)創(chuàng)建一個(gè)進(jìn)程/線程顯然需要時(shí)間,在一個(gè)繁忙的服務(wù)器上,如果每秒都有大量的連接建立和斷開,采用每個(gè)進(jìn)程/線程處理一個(gè)客戶連接的模式,每個(gè)新連接都要創(chuàng)建創(chuàng)建一個(gè)進(jìn)程/線程,當(dāng)連接斷開時(shí),銷毀對應(yīng)的線程/進(jìn)程。創(chuàng)建和銷毀進(jìn)程/線程的操作消耗了大量的CPU資源。使用進(jìn)程池和線程池可以緩解這個(gè)問題。
          *
          內(nèi)存占用。主要包含兩方面,一個(gè)是內(nèi)核數(shù)據(jù)結(jié)構(gòu)所占用的內(nèi)存空間,另外一個(gè)是Stack所占用的內(nèi)存。有些應(yīng)用的調(diào)用棧很深,比如Java應(yīng)用,經(jīng)常能看到幾十上百層的調(diào)用棧。
          *
          上下文切換的開銷。上下文切換時(shí),操作系統(tǒng)的調(diào)度器中斷當(dāng)前線程,選擇另外一個(gè)可運(yùn)行的線程在CPU上繼續(xù)運(yùn)行。調(diào)度器需要保存當(dāng)前線程的現(xiàn)場信息,然后選擇一個(gè)可運(yùn)行的線程,再將新線程的狀態(tài)恢復(fù)到寄存器中。保存和恢復(fù)現(xiàn)場所需要的時(shí)間和CPU型號有關(guān),選擇一個(gè)可運(yùn)行的線程則完全是軟件操作,Linux
          2.6才開始使用常量時(shí)間的調(diào)度算法。 以上是上下文切換的直接開銷。除此之外還有一些間接開銷,上下文切換導(dǎo)致相關(guān)的緩存失效,比如L1/L2
          Cache,TLB等,這些也會影響程序的性能,但是間接開銷很難衡量。
          有意思的是,這種模式雖然性能極差,但卻依然是我們今天最常見到的模式,很多Web程序都是這樣的方式在運(yùn)行。

          select/poll


          另外一種方式是使用select/poll,在一個(gè)線程內(nèi)處理多個(gè)客戶連接。select和poll能夠監(jiān)控多個(gè)socket文件描述符,當(dāng)某個(gè)文件描述符就緒,select/soll從阻塞狀態(tài)返回,通知應(yīng)用程序可以處理用戶連接了。使用這種方式,我們只需要一個(gè)線程就可以處理大量的連接,避免了多進(jìn)程/線程的開銷。之所以把select和poll放在一起說,原因在于兩者非常相似,性能上基本沒有區(qū)別,唯一的區(qū)別在于poll突破了select
          1024個(gè)文件描述符的限制,然而當(dāng)文件描述符數(shù)量增加時(shí),poll性能急劇下降,因此所謂突破1024個(gè)文件描述符實(shí)際上毫無意義。select/poll并不完美,依然存在很多問題:

          * 每次調(diào)用select/poll,都要把文件描述符的集合從用戶地址空間復(fù)制到內(nèi)核地址空間
          * select/poll返回后,調(diào)用方必須遍歷所有的文件描述符,逐一判斷文件描述符是否可讀/可寫。

          這兩個(gè)限制讓select/poll完全失去了伸縮性。連接數(shù)越多,文件描述符就越多,文件描述符越多,每次調(diào)用select/poll所帶來的用戶空間到內(nèi)核空間的復(fù)制開銷越大。最嚴(yán)重的是當(dāng)報(bào)文達(dá)到,select/poll返回之后,必須遍歷所有的文件描述符。假設(shè)現(xiàn)在有1萬個(gè)連接,其中只一個(gè)連接發(fā)送了請求,但是select/poll就要把1萬個(gè)連接全部檢查一遍。

          epoll

          FreeBSD 4.1引入了kqueue,此時(shí)是2000年7月,而在Linux上,還要等待2年后的2002年才開始引入kqueue的類似實(shí)現(xiàn):
          epoll。epoll最初于 2.5.44進(jìn)入Linux kernel mainline,此時(shí)已經(jīng)是2002年,距離C10K問題提出已經(jīng)過了3年。

          epoll是如何提供一個(gè)高性能可伸縮的IO多路復(fù)用機(jī)制呢?首先,epoll引入了epoll instance這個(gè)概念,epoll
          instance在內(nèi)核中關(guān)聯(lián)了一組要監(jiān)聽的文件描述符配置:interest
          list,這樣的好處在于,每次要增加一個(gè)要監(jiān)聽的文件描述符,不需要把所有的文件描述符都配置一次,然后從用戶地址空間復(fù)制到內(nèi)核地址空間,只需要把單個(gè)文件描述符復(fù)制到內(nèi)核地址空間,復(fù)制開銷從O(n)降到了O(1)。


          注冊完文件描述符后,調(diào)用epoll_wait開始等待文件描述符事件。epoll_wait可以只返回已經(jīng)ready的文件描述符,因此,在epoll_wait返回之后,程序只需要處理真正需要處理的文件描述符,而不用把所有的文件描述符全部遍歷一遍。假設(shè)在全部N個(gè)文件描述符中,只有一個(gè)文件描述符Ready,select/poll要執(zhí)行N次循環(huán),epoll只需要一次。


          epoll出現(xiàn)之后,Linux上才真正有了一個(gè)可伸縮的IO多路復(fù)用機(jī)制。基于epoll,能夠支撐的網(wǎng)絡(luò)連接數(shù)取決于硬件資源的配置,而不再受限于內(nèi)核的實(shí)現(xiàn)機(jī)制。CPU越強(qiáng),內(nèi)存越大,能支撐的連接數(shù)越多。

          編程模型

          Reactor和proactor


          不同的操作系統(tǒng)上提供了不同的IO多路復(fù)用實(shí)現(xiàn),Linux上有epoll,F(xiàn)reeBSD有kqueue,Windows有IOCP。對于需要跨平臺的程序,必然需要一個(gè)抽象層,提供一個(gè)統(tǒng)一的IO多路復(fù)用接口,屏蔽各個(gè)系統(tǒng)接口的差異性。

          Reactor是實(shí)現(xiàn)這個(gè)目標(biāo)的一次嘗試,最早出現(xiàn)在Douglas C. Schmidt的論文"The Reactor An Object-Oriented
          Wrapper for Event-Driven Port Monitoring and Service
          Demultiplexing"中。從論文的名字可以看出,Reactor是poll這種編程模式的一個(gè)面向?qū)ο蟀b。考慮到論文的時(shí)間,當(dāng)時(shí)正是面向?qū)ο蟾拍钫馃岬臅r(shí)候,什么東西都要蹭蹭面向?qū)ο蟮臒岫?。論文中,DC
          Schmidt描述了為什么要做這樣的一個(gè)Wrapper,給出了下面幾個(gè)原因

          * 操作系統(tǒng)提供的接口太復(fù)雜,容易出錯(cuò)。select和poll都是通用接口,因?yàn)橥ㄓ?,增加了學(xué)習(xí)和正確使用的復(fù)雜度。
          * 接口抽象層次太低,涉及太多底層的細(xì)節(jié)。
          * 不能跨平臺移植。
          * 難以擴(kuò)展。

          實(shí)際上除了第三條跨平臺,其他幾個(gè)理由實(shí)在難以站得住腳。select/poll這類接口復(fù)雜嗎,使用起來容易出錯(cuò)嗎,寫出來的程序難以擴(kuò)展嗎?不過不這么說怎么體現(xiàn)Reactor的價(jià)值呢。正如論文名稱所說的,Reactor本質(zhì)是對操作系統(tǒng)IO多路復(fù)用機(jī)制的一個(gè)面向?qū)ο蟀b,為了證明Reactor的價(jià)值,DC
          Schmidt還用C++面向?qū)ο蟮奶匦詫?shí)現(xiàn)了一個(gè)編程框架:ACE,實(shí)際上使用ACE比直接使用poll或者epoll復(fù)雜多了。

          后來DC Schmidt寫了一本書《面向模式的軟件架構(gòu)》,再次提到了Reactor,并重新命名為Reactor
          Pattern,現(xiàn)在網(wǎng)絡(luò)上能找到的Reactor資料,基本上都是基于Reactor Pattern,而不是早期的面向Object-Orientend
          Wrapper。

          《面向模式的軟件》架構(gòu)中還提到了另外一種叫做Proactor的模式,和Reactor非常類似,Reactor針對同步IO,Proactor則針對異步IO。

          Callback,F(xiàn)uture和纖程


          Reactor看上去并不復(fù)雜,但是想編寫一個(gè)完整的應(yīng)用程序時(shí)候就會發(fā)現(xiàn)其實(shí)沒那么簡單。為了避免Reactor主邏輯阻塞,所有可能會導(dǎo)致阻塞的操作必須注冊到epoll上,帶來的問題就是處理邏輯的支離破碎,大量使用callback,產(chǎn)生的代碼復(fù)雜難懂。如果應(yīng)用程序中還有非網(wǎng)絡(luò)IO的阻塞操作,問題更嚴(yán)重,比如在程序中讀寫文件。Linux中文件系統(tǒng)操作都是阻塞的,雖然也有Linux
          AIO,但是一直不夠成熟,難堪大用。很多軟件采用線程池來解決這個(gè)問題,不能通過epoll解決的阻塞操作,扔到一個(gè)線程池執(zhí)行。這又產(chǎn)生了多線程內(nèi)存開銷和上下文切換的問題。


          Future機(jī)制是對Callback的簡單優(yōu)化,本質(zhì)上還是Callback,但是提供了一致的接口,代碼相對來說簡單一些,不過在實(shí)際使用中還是比較復(fù)雜的。Seastar是一個(gè)非常徹底的future風(fēng)格的框架,從它的代碼可以看到這種編程風(fēng)格真的非常復(fù)雜,阻塞式編程中一個(gè)函數(shù)幾行代碼就能搞定的事情,在Seastar里需要上百行代碼,幾十個(gè)labmda
          (在Seastar里叫做continuation)。


          纖程是一種用戶態(tài)調(diào)度的線程,比如Go語言中的goroutine,有些人可能會把這種機(jī)制成為coroutine,不過我認(rèn)為coroutine和纖程還是有很大區(qū)別的,coroutine是泛化的子進(jìn)程,具有多個(gè)進(jìn)入和退出點(diǎn),用來一些一些相互協(xié)作的程序,典型的例子就是Python中的generator。纖程則是一種運(yùn)行和調(diào)度機(jī)制。


          纖程真正做到了高性能和易用,在Go語言中,使用goroutine實(shí)現(xiàn)的高性能服務(wù)器是一件輕松愉快的事情,完全不用考慮線程數(shù)、epoll、回調(diào)之類的復(fù)雜操作,和編寫阻塞式程序完全一樣。

          網(wǎng)絡(luò)優(yōu)化

          Kernel bypass


          網(wǎng)絡(luò)子系統(tǒng)是Linux內(nèi)核中一個(gè)非常龐大的組件,提供了各種通用的網(wǎng)絡(luò)能力。通用通常意味在在某些場景下并不是最佳選擇。實(shí)際上業(yè)界的共識是Linux內(nèi)核網(wǎng)絡(luò)不支持超大并發(fā)的網(wǎng)絡(luò)能力。根據(jù)我過去的經(jīng)驗(yàn),Linux最大只能處理1MPPS,而現(xiàn)在的10Gbps網(wǎng)卡通??梢蕴幚?0MPPS。隨著更高性能的25Gbps,40Gbps網(wǎng)卡出現(xiàn),Linux內(nèi)核網(wǎng)絡(luò)能力越發(fā)捉襟見肘。

          為什么Linux不能充分發(fā)揮網(wǎng)卡的處理能力?原因在于:

          * 大多數(shù)網(wǎng)卡收發(fā)使用中斷方式,每次中斷處理時(shí)間大約100us,另外要考慮cache
          miss帶來的開銷。部分網(wǎng)卡使用NAPI,輪詢+中斷結(jié)合的方式處理報(bào)文,當(dāng)報(bào)文放進(jìn)隊(duì)列之后,依然要觸發(fā)軟中斷。
          * 數(shù)據(jù)從內(nèi)核地址空間復(fù)制到用戶地址空間。
          * 收發(fā)包都有系統(tǒng)調(diào)用。
          * 網(wǎng)卡到應(yīng)用進(jìn)程的鏈路太長,包含了很多不必要的操作。
          Linux高性能網(wǎng)絡(luò)一個(gè)方向就是繞過內(nèi)核的網(wǎng)絡(luò)棧(kernel bypass),業(yè)界有不少嘗試

          * PF_RING 高效的數(shù)據(jù)包捕獲技術(shù),比libpcap性能更好。需要自己安裝內(nèi)核模塊,啟用ZC
          Driver,設(shè)置transparent_mode=2的情況下,報(bào)文直接投遞到客戶端程序,繞過內(nèi)核網(wǎng)絡(luò)棧。
          * Snabbswitch 一個(gè)Lua寫的網(wǎng)絡(luò)框架。完全接管網(wǎng)卡,使用UIO(Userspace IO)技術(shù)在用戶態(tài)實(shí)現(xiàn)了網(wǎng)卡驅(qū)動。
          * Intel DPDK,直接在用戶態(tài)處理報(bào)文。非常成熟,性能強(qiáng)大,限制是只能用在Intel的網(wǎng)卡上。根據(jù)DPDK的數(shù)據(jù),3GHz的CPU
          Core上,平均每個(gè)報(bào)文的處理時(shí)間只要60ns(一次內(nèi)存的訪問時(shí)間)。
          * Netmap
          一個(gè)高性能收發(fā)原始數(shù)據(jù)包的框架,包含了內(nèi)核模塊以及用戶態(tài)庫函數(shù),需要網(wǎng)卡驅(qū)動程序配合,因此目前只支持特定的幾種網(wǎng)卡類型,用戶也可以自己修改網(wǎng)卡驅(qū)動。
          * XDP,使用Linux eBPF機(jī)制,將報(bào)文處理邏輯下放到網(wǎng)卡驅(qū)動程序中。一般用于報(bào)文過濾、轉(zhuǎn)發(fā)的場景。
          kernel bypass技術(shù)最大的問題在于不支持POSIX接口,用戶沒辦法不修改代碼直接移植到一種kernel
          bypass技術(shù)上。對于大多數(shù)程序來說,還要要運(yùn)行在標(biāo)準(zhǔn)的內(nèi)核網(wǎng)絡(luò)棧上,通過調(diào)整內(nèi)核參數(shù)提升網(wǎng)絡(luò)性能。

          網(wǎng)卡多隊(duì)列


          報(bào)文到達(dá)網(wǎng)卡之后,在一個(gè)CPU上觸發(fā)中斷,CPU執(zhí)行網(wǎng)卡驅(qū)動程序從網(wǎng)卡硬件緩沖區(qū)讀取報(bào)文內(nèi)容,解析后放到CPU接收隊(duì)列上。這里所有的操作都在一個(gè)特定的CPU上完成,高性能場景下,單個(gè)CPU處理不了所有的報(bào)文。對于支持多隊(duì)列的網(wǎng)卡,報(bào)文可以分散到多個(gè)隊(duì)列上,每個(gè)隊(duì)列對應(yīng)一個(gè)CPU處理,解決了單個(gè)CPU處理瓶頸。


          為了充分發(fā)揮多隊(duì)列網(wǎng)卡的價(jià)值,我們還得做一些額外的設(shè)置:把每個(gè)隊(duì)列的中斷號綁定到特定CPU上。這樣做的目的,一方面確保網(wǎng)卡中斷的負(fù)載能分配到不同的CPU上,另外一方面可以將負(fù)責(zé)網(wǎng)卡中斷的CPU和負(fù)責(zé)應(yīng)用程序的CPU區(qū)分開,避免相互干擾。


          在Linux中,/sys/class/net/${interface}/device/msi_irqs下保存了每個(gè)隊(duì)列的中斷號,有了中斷號之后,我們就可以設(shè)置中斷和CPU的對應(yīng)關(guān)系了。網(wǎng)上有很多文章可以參考。

          網(wǎng)卡Offloading

          回憶下TCP數(shù)據(jù)的發(fā)送過程:應(yīng)用程序?qū)?shù)據(jù)寫到套接字緩沖區(qū),內(nèi)核將緩沖區(qū)數(shù)據(jù)切分成不大于MSS的片段,附加上TCP Header和IP
          Header,計(jì)算Checksum,然后將數(shù)據(jù)推到網(wǎng)卡發(fā)送隊(duì)列。這個(gè)過程中需要CPU全程參與,
          隨著網(wǎng)卡的速度越來越快,CPU逐漸成為瓶頸,CPU處理數(shù)據(jù)的速度已經(jīng)趕不上網(wǎng)卡發(fā)送數(shù)據(jù)的速度。經(jīng)驗(yàn)法則,發(fā)送或者接收1bit/s
          TCP數(shù)據(jù),需要1Hz的CPU,1Gbps需要1GHz的CPU,10Gbps需要10GHz的CPU,已經(jīng)遠(yuǎn)超單核CPU的能力,即使能完全使用多核,假設(shè)單個(gè)CPU
          Core是2.5GHz,依然需要4個(gè)CPU Core。

          為了優(yōu)化性能,現(xiàn)代網(wǎng)卡都在硬件層面集成了TCP分段、添加IP Header、計(jì)算Checksum等功能,這些操作不再需要CPU參與。這個(gè)功能叫做tcp
          segment offloading,簡稱tso。使用ethtool -k 可以檢查網(wǎng)卡是否開啟了tso

          除了tso,還有其他幾種offloading,比如支持udp分片的ufo,不依賴驅(qū)動的gso,優(yōu)化接收鏈路的lro

          充分利用多核


          隨著摩爾定律失效,CPU已經(jīng)從追求高主頻轉(zhuǎn)向追求更多的核數(shù),現(xiàn)在的服務(wù)器大都是96核甚至更高。構(gòu)建一個(gè)支撐C10M的應(yīng)用程序,必須充分利用所有的CPU,最重要的是程序要具備水平伸縮的能力:隨著CPU數(shù)量的增多程序能夠支撐更多的連接。


          很多人都有一個(gè)誤解,認(rèn)為程序里使用了多線程就能利用多核,考慮下CPython程序,你可以創(chuàng)建多個(gè)線程,但是由于GIL的存在,程序最多只能使用單個(gè)CPU。實(shí)際上多線程和并行本身就是不同的概念,多線程表示程序內(nèi)部多個(gè)任務(wù)并發(fā)執(zhí)行,每個(gè)線程內(nèi)的任務(wù)可以完全不一樣,線程數(shù)和CPU核數(shù)沒有直接關(guān)系,單核機(jī)器上可以跑幾百個(gè)線程。并行則是為了充分利用計(jì)算資源,將一個(gè)大的任務(wù)拆解成小規(guī)模的任務(wù),分配到每個(gè)CPU上運(yùn)行。并行可以
          通過多線程實(shí)現(xiàn),系統(tǒng)上有幾個(gè)CPU就啟動幾個(gè)線程,每個(gè)線程完成一部分任務(wù)。


          并行編程的難點(diǎn)在于如何正確處理共享資源。并發(fā)訪問共享資源,最簡單的方式就加鎖,然而使用鎖又帶來性能問題,獲取鎖和釋放鎖本身有性能開銷,鎖保護(hù)的臨界區(qū)代碼不能只能順序執(zhí)行,就像CPython的GIL,沒能充分利用CPU。

          Thread Local和Per-CPU變量

          這兩種方式的思路是一樣的,都是創(chuàng)建變量的多個(gè)副本,使用變量時(shí)只訪問本地副本,因此不需要任何同步。現(xiàn)代編程語言基本上都支持Thread
          Local,使用起來也很簡單,C/C++里也可以使用__thread標(biāo)記聲明ThreadLocal變量。


          Per-CPU則依賴操作系統(tǒng),當(dāng)我們提到Per-CPU的時(shí)候,通常是指Linux的Per-CPU機(jī)制。Linux內(nèi)核代碼中大量使用Per-CPU變量,但應(yīng)用代碼中并不常見,如果應(yīng)用程序中工作線程數(shù)等于CPU數(shù)量,且每個(gè)線程Pin到一個(gè)CPU上,此時(shí)才可以使用。

          原子變量


          如果共享資源是int之類的簡單類型,訪問模式也比較簡單,此時(shí)可以使用原子變量。相比使用鎖,原子變量性能更好。在競爭不激烈的情況下,原子變量的操作性能基本上和加鎖的性能一致,但是在并發(fā)比較激烈的時(shí)候,等待鎖的線程要進(jìn)入等待隊(duì)列等待重新調(diào)度,這里的掛起和重新調(diào)度過程需要上下文切換,浪費(fèi)了更多的時(shí)間。

          大部分編程語言都提供了基本變量對應(yīng)的原子類型,一般提供set, get, compareAndSet等操作。

          lock-free

          lock-free這個(gè)概念來自

          An algorithm is called non‐blocking if failure or suspension of any thread
          cannot cause failure or suspension of another thread; an algorithm is called
          lock‐free if, at each step, some thread can make progress.


          non-blocking算法任何線程失敗或者掛起,不會導(dǎo)致其他線程失敗或者掛起,lock-free則進(jìn)一步保證線程間無依賴。這個(gè)表述比較抽象,具體來說,non-blocking要求不存在互斥,存在互斥的情況下,線程必須先獲取鎖再進(jìn)入臨界區(qū),如果當(dāng)前持有鎖的線程被掛起,等待鎖的線程必然需要一直等待下去。對于活鎖或者饑餓的場景,線程失敗或者掛起的時(shí)候,其他線程完全不僅能正常運(yùn)行,說不定還解決了活鎖和饑餓的問題,因此活鎖和饑餓符合non-blocking,但是不符合lock-free。

          實(shí)現(xiàn)一個(gè)lock-free數(shù)據(jù)結(jié)構(gòu)并不容易,好在已經(jīng)有了幾種常見數(shù)據(jù)結(jié)構(gòu)的的lock-free實(shí)現(xiàn):buffer, list, stack, queue,
          map, deque,我們直接拿來使用就行了。

          優(yōu)化對鎖的使用


          有時(shí)候沒有條件使用lock-free,還是得用鎖,對于這種情況,還是有一些優(yōu)化手段的。首先使用盡量減少臨界區(qū)的大小,使用細(xì)粒度的鎖,鎖粒度越細(xì),并行執(zhí)行的效果越好。其次選擇適合的鎖,比如考慮選擇讀寫鎖。

          CPU affinity

          使用CPU affinity機(jī)制合理規(guī)劃線程和CPU的綁定關(guān)系。前面提到使用CPU
          affinity機(jī)制,將多隊(duì)列網(wǎng)卡的中斷處理分散到多個(gè)CPU上。不僅是中斷處理,線程也可以綁定,綁定之后,線程只會運(yùn)行在綁定的CPU上。為什么要將線程綁定到CPU上呢?綁定CPU有這樣幾個(gè)好處

          * 為線程保留CPU,確保線程有足夠的資源運(yùn)行
          * 提高CPU cache的命中率,某些對cache敏感的線程必須綁定到CPU上才行。
          *
          更精細(xì)的資源控制。可以預(yù)先需要靜態(tài)劃分各個(gè)工作線程的資源,例如為每個(gè)請求處理線程分配一個(gè)CPU,其他后臺線程共享一個(gè)CPU,工作線程和中斷處理程序工作在不同的CPU上。
          *
          NUMA架構(gòu)中,每個(gè)CPU有自己的內(nèi)存控制器和內(nèi)存插槽,CPU訪問本地內(nèi)存別訪問遠(yuǎn)程內(nèi)存快3倍左右。使用affinity將線程綁定在CPU上,相關(guān)的數(shù)據(jù)也分配到CPU對應(yīng)的本地內(nèi)存上。
          Linux上設(shè)置CPU affinity很簡單,可以使用命令行工具taskset,也可以在程序內(nèi)直接調(diào)用API sched_getaffinity和
          sched_setaffinity

          其他優(yōu)化技術(shù)

          使用Hugepage


          Linux中,程序內(nèi)使用的內(nèi)存地址是虛擬地址,并不是內(nèi)存的物理地址。為了簡化虛擬地址到物理地址的映射,虛擬地址到物理地址的映射最小單位是“Page”,默認(rèn)情況下,每個(gè)頁大小為4KB。CPU指令中出現(xiàn)的虛擬地址,為了讀取內(nèi)存中的數(shù)據(jù),指令執(zhí)行前要把虛擬地址轉(zhuǎn)換成內(nèi)存物理地址。Linux為每個(gè)進(jìn)程維護(hù)了一張?zhí)摂M地址到物理地址的映射表,CPU先查表找到虛擬地址對應(yīng)的物理地址,再執(zhí)行指令。由于映射表維護(hù)在內(nèi)存中,CPU查表就要訪問內(nèi)存。相對CPU的速度來說,內(nèi)存其實(shí)是相當(dāng)慢的,一般來說,CPU
          L1
          Cache的訪問速度在1ns左右,而一次內(nèi)存訪問需要60-100ns,比CPU執(zhí)行一條指令要慢得多。如果每個(gè)指令都要訪問內(nèi)存,比如嚴(yán)重拖慢CPU速度,為了解決這個(gè)問題,CPU引入了TLB(translation
          lookaside buffer),一個(gè)高性能緩存,緩存映射表中一部分條目。轉(zhuǎn)換地址時(shí),先從TLB查找,沒找到再讀內(nèi)存。


          顯然,最理想的情況是映射表能夠完全緩存到TLB中,地址轉(zhuǎn)換完全不需要訪問內(nèi)存。為了減少映射表大小,我們可以使用“HugePages”:大于4KB的內(nèi)存頁。默認(rèn)HugePages是2MB,最大可以到1GB。

          避免動態(tài)分配內(nèi)存


          內(nèi)存分配是個(gè)復(fù)雜且耗時(shí)的操作,涉及空閑內(nèi)存管理、分配策略的權(quán)衡(分配效率,碎片),尤其是在并發(fā)環(huán)境中,還要保證內(nèi)存分配的線程安全。如果內(nèi)存分配成為了應(yīng)用瓶頸,可以嘗試一些優(yōu)化策略。比如內(nèi)存復(fù)用i:不要重復(fù)分配內(nèi)存,而是復(fù)用已經(jīng)分配過的內(nèi)存,在C++/Java里則考慮復(fù)用已有對象,這個(gè)技巧在Java里尤其重要,不僅能降低對象創(chuàng)建的開銷,還避免了大量創(chuàng)建對象導(dǎo)致的GC開銷。另外一個(gè)技巧是預(yù)先分配內(nèi)存,實(shí)際上相當(dāng)于在應(yīng)用內(nèi)實(shí)現(xiàn)了一套簡單的內(nèi)存管理,比如Memcached的Slab。

          Zero Copy


          對于一個(gè)Web服務(wù)器來說,響應(yīng)一個(gè)靜態(tài)文件請求需要先將文件從磁盤讀取到內(nèi)存中,再發(fā)送到客戶端。如果自信分析這個(gè)過程,會發(fā)現(xiàn)數(shù)據(jù)首先從磁盤讀取到內(nèi)核的頁緩沖區(qū),再從頁緩沖區(qū)復(fù)制到Web服務(wù)器緩沖區(qū),接著從Web服務(wù)器緩沖區(qū)發(fā)送到TCP發(fā)送緩沖區(qū),最后經(jīng)網(wǎng)卡發(fā)送出去。這個(gè)過程中,數(shù)據(jù)先從內(nèi)核復(fù)制到進(jìn)程內(nèi),再從進(jìn)程內(nèi)回到內(nèi)核,這兩次復(fù)制完全是多余的。Zero
          Copy就是類似情況的優(yōu)化方案,數(shù)據(jù)直接在內(nèi)核中完成處理,不需要額外的復(fù)制。

          Linux中提供了幾種ZeroCopy相關(guān)的技術(shù),包括sendfile,splice,copy_file_range,Web服務(wù)器中經(jīng)常使用sendfile
          優(yōu)化性能。

          最后

          千萬牢記:不要過早優(yōu)化。

          優(yōu)化之前,先考慮兩個(gè)問題:

          * 現(xiàn)在的性能是否已經(jīng)滿足需求了
          * 如果真的要優(yōu)化,是不是已經(jīng)定位了瓶頸
          在回答清楚這兩個(gè)問題之前,不要盲目動手。

          友情鏈接
          ioDraw流程圖
          API參考文檔
          OK工具箱
          云服務(wù)器優(yōu)惠
          阿里云優(yōu)惠券
          騰訊云優(yōu)惠券
          京東云優(yōu)惠券
          站點(diǎn)信息
          問題反饋
          郵箱:[email protected]
          QQ群:637538335
          關(guān)注微信

                操逼内射合集视频 | 久久伦理视频 | 无码精品视频免费看 | 激情五月天成人网站 | 中国1级黄片 |