天天看點

高性能伺服器網絡模型詳解

作者:SDNLAB

作者簡介:謝友鵬,目前在網絡技術部任職技術專家,從事網絡和雲相關開發。涉及領域vpn、sdwan、全球網絡加速、區塊鍊網絡加速、cdn靜态緩存等。長期研究nginx、apache traffic server、frp等代理項目和基于k8s的雲原生項目。不定期更新個人微信公衆号—網絡技術修煉。關注領域:網絡、雲、linux高性能服務端。

1999年Dan Kegel在發表的論文中提出了The C10K problem,這篇論文對傳統伺服器架構處理大規模并發連接配接時的挑戰進行了較長的描述,并提出了一些解決方案和優化技術。這裡的C指的是Concurrent(并發)的縮寫,C10K問題是指怎麼在單台伺服器上并發一萬個請求。如果你分析過性能問題一定會注意到,性能極限通常受到一個或多個資源的限制,比如記憶體、檔案句柄個數、網絡帶寬、CPU等。這裡讨論的前提是機器的實體資源和系統配置能夠滿足一萬個請求。在這個前提下,網絡并發主要關注兩個方面:一是應用程式和作業系統核心之間如何進行IO事件通知,二是應用程式程序或線程的配置設定方式。

I/O模型

阻塞vs非阻塞、同步vs異步

你可能經常看到某某項目用的同步非阻塞模型、某某項目用的異步非阻塞模型,是以在正式讨論I/O模型前,還是先對齊一下關于阻塞、非阻塞以及同步、異步的概念吧。

阻塞、非阻塞指的是系統調用時“等待資料準備好”這個動作。比如read時候,如果核心判定資料還沒準備好:核心讓應用程序一直等待,直到資料準備好才通知應用程序就是阻塞;

核心立即通知應用程式說資料沒準備好,你先幹别的吧,就是非阻塞。

同步、異步指的是“核心空間與使用者空間複制資料”這個動作。比如read時候,如果核心判定資料還沒準備,過一段時間資料來了:核心通知應用程序你來讀取資料吧,應用程式再次系統調用将資料讀走就是同步;

核心将資料拷貝到使用者空間後再通知應用程序,資料已經拷貝好了直接用吧,就是異步。

I/O模型詳解

Stevens在《UNIX 網絡程式設計 卷1》一書的6.2章節介紹了五種 I/O 模型。以下模型以UDP接收封包為例來說明這五種I/O 的工作方式。

> 阻塞式I/O

高性能伺服器網絡模型詳解

該模型使用最簡,使用者程序調用讀取資料系統後就一直等待,直到核心資料準備好,并将資料從核心空間拷貝到使用者空間後,調用結束。這種模型效率顯然的低下,因為這種模型會導緻兩種可能結果:為每個請求配置設定一個程序或線程,那麼高并發意味着核心要排程的程序或線程數量很龐大,排程、上下文切換等開銷會使系統性能降低。

固定數量的程序或線程處理請求,那麼這些程序或線程全被被占用後,新請求就隻能等了。

該模型低效的根本原因在于阻塞,核心資料沒有準備好的時候,使用者态程序明明可以幹其他活的,現在隻能白白等待。

>非阻塞式 I/O

高性能伺服器網絡模型詳解

這種模型通過非阻塞的方式與核心打交道,如果核心中資料還未準備好,就立刻傳回給使用者程序,使用者程序就可以先幹别的事情,過一段再進行讀資料的系統調用,直到核心資料準備好,并将資料拷貝到使用者空間。相比于阻塞的模型,這種非阻塞+主動輪詢的模型避免了使用者程序白白等待核心準備資料的時間,是以效率有所提升,但是因為每次輪詢都是系統調用,是以上下文切換變多了,是以性能也不高。

>I/O 複用

高性能伺服器網絡模型詳解

既然不停主動查詢核心資料是否準備好這件事會引起系統性能下降,那能不能通過注冊+通知的方式呢?這就是大名鼎鼎的I/O複用模型。該模型允許使用者态通過一個程序将所有相關的讀寫事件(使用select、poll或epoll)注冊到核心,然後核心會主動通知使用者态程序,一旦任意一個或多個請求的讀寫資料準備好。這種方式在單個程序或線程中同時處理多個I/O通道的就緒狀态被稱為I/O多路複用。使用I/O多路複用既不會阻塞處理請求的程序,也不會因為輪詢核心資料是否準備好而導緻過多的系統調用,是以具有高效的特點。然而,需要注意的是,一旦核心通知應用程序資料準備就緒,仍然需要通過系統調用觸發資料的讀取過程。

Linux核心對這種模型的支援非常完善,是以許多高性能伺服器在Linux環境中廣泛采用這種模式。

>信号驅動式 I/O

高性能伺服器網絡模型詳解

I/O複用模型中是用一個程序(select、poll或epoll)阻塞或輪詢所有請求的資料是否準備好,進而讓所有請求程序的處理都不會阻塞。信号驅動式則沒有這個複用的I/O程序,每個請求程序自己去核心注冊,然後等資料準備好核心通知應用程序去處理。這種模型應用套接字處理的實踐場景為基于UDP的NTP服務,幾乎沒有在TCP上的應用,因為對于TCP來說信号産生過于頻繁,而且并沒有告訴應用程式發生了什麼事件,比如下面條件均會導緻TCP套接字産生SIGIO信号:

監聽套接字某個連接配接請求已經完成;

某個斷鍊請求已經發起;

某個斷鍊請求已經完成;

某個半連接配接已經關閉;

資料到達套接字;

資料已經從套接字發出;

發生某個異步錯誤。

>異步 I/O

高性能伺服器網絡模型詳解

前面幾種方式,無論是阻塞還是非阻塞,從核心空間到使用者空間複制資料的動作都是在核心通知使用者程序後,使用者程序再通過系統調用觸發完成的,是以都屬于同步操作。而異步I/O模型則不同,它允許使用者态程序通過系統調用讀取資料後,即使核心資料未準備好,也會立即傳回給使用者程序,告知資料未準備好,讓使用者程序可以執行其他操作。當資料準備好後,核心會将資料從核心空間拷貝到使用者态,并直接回調使用者程序,将資料送到使用者程序手中。這種模型不僅具備非阻塞特性,還能進一步減少系統調用的次數,是以在理論上相對于其他模型更加高效。需要注意的是,這種模型需要作業系統核心的支援。

在《UNIX網絡程式設計卷1》一書中,截至書稿時,支援POSIX異步I/O的系統相對較少。由于早期Linux核心對網絡異步I/O的支援不夠成熟,在Linux環境下,大多數高性能網絡伺服器選擇采用I/O複用的方式,如epoll。然而,從Linux核心5.0版本開始,引入了io_uring異步操作,随着該技術的成熟,越來越多的高性能網絡伺服器(例如nginx)開始支援使用這種異步I/O方式。

>如何簡單了解5種I/O?

下面通過一個例子對比一下5種模型,顧客是應用程序,餐飲人員為核心,餐桌為應用程序的資料buffer:

阻塞式I/O:交完錢也要在視窗排隊,等師傅做好,将飯端給你,你再端到自己餐桌。

非阻塞式I/O:交完錢你就可以離開視窗玩一會了,視窗有個螢幕,你過一會跑過來看一下自己的飯好了沒,直到飯做好,自己端到自己的餐桌。

I/O複用:好幾個同學都把飯卡交給你,你一個人跑到視窗排隊刷卡,誰的飯好了,你就打電話給誰,讓他自己将飯端到餐桌。

信号驅動式I/O:你去視窗手機刷卡後就可離開了,飯做好會通過手機通知你,然後自己過去将飯端到餐桌。

異步I/O:去視窗點餐後,告訴服務員你在哪個餐桌就可以離開了,飯做好,服務員會将飯幫你端到餐桌。

程序/線程配置設定

程序和線程的建立、排程都需要系統開銷。在高并發系統中,為每個請求配置設定一個程序或線程會對性能産生不利影響。為了克服這個問題,高性能的網絡模型通常采用程序池或線程池來管理程序或線程。程序/線程池的設計目的是降低建立和銷毀程序/線程的頻率,并限制系統中總程序/線程的數量,以減少核心排程的開銷。

常用高性能模式

reactor 模式:《The Design and Implementation of the Reactor》一文詳細介紹了reactor模式的工作方式,簡單來說,reactor模式=I/O複用 + 程序池/線程池。

proactor模式:《Proactor: An Object Behavioral Pattern for Demultiplexing and Dispatching Handlers for Asynchronous Events》一文詳細介紹了proactor模式的工作方式,簡單來說,proactor模式=異步I/O+ 程序池/線程池。

驚群效應:對于TCP請求來說,最理想的情況是每個事件每次從池中喚醒一個程序或線程去執行,這樣既不需要等待又不會引起競争。用一個程序或線程專門負責處理accept事件,然後将接下來的事件繼續分發給其他worker處理是可行的。有一些模型(比如nginx)存在多個程序或線程監聽同一個端口的情況,如果不加處理會出現一個accept事件喚醒所有worker程序的情況,即驚群效應。為了應對驚群效應,早期nginx引入了accept_mutex的機制,競争到鎖的worker才會執行accept操作,進而避免所有worker都被喚醒。Linux3.9 版本後提供了reuseport更好的解決了多個程序或線程監聽同一個端口引起的驚群問題,簡單說就是核心幫你輪詢,而不用在應用層面競争了。

更快、更強大的網絡模型

欲望是永無止境的,有人提出The C10K problem問題,就有人提出The C10M problem,前文中的讨論基本都是圍繞使用者态程序和核心的互動優化,既然和核心互動容易導緻性能瓶頸,那為何不旁路掉核心協定棧呢?是以有了更快的網絡方案,比如DPDK、XDP、甚至硬體加速等。

繼續閱讀