IO多路複用筆記

1 多路複用學習筆記

1.1 Ck10問題的歷史背景和問題處理的過程

在做技術規劃和架構設計的時候,我常常告誡技術人員,不要做過度設計,如果咱們只有1萬用戶,先別去操百萬用戶在線的心。淘寶那麼大,也是從 Apache、PHP、MySql 發展起來的,沒人能預見到淘寶會發展到這樣一個規模,一旦發展起來,業務的爆發性增長會驅動技術的迅速發展,在業務規模還不及格的時候,不用爲技術的未來擔心。

這個思路在業務領域不會有太大的問題,因爲需求的變化實在是太快了,需要時時去應對。但在底層技術的發展上,我們就有可能遭到「短視」的報復,比如:這個數據長度不會超過16位吧,這個程序不可能使用到2000年吧。於是就有了千年蟲的問題,也有了 C10K 的問題。

C10K 就是 Client 10000 問題即「在同時連接到服務器的客戶端數量超過 10000 個的環境中,即便硬件性能足夠, 依然無法正常提供服務」,簡而言之,就是單機1萬個併發連接問題。這個概念最早由 Dan Kegel 提出併發佈於其個人站點http://www.kegel.com/c10k.html

1.1.1 問題背景

爲什麼會這樣呢?因爲計算機的上古時代,比如沒有網絡的 PC 時代,不會有程序員高瞻遠矚的預測到互聯網時代的來臨,也不會想到一臺服務器會創建那麼多的進程,即使在互聯網初期,一臺服務器能有100個在線用戶已經是不得了的事情了。甚至,他們在設計 Unix 的 PID 的時候,採用了有符號的16位整數,這就導致一臺計算機上能夠創建出來的進程無法超過32767個。而計算機自己也得運行一些後臺進程,這樣應用軟件能夠創建的進程數就更少了。

當然,這個問題隨着技術的發展很快就解決了,現在大部分的個人電腦操作系統可以創建64位的進程,由於數據類型所帶來的進程數上限消失了,但是我們依然不能無限制的創建進程,因爲隨着並發連接數的上升會佔用系統大量的內存同樣會造成系統的不可用。

操作系統裏內存管理的主要作用是,進程請求內存的時候爲其分配可用內存,進程釋放後回收內存,並監控內存的使用狀況。爲了提高內存的使用率,現代操作系統需要程序能夠共享內存,並且內存的限制對開發者透明,有些程序佔用了內存空間,但不一定是一直使用的,這樣可以把這部分內存數據序列化到磁盤上,需要的時候再加載到內存裏,這樣內存資源永遠會給最需要的程序使用。於是程序員們發明了虛擬內存(Virtual Memory)。

虛擬內存技術支持程序訪問比物理內存大得多的內存空間,也使得多個程序共享內存更加高效。物理內存由 RAM 芯片提供,虛擬內存則依靠透明的使用磁盤空間,使程序運行起來好像有了更大的內存空間。

但是問題依然存在,進程和線程的創建都需要消耗一定的內存,每創建一個棧空間,都會產生內存開銷,當內存使用超過物理內存的時候,一部分數據就會持久化到磁盤上,隨之而來的就是性能的大幅度下降

這就像銀行擠兌,人們把現金存入銀行,收取一定的利息,平時只有少數人去銀行取現,銀行會拿人們存的錢去做更有價值的投資。但是,如果大部分人都去銀行取現,銀行是沒有那麼多現金的。取不到錢的用戶,被門擋在外面的用戶,一定會去拉橫幅喊口號「最喜歡雙截棍柔中帶剛,不喜歡銀行就上少林武當」云云,於是銀行就處於不可用狀態了。現在的 P2P 理財也是一個道理,投資者都去變現,無論是多麼良性的資產,一樣玩完。

爲什麼現在會有這麼大的連接需求呢?因爲業務驅動和技術發展嘛。除了普通的網頁瀏覽和表單提交,即時通信和實時互動交流越來越成爲主流需求,keep-alive 技術也能讓瀏覽器產生長連接,實時在線的客戶端越來越多,如果不能解決 C10K 問題,將導致服務商需要購買大量的服務器,而每一臺服務器都不能做到物盡其用,即使你配置了更好的 CPU 和更大的內存。

當然,現在我們早已經突破了 C10K 這個瓶頸,具體的思路就是通過單個進程或線程服務於多個客戶端請求,通過異步編程和事件觸發機制替換輪訓IO 採用非阻塞的方式,減少不必要的性能損耗,等等。

底層的相關技術包括 epoll、kqueue 等,應用層面的解決方案包括 OpenResty、Golang、Node.js 等,比如 OpenResty 的介紹中是這麼說的:

OpenResty 通過匯聚各種設計精良的 Nginx 模塊,從而將 Nginx 有效地變成一個強大的通用 Web 應用平臺。這樣,Web 開發人員和系統工程師可以使用 Lua 腳本語言調動 Nginx 支持的各種 C 以及 Lua 模塊,快速構造出足以勝任 C10K 乃至 C1000K 以上單機併發連接的高性能 Web 應用系統。

libevent
libevent 是以個C 庫,並不是解決的方案,它支持多種 I/O 多路複用技術, epoll、 poll、 dev/poll、 select 和 kqueue 等。
一般用作網絡庫來使用。

epoll是linux所特有,而select是POSIX所規定,kqueue 是 FreeBSD 上的一種的多路複用機制。它是針對傳統的 select/poll 處理大量的文件描述符性能較低效而開發出來的

據說現在都去搞 C10M 了,你們怕不怕?
實際操作中,每個解決方案都不是那麼容易實現的,很多技術領域油光水滑的東西,放到線上,往往會出現各種各樣的問題和毛病。松本行弘先生介紹了一個「最弱連接」的概念:
如果往兩端用力拉一條由很多環 (連接)組成的鎖鏈,其中最脆弱的一個連接會先斷掉。因此,鎖鏈整體的強度取決於其中最脆弱的一環。

C10K 問題的情況也很相似。一臺服務器同時應付超過一萬個(或者更多)併發連接的情況,哪怕只有一個要素沒有考慮到超過一萬個客戶端的情況,這個要素就會成爲「最弱連接」,從而導致問題的發生。

每個做架構設計和技術實現的程序員,都應當考慮這個最弱連接問題。

你是最弱的一環嗎?

1.1.2 C10K間題的本質

最初的服務器都是基於進程/線程模型的,新到來—個TCP連接,就需要分配1個進程(或者線程).而進程又杲操作系統錄昂貴的資源,—臺機器無法創建很多進程。如果C10k 個用戶創建1萬個進程,那麼操作系統足無法承受的。如果是採用分佈式系統,維持1億用戶在線需要10萬臺服務器

騰訊QQ也是有C10K問題的,只不過他們採用了UDP這種原始的包交換協議來實現的,繞開了這個難題.當然過程肯定痛苦的。如果當時有epoll技術,他們肯定會用TCP.後來的手機QQ,微信都採用TCP協議.

  1. 解決這—問題,主要思路
    對於每個連接處理分配一個獨立的進程/線程;
    另一個思路是用同一進程/線程來同時處理若干連接
  • 每個進程/線程處理一個連接
    需要解決的問題:資源佔用過多,可擴展性差。
  • 每個進程/線程同時處理多個連接(IO多路複用)

1.2.1 什麼是IO 多路複用

IO多路複用是一種系統調用,內核能夠同時對多個IO描述符進行就緒檢查。當所有被監聽的IO都沒有就緒時,調用將阻塞;當至少有一個IO描述符就緒時,調用將返回,用戶代碼可通過檢查究竟是哪個IO就緒來進一步處理業務。顯然,IO多路複用是解決系統裏面存在N個IO描述符的問題的。

1.2.2 從多路複用的角度看可用的編程模型

綜上討論,我們在進行實際的Socket編程的時候,無論是客戶端還是服務端,大致有幾種模式可以選擇:

  • 阻塞式。純採用阻塞式,這種方式很少見,基本只會出現在demo中。多個描述符需要用多個進程或者線程來一一對應處理。
  • 非阻塞式。純非阻塞式,對IO的就緒與否需要在用戶空間通過輪詢來實現。
  • IO多路複用+阻塞式。僅使用一個線程就可以實現對多個描述符的狀態管理,但由於IO輸入輸出調用本身是阻塞的,可能出現某個IO輸入輸出過慢,影響其他描述符的效率,從而體現出整體性能不高。此種方式編程難度比較低。
  • IO多路複用+非阻塞式。在多路複用的基礎上,IO採用非阻塞式,可以大大降低單個描述符的IO速度對其他IO的影響,不過此種方式編程難度較高,主要表現在需要考慮一些慢速讀寫時的邊界情況,比如讀黏包、寫緩衝不夠等。1

2 Linux下的五種I/O模型

Linux下主要有以下五種I/O模型:

I/O模型
談到 1/0 模型, 就不能不說當前主流且耳熟能詳的 5 種模型: 阻塞 I/0、非阻塞 I/0、 I/0多路複用、 信號驅動 I/0 和異步 I/0。每種 1/0 模型都有典型的使用場景, 比如 Socket 的阻塞模式和非阻塞模式就對應於前兩種模型, 而 Linux 中的 select 函數就屬於 I/0 多路複用模型,至千第 5 種模型其實很少有 UNIX 和類 UNIX 系統支持, Windows 的 IOCP (I/0 Completion Port, 簡稱 IOCP) 屬千此模型。 至千大名鼎鼎的 Linux epoll 模型, 則可以看作兼具第 3 種和第4種模型的特性。

  1. 阻塞I/O(blocking IO)
  2. 非阻塞I/O (nonblocking I/O)
  3. I/O 複用 (I/O multiplexing)
  4. 信號驅動I/O (signal driven I/O (SIGIO))
  5. 異步I/O (asynchronous I/O)

2.1 阻塞IO模型

進程會一直阻塞,直到數據拷貝完成 應用程序調用一個IO函數,導致應用程序阻塞,等待數據準備好。數據準備好後,從內核拷貝到用戶空間,IO函數返回成功指示。阻塞IO模型圖如下所示:

輸入圖片描述

2.2 非阻塞IO模型

通過進程反覆調用IO函數,在數據拷貝過程中,進程是阻塞的。模型圖如下所示:輸入圖片描述

2.3 IO複用模型

主要是select和epoll。一個線程可以對多個IO端口進行監聽,當socket有讀寫事件時分發到具體的線程進行處理。模型如下所示:
輸入圖片描述

2.4 信號驅動IO模型

另外,Richard Stevens 在《Unix 網絡編程》卷1中提到的基於信號驅動的IO(Signal Driven IO)模型,由於該模型並不常用,本文不作涉及。接下來,我們詳細分析四種常見的IO模型的實現原理。爲了方便描述,我們統一使用IO的讀操作作爲示例。

信號驅動式I/O:首先我們允許Socket進行信號驅動IO,並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。過程如下圖所示:
輸入圖片描述

2.5 異步IO模型

相對於同步IO,異步IO不是順序執行。用戶進程進行aio_read系統調用之後,無論內核數據是否準備好,都會直接返回給用戶進程,然後用戶態進程可以去做別的事情。等到socket數據準備好了,內核直接複製數據給進程,然後從內核向進程發送通知。IO兩個階段,進程都是非阻塞的。異步過程如下圖所示:
在這裏插入圖片描述

  • 注意
    阻塞IO和非阻塞IO的區別
    調用阻塞IO後進程會一直等待對應的進程完成,而非阻塞IO不會等待對應的進程完成,在kernel還在準備數據的情況下直接返回。
    同步IO 和非同步IO的區別
    兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。
    按照這個定義,之前所述的blocking IOnon-blocking IOIO multiplexing都屬於synchronous IO。注意到non-blocking IO會一直輪詢(polling),這個過程是沒有阻塞的,但是recvfrom階段blocking IO,non-blocking IO和IO multiplexing都是阻塞的。
    而asynchronous IO則不一樣,當進程發起IO 操作之後,就直接返回再也不理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程中,進程完全沒有被block。

輸入圖片描述
補充問題:

  • 問題1:在高併發問題(C10k)問題的解決方案?
    答:高併發問題的解決方案主要是I/O多路複用,傳統的方式是多線程和多進程(但是並不能有效解決問題)
  • 問題2:信號異步模型和異步模型可以解決的高併發問題嗎?
    答: 不能,因爲兩種模型的使用場景不同。可以理解爲IO模型的使用目的不同。2

參考網頁:https://www.itcodemonkey.com/article/8293.html

3 select、poll、epoll Kqueue簡介

輸入圖片描述

4 java NIO 簡介

多路複用(Multiplexing,又稱“多工”)是一個通信計算機網絡領域的專業術語,在沒有歧義的情況下,“多路複用”也可被稱爲“複用”。多路複用通常表示在一個信道上傳輸多路信號或數據流的過程和技術。因爲多路複用能夠將多個低速信道整合到一個高速信道進行傳輸,從而有效地利用了高速信道。通過使用多路複用,通信運營商可以避免維護多條線路,從而有效地節約運營成本

Java提供了哪些IO方式? NIO如何實現多路複用?

Java IO 方式有很多種,基於不同的 IO 抽象模型和交互方式,可以進行簡單區分。

首先,傳統的java.io包,它基於流模型實現,提供了我們最熟知的一些 IO 功能,比如 File 抽象、輸入輸出流等。 交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時, 在讀、寫動作完成之前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。

java.io 包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。

很多時候,人們也把 java.net 下面提供的部分網絡 API,
比如 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,因爲網絡通信同樣是 IO 行爲。

4.1 java NIO

在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以構建多路複用的、同步非阻塞 IO 程序,同時提供了更接近操作系統底層的高性能數據操作方式。

在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了異步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。
異步 IO 操作基於事件和回調機制,可以簡單理解爲,應用操作直接返回,而不會阻塞在那裏,當後臺處理完成,操作系統會通知相應線程進行後續工作。

4.1.1 java io 和java nio 的不同之處

輸入圖片描述

4.2 java nio

結合None-blocking 的實現方式,將不停訪問數據是否準備好的操作進行修改爲:將訪問修改多了通道channel 的方式,實現selector 方式,多路複用方式的實現。

注意:在之前只是會用代碼寫,但是不明白其中的原理。

5 補充

僞異步 IO

爲了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,後來有人對它的線程模型進行了優化一一一後端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M:線程池最大線程數N的比例關係,其中M可以遠遠大於N.通過線程池可以靈活地調配線程資源,設置線程的最大值,防止由於海量併發接入導致線程耗盡。
採用線程池和任務隊列可以實現一種叫做僞異步的 I/O 通信框架,它的模型圖如上圖所示。當有新的客戶端接入時,將客戶端的 Socket 封裝成一個Task(該任務實現java.lang.Runnable接口)投遞到後端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程,對消息隊列中的任務進行處理。由於線程池可以設置消息隊列的大小和最大線程數,因此,它的資源佔用是可控的,無論多少個客戶端併發訪問,都不會導致資源的耗盡和宕機。

僞異步I/O通信框架採用了線程池實現,因此避免了爲每個請求都創建一個獨立線程造成的線程資源耗盡問題。不過因爲它的底層仍然是同步阻塞的BIO模型,因此無法從根本上解決問題。

補充問題:

爲什麼大家都不願意用 JDK 原生 NIO 進行開發呢?從上面的代碼中大家都可以看出來,是真的難用!除了編程複雜、編程模型難之外,它還有以下讓人詬病的問題:

  • JDK 的 NIO 底層由 epoll 實現,該實現飽受詬病的空輪詢 bug 會導致 cpu 飆升 100%
  • 項目龐大之後,自行實現的 NIO 很容易出現各類 bug,維護成本較高,上面這一坨代碼我都不能保證沒有 bug

Netty 的出現很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題3

JAVA AIO框架在windows下使用windows IOCP技術,在Linux下使用epoll多路複用IO技術模擬異步IO,這個從JAVA AIO框架的部分類設計上就可以看出來。
linux 中的select poll epoll的區別

epoll 並沒有實現異步方式,epoll /iocp 的多路複用都是的同步方式,這裏進一步深入學習需要了解是如何實現異步方式的。


  1. I/O多路複用和Socket ↩︎

  2. https://juejin.im/post/5c725dbe51882575e37ef9ed ↩︎

  3. BIO,NIO,AIO 的基本概念以及一些常見問題 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章