服務端經典的C10k問題(譯)

愛布偶的zzy發表於2019-04-09

服務端經典的C10K問題

最近看了一下Unix網路程式設計相關的內容,然後發現了一篇非常經典的文章, 內容可能不是很新,不過真的很經典,C10K問題,簡單翻譯了一下(markdown轉換過來格式可能存在少量問題)對掌握linux io和 linux 執行緒會有更深的理解.(文記憶體在大量的連結.) 原文連結 The C10K problem

現在 web 伺服器需要同時處理上萬請求,難道不是嗎?畢竟如今的網路將會有很大的發展空間. 計算機也同樣強大.你可以花1200美元買一臺 1000MHz,2G 記憶體和1000Mbits/sec的網路卡的機器.讓我們來看看-- 20000 客戶端,每個客戶端 50KHz, 1000Kb 和每秒 50Kb,那沒有什麼比這兩萬個客戶端每個每秒從磁碟中取出4千位元組並將它們每秒傳送到網路上去更消耗資源了.(順便說一下,每個客戶端0.0.8美元,一些作業系統收費的單個客戶端 美元的許可費看起來有點貴)所以硬體不再是一種瓶頸.

在1999年,最繁忙的 ftp 網站之一, cdrom.com, 實際上通過一個千兆乙太網網路卡同時處理 10000個客戶端.現在相同的速度也被ISP 提供,他們希望它變得越來越受大型企業客戶的歡迎.

輕量級的客戶端計算模型似乎又開始變得流行起來了 - 伺服器在網際網路上執行,為數千個客戶提供服務.

基於以上的一些考慮,這有一些關於如何配置作業系統或者編寫支援數千客戶端的程式碼問題提出了一些注意點. 討論的中心的主要是圍繞著類Unix作業系統,因為這是我個人感興趣的領域,但是Windows也會涉及一點.

內容

  • C10K問題
    • 內容]
    • 相關網站
    • 預讀書籍
    • I/O 框架
    • I/O 策略
      • 1. 一個執行緒服務多個客戶端,使用非阻塞 IO 和水平觸發的就緒通知
      • 2. 一個執行緒服務多個客戶端,使用非阻塞 IO 和就緒改變通知
      • 3. 一個執行緒服務多個客戶端,使用非同步 I/O
      • 4. 一個執行緒服務一個客戶端
      • Linux執行緒
        • NGPT: Linux的下一代 Posix 執行緒
        • NPTL: Linux原生的 Posix 執行緒庫
        • FreeBSD 執行緒支援
        • NetBSD 執行緒支援
        • Solaris 執行緒支援
        • JDK 1.3.x及更早版本中的Java執行緒支援
        • 注意:1:1 執行緒與 M:N執行緒
      • 5. 將服務端程式碼構建到核心中
    • 將 TCP 協議棧帶入使用者空間
    • 評論
    • 開啟檔案控制程式碼的限制
    • 執行緒限制
    • Java問題]
    • 其他建議
    • 其他限制
    • 核心問題
    • 測試服務效能
    • 例子
    • 有趣的基於 select() 的伺服器
    • 有趣的基於 /dev/poll 的伺服器
    • 有趣的基於 epoll 的伺服器
    • 有趣的基於 kqueue() 的伺服器
    • 有趣的基於實時訊號的伺服器
    • 有趣的基於執行緒的伺服器
    • 有趣的核心伺服器
    • 其他有趣的連結

相關網站

參閱下 Nick Black 的傑出的 快速的Unix伺服器網頁,瞭解大約2009的情況.

在2003年10月,Felix von Leitner 整理了一個優秀的關於網路可擴充套件性的網站演示,完成了多種不同的網路系統呼叫和作業系統的效能比較.其中一個發現是 linux 2.6 核心擊敗了 2.4 核心,當然這裡有很多很好的圖片會讓作業系統開發人員在平時提供點想法.

預讀書籍

如果你沒有閱讀過the late W. Richard Stevens的Unix網路程式設計: 網路Apis:套接字和Xti(第1卷)的拷貝,請儘快獲取一份,它描述了很多的於 I/O 策略和編寫高效能伺服器的陷阱.它甚至談到了 'thundering herd'問題.當你在閱讀它時,請閱讀 Jeff Darcy寫的關於高效能伺服器設計.

(另外一本書構建可擴充套件的網站可能會對使用而不是編寫一個web伺服器的人會有幫助)

I/O 框架

以下提供了幾個預打包的庫,它們抽象了下面介紹的一些技術,使程式碼與作業系統隔離,並使其更具可移植性.

  • ACE,一個輕量級的 C++ I/O 框架,包含一些用面對物件的思想實現的 I/O 策略和許多其他有用的事情.特別的,他的 Reactor 以面對物件的方式執行非阻塞 I/O,Proactor 是一種面對物件處理非同步 I/O 的的方式.
  • ASIO 是一個 C++ I/O 框架,它正在成為Boost的一部分.這就像是為 STL 時代更新的ACE.
  • libevent 是 Niels Provos 寫的一個輕量級的 C I/O 框架.它支援 kqueue 和 select,即將支援 poll 和 epoll.我想它應該只採用了水平觸發,這具有兩面性.Niels給了一個圖來說明時間和連線數目在處理一個事件上的功能,圖中可以看出kqueue 和 sys_epoll 是明顯的贏家.
  • 我自己在輕量級框架的嘗試(可惜的是沒有保持更新)
    • Poller 是一個輕量級的 C++ I/O 框架,它使用任何一種準備就緒API(poll, select, /dev/poll, kqueue, sigio)實現水平觸發準備就緒API. 以其他多種 API 為基礎測試,Poll的效能好的多.文件鏈到下面的Poller 子類,該連結文件的下面一部分說明了如何使用這些準備就緒API.
    • rn 是一個輕量級的C I/O 框架,這是我在Poller之後的第二次嘗試. 他使用lgpl(因此它更容易在商業應用程式中使用) 和 C(因此更容易在非 C++ 的產品中使用).如今它被應用在一些商業產品中.
  • Matt Welsh 在2000年4月寫了一篇關於如何在構建可擴充套件性服務時去平衡工作執行緒和事件驅動使用的論文,該論文描述了他的 Sandstorm I/O 框架.
  • Cory Nelson 的Scale!庫 - 一個Windows下的非同步套接字, 檔案, 和管道 I/O 庫.

I/O 策略

網路軟體的設計者有多種選擇.這有一些:

  • 是否以及如何在單個執行緒發出多個 I/O 呼叫
    • 不處理;使用阻塞和同步呼叫,儘可能的使用多個執行緒和程式實現併發.
    • 使用非阻塞呼叫(如,在一個socket write()上設定 O_NONBLOCK) 去啟動 I/O,就緒通知(如,poll() 或則 /dev/poll)知道什麼時候通道是 OK 的然後開啟下一個 I/O.通常這隻能用於網路 I/O,而不能用於磁碟 I/O.
    • 使用非同步呼叫(如,aio_write())去啟動 I/O,完成通知(如,訊號或完成埠)去通知 I/O 完成.這同時適用於網路和磁碟 I/O.
  • 如何控制每個客戶的服務
    • 一個程式服務一個客戶(經典的 Unix 方法,從1980年左右就開始使用)
    • 一個系統級別執行緒服務多個客戶;每個客戶通過以下控制:
      • 一個使用者級別執行緒(如. GNU 狀態執行緒, 帶綠色執行緒的經典 java)
      • 狀態機(有點深奧,但在某些圈子裡很受歡迎; 我的最愛)
      • continuation (有點深奧,但在某些圈子裡很受歡迎; 我的最愛)
    • 一個系統級執行緒服務單個客戶(如,經典的帶有原生執行緒的Java)
  • 一個系統級執行緒服務每個活躍的客戶(如. Tomcat與apache的前端;NT完成埠; 執行緒池)
  • 是否使用標準系統服務,或者構建服務到核心中(如,在一些自定義驅動,核心模組,或者 VxD)

下邊的5中組合似乎非常流行:

  1. 一個執行緒服務多個客戶端.使用非阻塞 I/O 和水平觸發就緒通知.
  2. 一個執行緒服務多個客戶端.使用非阻塞 I/O 和就緒更改通知.
  3. 一個執行緒服務多個客戶端. 使用非同步 I/O.
  4. 一個執行緒服務多個客戶端.使用阻塞 I/O
  5. 將服務端程式碼構建到核心

1. 一個執行緒服務多個客戶端,使用非阻塞 IO 和水平觸發就緒通知

... 在所有的網路控制程式碼上都設定為非阻塞模式,使用 select() 或則 poll() 去告知哪個網路控制程式碼處理有資料等待.此模型是最傳統的.這種模式下,核心告訴你是否一個檔案描述符就緒,自從上次核心告訴你它以來,你是否對該檔案描述符做了任何事情.('水平觸發'這個名詞來自計算機硬體設計;它與'邊緣觸發'相反).Jonathon Lemon在他的關於BSDCON 2000 paper kqueue()的論文中介紹了這些術語

注意: 牢記來自核心的就緒通知只是一個提示,這一點尤為重要;當你嘗試去讀取檔案描述符的時候,它可能沒有就緒.這就是為什麼需要在使用就緒通知時使用非阻塞模式的原因.

一個重要的瓶頸是 read()或 sendfile() 從磁碟塊讀取時,如果該頁當前並不在記憶體中.在設定非阻塞模式的磁碟檔案處理是沒有影響的.記憶體對映磁碟檔案也是如此.首先一個服務需要磁碟 I/O時,他的處理塊,所有客戶端必須等待,因此原生非執行緒效能將會被浪費了.

這也就是非同步 I/O 的目的,當然僅限於沒有 AIO 的系統上,用多執行緒和多程式進行磁碟 I/O 也可能解決這個瓶頸.一種方法是使用記憶體對映檔案,如果 mincore() 表示需要 I/O,讓一個工作執行緒去進行 I/O 操作,並繼續處理網路流量.Jef Poskanzer 提到 Pai, Druschel, and Zwaenepoel的1999 Flash web伺服器使用這個技巧;他們在Usenix '99發表了關於它的演講.看起來 mincore() 在BSD-derived Unixes 上是可用的,如像FreeBSD和Solaris,但它不是單Unix規範的一部分.從kernel2.3.51 開始,它也開始是linux的一部分,感謝Chuck Lever.

但是在2003年十一月的 freebsd-hackers list, Vivek Pei 等人報導了使用他們的 Flash web伺服器有一個很好的結果.然後在攻擊其瓶頸,其中發現一個瓶頸是 mincore(猜測之後這不是一個好辦法),另外一個就是 sendfile 阻塞磁碟訪問;他們一種修改的 sendfile(),當他的訪問磁碟頁尚未處於核心狀態時返回類似 EWOULDBLOCK 的內容,提升了效能.(不知道怎麼告訴使用者頁現在是常駐的...在我看來真正需要的是aio_sendfile().)他們優化的最終結果是在 1GHZ/1GB 的FreeBSD盒子上 SpecWeb99 得分約為800,這比spec.org上的任何檔案都要好.

在非阻塞套接字的集合中,關於單一執行緒如何告知哪個套接字是準備就緒的,列出了幾種方法:

  • 傳統的 select()
    不幸的, select() 限制了 FD_SETSIZE 的處理.這個限制被編譯到標準庫和使用者程式中.(一些 C 庫版本讓你在編譯應用程式的時候提示這個限制.)
    參閱Poller_select (cc,h)是一個如何使用 select() 與其他就緒通知模式互動的示例.
  • 傳統的 poll()
    對於 poll() 能處理的檔案描述符數量的沒有硬編碼限制,但是當有上千連線時會變得慢,因為大多數檔案描述符在某個時刻都是是空閒的,完全掃描成千上萬個檔案描述符會花費時間.
    一些作業系統(像,Solaris 8)使用像 poll hinting 的技術加速了 poll() 等,Niels Provos 在1999年為Linux實現和並利用基準測試程式測試.
    參閱Poller_poll (cc,h, benchmarks)是一個如何使用 poll() 與其他就緒通知模式互動的示例.
  • /dev/poll
    這是推薦在Solaris 代替poll的方法
    /dev/poll 的背後思想就是利用 poll() 在大部分的呼叫時使用相同的引數.使用/dev/poll,獲取一個 /dev/poll 的檔案描述符,然後把你關心的檔案描述符寫入到/dev/poll的描述符;然後,你就可以從該控制程式碼中讀取當前就緒檔案描述符集.
    它悄悄的出現在 Solaris 7 中(看 patchid 106541),但是它第一次公開現身是在Solaris 8中;據 Sun 透露,在750客戶端的情況下,這隻有 poll() 的10%的開銷.
    在Linux上嘗試了 /dev/poll 的各種實現,但它們都沒有像 epoll 一樣高效,並且從未真正完成.不推薦在Linux上使用 /dev/poll.
    參閱Poller_devpoll (cc, h基礎測試)是一個如何使用 /dev/poll 與其他就緒通知模式互動的示例.(注意 - 該示例適用於Linux /dev/poll,可能無法在 Solaris 上正常執行.)
  • kqueue()
    是在FreeBSD系統上推薦使用的代替poll的方法(很快,NetBSD).
    看下邊 kqueue() 可以指定邊緣觸發或水平觸發.

2. 一個執行緒服務多個客戶端, 使用非阻塞 IO 和就緒改變通知

就緒改變通知(或邊緣就緒通知)意味著你向核心提供檔案描述符,然後,當該描述符從 not ready 轉換為 ready 時,核心會以某種方式通知你.然後它假定你已知檔案描述符已準備好,同時不會再對該描述符傳送類似的就緒通知,直到你在描述符上進行一些操作使得該描述符不再就緒(例如,直到你收到 EWOULDBLOCK 錯誤為止)傳送,接收或接受呼叫,或小於請求的位元組數的傳送或接收傳輸).

當你使用就緒改變通知時,你必須準備處理好虛假事件,因為最常見的實現是隻要接收到任何資料包都發出就緒訊號,而不管檔案描述符是否準備就緒.

這與"水平觸發"就緒通知相反.它對程式設計錯誤的寬容度要低一些,因為如果你只錯過一個事件,那麼事件的連線就會永遠停滯不前.可以儘管如此,我發現邊緣觸發的就緒通知能讓使用OpenSSL程式設計非阻塞客戶端變得更容易,因此還是值得嘗試.

[Banga, Mogul, Drusha '99]在1999年描述了這種型別的模式.

有幾種API使應用程式檢索"檔案描述符準備就緒"通知:

/* Mask off SIGIO and the signal you want to use. */
sigemptyset(&sigset);
sigaddset(&sigset, signum);
sigaddset(&sigset, SIGIO);
sigprocmask(SIG_BLOCK, &m_sigset, NULL);
/* For each file descriptor, invoke F_SETOWN, F_SETSIG, and set O_ASYNC. */
fcntl(fd, F_SETOWN, (int) getpid());
fcntl(fd, F_SETSIG, signum);
flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK|O_ASYNC;
fcntl(fd, F_SETFL, flags);複製程式碼
  • 當 read() 或 write() 等普通 I/O 函式完成時,傳送該訊號.要使用該段的話,在迴圈裡面,當poll()處理完所有的描述符後,進入 sigwaitinfo()sigwaitinfo() 迴圈.
    如果 sigwaitinfo 或 sigtimedwait 返回你的實時訊號,siginfo.si_fd 和 siginfo.si_band 提供的資訊幾乎與 pollfd.fd 和 pollfd.revents 在呼叫 poll() 之後的資訊相同,如果你處理該 I/O,那麼就繼續呼叫sigwaitinfo()
    如果 sigwaitinfo 返回傳統的 SIGIO,那麼訊號佇列溢位,你必須通過臨時將訊號處理程式更改為SIG_DFL來重新整理訊號佇列,然後回到外層poll()迴圈.
    參閱Poller_sigio (cc, h)是一個如何使用 rtsignals 與其他就緒通知模式互動的示例.
    參閱Zach Brown的phhttpd,例如直接使用此功能的程式碼.(還是不要; phhttpd有點難以弄清楚......)
    [Provos,Lever和Tweedie 2000]描述了 phhttpd 的最新基準,使用的不同的sigtimedwait(),sigtimedwait4(),它允許你通過一次呼叫檢索多個訊號.有趣的是 sigtimedwait4() 對他們的主要好處似乎是允許應用程式來衡量系統過載(因此它可以行為恰當).(請注意,poll()也提供了同樣的系統負載測量.)

每個fd一個訊號
Signal-per-fd是由Chandra和Mosberger提出的對實時訊號的一種改進,它通過合併冗餘事件來減少或消除實時訊號佇列溢位.但它並沒有超越 epoll.他們的論文 (www.hpl.hp.com/techreports…)將此方案的效能與select() 和 /dev/poll 進行了比較.
Vitaly Luban於2001年5月18日宣佈了一項實施該計劃的補丁;他的補丁產生於www.luban.org/GPL/gpl.htm….(注意:截至2001年9月,這個補丁在高負載下可能存在穩定性問題.dkftpbench在大約4500個使用者可能會觸發oops.)
參閱Poller_sigfd (cc,h)是一個如何使用 signal-per-fd 與其他就緒通知模式互動的示例.

3. 一個執行緒服務多個客戶端,使用非同步 I/O.

這在Unix至今都沒有流行起來,可能是因為較少的作業系統支援了非同步 I/O,也可能是因為(像非阻塞 I/O)它要求重新思考應用程式.在標準 Unix 下,非同步 I/O 被aio_ 介面提供(從該連結向下滾動到"非同步輸入和輸出"),它將訊號和值與每個 I/O操作相關聯.訊號及其值排隊並有效地傳遞給使用者程式.這是來自 POSIX 1003.1b 實時擴充套件,也是單Unix規範第二版本.

AIO通常與邊緣觸發完成通知一起使用,即當操作完成時,訊號排隊.(它也可以通過呼叫aio_suspend()與水平觸發的完成通知一起使用,雖然我懷疑很少有人這樣做.)

glibc 2.1和後續版本提供了一個普通的實現,僅僅是為了相容標準,而不是為了獲得效能上的提高.

截止linux核心 2.5.32,Ben LaHaise的 Linux AIO 實現已合併到主 Linux 核心中.它不使用核心執行緒,同時還具有非常高效的底層api,但是(從2.6.0-test2開始)還不支援套接字.(2.4核心還有一個 AIO 補丁,但 2.5/2.6 實現有些不同.)更多資訊:

Suparna還建議看看DAFS API 對 AIO 的方法.

Red Hat AS和 Suse SLES 都在2.4核心上提供了高效能的實現.它與2.6核心實現有關,但並不完全相同.

2006年2月,網路AIO有一個新的嘗試;看上面關於Evgeniy Polyakov基於kevent的AIO的說明

在1999年,SGI為 Linux 實現了高速 AIO,從版本1.1開始,據說可以很好地相容磁碟 I/O 和套接字.它似乎使用核心執行緒.對於那些不能等待 Ben 的 AIO 支援套接字的人來說,會仍然很有用.

O'Reilly的書POSIX.4: 真實世界的程式設計據說涵蓋了對aio的一個很好的介紹.

Solaris早期非標準的aio實現的教程線上Sunsite.這可能值得一看,但請記住,你需要在精神上將"aioread"轉換為"aio_read"等.

請注意,AIO不提供在不阻塞磁碟 I/O 的情況下開啟檔案的方法; 如果你關心開啟磁碟檔案導致休眠,Linus建議你只需在另一個執行緒中執行 open()而不是是進行 aio_open() 系統呼叫.

在Windows下,非同步 I/O 與術語"重疊 I/O "和 IOCP 或"I/O完成埠"相關聯.微軟的 IOCP 結合了現有技術的技術,如非同步 I/O(如aio_write)和排隊完成通知(如將 aio_sigevent 欄位與 aio_write 一起使用時),以及阻止某些請求嘗試保持執行執行緒數量相關的新想法具有單個 IOCP 常量.欲獲得更多資訊,請參閱 sysinternals.com 上的 Mark Russinovich 撰寫的I/O 完成埠的內部,Jeffrey Richter的書 "為Microsoft Windows 2000編寫服務端程式"(Amazon, MSPress), U.S. patent #06223207, 或者MSDN.

4. 一個執行緒服務多個客戶端

...讓 read() 和 write() 阻塞.每個客戶端使用整個棧偵會有很大的缺點,就是消耗記憶體.很多作業系統也難以操作處理上百個執行緒.如果每個執行緒獲得2MB堆疊(不是非常見的預設值),則在 32 位機器上的 (2^30/2 ^21)= 512 個執行緒上會耗盡虛擬記憶體,具有 1GB 使用者可訪問的VM(比如,Linux 通常在 x86 上允許)你可以通過提供更小的棧解決這個問題,但是執行緒一旦建立,大多數執行緒庫都不允許增加執行緒棧,所以這樣做就意味著你必須使你的程式最小程度地使用記憶體.你也可以通過轉移到64位處理器來解決這個問題.

在Linux, FreeBSD, Solaris上的執行緒支援是正在完善,即使對於主流使用者來說,64位處理器也即將到來.也許在不就的將來,那些喜好每個客戶端使用一個執行緒的人也有能力服務10000個客戶端了.然而,在目前這個時候,如果你真的想要支援那麼多客戶,你可能最好還是使用其他一些方法.

對於毫不掩飾的親執行緒觀點的人,請參閱為什麼事件是一個壞主意(對於高併發伺服器)由von Behren,Condit和Brewer,UCB,在HotOS IX上釋出.有反線營地的任何人能指出一篇反駁這篇論文的論文嗎?:-)

Linux 執行緒

Linux執行緒是標準Linux執行緒庫的名稱.從 glibc2.0 開始,它就整合到 glibc 中,主要是符合 Posix 標準,但效能和訊號支援度上都不盡如人意.

NGPT: Linux 的下一代的 Posix 執行緒

NGPT是 IBM 啟動的為 Linux 帶來更好的 Posix 執行緒相容性的專案.他目前的穩定版本是2.2,工作的非常好...但是 NGPT 團隊宣佈他們將 NGPT 程式碼庫置於support-only模式,因為他們覺得這是"長期支援社群的最佳方式". NGPT團隊將會繼續改進 Linux 的執行緒支援,但是現在主要集中在NPTL.(感謝NGPT團隊的出色工作以及他們以優雅的方式轉向NPTL.)

NPTL: Linux 原生的 Posix 執行緒庫

NPTL是由Ulrich Drepper(glibc的維護人員)和Ulrich Drepper發起的,目的是為Linux帶來的world-class Posix執行緒庫支援.

截至2003年10月5日,NPTL 現在作為附加目錄合併到 glibc cvs 樹中(就像linux執行緒),所以它幾乎肯定會與 glibc 的下一個版本一起釋出.

Red Hat 9是最早的包含NPTL的發行版本(這對某些使用者來說有點不方便,但有人不得不打破僵局...)

NPTL 連結:

這是我嘗試寫的描述NPTL歷史的文章(也可以看看Jerry Cooperstein的文章):

在2002年3月, NGPT團隊的Bill Abt, glibc的維護者與Ulrich Drepper和其他人會面探討LinuxThreads的發展.會議產生的一個想法是提高互斥效能;Rusty Russell 等人後來實現了快速使用者空間鎖(futexes)),它現在被用在 NGPT 和 NPTL 中.大多數與會者認為NGPT應該被合併到glibc.

但Ulrich Drepper並不喜歡 NGPT,認為他可以做得更好.(對於那些試圖為 glibc 做出補丁的人來說,這可能不會讓人大吃一驚:-)在接下來的幾個月裡,Ulrich Drepper,Ingo Molnar致力於 glibc 和核心的變化,這些變化構成了 Native Posix執行緒庫(NPTL).NPTL使用了NGPT設計的所有核心改進,並利用一些新功能:

> NPTL使用NGPT引入的三個核心特性:getpid()返回 PID,CLONE_THREAD 和 futexes;NPTL還使用(並依賴)更廣泛的新核心功能,作為該專案的一部分開發.

> 引入 2.5.8 核心的 NGPT 中的一些專案得到了修改,清理和擴充套件,例如執行緒組處理(CLONE_THREAD).[影響 NGPT 相容性的 CLONE_THREAD 更改與 NGPT 人員同步,以確保NGPT不會以任何不可接受的方式破壞.]

> NPTL開發和使用的核心功能在設計白皮書中有描述,people.redhat.com/drepper/npt… ...

> 簡短列表:TLS支援,各種克隆擴充套件(CLONE_SETTLS,CLONE_SETTID,CLONE_CLEARTID),POSIX執行緒訊號處理,sys_exit()擴充套件(在VM釋出時釋出TID futex)sys_exit_group()系統呼叫,sys_execve()增強功能 並支援分離的執行緒.

> 還有擴充套件 PID 空間的工作 - 例如,procfs由於 64K PID 的設計,為max_pid 和 pid 分配可伸縮性的工作而崩潰.此外,還進行了許多僅針對效能的改進.

> 本質上,新功能完全是使用1:1執行緒方法 - 核心現在可以幫助改進執行緒的所有內容,並且我們為每個基本執行緒原語精確地執行最低限度必需的上下文切換和核心呼叫.

FreeBSD執行緒支援

FreeBSD同時支援 linux 執行緒和使用者空間執行緒庫.此外,在 FreeBSD 5.0 中引入了一個名為 KSE 的 M:N 實現.概述,參閱www.unobvious.com/bsd/freebsd….

2003年3月25日,Jeff Roberson在 freebsd-arch 上釋出了帖子:

...感謝Julian,David Xu,Mini,Dan Eischen,和其它的每一位參加了KSE和libpthread開發的成員所提供的基礎,Mini和我已經開發出了一個 1:1 模型的執行緒實現.此程式碼與 KSE 並行工作,不會以任何方式更改它.它實際上有助於通過測試共享位來使M:N執行緒更接近...

並於2006年7月,Robert Watson提出的 1:1 執行緒應該成為FreeBsd 7.x中的預設實現:

我知道過去曾經討論過這個問題,但我認為隨著7.x向前推進,是時候重新考慮一下這個問題.在許多常見應用程式和特定場景的基準測試中,libthr 表現出比 libpthread 更好的效能... libthr也在我們的大量平臺上實現的,並且已經在幾個平臺上實現了 libpthread.我們對 MySQL 和其他大量執行緒的使用者建議是"切換到libthr",這也是暗示性的! ...所以草書建議是:使libthr成為7.x上的預設執行緒庫.

NetBSD執行緒支援

根據Noriyuki Soda的說明:

核心支援 M:N 基於 Scheduler Activations 模型執行緒庫將於2003年1月18日合併到NetBSD-current中.

更多細節,看由NethanD系統公司的 Nathan J. Williams在2002年的FREENIX上的演示An Implementation of Scheduler Activations on the NetBSD Operating System.

Solaris 執行緒支援

Solaris中的執行緒支援發展...從 Solaris 2 到 Solaris 8,預設執行緒庫使用 M:N 模型,但 Solaris 9 預設為 1:1 模型執行緒支援.看Sun的多執行緒程式設計指導Sun關於 Java 和 Solaris 執行緒的筆記

Java執行緒從JDK 1.3.x及以後開始支援

眾所周知,直到 JDK1.3.x 的 Java 不支援處理除每個客戶端一個執行緒之外的任何網路連線方法.Volanomark是一個很好的微基準測試,它可以在不同數量連線中測量每秒訊息的吞吐量.截至2003年5月,來自不同供應商的 JDK 1.3實際上能夠處理一萬個同時連線 - 儘管效能顯著下降.請參閱表4,瞭解哪些 JVM 可以處理10000個連線,以及隨著連線數量的增加效能會受到影響.

注意:1:1 執行緒與 M:N 執行緒

在實現執行緒庫時有一個選擇: 你可以將所有執行緒支援放在核心中(這稱為 1:1 執行緒模型),或者您可以將其中的相當一部分移動到使用者空間(這稱為 M:N 執行緒模型).有一點,M:N被認為是更高的效能,但它太複雜了,很難做到正確,大多數人都在遠離它.

5. 將伺服器程式碼構建到核心中

據說 Novell 和微軟已經在不同的時間做過這個,至少有一個 NFS 實現是這樣做的,khttpd為Linux和靜態網頁做了這個,"TUX"(執行緒linux web伺服器)是Ingo Molnar為Linux的一個快速且靈活的核心空間HTTP伺服器. Ingo的2000年9月1日公告表示可以從ftp://ftp.redhat.com/pub/redhat/tux 下載 TUX 的alpha版本,並解釋如何加入郵件列表以獲取更多資訊.

linux-kernel列表一直在討論這種方法的優點和缺點,而且似乎不是將 Web 伺服器移動到核心中,核心應該新增最小的鉤子來提高Web伺服器的效能.這樣,其他型別的伺服器可以受益.參見例如Zach Brown的評論關於 userland 與核心 http 伺服器的關係.似乎2.4 linux 核心為使用者程式提供了足夠的功能,因為X15伺服器的執行速度與Tux一樣快,但不使用任何核心修改.

Bring the TCP stack into userspace

例如,參見netmap資料包 I/O 框架和Sandstorm基於這個概念驗證Web伺服器.

評論

Richard Gooch已經寫了一篇關於討論 I/O 選項的論文.

在2001年,Tim Brecht和MMichal Ostrowski測試了多種策略為簡化基於 select 的伺服器.他們的資料值得一看.

在2003年,Tim Brecht釋出了userver的原始碼,由Abhishek Chandra, David Mosberger, David Pariag 和 Michal Ostrowski 編寫的幾臺伺服器組成的小型Web伺服器.它能使用select(), poll(),或者sigio.

早在1999年3月,Dean Gaudet的文章:

我不斷被問到"為什麼你們不使用像Zeus這樣的基於select/event的模型?它顯然是最快的."...

他的理由歸結為"這真的很難,收益還不清楚",然而,在幾個月內,很明顯人們願意繼續努力.

Mark Russinovich 寫了一篇社論文章討論在 linux核心2.2 中的 I/O 策略問題.值得一看,甚至他似乎在某些方面也被誤導了.特別是,他似乎認為Linux 2.2 的非同步 I/O (參見上面的F_SETSIG)在資料就緒時不會通知使用者程式,只有當新連線到達時.這似乎是一個奇怪的誤解.也可以看看更早的草案,Ingo Molnar於1999年4月30日的反駁,Russinovich對1999年5月2日的評論, 一個來自Alan Cox的反駁,和各種linux-kernel的帖子,我懷疑他試圖說 Linux 不支援非同步磁碟I/O,這曾經是真的,但是現在 SGI 已經實現了KAIO,它不再那麼真實了.

有關"完成埠"的資訊,請參閱sysinternals.comMSDN上的這些網頁,他說這是NT獨有的;簡而言之,win32的"重疊 I/O "結果太低而不方便,"完成埠"是一個提供完成事件佇列的包裝器,加上除錯魔術試圖保持執行的數量,如果從該埠獲取完成事件的其他執行緒正在休眠(可能阻塞I/O)則允許更多執行緒獲取完成事件,從而使執行緒保持不變。

另請參閱OS/400對I/O完成埠的支援

1999年9月對linux-kernel進行了一次有趣的討論"> 15,000個同時連線"(和執行緒的第二週).強調:

  • Ed Hall釋出關於他的經歷的一些筆記; 他在執行Solaris的UP P2/333上實現了>1000次連線/秒.他的程式碼使用了一小塊執行緒(每個CPU1或2個),每個執行緒使用"基於事件的模型”管理大量客戶端.
  • Mike Jagdis 釋出了對 poll/select 效能開銷的分析,並說"當前的select/poll 實現可以得到顯著改善,特別是在阻塞情況下,但由於 select/poll 沒有,因此開銷仍會隨著描述符的數量而增加,並且不能,記住哪些描述符很有趣的.這很容易用新的API修復.歡迎提出建議......"
  • Mike釋出關於他改進select()和poll()的工作.
  • Mike 釋出了一些可能的API來替換poll()/select(): "你可以編寫'pollfd like'結構的'device like'API,'device'監聽事件並在你讀它時提供代表它們的'pollfd like'結構?..."
  • Rogier Wolff 建議使用"數字傢伙建議的API",www.cs.rice.edu/~gaurav/pap…
  • Joerg Pommnitz 指出沿著這些線路的任何新API應該不僅能夠等待檔案描述符事件,還能夠等待訊號和SYSV-IPC.我們的同步原語至少應該能夠做到Win32的WaitForMultipleObjects.
  • Stephen Tweedie斷言,_SETSIG,排隊的實時訊號和 sigwaitinfo() 的組合是 www.cs.rice.edu/~gaurav/pap… 中提出的API的超集.他還提到,如果你對效能感興趣,你可以隨時阻止訊號;而不是使用非同步傳遞訊號,程式使用sigwaitinfo()從佇列中獲取下一個訊號.
  • Jayson Nordwick 比較完成埠和F_SETSIG 同步事件模型,得出的結論是它們非常相似.
  • Alan Cox 指出SCT的 SIGIO 補丁的舊版本包含在2.3.18ac中.
  • Jordan Mendelson 釋出一些示例程式碼,展示瞭如何使用F_SETSIG.
  • Stephen C. Tweedie 繼續比較完成埠和F_SETSIG,並注意到:"使用訊號出隊機制,如果庫使用相同的機制,您的應用程式將獲取發往各種庫元件的訊號",但庫可以設定自己的訊號處理程式,所以這不應該影響程式(很多).
  • Doug Royer指出,當他在 Sun 日曆伺服器上工作時,他在 Solaris 2.6 上獲得了 100,000 個連線.其他人則估計Linux需要多少RAM,以及會遇到什麼瓶頸。

有趣的閱讀!

開啟檔案控制程式碼的限制

  • 任何Unix: 都由ulimit或setrlimit設定限制
  • Solaris: 看 Solaris FAQ,問題3.46 (或左右; 他們定期重新編號)
  • FreeBSD:
    編輯 /boot/loader.conf, 增加行
    set kern.maxfiles=XXXX
    其中XXXX是檔案描述符所需的系統限制,並重新啟動.感謝一位匿名讀者,他寫道,他說他在FreeBSD 4.3上獲得了超過10000個連線,並說
    "FWIW: 你實際上無法通過sysctl輕鬆調整FreeBSD中的最大連線數....你必須在/boot/loader.conf檔案中這樣做.
    這樣做的原因是 zalloci() 呼叫初始化套接字和 tcpcb 結構區域在系統啟動時很早就發生了,這樣區域既可以是型別穩定的又可以交換。
    您還需要將 mbuf 的數量設定得更高,因為您在(在未修改的核心上)為 tcptempl 結構每個連線消耗一個mbuf,用於實現 keepalive."

    其他的讀者說到:
    "從FreeBSD 4.4開始,不再分配 tcptempl 結構; 你不再需要擔心每個連線都會被消耗一個mbuf

    也可以看看:


  • OpenBSD: 讀者說
    "在OpenBSD,需要額外的調整來增加每個程式可用的開啟檔案控制程式碼的數量: /etc/login.conf 的openfiles-cur引數需要被增加. 您可以使用sysctl -w 或 sysctl.conf 更改 kern.max 檔案,但它不起作用.這很重要,因為對於非特權程式,login.conf限制為非常低的64,對於特權程式為128

  • Linux: 參閱Bodo Bauer的 /proc 文件. 在2.4核心
    echo 32768 > /proc/sys/fs/file-max
    增大系統開啟檔案的限制.和
    ulimit -n 32768
    ulimit -n 32768
    增大當前程式的限制
    On 2.2.x kernels,
    在 2.2.x 核心,
    echo 32768 > /proc/sys/fs/file-max echo 65536 > /proc/sys/fs/inode-max
    增大系統開啟檔案的限制.和
    ulimit -n 32768
    ulimit -n 32768
    增大當前程式的限制
    我驗證了 Red Hat 6.0 上的程式(2.2.5 左右加補丁)可以通過這種方式開啟至少31000 個檔案描述符.另一位研究員已經證實,2.2.12 上的程式可以通過這種方式開啟至少90000 個檔案描述符(具有適當的限制).上限似乎是可用的記憶體。
    Stephen C. Tweedie 發表 關於如何使用 initscript 和 pam_limit 在引導時全域性或按使用者設定 ulimit 限制.
    在 2.2 更老的核心,但是,即使進行了上述更改,每個程式的開啟檔案數仍限制為1024
    另見Oskar1998年的帖子,其中討論了 2.0.36 核心中檔案描述符的每個程式和系統範圍限制。

執行緒限制

在任何體系結構上,您可能需要減少為每個執行緒分配的堆疊空間量,以避免耗盡虛擬記憶體.如果使用pthreads,可以使用pthread_attr_init() 在執行時設定它。

Java 問題

通過JDK 1.3, Java的標準網路庫大多提供了一個客戶端一個執行緒模型.這是一種非阻塞讀的方式,但是沒有辦法去做非阻塞寫.

在2001年5月. JDK 1.4 引進了包 java.nio 去提供完全支援非阻塞 I/O (和其他好的東西).看發行說明警告.嘗試一下,給Sun反饋!

HP 的 java 也包含了一個執行緒輪訓API.

在2000, Matt Welsh為java實現了非阻塞套接字.他的效能基準測試顯示他們優於在處理大量(大於10000)連線的伺服器中的阻塞套接字.他的類庫被稱作java-nbio;他是Sandstorm專案的一部分.基準測試顯示10000連線的效能是可用的.

參閱 Dean Gaude關於 Java , 網路 I/O, 和執行緒主題的文章,和 Matt Welsh 寫的關於事件對比工作執行緒的論文

在 NIO 之前,有幾個改進Java的網路API的建議:

  • Matt Welsh 的Jaguar 系統提出預序列化物件,新的 Java 位元組碼和記憶體管理更改允許使用 Java 進行非同步I/O.
  • C-C. Chang and T. von Eicken寫的將Java連線到虛擬介面體系結構提出記憶體管理更改允許 Java 使用非同步 I/O.
  • JSR-51是提出 java.nio 包的Sun工程專案. Matt Welsh參加了(誰說Sun不聽?).

其他建議

  • 零拷貝
    通常情況下,資料會從一處到其他處多次複製.任何將這些副本消除到裸體物理最小值的方案稱為"零拷貝".
    • Thomas Ogrisegg 在 Linux 2.4.17-2.4.20 下為 mmaped 檔案傳送零拷貝傳送補丁.聲稱它比 sendfile() 更快.
    • IO-Lite 是一組 I/O 原語的提議,它擺脫了對許多副本的需求.
    • 在1999年, Alan Cox指出零拷貝有時是不值的會遇到麻煩.(但他確實喜歡sendfile())
  • Ingo於2000年7月在 2.4 核心中為 TUX 1.0實現了一種零拷貝TCP,他說他很快就會將其提供給使用者空間.
    • Drew Gallatin and Robert 已經為FreeBSD增加了一些零拷貝特性;想法似乎是如果你在一個套接字上呼叫 write() 或者 read(),指標是頁對齊的,並且傳輸的資料量至少是一個頁面, 同時你不會馬上重用緩衝區,記憶體管理技巧將會用於避免拷貝. 但是請參閱linux-kernel上關於此訊息的後續內容,以瞭解人們對這些記憶體管理技巧速度的疑慮.
      根據Noriyuki Soda的說明:
      自NetBSD-1.6釋出以來,通過指定 "SOSEND_LOAN" 核心選項,支援傳送端零拷貝.此選項現在是 NetBSD-current 的預設選項(你可以通過在 NetBSD_current 上的核心選項中指定"SOSEND_NO_LOAN"來禁用此功能).使用此功能時,如果將超過4096位元組的資料指定為要傳送的資料,則會自動啟用零複製.

    • sendfile() 系統呼叫可以實現零拷貝網路.
      Linux和FreeBSD中的sendfile()函式允許您告訴核心傳送部分或全部檔案. 使作業系統儘可能高效地完成。 它可以在使用非阻塞 I/O 的執行緒或伺服器的伺服器中同樣使用.(在 Linux中,目前他的記錄還很少;使用_syscall4 去呼叫它.Andi Kleen 正在寫覆蓋該內容的 man 頁面.另請參閱Jeff Tranter在Linux Gazette issue 91中探索 sendfile 系統呼叫.) 有傳言稱 ftp.cdrom.com 受益於 sendfile().
      sendfile() 的零拷貝實現正在為2.4核心提供.看LWN Jan 25 2001.
      一個開發者在 Freebsd 下使用 sendfile() 的報告顯示使用 POLLWRBAND 而不是 POLLOUT 會產生很大的不同.
      Solaris 8 (截至2001年7月更新) 有一個新的系統呼叫'sendfilev'.手冊頁的副本在這裡. Solaris 8 7/01 發版說明 也提到了它.我懷疑這在以阻塞模式傳送到套接字時最有用;使用非阻塞套接字會有點痛苦。
  • 使用writev避免使用小幀(或者 TCP_CORK)
    一個新的在 Linux 下的套接字選項, TCP_CORK,告訴核心去避免傳送部分幀,這有點幫助,例如當有很多小的 write() 呼叫時,由於某種原因你不能捆綁在一起.取消設定選項會重新整理緩衝區.最好使用writev(),但......
    LWN Jan 25 2001,關於TCP-CORK和可能的替代MSG_MORE的關於linux-kernel的一些非常有趣的討論的摘要.
  • 在過載時表現得智慧.
    [Provos, Lever, and Tweedie 2000]提到在伺服器過載時丟棄傳入連線可以改善效能曲線的形狀,並降低整體錯誤率.他們使用平滑版本的" I/O 就緒客戶端數"作為過載的衡量標準.此技術應該很容易應用於使用 select, poll 或任何系統呼叫編寫的伺服器,這些呼叫返回每次呼叫的就緒事件技術(例如 /dev/poll 或 sigtimedwait4()).
  • 某些程式可以從使用非Posix執行緒中受益.
    並非所有執行緒都是相同的.例如,Linux 中的 clone() 函式(及其在其他作業系統中的朋友)允許您建立具有其自己的當前工作目錄的執行緒,這在實現ftp伺服器時非常有用.有關使用本機執行緒而不是 pthreads 的示例,請參閱 Hoser FTPd。
  • 快取自己的資料有時可能是一個勝利.
    Vivek Sadananda Pai(vivek@cs.rice.edu)在 new-httpd"回覆: 修復混合伺服器問題",5月9日,宣告:
    "我在 FreeBSD 和 Solaris/x86 上比較了基於 select 的伺服器和多程式伺服器的原始效能.在微基準測試中,軟體架構的效能差異很小.基於 select 的伺服器的巨大效能優勢源於進行應用程式級快取.雖然多程式伺服器可以以更高的成本實現,但實際工作負載(與微基準測試相比)更難獲得相同的好處.我將把這些測量結果作為論文的一部分展示,這些論文將出現在下一屆Usenix會議上.如果你有後記,那麼論文可以在www.cs.rice.edu/~vivek/flas…"

其他限制

  • 舊系統庫可能使用16位變數來儲存檔案控制程式碼,這會導致32767控制程式碼之上的麻煩.glibc2.1應該沒問題。
  • 許多系統使用16位變數來儲存程式或執行緒ID.將Volano可伸縮性基準測試 移植到C會很有意思,看看各種作業系統的執行緒數上限是多少.
  • 某些作業系統預先分配了過多的執行緒本地記憶體;如果每個執行緒獲得1MB,並且總VM空間為2GB,則會建立2000個執行緒的上限.
  • 檢視www.acme.com/software/th… 底部的效能對比圖.請注意各種伺服器如何在 128個 以上的連線上出現問題,甚至在 Solaris 2.6上 知道原因的人,讓我知道.

注意: 如果TCP堆疊有一個bug,導致 SYN 或 FIN 時間更短(200ms)延遲,如 Linux 2.2.0-2.2.6 所示,並且作業系統或 http 守護程式對連線數有硬限制,你會期待這種行為.可能還有其他原因.

核心問題

對於Linux,看起來核心瓶頸正在不斷修復.看Linux Weekly News,Kernel Traffic, the Linux-Kernel mailing list,和my Mindcraft Redux page.

1999年3月,微軟贊助了一項比較 NT 和 Linux 的基準測試,用於服務大量的 http 和 smb客戶端,linux的結果不如人意.另見關於Mindcraft 1999年4月基準測試的文章瞭解更多資訊

另請參見Linux可擴充套件性專案.他們正在做有趣的工作.包括Niels Provos的暗示民意調查補丁,關於雷鳴般的群體問題的一些工作.

另請參與Mike Jagdis致力於改進 select() 和 poll();這是Mike關於它的帖子.

Mohit Aron(aron@cs.rice.edu)寫道,TCP中基於速率的時鐘可以將"緩慢"連線上的HTTP響應時間提高80%

測量伺服器效能

特別是兩個測試簡單,有趣,而且很難:

  1. 每秒原始連線數(每秒可以提供多少512位元組檔案?)
  2. 具有許多慢速客戶端的大型檔案的總傳輸速率(在效能進入底池之前,有多少 28.8k 調變解調器客戶端可以同時從伺服器下載?)

Jef Poskanzer釋出了比較許多Web伺服器的基準測試.看他的結果www.acme.com/software/th…

我也有關於將thttpd與Apache比較的一些舊筆記可能對初學者感興趣.

Chuck Lever不斷提醒我們關於Banga和Druschel關於Web伺服器基準測試的論文.值得一讀。

IBM有一篇名為Java伺服器基準測試的優秀論文.[Baylor 等,2000年].值得一讀。

例子

Nginx 是一個web伺服器,它使用目標作業系統上可用的任何高效網路事件機制.它變得非常流行;這甚至有關於它的兩本書

有趣的基於 select() 的伺服器

有趣的基於 /dev/poll 伺服器

  • N.Provos,C.Lever,"Scalable Network I/O in Linux"2000年5月.[ FREENIX track,Proc.USENIX 2000,San Diego,California(2000年6月).]描述了被修改為支援 /dev/poll的 thttpd 版.將效能與phhttpd進行比較。

有趣的基於 epoll 伺服器

  • ribs2
  • cmogstored - 對大多數網路使用epoll/kqueue,對磁碟和accept4使用執行緒

有趣的基於 kqueue() 伺服器

有趣的基於實時訊號伺服器.

  • Chromium 的 X15.使用2.4核心 SIGIO 功能以及 sendfile() 和 TCP_CORK,據報導甚至比TUX實現更高的速度. 在社群許可下的原始碼是可用的.看 Fabio Riccardi 原始公告
  • Zach Brown 的 phhttpd - "一個更快的服務伺服器, 它用於展示 sigio/siginfo 事件模型.如果你嘗試在生產環境中使用它,請將此程式碼視為高度實驗性的,同時您自己也格外注意" ,使用 2.3.21或之後的 siginfo 特性, 包含了需要的更新核心補丁.據傳甚至比khttpd更快.見他1999年5月31日的一些說明

有趣的基於執行緒的伺服器

有趣的基於核心的伺服器

其他有趣的連結


相關文章