從根上理解高性能、高併發(六):通俗易懂,高性能服務器究竟是如何實現的

本文原題「高併發高性能服務器是如何實現的」,轉載請聯繫做者。php

一、系列文章引言

1.1 文章目的

做爲即時通信技術的開發者來講,高性能、高併發相關的技術概念早就瞭然與胸,什麼線程池、零拷貝、多路複用、事件驅動、epoll等等名詞信手拈來,又或許你對具備這些技術特徵的技術框架好比:Java的Netty、Php的workman、Go的gnet等熟練掌握。但真正到了面視或者技術實踐過程當中遇到沒法釋懷的疑惑時,方知自已所掌握的不過是皮毛。html

返璞歸真、迴歸本質,這些技術特徵背後的底層原理究竟是什麼?如何能通俗易懂、絕不費力真正透徹理解這些技術背後的原理,正是《從根上理解高性能、高併發》系列文章所要分享的。git

1.2 文章源起

我整理了至關多有關IM、消息推送等即時通信技術相關的資源和文章,從最開始的開源IM框架MobileIMSDK,到網絡編程經典鉅著《TCP/IP詳解》的在線版本,再到IM開發綱領性文章《新手入門一篇就夠:從零開發移動端IM》,以及網絡編程由淺到深的《網絡編程懶人入門》、《腦殘式網絡編程入門》、《高性能網絡編程》、《鮮爲人知的網絡編程》系列文章。程序員

越往知識的深處走,越以爲對即時通信技術瞭解的太少。因而後來,爲了讓開發者門更好地從基礎電信技術的角度理解網絡(尤爲移動網絡)特性,我跨專業收集整理了《IM開發者的零基礎通訊技術入門》系列高階文章。這系列文章已然是普通即時通信開發者的網絡通訊技術知識邊界,加上以前這些網絡編程資料,解決網絡通訊方面的知識盲點基本夠用了。github

對於即時通信IM這種系統的開發來講,網絡通訊知識確實很是重要,但迴歸到技術本質,實現網絡通訊自己的這些技術特徵:包括上面提到的線程池、零拷貝、多路複用、事件驅動等等,它們的本質是什麼?底層原理又是怎樣?這就是整理本系列文章的目的,但願對你有用。數據庫

1.3 文章目錄

從根上理解高性能、高併發(一):深刻計算機底層,理解線程與線程池編程

從根上理解高性能、高併發(二):深刻操做系統,理解I/O與零拷貝技術後端

從根上理解高性能、高併發(三):深刻操做系統,完全理解I/O多路複用服務器

從根上理解高性能、高併發(四):深刻操做系統,完全理解同步與異步微信

從根上理解高性能、高併發(五):深刻操做系統,理解高併發中的協程

從根上理解高性能、高併發(六):通俗易懂,高性能服務器究竟是如何實現的》(* 本文

1.4 本篇概述

接上篇《從根上理解高性能、高併發(五):深刻操做系統,理解高併發中的協程》,本篇是高性能、高併發系列的第6篇文章(也是完結篇)。

本篇是本系列文章的完結篇,你將能瞭解到,一個典型的服務器端是如何利用前5篇中講解的各單項技術從而實現高性能高併發的。

本文已同步發佈於「即時通信技術圈」公衆號,歡迎關注。公衆號上的連接是:點此進入

二、本文做者

應做者要求,不提供真名,也不提供我的照片。

本文做者主要技術方向爲互聯網後端、高併發高性能服務器、檢索引擎技術,網名是「碼農的荒島求生」。感謝做者的無私分享。

三、正文引言

當你在閱讀本篇文章的時候,有沒有想過,服務器是怎麼把這篇文章發送給你的呢?

提及來很簡單:不就是一個用戶請求嗎?服務器根據請求從數據庫中撈出這篇文章,而後經過網絡發回去嗎。

其實有點複雜:服務器端究竟是如何並行處理成千上萬個用戶請求的呢?這裏面又涉及到哪些技術呢?

這篇文章就是來爲你解答這個問題的。

四、多進程

歷史上最先出現也是最簡單的一種並行處理多個請求的方法就是利用多進程

好比在Linux世界中,咱們可使用fork、exec等系統調用建立多個進程,咱們能夠在父進程中接收用戶的鏈接請求,而後建立子進程去處理用戶請求。

就像這樣:

這種方法的優勢就在於:

  • 1)編程簡單,很是容易理解;
  • 2)因爲各個進程的地址空間是相互隔離的,所以一個進程崩潰後並不會影響其它進程;
  • 3)充分利用多核資源。

多進程並行處理的優勢很明顯,可是缺點一樣明顯:

  • 1)各個進程地址空間相互隔離,這一優勢也會變成缺點,那就是進程間要想通訊就會變得比較困難,你須要藉助進程間通訊(IPC,interprocess communications)機制,想想你如今知道哪些進程間通訊機制,而後讓你用代碼實現呢?顯然,進程間通訊編程相對複雜,並且性能也是一大問題;
  • 2)咱們知道建立進程開銷是比線程要大的,頻繁的建立銷燬進程無疑會加劇系統負擔。

幸虧,除了進程,咱們還有線程。

五、多線程

不是建立進程開銷大嗎?不是進程間通訊困難嗎?這些對於線程來講通通不是問題。

什麼?你還不瞭解線程,趕忙看看這篇《深刻計算機底層,理解線程與線程池》,這裏詳細講解了線程這個概念是怎麼來的。

因爲線程共享進程地址空間,所以線程間通訊自然不須要藉助任何通訊機制,直接讀取內存就行了。

線程建立銷燬的開銷也變小了,要知道線程就像寄居蟹同樣,房子(地址空間)都是進程的,本身只是一個租客,所以很是的輕量級,建立銷燬的開銷也很是小。

咱們能夠爲每一個請求建立一個線程,即便一個線程因執行I/O操做——好比讀取數據庫等——被阻塞暫停運行也不會影響到其它線程。

就像這樣:

但線程就是完美的、包治百病的嗎,顯然,計算機世界歷來沒有那麼簡單。

因爲線程共享進程地址空間,這在爲線程間通訊帶來便利的同時也帶來了無盡的麻煩。

正是因爲線程間共享地址空間,所以一個線程崩潰會致使整個進程崩潰退出,同時線程間通訊簡直太簡單了,簡單到線程間通訊只須要直接讀取內存就能夠了,也簡單到出現問題也極其容易,死鎖、線程間的同步互斥、等等,這些極容易產生bug,無數程序員寶貴的時間就有至關一部分用來解決多線程帶來的無盡問題。

雖然線程也有缺點,可是相比多進程來講,線程更有優點,但想單純的利用多線程就能解決高併發問題也是不切實際的。

由於雖然線程建立開銷相比進程小,但依然也是有開銷的,對於動輒數萬數十萬的連接的高併發服務器來講,建立數萬個線程會有性能問題,這包括內存佔用、線程間切換,也就是調度的開銷。

所以,咱們須要進一步思考。

六、事件驅動:Event Loop

到目前爲止,咱們提到「並行」二字就會想到進程、線程。

可是:並行編程只能依賴這兩項技術嗎?並非這樣的!

還有另外一項並行技術普遍應用在GUI編程以及服務器編程中,這就是近幾年很是流行的事件驅動編程:event-based concurrency。

PS:搞IM服務端開發的程序員確定不陌生,著名的Java NIO高性能網絡編程框架Netty中EvenLoop 這個接口意味着什麼(有關Netty框架的高性能原理能夠讀這篇《新手入門:目前爲止最透徹的的Netty高性能原理和框架架構解析》)。

你們不要以爲這是一項很難懂的技術,實際上事件驅動編程原理上很是簡單。

這一技術須要兩種原料:

  • 1)event;
  • 2)處理event的函數,這一函數一般被稱爲event handler;

剩下的就簡單了:你只須要安靜的等待event到來就好,當event到來以後,檢查一下event的類型,並根據該類型找到對應的event處理函數,也就是event handler,而後直接調用該event handler就行了。

That's it !

以上就是事件驅動編程的所有內容,是否是很簡單!

從上面的討論能夠看到:咱們須要不斷的接收event而後處理event,所以咱們須要一個循環(用while或者for循環均可以),這個循環被稱爲Event loop。

使用僞代碼表示就是這樣:

while(true) {

    event = getEvent();

    handler(event);

}

Event loop中要作的事情實際上是很是簡單的,只須要等待event的帶來,而後調用相應的event處理函數便可。

注意:這段代碼只須要運行在一個線程或者進程中,只須要這一個event loop就能夠同時處理多個用戶請求。

有的同窗能夠依然不明白:爲何這樣一個event loop能夠同時處理多個請求呢?

緣由很簡單:對於網絡通訊服務器來講,處理一個用戶請求時大部分時間其實都用在了I/O操做上,像數據庫讀寫、文件讀寫、網絡讀寫等。當一個請求到來,簡單處理以後可能就須要查詢數據庫等I/O操做,咱們知道I/O是很是慢的,當發起I/O後咱們大能夠不用等待該I/O操做完成就能夠繼續處理接下來的用戶請求。

如今你應該明白了吧:雖然上一個用戶請求尚未處理完咱們其實就能夠處理下一個用戶請求了,這也是並行,這種並行就能夠用事件驅動編程來處理。

這就比如餐廳服務員同樣:一個服務員不可能一直等上一個顧客下單、上菜、吃飯、買單以後才接待下一個顧客,服務員是怎麼作的呢?當一個顧客下完單後直接處理下一個顧客,當顧客吃完飯後會本身回來買單結帳的。

看到了吧:一樣是一個服務員也能夠同時處理多個顧客,這個服務員就至關於這裏的Event loop,即便這個event loop只運行在一個線程(進程)中也能夠同時處理多個用戶請求。

相信你已經對事件驅動編程有一個清晰的認知了,那麼接下來的問題就是,這個事件也就是event該怎麼獲取呢?

七、事件來源:IO多路複用

在《深刻操做系統,完全理解I/O多路複用》這篇文章中咱們知道,在Linux/Unix世界中一切皆文件,而咱們的程序都是經過文件描述符來進行I/O操做的,固然對於網絡編程中的socket也不例外。

那咱們該如何同時處理多個文件描述符呢?

IO多路複用技術正是用來解決這一問題的:經過IO多路複用技術,咱們一次能夠監控多個文件描述,當某個「文件」(實際多是im網絡通訊中socket)可讀或者可寫的時候咱們就能獲得通知啦。

這樣IO多路複用技術就成了event loop的原材料供應商,源源不斷的給咱們提供各類event,這樣關於event來源的問題就解決了。

固然:關於IO多路複用技術的詳細講解請參見《深刻操做系統,完全理解I/O多路複用》,本文做爲綱領性文章,就再也不贅述了。

至此:關於利用事件驅動來實現併發編程的全部問題都解決了嗎?event的來源問題解決了,當獲得event後調用相應的handler,看上去大功告成了。

想想還有沒有其它問題?

八、問題:阻塞式IO

如今:咱們可使用一個線程(進程)就能基於事件驅動進行並行編程,再也沒有了多線程中讓人惱火的各類鎖、同步互斥、死鎖等問題了。

可是:計算機科學中歷來沒有出現過一種能解決全部問題的技術,如今沒有,在可預期的未來也不會有。

那上述方法有什麼問題嗎?

不要忘了,咱們event loop是運行在一個線程(進程),這雖然解決了多線程問題,可是若是在處理某個event時須要進行IO操做會怎麼樣呢?

在《深刻操做系統,理解I/O與零拷貝技術》一文中,咱們講解了最經常使用的文件讀取在底層是如何實現的,程序員最經常使用的這種IO方式被稱爲阻塞式IO。

也就是說:當咱們進行IO操做,好比讀取文件時,若是文件沒有讀取完成,那麼咱們的程序(線程)會被阻塞而暫停執行,這在多線程中不是問題,由於操做系統還能夠調度其它線程。

可是:在單線程的event loop中是有問題的,緣由就在於當咱們在event loop中執行阻塞式IO操做時整個線程(event loop)會被暫停運行,這時操做系統將沒有其它線程能夠調度,由於系統中只有一個event loop在處理用戶請求,這樣當event loop線程被阻塞暫停運行時全部用戶請求都沒有辦法被處理。你能想象當服務器在處理其它用戶請求讀取數據庫致使你的請求被暫停嗎?

所以:在基於事件驅動編程時有一條注意事項,那就是不容許發起阻塞式IO。

有的同窗可能會問,若是不能發起阻塞式IO的話,那麼該怎樣進行IO操做呢?

PS:有阻塞式IO,就有非阻塞式IO。咱們繼續往下討論。

九、解決方法:非阻塞式IO

爲克服阻塞式IO所帶來的問題,現代操做系統開始提供一種新的發起IO請求的方法,這種方法就是異步IO。對應的,阻塞式IO就是同步IO,關於同步和異步這兩個概念能夠參考《從根上理解高性能、高併發(四):深刻操做系統,完全理解同步與異步》。

異步IO時,假設調用aio_read函數(具體的異步IO API請參考具體的操做系統平臺),也就是異步讀取,當咱們調用該函數後能夠當即返回,並繼續其它事情,雖然此時該文件可能尚未被讀取,這樣就不會阻塞調用線程了。此外,操做系統還會提供其它方法供調用線程來檢測IO操做是否完成。

就這樣,在操做系統的幫助下IO的阻塞調用問題也解決了。

十、基於事件驅動並行編程的難點

雖然有異步IO來解決event loop可能被阻塞的問題,可是基於事件編程依然是困難的。

首先:咱們提到,event loop是運行在一個線程中的,顯然一個線程是沒有辦法充分利用多核資源的,有的同窗可能會說那就建立多個event loop實例不就能夠了,這樣就有多個event loop線程了,可是這樣一來多線程問題又會出現。

另外一點在於編程方面,在《從根上理解高性能、高併發(四):深刻操做系統,完全理解同步與異步》這篇文章中咱們講到過,異步編程須要結合回調函數(這種編程方式須要把處理邏輯分爲兩部分:一部分調用方本身處理,另外一部分在回調函數中處理),這一編程方式的改變加劇了程序員在理解上的負擔,基於事件編程的項目後期會很難擴展以及維護。

那麼有沒有更好的方法呢?

要找到更好的方法,咱們須要解決問題的本質,那麼這個本質問題是什麼呢?

十一、更好的方法

爲何咱們要使用異步這種難以理解的方式編程呢?

是由於:阻塞式編程雖然容易理解但會致使線程被阻塞而暫停運行。

那麼聰明的你必定會問了:有沒有一種方法既能結合同步IO的簡單理解又不會因同步調用致使線程被阻塞呢?

答案是確定的:這就是用戶態線程(user level thread),也就是大名鼎鼎的協程(關於協程請詳讀本系列的上篇《從根上理解高性能、高併發(五):深刻操做系統,理解高併發中的協程》,本文就再也不贅述了)。

雖然基於事件編程有這樣那樣的缺點,可是在當今的高性能高併發服務器上基於事件編程方式依然很是流行,但已經不是純粹的基於單一線程的事件驅動了,而是 event loop + multi thread + user level thread。

關於這一組合,一樣值得拿出一篇文章來說解,咱們將在後續文章中詳細討論。

十二、本文小結

高併發技術從最開始的多進程一路演進到當前的事件驅動,計算機技術就像生物同樣也在不斷演變進化,但無論怎樣,瞭解歷史才能更深入的理解當下。但願這篇文章能對你們理解高併發服務器有所幫助。

附錄:更多高性能、高併發文章精選

高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少

高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題

高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了

高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索

高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型

高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型

高性能網絡編程(七):到底什麼是高併發?一文即懂!

以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰

知乎技術分享:知乎千萬級併發的高性能長鏈接網關技術實踐

淘寶技術分享:手淘億級移動端接入層網關的技術演進之路

一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)

一套原創分佈式即時通信(IM)系統理論架構方案

微信後臺基於時間序的海量數據冷熱分級架構設計實踐

微信技術總監談架構:微信之道——大道至簡(演講全文)

如何解讀《微信技術總監談架構:微信之道——大道至簡》

快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)

17年的實踐:騰訊海量產品的技術方法論

騰訊資深架構師乾貨總結:一文讀懂大型分佈式系統設計的方方面面

以微博類應用場景爲例,總結海量社交系統的架構設計步驟

新手入門:零基礎理解大型分佈式架構的演進歷史、技術原理、最佳實踐

重新手到架構師,一篇就夠:從100到1000萬高併發的架構演進之路

本文已同步發佈於「即時通信技術圈」公衆號。

▲ 本文在公衆號上的連接是:點此進入。同步發佈連接是:http://www.52im.net/thread-3315-1-1.html

相關文章
相關標籤/搜索