愛奇藝網路協程編寫高併發應用實踐
⼆、⽹絡協程基本原理
⽹絡協程的本質是將應⽤層的阻塞式 IO 過程在底層轉換成⾮阻塞 IO 過程,並透過程式運⾏棧的上下⽂切換使 IO 準備就緒的協程交替運⾏,從⽽達到以簡單⽅式編寫⾼併發⽹絡程式的⽬的。既然⽹絡協程的底層也是⾮阻塞IO過程,所以在介紹⽹絡協程基本原理前,我們先了解⼀下⾮阻塞⽹絡通訊的基本過程。
下⾯給出了⾮阻塞⽹絡程式設計的常⻅設計⽅式:
• ⼀次完整的 IO 會話過程會被分割成多次的 IO 過程;
• 每次 IO 過程需要快取部分資料及當前會話的處理狀態;
• 要求解析器(如:Json/Xml/Mime 解析器)最好能⽀持流式解析⽅式,否則就需要讀到完整資料後才能交給解析器去處理,當遇到業資料較⼤時就需要分配較⼤的連續記憶體塊,必然會造成系統的記憶體分配壓⼒;
• 當前⼤部分後臺系統(如資料庫、儲存系統、快取系統)所提供的客戶端驅動都是阻塞式的,⽆法直接應⽤在⾮阻塞通訊應⽤中,從⽽限制了⾮阻塞通訊⽅式的應⽤範圍;
最⼩排程單元:當前⼤部分作業系統的最⼩排程單元是執行緒,即在單核或多核 CPU 環境中,作業系統是以執行緒為基本排程單元的,作業系統負責將多個執行緒任務喚⼊喚出; 上下⽂切換: 當作業系統需要將某個執行緒掛起時,會將該執行緒在 CPU 暫存器中的棧指標、狀態字等儲存⾄該執行緒的記憶體棧中;當作業系統需要喚醒某個被掛起的執行緒時(重新放置在CPU中運⾏),會將該執行緒之前被掛起的棧指標重新置⼊ CPU 暫存器中,並恢復之前保留的狀態字等資訊,從⽽使該執行緒繼續運⾏;透過這樣的掛起與喚醒操作,便完成了不同執行緒間的上下⽂切換; 並⾏與⽹絡併發:並⾏是指同⼀『時刻』同時運⾏的任務數,並⾏任務數量取決於 CPU 核⼼數量;⽽⽹絡併發是指在某⼀『時刻』⽹絡連線的數量;類似於⼆⼋定律,在客戶端與服務端保持 TCP ⻓連線時,⼤部分連線是空閒的,所以服務端只需響應少量活躍的⽹絡連線即可,如果服務端採⽤多路復⽤技術,即使使⽤單核也可以⽀持 100K 個⽹絡併發連線。
(二)協程的切換過程
因此,存在於執行緒中的⼤量協程需要相互協作,合理地佔⽤ CPU 時間⽚,在合適的運⾏點(如:⽹絡阻塞點)主動讓出 CPU,給其它協程提供運⾏的機會,這也正是『協程』這一概念的由來。每個協程一般都會經歷如下過程:
下圖是使用網路過程協程化示意圖:
在網路協程庫中,內部有一個預設的IO排程協程,其負責處理與網路IO相關的協程排程過程,故稱之為IO排程協程:
每⼀個⽹絡連線繫結⼀個套接字控制程式碼,該套接字繫結⼀個協程;
當對⽹絡套接字進⾏讀或寫發生阻塞時,將該套接字新增⾄ IO 排程協程的事件引擎中並設定讀寫事件,然後將該協程掛起;這樣所有處於讀寫等待狀態的⽹絡協程都被掛起,且與之關聯的⽹絡套接字均由 IO 排程協程的事件引擎統⼀監控管理; 當某些⽹絡套接字滿⾜可讀或可寫條件時,IO 排程協程的事件引擎返回這些套接字的狀態,IO 排程協程找到與這些套接字繫結的協程物件,然後將這些協程追加至協程排程佇列中,使其依次運⾏; IO 事件協程內部本身是由系統事件引擎(如:Linux 下的 epoll 事件引擎)驅動的,其內部 IO 事件的驅動機制和上⾯介紹的⾮阻塞過程相似,當某個套接字控制程式碼『準備就緒』時,IO 排程協程便將其所繫結的協程新增進協程排程佇列中,待本次 IO 排程協程返回後,會依次運⾏協程排程佇列⾥的所有協程。
建立⼀個監聽協程,使其『堵』在 accept() 調⽤上,等待客戶端連線;
啟動協程排程器,啟動新建立的監聽協程及內部的 IO 排程協程;
監聽協程每接收⼀個網路連線,便建立⼀個客戶端協程去處理,然後監聽協程繼續等待新的網路連線;
客戶端協程以『阻塞』⽅式讀寫⽹絡連線資料;網路連線處理完畢,則關閉連線,協程退出。
在介紹了⽹絡協程的基本原理後,本章節主要介紹 libfiber ⽹絡協程的核⼼設計要點,為⽹絡協程應⽤實踐化提供了基本的設計思路。
多核環境下 CPU 快取的親和性:CPU 本身配有⾼效的多級快取,雖然 CPU 多級快取容量較記憶體⼩的多,但其訪問效率卻遠⾼於記憶體,在單執行緒排程⽅式下,可以⽅便編譯器有效地進⾏ CPU 快取使⽤最佳化,使運⾏指令和共享資料儘可能放置在 CPU 快取中,⽽如果採⽤多執行緒排程⽅式,多個執行緒間共享的資料就可能使 CPU 快取失效,容易造成排程執行緒越多,協程的運⾏效率越低的問題;
多執行緒分配任務時的同步問題:當多個執行緒需要從公共協程任務資源中獲取協程任務時,需要增加『鎖』保護機制,⼀旦產⽣⼤量的『鎖』衝突,則勢必會造成運⾏效能的嚴重損耗;
事件引擎操作最佳化:在多執行緒排程則很難進⾏如此最佳化,下⾯會介紹在單執行緒排程模式下的事件引擎操作最佳化。
啟動多個程式,每個程式運⾏⼀個執行緒,該執行緒執行一個協程排程器;
同⼀程式內啟動多個執行緒,每個執行緒運⾏獨⽴的協程排程器;
協程鎖需要⽀持『同⼀執行緒內的協程之間、不同執行緒的協程之間、協程執行緒與⾮協程執行緒之間』的互斥;
⽹絡連線池的執行緒隔離機制,需要為每個執行緒建⽴各⾃獨⽴的連線池,防⽌連線物件在不同執行緒的協程之間共享,否則便會造成同⼀⽹絡連線在不同執行緒的協程之間使⽤,破壞單執行緒排程規則;
需要防⽌執行緒內的某個協程『瘋狂』佔⽤ CPU 資源,導致本執行緒內的其它協程得不到運⾏的機會,雖然此類問題在多執行緒排程時也會造成問題,但顯然在單執行緒排程時造成的後果更為嚴重。
3.2、協程事件引擎設計
libfiber 的事件引擎⽀持當今主流的作業系統,從⽽為 libfiber 的跨平臺特性提供了有⼒的⽀撐,下⾯為 libfiber 事件引擎所⽀持的平臺:
libfiber ⽀持採⽤界⾯訊息引擎做為底層的事件引擎,這樣在編寫 Windows 界⾯程式的⽹絡模組時便可以使⽤協程⽅式了,之前⼈們在 Windows 平臺編寫界⾯程式的⽹絡模組時,⼀般採⽤如下兩種⽅式:
現在 libfiber ⽀持 Windows 界⾯訊息引擎,我們就可以在界⾯執行緒中直接建立⽹絡協程,直接進⾏阻塞式⽹絡程式設計。
⼤家在談論⽹絡協程程式的運⾏效率時,往往只重視協程的切換效率,卻忽視了事件引擎對於效能影響的重要性,雖然現在很⽹絡協程庫所採⽤的事件引擎都是核心級的,但仍需要合理使⽤才能發揮其最佳效能。
在使⽤ libfiber 的早期版本編譯⽹絡協程服務程式時,雖然在 Linux 平臺上也是採⽤了 epoll 事件引擎,但在對⽹絡協程服務程式進⾏效能壓測(使⽤⽤系統命令 『# perf top -p pid』 觀察運⾏狀態)時,卻發現 epoll_ctl API 佔⽤了較⾼的 CPU,分析原因是 epoll_ctl 使⽤次數過多導致的:因為 epoll_ctl 內部在對套接字控制程式碼進⾏新增、修改或刪除事件操作時,需要先透過紅⿊樹的查詢演算法找到其對應的內部套接字物件(紅⿊樹的查詢效率並不是O (1)的),如果 epoll_ctl 的調⽤次數過多必然會造成 CPU 的佔⽤較⾼。
在 libfiber 中之所以可以針對中間的事件操作過程進⾏合併處理,主要是因為 libfiber 的排程過程是單執行緒模式的,如果想要在多執行緒排程器中合併中間態的事件操作則要難很多:在多執行緒排程過程中,當套接字所繫結的協程因IO 可讀被喚醒時,假設不取消該套接字的讀事件,則該協程被某個執行緒『拿⾛』後,恰巧該套接字又收到新資料,核心會再次觸發事件引擎,協程排程器被喚醒,此時協程排程器也許就不知該如何處理了。
3.3.1、單⼀執行緒內部的協程互斥
• 執行緒B 中的協程B2 對執行緒鎖2成功加鎖;
透過使⽤原⼦數可以使協程快速加鎖空閒的事件鎖,原⼦數在多執行緒或協程環境中的⾏為相同的,可以保證安全性;
當鎖被佔⽤時,該協程進入IO管道讀等待狀態而被掛起,這並不會影響其所屬的執行緒排程器的正常執行;在 Linux 平臺上可以使⽤ eventfd 代替管道,其佔⽤資源更少。
3.3.3、協程條件變數
在使⽤執行緒程式設計時,都知道執行緒條件變數的價值:線上程之間傳遞訊息時往往需要組合執行緒條件變數和執行緒鎖。因此,在 libfiber 中也設計了協程條件變數(原始碼⻅ fiber_cond.c),透過組合使⽤ libfiber 中的協程事件鎖(fiber_event.c)和協程條件變數,⽤戶便可以編寫出⽤於線上程之間、執行緒與協程之間、執行緒內的協程之間、執行緒間的協程之間進⾏訊息傳遞的訊息佇列。下圖為使⽤ libfiber 中協程條件變數時的互動過程:
3.3.4、協程訊號量
3.4、域名解析
3.5、Hook 系統 API
在網路協程廣泛使用前,很多⽹絡庫很早就存在了,並且⼤部分這些⽹絡庫都是阻塞式的,要改造這些⽹絡庫使之協程化的成本是⾮常巨⼤的,我們不可能採⽤協程⽅式將這些⽹絡庫重新實現⼀遍,⽬前⼀個⼴泛採⽤的⽅案是 Hook 與 IO 及網路相關的系統中 API,在 Unix 平臺上 Hook 系統 API 相對簡單,在初始化時,先載入並保留系統 API 的原始地址,然後編寫⼀個與系統 API 函式名相同且引數也相同的函式,將這段程式碼與應⽤程式碼⼀起編譯,則編譯器會優先使⽤這些 Hooked API,下⾯的程式碼給出了在 Unix 平臺上 Hook 系統 API 的簡單示例:
在 libfiber 中Hook 了⼤部分與 IO 及⽹絡相關的系統 API,下⾯列出 libfiber 所 Hook 的系統 API:
• 讀 API:read/readv/recv/recvfrom/recvmsg;
• 寫API:write/writev/send/sendto/sendmsg/sendfile64;
⽹絡相關 API
• 域名解析 API:gethostbyname/gethostbyname_r, getaddrinfo/freeaddrinfo。
透過 Hook API ⽅式,libfiber 已經可以使 Mysql 客戶端庫、⼀些 HTTP 通訊庫及 Redis 客戶端庫的⽹絡通訊協程化,這樣在使⽤⽹絡協程編寫服務端應⽤程式時,⼤⼤降低了程式設計複雜度及改造成本。
4.1.1、專案背景
• 合併回源:當多個使用者訪問同一段資料內容時,回源軟體應合併相同請求,只向源站發起一個請求,一方面可以降低源站的壓力,同時可以降低迴源頻寬;
• 斷點續傳:當資料回源時如果因網路或其它原因造成回源連線中斷,則回源軟體應能在原來資料斷開位置繼續下載剩餘資料;
• 隨機位置下載:因為很多使用者喜歡跳躍式點播影片內容,為了能夠在快速響應使用者請求的同時節省頻寬,要求回源軟體能夠快速從影片資料的任意位置下載、同時停止下載使用者跳過的內容;
• 資料完整性:為了防止資料在傳輸過程中因網路、機器或軟體重啟等原因造成損壞,需要對已經下載的塊資料和完整資料做完整性校驗;
在愛奇藝的自建 CDN 系統中,作為資料回源及本地快取的核心軟體,奇迅承擔了重要角色,該模組採用多執行緒多協程的軟體架構設計,如下所示奇迅回源架構設計的特點總結如下:
• 更有助於客戶端與奇迅之間保持長連線,提升響應效能。
對於後端下載模組,由於採用協程方式,在資料回源時允許建立更多的併發連線去多個源站下載資料,從而獲得更快的下載速度;同時,為了節省頻寬,奇迅採用合併回源策略,即當前端多個客戶端請求同一段資料時,下載模組將會合並相同的請求,向源站發起一份資料請求,在合併回源請求過程中,因資料共享原因,必然存在如 “3.3.2、多執行緒之間的協程互斥”章節所提到的多個執行緒之間的協程同步互斥的需求,透過使用 libfiber 中的事件鎖完美地解決了一這需求(其實,當初事件鎖就是為了滿足奇迅的這一需求而設計編寫)。
4.1.3、專案成果
採用協程方式編寫的回源與快取軟體『奇迅』上線後,愛奇藝自建CDN影片卡頓比小於 2%,CDN 影片回源頻寬小於 1%。
4.2、⾼效能 DNS 模組使⽤協程
4.2.1、專案背景
4.2.2、軟體架構
DNS 做為網際網路的基礎設施,在整個網際網路中發揮著舉足輕重的作用,愛奇藝為了滿足自身業務的發展需要,自研了高效能 DNS(簡稱 HPDNS),該 DNS 的軟體架構如下圖所示:
HPDNS 服務的特點如下:
優點 | 說明 |
高效能 | 啟用 Linux 3.0 核心的 REUSEPORT 功能,提升多執行緒並行收發包的能力 |
採用 Linux 3.0 核心的 recvmmsg/sendmmsg API,提升單次 IO 資料包收發能力 | |
採用記憶體預分配策略,減少記憶體動態分配/釋放時的“鎖”衝突 | |
針對 TCP 服務模式,採用網路協程框架,最大化 TCP 併發能力 | |
高可用 | 採用RCU(Read Copy Update)方式更新檢視資料及配置項,無需停止服務,且不影響效能 |
網路卡 IP 地址變化自動感知(即可自動新增新 IP 或摘除老IP而不必停止服務) | |
採用 Keepalived 保證服務高可用 | |
易管理 | 由 master 服務管理模組管理 DNS 程式,控制 DNS 程式的啟動、停止、重讀配置/資料、異常重啟及異常報警等 |
由於 DNS 協議要求 DNS 服務端需要同時支援 UDP 及 TCP 兩種通訊方式,除了要求 UDP 模組具備高效能外,對 TCP 模組也要求支援高併發及高效能,該模組的網路通訊部分使用 libfiber 編寫,從而支援更高的併發連線,同時具備更高的效能,又因啟用多個執行緒排程器,從而可以更加方便地使用多核。
4.2.3、專案成果
五、總結
本文講述了愛奇藝開源專案 libfiber 網路協程庫的設計原理及核心設計要點,方便讀者瞭解網路協程的設計原理及執行機制,做到知其然且知其所以然;還從愛奇藝自身的專案實踐出發,總結了在應用網路協程程式設計時遇到的問題及解決方案,使讀者能夠更加全面地瞭解編寫網路協程類應用的注意事項。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69945252/viewspace-2704731/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 攜程大資料實踐:高併發應用架構及推薦系統案例大資料應用架構
- 理解 TCP/IP 網路棧 & 編寫網路應用TCP
- Nginx Ingress 高併發實踐Nginx
- 愛奇藝iOS深度實踐 | SiriKit詳解應用篇iOS
- Go 併發 -- 協程Go
- 基於協程的高效能高併發伺服器框架—協程模組伺服器框架
- 愛奇藝內容中臺之Serverless應用與實踐Server
- 高併發解決方案orleans實踐
- Java併發:分散式應用限流 Redis + Lua 實踐Java分散式Redis
- 使用socket+gevent實現協程併發
- 漫談OB | OceanBase 在海量資料和高併發下的應用實踐
- 愛奇藝混合雲內網DNS實踐內網DNS
- async-rdma:編寫高吞吐量、低延遲網路應用的Rust庫Rust
- 線上Redis高併發效能調優實踐Redis
- 京東搶購服務高併發實踐
- 網際網路高併發架構設計模式架構設計模式
- 網站高併發網站
- 如何應用TFGAN快速實踐生成對抗網路?
- PHP協程:併發 shell_execPHP
- 併發技術2:多協程
- 愛奇藝元件化設計在會員業務的應用和實踐元件化
- 阿里雲SLB負載均衡實踐,解決高併發響應慢阿里負載
- KubeSphere 在網際網路電商行業的應用實踐行業
- 協程應用開發框架 FibJS框架JS
- Elasticsearch高併發寫入優化的開源協同經歷Elasticsearch優化
- 常用高併發網路執行緒模型設計及mongodb執行緒模型優化實踐執行緒模型MongoDB優化
- [分散式][高併發]熔斷策略和最佳實踐分散式
- 高併發場景下JVM調優實踐之路JVM
- 高併發IM系統架構優化實踐架構優化
- 構建高併發高可用的電商平臺架構實踐架構
- 用PHP實現高併發伺服器PHP伺服器
- 後臺開發-核心技術與應用實踐--TCP協議TCP協議
- 《用Python寫網路爬蟲》--編寫第一個網路爬蟲Python爬蟲
- Golang協程併發的流水線模型Golang模型
- 智慧編撰:使用神經網路協助編寫電子郵件神經網路
- Golang 高效實踐之併發實踐Golang
- python網路-多工實現之協程Python
- 愛奇藝在服務網格方向的落地實踐