主呵吗货,是時候了泳唠。 -- 《秋日》
什么是服務器?
不就是提供“付費”宙搬、“免費”服務的高檔電腦嘛笨腥!
你提到服務?
存儲一個圖片勇垛,讀取一篇文字脖母,觀看一個動作片,計算一個賬戶存款闲孤,...
什么是并發(fā)谆级?
不如講一講什么是不并發(fā)。
我有一臺服務器讼积,1核CPU肥照,連接到互聯(lián)網(wǎng)提供服務。在09:00時刻勤众,突然有100個用戶同時要看服務器的數(shù)據(jù)舆绎,服務器怎么辦?
+-------+ 09:00
| |
| 服務器 |
| |
+-------+
|
|
----------------------------
互聯(lián)網(wǎng)
----------------------------
| | | ...... |
客戶1 客戶2 客戶3 客戶100
服務器:
--> 讀取客戶1的請求们颜, 驗證客戶身份吕朵, 把數(shù)據(jù)發(fā)送給你猎醇, 用時1秒 [ 客戶2到100等待中 ]
--> 讀取客戶2的請求, 驗證客戶身份努溃, 把數(shù)據(jù)發(fā)送給你硫嘶, 用時1秒 [ 客戶3到100等待中 ]
--> 讀取客戶3的請求, 驗證客戶身份梧税, 把數(shù)據(jù)發(fā)送給你音半, 用時1秒 [ 客戶4到100等待中 ]
........................................................................
--> 讀取客戶100的請求,驗證客戶身份贡蓖, 把數(shù)據(jù)發(fā)送給你, 用時1秒 []
這就是“不并發(fā)”煌茬,即“迭代”斥铺,也就是“循環(huán)”的意思。
迭代 == 循環(huán)
既然來了100個客戶坛善,那么一個一個的處理晾蜘,循環(huán)從客戶1一直到客戶100。處理完成客戶1才去處理客戶2眠屎,...剔交。這樣我們可以看出:
- 客戶1從發(fā)出請求到收到響應,等待了1秒
- 客戶2從發(fā)出請求到收到響應改衩,等待了2秒
- 客戶3從發(fā)出請求到收到響應岖常,等待了3秒
- ..................................
- 客戶100從發(fā)出請求到收到響應,等待了100秒
這就是“不并發(fā)”的問題葫督,同時來100個客戶竭鞍,這些用戶會排起長長的隊伍,等待很長的時候橄镜,服務器才會去為他服務偎快。客戶可不喜歡這樣的地方洽胶。
把服務器比喻成一個KFC晒夹,那么“不并發(fā)”就意味著只提供一個服務員,來了100個客戶姊氓,當然要排個長長的隊伍了丐怯。
+-------+ 09:00
| |
| KFC |
| |
+-- S --+
客戶1
客戶2
客戶3
......
客戶100
那么,如何并發(fā)他膳?
這個問題太廣泛了响逢,需要先從 "CPU" "操作系統(tǒng)" "進程" 開始。
CPU 操作系統(tǒng) 進程
操作系統(tǒng)運行時棕孙,采用“搶占”的方式舔亭。當今絕大多數(shù)操作系統(tǒng)采用資源“搶占”些膨。資源就是CPU計算,內(nèi)存使用钦铺,磁盤讀寫订雾,...基于此設計,在單核CPU的環(huán)境里矛洞,可以同時運行多個進程洼哎。
操作系統(tǒng)會劃分時間片,并使用一個任務隊列沼本,把每個進程每個階段的任務分配一個時間片噩峦,比如1ms(實際小的多)。1ms運行進程的任務抽兆,沒有完成就掛起放入隊列末端识补,下一次再運行。然后操作系統(tǒng)運行任務隊列的下一個任務辫红。
比如有個進程凭涂,他的任務是打開一個文件,然后讀取100個字符贴妻,并把文件寫入10個字符切油。
操作系統(tǒng)會運行進程,先打開文件名惩,如果這時候時間片時間到了澎胡,掛起進程,放入隊列后面娩鹉,運行下一個進程滤馍。
當操作系統(tǒng)根據(jù)任務隊列的前進,又一次到達這個進程底循,操作系統(tǒng)讀取100個字符巢株,時間到,掛起進程熙涤,放入隊列后面阁苞,運行下一個進程。
... 如此重復 ...
時間片祠挫?
事實上那槽,操作系統(tǒng)被聰明的設計,即便是單核CPU等舔,也可以同時運行多個進程骚灸。操作系統(tǒng)經(jīng)常同時運行多個進程,比如 Photoshop, Firefox, Vim, ... 他們是同時運行的慌植,而且能“同時”工作甚牲。
對于進程义郑,操作系統(tǒng)不會把 CPU 和內(nèi)存一直放在某個進程中。如果這樣丈钙,當有個進程耗費時間特別長時非驮,其他的進程就罷工了,也就無法同時運行多進程了雏赦。
所以劫笙,操作系統(tǒng)會給每個進程一個時間片,即運行進程中任務的時間上限星岗,到達時間后會掛起進程填大,放入任務隊列后面,直到下一次任務隊列取出這個進程任務俏橘。
任務隊列栋盹?
操作系統(tǒng)使用一個線性的隊列,管理ta自己的工作流程敷矫。操作系統(tǒng)不停地取出任務,運行汉额,取出任務曹仗,運行,...
從編程的角度觀看蠕搜,就好比是一個數(shù)組怎茫。
IO的根本:內(nèi)核緩沖區(qū)
對磁盤寫需要花費大量時間,而對內(nèi)存寫則小的多妓灌。
一個高效的做法是:在內(nèi)核區(qū)域開辟一塊內(nèi)存轨蛤,用來放置讀取和寫入的內(nèi)容。
比如
程序員在9:00寫入100個字符虫埂,這些字符被復制到內(nèi)核緩沖區(qū)中祥山,這是屬于內(nèi)核的一塊內(nèi)存。
在9:10寫入20個字符掉伏,這些字符也被復制到內(nèi)核緩沖區(qū)中缝呕。
在某個時間,比如9:30斧散,操作系統(tǒng)把內(nèi)核緩沖區(qū)中寫入的所有內(nèi)容排序供常,然后一次性寫入到磁盤。
從9:00到9:30之間可能寫入了幾百次內(nèi)核緩沖區(qū)鸡捐,但都是在內(nèi)存區(qū)域栈暇,速度會很快,而在9:30只進行了一次磁盤寫入箍镜。這樣把數(shù)百次的寫入操作集合成一次磁盤寫入源祈。從而減少磁盤寫入次數(shù)煎源。
----------+----------------+----------------+----------+---------------> 時間線
[100個字符] [20個字符] ...
9:00 |寫入 9:10 |寫入 9:xx|寫入
v v v
+-----------------------------------------------------------------+
| 內(nèi)核緩沖區(qū) |
+-----------------------------------------------------------------+
9:30 | 寫入
v
+-----------------------------------------------------------------+
| 磁盤 |
+-----------------------------------------------------------------+
實際的計算機其運行速度非常高,9:00~9:30只不過是我們的人為假設新博,計算機這段時間間隔大概只有1分鐘薪夕,或者更低,而在這1分鐘內(nèi)赫悄,可能已經(jīng)運行了成百上千次不同的寫入原献。
緩沖?
緩沖是一塊內(nèi)存埂淮,里面放著亂糟糟的東西姑隅。內(nèi)存由小格子組成,每個小格子代表一位倔撞,可以放置一個0或者1讲仰。8個小格子稱為1個字節(jié),1024個小格子稱為1千個字節(jié)痪蝇,二進制都是用2的倍數(shù)表示鄙陡,所以2進制的1千是1024。
計算機啟動后躏啰,內(nèi)存中的每個小格子都是有值的趁矾,我沒有深入研究過初始是什么值。但是我們可以假定是0.
現(xiàn)在给僵,需要一塊緩沖毫捣,那就是從內(nèi)存中拿出一塊沒有使用的區(qū)域,里邊是很多小格子帝际,每個小格子放置了0或者1蔓同。小格子中肯定有0或者1,不可能是空白的蹲诀。
既然小格子都是0 1斑粱,那么這塊內(nèi)存中就有一些不確定的數(shù)值。這些數(shù)值在開始是無用的數(shù)據(jù)脯爪。這塊內(nèi)存可能剛才被某個進程使用過珊佣,存儲了一些用戶的賬號密碼,然后你的這塊內(nèi)存還放著這些數(shù)據(jù)披粟。但是這些數(shù)據(jù)對你現(xiàn)在當前的進程是沒用的咒锻。
使用賦值可以覆蓋掉原有的格子中的值。小格子被新的0 1填充守屉,獲得一個新的數(shù)據(jù)惑艇。
看到這里你也應該明白了,如果我們申請了長度是64個小格子的內(nèi)存,也就是可以放置64個0 1的內(nèi)存滨巴,64個小格子是8個字節(jié)思灌,可以放置8個ASCII字符,4個JavaScript字符(16位)恭取。
如果我們在小格子里只填充了32個泰偿,那么剩下的32個是一些混亂的數(shù)據(jù),我們不需要蜈垮,所以我們需要精確定位要使用的小格子數(shù)耗跛。
也就是4個字節(jié)。我們填充4個字節(jié)的數(shù)據(jù)攒发,然后操作這塊緩沖的時候调塌,也只操作4個字節(jié)的(讀出到另一塊內(nèi)存,或者寫入其他文件)惠猿。剩下的4個字節(jié)就不要去動羔砾,那些是混亂的數(shù)據(jù)。
Buffer 對象就是Node.js對緩沖的一個對象表示偶妖,通過提供的函數(shù) API 我們可以操作緩沖姜凄。包括申請一塊內(nèi)存做緩沖,填充這塊緩沖趾访,操作這塊緩沖的數(shù)據(jù)(里邊的小格子)态秧。
如何并發(fā)?
-
多進程 多線程
對于大量占用CPU的程序腹缩,如果要給1千個人同時提供CPU使用,最理想的狀態(tài)就是提供1000個CPU空扎,每個CPU占用一個線程藏鹊。
不過現(xiàn)實還沒有這么多核的服務器谜疤。如果我們沒錢怔接,那么我們只有4個核,充其量我們可以提供4個CPU服務侠畔。也就是同時并行4個線程撮慨。同時可以為4個客戶提供CPU計算服務竿痰。
然而,大部分客戶在使用服務的時間中砌溺,需要CPU計算的時間比例較少影涉。
當你需要CPU密集的服務時,C語言是最好的選擇规伐,過去貌似更多的選擇C++蟹倾,但是現(xiàn)在的流行表示,以C++為代表的面向?qū)ο笾粫殉绦蚋愕糜纺[難以擴展。許多人在對C C++的反思后鲜棠,仍然認為C才是最有價值的肌厨。比如版本管理系統(tǒng)Git,簡潔有序豁陆。比如Redis柑爸,快速簡潔。
C能做的是盒音,用最簡單的代碼表達內(nèi)容表鳍,獲得最快速的CPU和內(nèi)存操作。
多進程里逆,為每一個客戶啟動一個進程提供服務进胯。
多線程,在一個進程內(nèi)為每一個客戶提供一個線程服務原押。多進程和多線程的過程是相似的胁镐,但是多線程的內(nèi)存開銷要比進程少,并且切換速度快一些诸衔,但是多線程編程會變復雜盯漂,并且會出現(xiàn)多個線程同時操作同一個數(shù)據(jù),從而引入鎖的問題笨农。
當CPU密集時就缆,顯然需要多線程更好一些,每一個客戶連接對應一個線程谒亦。因為在這樣的系統(tǒng)里竭宰,每一個任務都沒有等待的機會,所有的內(nèi)容都一直在不停地運算份招,直到結(jié)束切揭。
數(shù)據(jù)庫就是很好的代表。
---> 連接進入 ---> 領(lǐng)取一個線程 ---> 計算 ---> 返回 ---> 收回線程 ---> 連接進入 ---> 領(lǐng)取一個線程 ---> 計算 ---> 返回 ---> 收回線程 ---> 連接進入 ---> 領(lǐng)取一個線程 ---> 計算 ---> 返回 ---> 收回線程 ...........................................................
理想情況下锁摔,進入 1萬 個用戶廓旬,我們希望有 1萬 個線程在同時處理任務。顯然谐腰,事實上硬件還達不到孕豹。這就需要一些操作系統(tǒng)排隊。而一旦進入排隊十气,后續(xù)進入的客戶就會進入等待励背,他們會明顯的感受到延遲的存在。
比如進入 1萬 個用戶砸西,很有可能 10 個在運行計算椅野,另外的在操作系統(tǒng)中排隊。
這樣的服務需要多核速度很快的 CPU,并且服務吞吐量并不特別高竟闪。
-
IO 多路復用
與操作系統(tǒng)達成協(xié)議离福,同時監(jiān)測多個文件描述符(讀寫源頭),操作系統(tǒng)提交程序控制權(quán)的時候炼蛤,可以一次提交多個變動的描述符妖爷。從而可以一次控制多個源頭讀寫。
最典型的就是Unix提供的 select, poll理朋。
使用 select 模型的時候絮识,工作過程是這樣的:
- 首先打開多個文件描述符
- 交給操作系統(tǒng)處理數(shù)據(jù)讀寫
- 操作系統(tǒng)發(fā)現(xiàn)數(shù)據(jù)變動后,對這個標識符打上標簽嗽上,停止阻塞次舌,喚醒主程序
- 主程序遍歷文件描述符,發(fā)現(xiàn)有變化的兽愤,就對其運行一個小任務
- 全部運行完任務彼念,再次提供給操作系統(tǒng)
-
優(yōu)化的 IO 多路復用 epoll kqueue
當操作系統(tǒng)發(fā)出通知后,我們使用一個小緩沖內(nèi)存浅萧,讀取數(shù)據(jù)中的一塊逐沙,并運行任務,然后交回操作系統(tǒng)洼畅。因為處理的數(shù)據(jù)量很小吩案,所以感覺上去像是沒有阻塞。
交給操作系統(tǒng)后帝簇,操作系統(tǒng)就可以再次加入新變動的描述符用于下一次的任務徘郭。
文件描述符:是數(shù)據(jù)可以讀寫的源頭表示,比如一個文件描述符丧肴,就是代表可以讀寫的文件残揉。一個套接字描述符,也是可以讀寫的闪湾,網(wǎng)絡進入出去的數(shù)據(jù)使用“套接字描述符”這個術(shù)語來表示冲甘。
config fds[1, 2, 3, ...] // 配置文件描述符绩卤,他們關(guān)聯(lián)了數(shù)據(jù)讀寫的源頭 loop { // 循環(huán)運行 change_fds = epoll wait fds // 交給操作系統(tǒng)途样,并等待(睡眠) forEach change_fds { // 當操作系統(tǒng)通知時,會把變動的描述符放入change_fds if fd === socket in read socket if fd === file a write file a if fd === socket out write socket ... } }
首先打開多個文件描述符濒憋,
交給操作系統(tǒng)處理數(shù)據(jù)讀寫
操作系統(tǒng)發(fā)現(xiàn)數(shù)據(jù)變動后何暇,把變動的標識符放入一個變動描述符隊列,停止阻塞凛驮,喚醒主程序
主程序遍歷變動的文件描述符裆站,對其運行一個小任務
全部運行完任務,再次提供給操作系統(tǒng)
我看不明白!
Apache 在以往的服務中提供多線程服務器模型宏胯,Nginx 提供IO多路復用的模型羽嫡。
流行的數(shù)據(jù)庫,像 Mysql肩袍,采用多線程模型杭棵,因為 ta 面對的是密集的數(shù)據(jù)操作。而應用服務器面對的是套接字的讀取寫入氛赐,等待魂爪,很多時候,客戶都是沒有數(shù)據(jù)可以收發(fā)的艰管。
每個平臺實現(xiàn)了不同的 IO 多路復用滓侍,Linux 采用 epoll,BSD 采用 kqueue牲芋,還有的沒有采用撩笆,停留在多線程。libev是libevent的新版本街图,采用了統(tǒng)一封裝浇衬,針對不同平臺使用不同的IO吞吐。而在上層的編碼中餐济,采用統(tǒng)一的函數(shù)庫耘擂。
Node.js,底層是 C 編寫的 libev 框架絮姆,libev 在 Linux醉冤,BSD Unix上分別是用 epoll kqueue 多路復用模型,這在編程的抽象層常被叫做事件驅(qū)動篙悯。事實上蚁阳,ta 是多路復用,同時監(jiān)測多個文件描述符鸽照,采用非阻塞讀寫螺捐,從而在單線程進行并發(fā)。
非阻塞矮燎?IO...
所謂阻塞定血,是進程會進入睡眠,從而不再提供服務诞外,直到讀寫的數(shù)據(jù)已經(jīng)被放到內(nèi)核緩沖區(qū)澜沟,操作系統(tǒng)內(nèi)核會再次喚醒進程。
普通文件峡谊,也就是操作系統(tǒng)磁盤的文件茫虽,一般沒有讀和寫的阻塞刊苍,一旦通過open打開后,會立刻有內(nèi)存的映射濒析。你可以讀這個文件到內(nèi)存正什,然后再寫入別的地方。這些操作不需要等待數(shù)據(jù)準備号杏,操作是直接運行的埠忘,時間花費在磁盤尋址和內(nèi)存復制,沒有數(shù)據(jù)準備的等待馒索。
網(wǎng)絡套接字數(shù)據(jù)被認為要到來時莹妒,會有一些等待期,被認為是阻塞绰上。比如網(wǎng)絡的數(shù)據(jù)要一條一條傳過來旨怠,期間要經(jīng)過漫長的光纖。
套接字是怎么利用多路復用無阻塞讀寫的蜈块?
首先套接字是個讀寫雙工模式的鉴腻。套接字的數(shù)據(jù)來源于網(wǎng)絡,并從網(wǎng)絡發(fā)布出去百揭。因為此爽哎,每一階段從網(wǎng)絡來的數(shù)據(jù)量非常小∑饕唬可以比作一個水龍頭课锌,雖然水(數(shù)據(jù))確實一直不停地從水龍頭中涌出,但是每一點的水量都是非常小的祈秕,CPU 內(nèi)存處理這點流量幾乎不費吹灰之力渺贤。
所以,程序可以不停地檢測到數(shù)據(jù)流入请毛,并且擠滿內(nèi)核緩沖區(qū)志鞍,然后wait完畢,操作系統(tǒng)內(nèi)核通知進程(這個時間非常的短方仿,CPU是很快的)固棚,進程讀走內(nèi)核緩沖區(qū)的內(nèi)容,并返還給操作系統(tǒng)控制權(quán)仙蚜。操作系統(tǒng)再次把內(nèi)核緩沖區(qū)寫滿此洲,然后通知進程,...鳍征,如此黍翎,周而復始面徽,直到?jīng)]有數(shù)據(jù)變動了艳丛,操作系統(tǒng)就一直wait匣掸,進程則一直睡眠,直到有新的數(shù)據(jù)變化氮双。
每個循環(huán)階段碰酝,每個讀寫占用的時間和讀寫的數(shù)據(jù)量都是小塊的,幾乎可以看做瞬時戴差。完成后立刻交還給操作系統(tǒng)控制權(quán)送爸,等待操作系統(tǒng)內(nèi)核下一次的通知。
一個大文件如何在寫入讀取時不造成其他客戶等待暖释?
一個大型文件袭厂,可以使用一個游標記錄每次讀寫的位置,每次只讀寫一小塊球匕,然后記錄游標纹磺,停止讀寫,并返回到循環(huán)亮曹,進入等待橄杨。當操作系統(tǒng)下一次發(fā)出通知時,讀寫游標后面的一小塊照卦,并如此重復式矫,直到完全讀寫。這樣可以在最小的時間返還操作系統(tǒng)的控制權(quán)役耕,以此達到無阻塞采转。
我如何搭建我的超級服務器?
你需要運行一個CPU極度密集的業(yè)務瞬痘,并把ta投入到互聯(lián)網(wǎng)上提供服務氏义?
-
你需要一個服務器用來運行你的 CPU 密集型的業(yè)務,這臺服務器是用 C 編寫的图云,提供了高性能 CPU 和大量的內(nèi)存用來提供可靠的快速的服務惯悠。
這個服務提供 TCP UDP 級別的服務,也就是說通過套接字與其他進程通信竣况。
另外克婶,這個服務應該使用多線程,對每一個進入的請求提供一個線程丹泉,并使用線程池提升反應質(zhì)量情萤。并要有一個良好的請求隊列控制程序,以免請求過度摹恨,導致服務器崩潰筋岛。
這些服務可以是自己編寫的,也可以是第三方提供的晒哄,比如 Mysql, Orcale, ... 也可以是復雜的散點計算睁宰,或者大數(shù)據(jù)分析肪获,... 他們通過套接字與下面的IO服務通信。
-
你需要一個服務器用來運行你的 IO 密集型的業(yè)務柒傻,一旦你的服務是面向網(wǎng)絡孝赫,那么就意味著你需要驗證成千上萬的客戶,這些業(yè)務內(nèi)容不需要大量的 CPU 計算红符,就算是循環(huán)也可能只在100次量級之內(nèi)青柄。這時候應該使用編寫更快速,更容易管理的語言预侯,比如 Node.js致开,Python,Ruby萎馅。
這樣的語言編寫出來的程序喇喉,更容易擴展,和與他人合作校坑。因為是IO服務拣技,所以不存在計算的性能問題,存在的差別則是IO吞吐上耍目。
基于 IO 服務器的演化膏斤,大致經(jīng)歷了多進程 -> 多線程 -> select poll多路復用 -> epoll kqueue 多路復用。現(xiàn)在最快速的 IO 服務器是采用 epoll kqueue 多路復用邪驮,比如 Nginx莫辨。Node.js 本身就是 epoll 的,其核心是基于 epoll 的 libev 事件驅(qū)動庫毅访。
如果你打算使用 Python沮榜,Ruby,選用他們的 epoll kqueue 事件驅(qū)動庫喻粹,要比多線程庫快速并穩(wěn)定的多蟆融。
說明:epoll 只能在Linux服務器使用,在 BSD Unix 則是采用了kqueue守呜,與 epoll 達到相似的目的型酥。libev 在底層對多個平臺進行了統(tǒng)一封裝。
另外查乒,基于事件驅(qū)動的服務弥喉,也更容易水平擴展,搭建集群可以將服務器拓展至幾十個玛迄。
福利:甚至對于拓展的管理程序由境,Nginx 就可以提供現(xiàn)成的服務。
把你的 IO 密集服務器和你的 CPU 密集服務器蓖议,通過內(nèi)部局域網(wǎng)進行連接通信(套接字通信)虏杰,這樣你就提供了一個 CPU 極度密集的互聯(lián)網(wǎng)服務讥蟆。