acl 伺服器程式設計框架設計要點

鄭樹新發表於2016-08-29

一、概述

軟體技術發展至今,存在著很多成熟的開發框架(如廣大 Java 程式設計師所熟知的 SSH 框架),這些開發框架或面向資料庫,或面向網路通訊,或面向應用伺服器,或面向介面設計,甚至面向某類業務模型。這些開框架的存在,大大提高了程式設計師的開發效率,這樣使技術人員將精力更多地集中於業務本身,而不必拘泥於技術的底層實現細節,但也造成了眾多知其然不知其所以然的所謂“碼農”,尤其對於那些使 用 Java、PHP、.NET 等高階語言進行業務開發的程式設計師而言,更是如此。

acl 網路通訊與伺服器程式設計框架是一個開源的 C/C++庫,提供了豐富的多種網路伺服器程式設計模型,同時提供了大量的常見網路應用協議,有利於技術人員快速地編寫出安全、穩定、高效的服務端程式。

二、常見的幾種網路伺服器模型

程式設計師應該去關注一下底層的實現原理,甚至需要去研究其實現細節。有很多著名的開源伺服器程式值得我們去研究學習,比如 postfix,nginx,mysql,redis,varnish,squid,ircd,apache 等,通過研究這些開源服務軟體,可以使我們懂得真實執行環境中的伺服器軟體設計法則。下面的表格列出了常見的伺服器設計模型:

 伺服器模型  描述  優點 缺點  舉例
 多程式方式  一個連線一個程式  安全、穩定  併發度低  Postfix、Apache1.3.x
 多執行緒阻塞方式  一個連線一個執行緒  併發度略有提升、資源佔用稍低  併發底較低  Mysql、Mongodb、Apache2.0.x
 單執行緒非阻塞方式  單一執行緒採用事件觸發支撐大量連線  併發度高、資源佔用低  程式設計複雜度高、需多個程式例項才可使用多核  Nginx、lighttpd、Redis、Squid、ircd
 多執行緒事件觸發方式  多個執行緒採用事件觸發支撐大量連線  併發度高、資源佔用低、有效使用多核、程式設計複雜度低  資源共享需要互斥  Memcached、Varnish、Apache2.2.x
 UDP無連線方式  採用UDP的無連線通訊模式  併發度高、資源佔用低  通訊可靠性差  bind
以上表格將一些著名的開源伺服器軟體進行了歸類,同時對比了不同的伺服器程式設計模型的優缺點,究竟該採用何種伺服器模型,則需根據實際應用場景進行選擇。如 果你非常注重系統安全穩定性但併發度要求不高時則可以選擇“多程式方式”;如果你的應用服務要求支援高併發,同時要求非常低的資源消耗可以選擇“單執行緒非 阻塞方式”(當然選擇這種方式得需要注意程式設計的複雜度,畢竟多數情況下,我們的實際應用並不需要象 nginx,redis 那樣的高效能、高併發);如果你想要支援一定的高併發,但又不想要非常高的程式設計複雜度,則“多執行緒事件觸發方式”就是你的選擇了(本人在實踐中的專案大多 採用此類模型)。

三、acl 網路通訊與伺服器程式設計框架介紹

對 C/C++ 程式設計師而言,雖然存在著如此眾多的開源伺服器應用軟體,但想要直接應用於自己的業務上是不太可能的,畢竟業務型別是千變萬化,私有應用協議也是五花八門。 是否存在一些能適應多種業務型別的伺服器程式設計框架呢?答案是肯定的,其中 ACE 就是一個非常著名的開源網路通訊與伺服器開發框架庫,這是由 Douglas C. Schmidt 在做博士論文期間用 C++ 編寫的網路通訊與伺服器開發框架,該框架出現的比較早,應用範圍也比較廣泛,但是程式設計複雜度很高,裡面充斥著大量的設計模式,有人形容其學術味未免太濃。 acl 網路通訊與伺服器框架是另一個選擇,該框架至今也有近十年的歷史,最初來源於著名的郵件伺服器軟體 Postfix,從中借鑑了大量伺服器設計思想及程式碼,後來逐漸演變成一個通用的伺服器開發框架。在介紹 acl 伺服器框架前,不妨先介紹一下 Postfix 的伺服器設計模式以及 acl 伺服器框架與 Postfix 的伺服器的異同點。

3.1、Postfix 伺服器框架的設計特點

1)、父子程式協作:父程式(master)複雜排程及監控服務子程式,服務子程式負責接收處理具體的業務型別
2)、穩定:主控程式(master)監控所有子程式的執行狀態,子程式異常行為可控
3)、安全:子程式以普通使用者身份執行
4)、資源可控:子程式為半駐留服務方式,可在完成一定任務量或空閒一定時間後主動退出
5)、模組化:每種服務為獨立程式,有多個伺服器模型根據需要選擇
6)、併發度:因為採用程式池方式,每個連線一個程式,所以併發度很低

下圖是父程式的流程圖:
father_proc

下圖為服務子程式的流程圖:

child_proc
3.2、acl 伺服器框架與 Postfix 伺服器框架的異同

雖然 acl 中的伺服器框架設計源於 Postfix,但 acl 的設計目標與 Postfix 並不相同,Postfix 的作者Wietse Venema 在設計 Postfix 之初主要是為了設計一個比 sendmail 更為安全、穩定、擴充套件性更好的郵件 MTA軟體,而 acl 伺服器框架的主要目標是希望該框架能夠適應更多的應用業務場景,下表是二者一些主要異同點:

功能點 Postfix master acl_master
半駐留服務模式 支援 支援
安全控制 嚴格的使用者許可權控制 嚴格的使用者許可權控制
配置方式 所有服務配置在同一個配置檔案中 一個服務一個配置檔案
程式池模式 支援 支援
觸發器模式 支援 支援
非阻塞模式 功能一般 功能強大
執行緒池模式 不支援 支援
線上升級 支援 支援
預啟動 不支援 支援
最小程式數控制 不支援 支援
最大程式數控制 支援 支援
監控子程式報警機制 不支援 支援
開發過程除錯功能 不太方便 方便(很容易使用 valgrind 檢查)
客戶端連線訪問控制 應用自己保證 框架自動支援
單一程式監聽多個地址  受限  支援
單一程式同時監聽TCP及域套介面 不支援 支援
子程式執行身份控制 支援 支援
日誌記錄方式 支援 syslog 支援syslog-ng;允許使用者註冊自己的日誌處理過程;允許同時寫入多個目標日誌物件中
子程式崩潰是否允許產生 core 檔案 通過配置項控制,便於快速消除錯誤
是否支援UDP通訊模式 不支援 支援
是否支援多程式TCP連線均勻化 不支援 支援

以上為 Postfix 的 master 伺服器模組與 acl 中的 acl_master 伺服器模組的主要區別,當然這個對比並不是說明 acl 的 acl_master 伺服器模組優於 Postfix 的 master(畢竟 acl 的伺服器模組是來源於 Postfix),而是為了說明 acl 的 acl_master 服務模組可能更方便技術人員開發自己的服務應用。

四、acl 伺服器程式設計框架設計要點

從上面的表格可以看出,設計一個高效實用的伺服器框架需要考慮的層面還是不少,下面從幾個角度列出了 acl 網路通訊與伺服器開發框架的設計要點。

1、網路通訊功能的重要作用

在網路伺服器架構設計中,網路通訊作為基礎模組是不可或缺的,在 acl 庫中有豐富的網路通訊功能模組,雖然該模組是對底層系統 API 的封裝,但卻提供了豐富的高階功能,同時遮蔽了在使用底層系統  API 容易出錯的地方,因而可以方便程式設計師快速地開發出高效、穩定、安全的網路通訊應用。在將系統 IO API 封裝成流時,其中一個重要的作法就是資料快取,資料快取可以降低對系統 API 的呼叫次數(這可以減少系統的上下文切換,從而減少系統 CPU 負載),acl 庫的網路流的設計也存在著資料快取層,可以支援網路流和檔案流,同時提供了豐富的讀操作介面:讀指定位元組長度資料,按行讀資料(可以相容 \r\n 及 \n 兩種情況),以及其它大量的讀操作函式。下圖分別是阻塞 IO 和非阻塞 IO 的類繼承關係:

aio
阻塞 IO 繼承關係圖
stream
非阻塞 IO 類繼承關係圖

acl 庫中的網路通訊模組除了大量的 IO 讀寫介面外,還有域名解析、網路監聽、網路連線等介面,基本上涵蓋了常見的網路操作;此外,acl 中的網路模組支援阻塞網路 IO 以及非阻塞 IO 兩種 IO 模型,其中非阻塞 IO 又支援 reactor 和 proactor 兩種非阻塞 IO 模型;acl 網路模組本身並不支援 SSL/TLS 功能(這畢竟是另一個重要領域),但卻對外提供了 IO 操作註冊介面,目前通過封裝著名的嵌入式 SSL/TLS 庫(polarssl,據說最近因併入 arm 而改名了)而具備了 SSL/TLS 的通訊能力(阻塞及非阻塞 IO 均已支援 SSL/TLS 通訊功能)。

2、IO 事件引擎的關鍵作用

一般來講,目前常見的網路伺服器內部都會封裝系統的 IO 事件引擎(如:select/poll/epool/kquque/devpoll/iocp/win32 message),以此作為網路 IO 的訊息驅動引擎,acl 庫內部也封裝了這些 IO 事件引擎,為了適應不同的網路服務框架模型,acl 庫封裝的 IO 事件引擎分為單執行緒事件引擎以及多執行緒事件引擎(目前 iocp/win32 message 除外)。其中單執行緒 IO 事件引擎主要用在高併發非阻塞網路服務模型中,而多執行緒 IO 事件引擎則用在多執行緒伺服器模型中。

在 acl 庫中封裝的事件模型中 select 是一個通用的事件引擎(可以支援WIN32/LINUX/UNIX);epoll 是 LINUX 下核心級的高效事件引擎(尤其是在高併發環境下存在大量空閒連線時效能尤佳);iocp 是 WIN32 下的高效事件引擎,acl 中的封裝與網際網路上大多數使用方式不同,在 acl 中採用了單執行緒封裝方式;win32 message 是 acl 庫中專門針對基於 win32 介面訊息而封裝的事件 IO 引擎。

3、執行緒池設計中的注意要點

多執行緒伺服器模型也許是很多公司使用最多的伺服器模型,因為此伺服器型的開發效率較高,容易實現一些複雜的業務邏輯(例如,現在多數資料庫驅動也是阻塞 的,為了與之結合,應用伺服器程式只能採用阻塞模型)。為了提高任務執行效率,設計一個高效的執行緒池是非常有必要的,網上一些經典的執行緒池設計方式大同小 異,基本都是通過組合使用執行緒鎖(pthread_mutex_lock/unlock)與執行緒條件變數(pthread_cond_signal) 等系統 API 實現任務入隊、出隊的過程,這些設計中基本都是一個執行緒池共享一把執行緒鎖和一個執行緒條件變數,在新增任務時先加鎖,然後解鎖並通知執行緒條件變數來喚醒一個 或幾個工作執行緒,這些工作執行緒在加鎖後從任務佇列中取出任務後立即解鎖,然後開始執行取得的任務。這種執行緒池設計模型看起來並沒有什麼問題,但線上程數較 多(過百)且任務通知非常頻繁時卻存在著 CPU 佔用較多的問題,即所謂執行緒池驚群現象。出現此類問題的原因主要線上程條件變數通知的系統 API (pthread_cond_signal) 上,通過檢視該 API 的線上幫助,可以看到這麼一段話:pthread_cond_signal 將會喚醒一個或者多個等待線上程條件變數上的執行緒,也正是這其中的”多個“關鍵詞造成了高壓力下執行緒池使用中出現的驚群現象。

那該如何避免執行緒池設計中的驚群現象呢?在 acl 的執行緒池設計是這樣的:在仍然共用一個執行緒互斥鎖的條件下,給每一個消費者執行緒分配一個獨立的執行緒條件變數和一個獨立的任務佇列,生產者執行緒在新增任務 時,找到空閒的消費者執行緒,將任務置入該消費者的任務佇列中同時只通知 (pthread_cond_signal) 該消費者的執行緒條件變數,消費者執行緒與生產者執行緒雖然共用相同的執行緒互斥鎖(因為有全域性資源及呼叫 pthread_cond_wait 所需),但執行緒條件變數的通知過程卻是定向通知的,未被通知的消費者執行緒不會被喚醒,這樣驚群現象也就不會產生了。

4、通過 IO 事件引擎將網路連線池與執行緒池隔離

通常的多執行緒伺服器設計是這樣的:給每一個網路連線分配一個獨立的執行緒,連線不關閉,則該執行緒一直被該連線所佔用。這樣設計的好處是實現該服務模型非常簡 單,但缺點也是顯而易見的,那就是:實際應用中,客戶端為了提高網路傳輸效率,大量採用連線池方式,每次處理任務時從連線池取得一個空閒連線與服務端進行 通訊,獲得服務端的處理結果後再將該連線放回空閒連線池中,此時服務端卻被這個空閒連線佔用著,這樣就造成了此類伺服器程式併發度較低的問題。

而在 acl 多執行緒伺服器模型中網路連線池與執行緒池是通過 IO 事件引擎隔離的,如何理解”隔離“二字?首先得需要理解 acl 多執行緒伺服器模型的工作機制:

服務端接收到客戶端連線 —> 將該連線置入 IO 事件引擎中,等待該連線可讀或出錯 —> IO 事件引擎中的某個連線有資料可讀時 —> 該連線被交給執行緒池中的一個空閒執行緒去處理 IO 過程 —> 執行緒處理完本次 IO 過程,則重將該連線歸還給 IO 事件引擎 —> 該工作執行緒也重新被置為空閒狀態歸還給執行緒池。

通過 IO 事件引擎就做到了當客戶端連線有資料可讀時其與執行緒池中的某個空閒執行緒繫結,當該連線空閒時便與該執行緒解綁。acl 中的這種多執行緒服務設計模型適用了真實生產環境大多數的應用場景,做到了僅需建立幾十至幾百個執行緒便可與成千上萬個客戶端保持長連線。

5、記憶體管理應如何設計

在多執行緒執行環境中,記憶體的頻繁動態分配及釋放往往會影響整體執行效能,原因是程式在在堆上動態分配與釋放記憶體時,需要不斷地使用執行緒鎖進行互斥,所以當 執行緒數非常多時,如果每個執行緒都有大量的記憶體分配/釋放操作,則鎖競爭非常嚴重,象 malloc/free 標準 C 函式內部的執行緒鎖往往使用自旋鎖,所以會發現程式的 CPU 佔用非常高(在 RHL6/Centos6 上可以使用 perf top -p pid 監控程式執行狀態,發會 spin_lock 呼叫頻率非常高,這也說明了多執行緒進行記憶體分配時的競爭是非常嚴重的)。

如果降低多執行緒環境記憶體動態管理時的鎖競爭呢?一般有兩種方式,其一:使用建立線上程區域性變數上的記憶體池,其二:使用會話記憶體管理策略。

所謂”建立線上程區域性變數上的記憶體池“,其主要思想是使用每個執行緒上的執行緒區域性變數給其分配一個記憶體池,這樣當執行緒需要分配/釋放記憶體時只需引用自己的線 程區域性記憶體池即可,不會發生與其它執行緒產生記憶體分配的衝突問題;但對於這樣一個應用場景:記憶體在一個執行緒中分配而在另一個執行緒釋放時,這種分配機制就不會 有效減少鎖衝突,尤其是執行緒區域性記憶體池還進行了記憶體分片時鎖衝突問題就會更為嚴重,因為當某個執行緒獲得了其它執行緒分配的記憶體後需要釋放時,並不能立即釋 放,而是要先歸還給該記憶體片的”屬主“執行緒,由”屬主“執行緒負責釋放。因此,這種分配機制主要用在記憶體的跨執行緒操作相對不”頻繁“的應用場景中。在 acl 庫中也提供了此類記憶體管理模組,參見 acl_slice.h 標頭檔案的函式說明。當然,大家比較熟悉應該是 google 開源的 tcmalloc 庫。

而何為”使用會話記憶體管理策略“呢?其主要方式是:在一個任務會話開始時建立一個記憶體分配器(其管理著一個記憶體池),在下面的所有操作步驟中都將該分配器 傳遞,在所有處理過程中的記憶體分配在該分配器上進行,當該任務會話結束時釋放記憶體分配器,從而統一釋放了在該記憶體分配器上的記憶體池。這樣做的好處很明顯, 就是大大降低了 malloc/free 的次數。缺點也是很明顯的,就是在每的個操作過程都得“帶”著這個記憶體分配器。使用此方式的經典的例子就是 apache;當然在 acl 庫也存在類似的一個簡單的記憶體分配器(參見 acl_dbuf_pool.h ),在 acl 的 redis 客戶端庫中大量使用了該記憶體分配器,從而使之在多執行緒環境依賴具有很高的效能。

6、更好地使用多程式例項

acl 中的伺服器框架有一個是多執行緒伺服器模型,但其仍然可以被啟動多個程式例項,每個程式例項內採用執行緒池方式,大家也許會問:既然多執行緒已經可以使用多核且 效能也不錯,那為何還要啟動多個程式例項呢?好處是什麼?當然,只啟動一個程式是可以有效地使用多核的,只所以要用啟動多個程式例項,原因主要是兩個:

第一:安全穩定性,多程式具備更好的安全隔離機制,當一個程式因為某種原因”意外“停止響應而崩潰了,其它程式還能繼續對外提供服務,儘量保證業務不中斷;

第二:還是記憶體管理的高效性,雖然使用了一些高效的記憶體管理庫(如:tcmalloc),但執行緒鎖的競爭依然存在,尤其是當執行緒數增大時。而使用多程式方 式,則可以大大降低這種鎖衝突,有時甚至不再需要諸如 tcmalloc 之類的記憶體管理器(當每個程式內執行緒數並不太多時)。例如:希望某個服務最多啟動 512個執行緒,如果啟動 8 個程式內則每個程式最大隻需啟動 64 個執行緒即可,在這種情況下即使用 malloc/free 標準 API,記憶體的鎖衝突仍然是很低的。

當然,採用多程式方式也存在一個問題,就是客戶端連線分配的不均勻,有的子程式得到的客戶端連線多,有的得到少,因為作業系統並不能保證這種分配的均勻 性。採用多程式的一些服務(如 nginx)有時會採用一種程式間鎖的方式來保證各個服務子程式得到客戶端連線數均衡,但在 acl 的伺服器框架中採用了另外一種方式:提供了一個連線分配器子程式,應用服務子程式與這個分配器之間建立了 UNIX 域套介面,所有前端客戶端在 TCP 握手時首先連線該分配器,分配器會根據應用服務的各個子程式的負載情況將獲得的 TCP 連線通過 UNIX 域套介面傳遞給後端的服務子程式,這樣就保證了各個服務子程式獲得的客戶端連線是均勻的。目前,該分配器還定期彙總各個服務子進行的執行狀態,這樣,我們就可以寫一些前端 WEB 程式,查詢各臺機器上的分配器來檢視所有機器上的客戶端連線及負載狀態。

7、安全穩定性原則

作為一個需要長時間執行的伺服器程式,安全穩定性是至關重要的。

在安全性方面,acl 的伺服器框架在啟動服務子程式後會首先修改子程式的執行身份,將其降為普通使用者身份,同時限制該子程式的執行目錄,這樣即使因程式存在一些 BUG 而被黑客攻破,其獲得的身份也只能擁有最低的普通使用者許可權;

為了保證穩定性,acl 的伺服器模型支援服務子程式服務次數退出機制,即當一個子程式處理的客戶端連線數達到配製檔案中設定的值後會自動退出(在處理完所有的連線後),服務框架 會自動啟動新的子程式處理新到的連線,這樣做的好處是:對於一個新上線的服務程式,有可能存在一些輕微的記憶體洩露,通過此自動退出與自動啟動機制,就可以 有效地減少這種記憶體洩露所帶來的危害;另外,如果服務子程式異常退出,acl 的服務主程式會將該子程式退出的訊息通知一個報警子程式,由報警子程式以郵件或簡訊方式通知技術人員進行處理。

8、模組化原則

使用 acl 伺服器框架編寫伺服器程式,建議將不同功能的功能模組寫成獨立的應用服務程式,由主控程式(acl_master)統一進行管理,這樣既便於各個功能模組 的分散式部署以及將來進行各自的功能擴充套件,同時還將不同的功能模組進行有效隔離,避免產生過多的耦合性問題。

9、配置管理性要求

在 acl 的伺服器框架設計中,有一個主控制程式(acl_master),這個主控制掃描應用服務配置目錄下的配置檔案,啟動多個服務子程式,這樣,每個應用服務 程式有一個自己的配置檔案,配置項中有:監聽埠、程式數、執行緒數、執行身份、日誌輸出、訪問控制 等等;另外,acl 伺服器框架還支援軟體線上升級,可以做到不中斷當前業務的前提下更新伺服器程式。

10、快速開發部署原則
為了方便技術員快速入門,acl 庫中還提供了伺服器程式生成嚮導,只需幾步便可以搭建一個基於 acl 的伺服器程式設計框架。同時 acl 中還提供了用於快速安裝部署的指令碼程式,方便實施人員一鍵式安裝部署 acl 伺服器應用程式

11、大量實用功能庫
在 acl 網路通訊與伺服器框架庫中,不僅提供了一套完整的伺服器框架,而且還提供了大量的常見應用庫:比如常見的編碼庫(XML/JSON/HEX/URL CODE/MIME/BASE64/UUCODE/QPCODE/RFC2047/RFC822 等),常見的網路協議庫(http、smtp、icmp、redis、memcache、beanstalk、mysql、handler socket 等),常見的資料結構演算法(雜湊表、動態陣列、先進先出佇列、二叉樹、二分塊查詢、平衡二叉樹、256叉匹配樹等)。正所謂獨木不成林,結合這些常見應用 庫以及常見的開源伺服器軟體,技術人員就可以非常快速地開發出服務應用程式。下圖列出了 acl 中 lib_acl_cpp 庫中包含的絕大部分功能類索引:

index

五、參考資源

acl github: https://github.com/zhengshuxin/acl

相關文章