ofo長連接鎖服務優化實踐
1
背景
ofo有多個業務涉及到長連接,其中智能鎖和鎖服務通信是使用長連接的一個重要的應用場景,本文先從C10K問題入手,逐步介紹長連接鎖服務和優化的過程。
2
C10K問題
早期的互聯網功能簡單,用戶也不多,客戶端和伺服器端不需要太多的交互。隨著互聯網的普及和發展,應用程序的邏輯也變的更複雜,從簡單的表單提交,到即時通信和在線實時互動,C10K的問題才體現出來了,每一個用戶都必須與伺服器保持TCP連接才能進行實時的數據交互,一些大型的網站同一時間的並發TCP連接可能會過億。
最初的伺服器都是基於進程/線程模型的,新到來一個TCP連接,就需要分配1個進程(或者線程),而進程又是操作系統最昂貴的資源,一台機器無法創建很多進程。如果是C10K就要創建1萬個進程,那麼操作系統是無法承受的。如果採用分散式系統,維持1億用戶在線也需要10萬台伺服器,成本巨大,這就是C10K問題的本質。
3
C10K問題的解決方案
為了解決C10K問題,主要思路有兩個:一個思路是對於每個連接處理分配一個獨立的進程/線程,但是這種資源佔用過多,可擴展性差,不可行;另一個思路是用同一進程/線程來同時處理若干個連接,也就是IO多路復用。
3.1傳統思路
每個連接對應一個socket,然後循環順序處理各個連接,當所有socket都有數據的時候,這個方法可行。但是當某個socket數據未就緒,即使後面的socket數據就緒了,應用也會一直阻塞等待,效率低。
3.2 select
為了解決阻塞的問題,select增加了狀態檢查的機制。用一個 fd_set 結構體來告訴內核同時監控多個文件句柄,當其中有文件句柄的狀態發生指定變化(例如某句柄由不可用變為可用)或超時,則調用返回。之後應用可以使用 FD_ISSET 來逐個查看是哪個文件句柄的狀態發生了變化。
這樣做,小規模的連接問題不大,但當連接數很多的時候,逐個檢查狀態就很慢了。因此,select 往往存在管理的句柄上限(FD_SETSIZE,默認是1024個)。同時,在使用上,因為只有一個欄位記錄關注和發生事件,每次調用之前要重新初始化 fd_set 結構體,開銷比較大。
3.3 poll
poll 主要解決 select 的前兩個問題,通過一個 pollfd 數組向內核傳遞需要關注的事件消除文件句柄上限,同時使用不同欄位分別標註關注事件和發生事件,來避免重複初始化,但是逐個排查所有文件句柄狀態效率也不高。
3.4 epoll
既然逐個排查所有文件句柄狀態效率不高,如果調用返回的時候只給應用提供發生了狀態變化的文件句柄,進行排查的效率就能提高很多,epoll 採用了這種設計,適用於大規模連接的應用場景。
實驗表明,當文件句柄數目超過 10 之後,epoll 性能將優於 select 和 poll;當文件句柄數目達到 10K 的時候,epoll 已經超過 select 和 poll 兩個數量級。
因為Linux是互聯網企業中使用率最高的操作系統,epoll就成為C10K killer、高並發、高性能、非同步非阻塞這些技術的代名詞了。FreeBSD推出了kqueue、Linux推出了epoll、Windows推出了IOCP、Solaris推出了/dev/poll,這些操作系統提供的功能就是為了解決C10K問題。epoll技術的編程模型就是非同步非阻塞回調,也可以叫做Reactor,事件驅動,事件輪循。nginx、libevent、nodejs這些都是epoll時代的產物。
3.5 libevent
由於epoll、kqueue、IOCP每個介面都有自己的特點,程序移植非常困難,於是需要對這些介面進行封裝,讓它們方便使用和移植,其中libevent庫就是其中之一。跨平台,封裝底層平台的調用,提供統一的 API,底層在不同平台上自動選擇合適的調用,因此libevent非常容易移植,也使它的擴展性很強。目前,libevent已在以下操作系統中編譯通過:Linux、BSD、Mac OS X、Solaris和Windows。
3.6 libev和libeio
libevent嘗試給你全套解決方案,包括事件庫、非阻塞IO庫、http庫、DNS客戶端等,讓它變的比較重。另外,全局變數的使用,讓libevent很難在多線程環境中安全的使用,但是libevent也提供了不帶全局變數的API。libev則修復這個缺陷,同時它只試圖做好一件事,目標是成為POSIX的事件庫,主要用於事件驅動的網路編程。
libeio是全功能的用於C語言的非同步I/O庫,建模風格和秉承的精神與libev類似,特性包括:非同步的read、write、open、close、stat、unlink、readdir等。libeio完全基於事件庫,可以容易地集成到事件庫(或獨立,甚至是以輪詢方式)使用。libeio非常輕便,且只依賴於POSIX線程,主要提供文件I/O操作。
4
長連接鎖服務開發語言選型
隨著智能鎖數量的增長,我們要解決的不僅僅是C10K的問題,而是C10M的問題,怎麼樣高效率的支持大量長連接設備的接入是我們要解決的問題。智能鎖是一種雙工通信設備,可以接收實時指令,也可以上報數據,這時就需要一個支持大規模連接和非同步處理的框架。
Nodejs具有事件驅動、非同步、非阻塞IO的特性,也有各種擴展功能的依賴包,相比Java語言來說,又具有快速開發、輕量等優勢,符合目前的需求。
圖1 nodejs內部構造
圖1是nodejs的內部構造,node-bindings是指對底層c/c++代碼的封裝後和js打交道的部分,屬於適配層。
底層首先是V8引擎,它就是 js 的解析引擎,它的作用就是「翻譯」js給計算機處理。接下來是libuv,早期是由libev和libeio組成,後來被抽象成libuv,它就是node和操作系統打交道的部分,由它來負責文件系統、網路等底層工作。
圖2 libuv架構
從圖2可以看出,幾乎所有和操作系統打交道的部分都離不開 libuv的支持,libuv也是node實現跨操作系統的核心所在。IO分為網路IO和磁碟IO,對於網路IO,使用epoll、kqueue之類的就可以了。但是對於磁碟IO,由於O_NOBLOCK 方式對於傳統文件句柄是無效的,也就是說open、read、mkdir 之類的Regular File操作必定會導致阻塞,所以對於Regular File 來說,是不能夠採用 poll/epoll 的,都是使用多線程阻塞來模擬的非同步文件操作。要實現這個功能,就需要引入線程池模塊,線程池默認大小是4,同時只能有4個線程去做文件i/o的工作,剩下的請求會被掛起等待直到線程池有空閑。
5
伺服器性能優化
nodejs已經能幫助業務系統很好的處理大規模長連接,可優化的空間有限。但是這些長連接的建立和釋放都是基於TCP的,可以在伺服器層面通過優化內核參數來支持大規模的連接和並發,我們主要優化了以下幾個地方:
5.1 backlog參數
backlog參數主要用於底層方法int listen(int sockfd, int backlog), 在解釋backlog參數之前,我們先了解下tcp在內核的請求過程,其實就是tcp的三次握手:
圖3 tcp建立過程
在linux系統內核中維護了兩個隊列:syns queue和accept queue
syns queue
用於保存半連接狀態的請求,其大小通過/proc/sys/net/ipv4/tcp_max_syn_backlog指定,一般默認值是512,不過這個設置有效的前提是系統的syncookies功能被禁用。互聯網常見的TCP SYN FLOOD惡意DOS攻擊方式就是建立大量的半連接狀態的請求,然後丟棄,導致syns queue不能保存其它正常的請求。
accept queue
用於保存全連接狀態的請求,其大小通過/proc/sys/net/core/somaxconn指定,在使用listen函數時,內核會根據傳入的backlog參數與系統參數somaxconn,取二者的較小值。
如果accpet queue隊列滿了,server將發送一個ECONNREFUSED錯誤信息Connection refused到client。
前面已經提到過,內核會根據somaxconn和backlog的較小值設置accept queue的大小,如果想擴大accept queue的大小,必須要同時調整這兩個參數,伺服器的參數值調整如下:
如果業務代碼不顯式設置backlog,node程序默認是511,如圖4所示:
圖4 backlog默認值
當在代碼中設置了backlog值之後,內核會根據somaxconn和backlog的較小值設置accept queue的大小,如圖5所示:
圖5 backlog優化值
5.2 limits.conf文件修改
為了讓伺服器能支持100萬個連接,需要打破系統默認的65535個文件句柄數的限制,通過修改/etc/security/limits.conf文件來讓系統能支持最多100萬個連接,修改後的參數如下:
* soft core unlimited
* hard core unlimited
* soft nofile 1048576
* hard nofile 1048576
* soft nproc 32768
* hard nproc 32768
soft 指的是當前系統生效的設置值,hard 表明系統中所能設定的最大值,soft 的限制值不能高於hard 限制值,core表示限制內核文件的大小,nofile 表示打開文件的最大數目,noproc 表示進程的最大數目。
5.3 sysctl.conf文件修改
為了讓系統能支持100萬的連接,還需要修改fs.file-max參數,其他的參數調整如下:
kernel.randomize_va_space = 0
kernel.core_pattern = /data/crash/core.%p.%e
vm.min_free_kbytes = 1048576
fs.aio-max-nr = 1048576
fs.file-max = 1048576
kernel.panic = 0
vm.panic_on_oom = 0
5.4 壓測結果
伺服器配置:8核16G的CentOS機器
服務配置:pm2單進程
客戶端配置:4台8核16G的CentOS機器
最大連接數受系統最大文件句柄數的影響,同時在發送業務數據的時候,每個連接還會佔用一定的內存,最大連接數也受系統內存大小的影響,用默認參數和優化後的參數的對比數據如下:
圖6 最大連接數對比
從圖6可以看出,文件句柄參數優化對最大連接數的影響很大。由於只有4台測試客戶端伺服器,伺服器端能承受的不帶業務數據的連接數並沒有達到上限,實際值要大於圖中的200000。
backlog參數不影響建立的連接數,但是會影響連接建立的性能,在傳數據包的時候,系統需要讀寫redis和mysql,對QPS的影響較大。同時,系統在接收到不同種類的數據包時,執行的操作也不一樣,QPS值也有差異, backlog值為511和8000時的對比數據如下:
圖7 backlog參數對QPS的影響
從圖7可以看出,在不傳業務數據時,backlog值大的時候,系統的性能比之前有很大提升。但是在傳業務數據時,由於伺服器的開銷主要是在業務處理上,不同的backlog值對QPS的影響有限。
由於其他參數不影響連接的數量和性能,會影響維持長連接佔用的內存等資源,如果設置過大會導致系統oom。通過優化socket緩衝區的默認值和最大值,發現對QPS的影響很小,對比數據如圖8所示:
圖8 socket緩衝區參數對QPS的影響
6
其他技術調研
6.1 協程
epoll 已經可以較好的處理 C10K 問題,但是如果要支持 10M 規模的並發連接,原有的技術就會有瓶頸了。從前面的C10K解決方案的演化過程中,我們可以看到,根本的思路是要高效的去阻塞,讓 CPU可以干核心的任務。這意味著,不要讓內核執行所有繁重的任務。將數據包處理、內存管理、處理器調度等任務從內核轉移到應用程序高效地完成,讓Linux只處理控制層,數據層完全交給應用程序來處理。
當連接很多時,首先需要大量的進程/線程來工作。同時系統中的應用進程/線程們可能大量的都處於 ready 狀態,需要系統去不斷的進行快速切換,而我們知道系統上下文的切換是有代價的。雖然現在 Linux 系統的調度演算法已經設計的很高效了,但仍然滿足不了10M 這樣大規模的場景。
所以我們面臨的瓶頸有兩個,一個是進程/線程作為處理單元還是太厚重了;另一個是系統調度的代價太高了。很自然地,我們會想到,如果有一種更輕量級的進程/線程作為處理單元,而且它們的調度可以做到很快(最好不需要鎖),那就完美了。
這樣的技術現在在某些語言中已經有了一些實現,它們就是 coroutine(協程),或協作式常式。具體的,Python、Lua 語言中的 coroutine(協程)模型,Go 語言中的 goroutine(Go 程)模型,都是類似的一個概念。實際上,多種語言(甚至 C 語言)都可以實現類似的模型。
它們在實現上都是試圖用一組少量的線程來實現多個任務,一旦某個任務阻塞,則可能用同一線程繼續運行其他任務,避免大量上下文的切換。每個協程所獨佔的系統資源往往只有棧部分。而且,各個協程之間的切換,往往是用戶通過代碼來顯式指定的(跟各種 callback 類似),不需要內核參與,可以很方便的實現非同步。
這個技術本質上也是非同步非阻塞技術,它是將事件回調進行了包裝,讓程序員看不到裡面的事件循環。這就是協程的本質,協程是非同步非阻塞的另外一種展現形式,Golang、Erlang、Lua協程都是這個模型。
6.2 設備影子
設備影子可以理解成是一個數據集,用於存儲設備上報狀態、應用程序期望的狀態信息。每個設備有且只有一個設備影子,設備可以獲取和設置設備影子以此來同步狀態,這個同步可以是影子同步給設備,也可以是設備同步給影子;應用程序也可以通過API獲取和設置設備影子以此來獲取設備最新狀態或者下發期望狀態給設備。
一個典型的應用場景是:假如設備網路穩定,有很多應用程序來請求設備狀態,那就意味著設備需要根據這些請求響應多次,哪怕這些響應的結果都是一樣的,這樣做根本就是沒有必要的,而且設備本身處理能力有限,可能根本承受不了這種被請求多次的情況。
當有設備影子這個機制時,這個問題就比較好解。設備只需要主動同步狀態一次給設備影子,然後多個應用程序只需要請求設備影子即可獲取設備最新狀態,這就做到了應用程序和設備的解耦,設備的能力得到了解放。
7
總結
本文只是大概介紹了長連接鎖的服務實現和個別優化實踐,實際工作中還要根據實際情況,針對具體的場景一個一個的來優化,同時還要和具體的業務結合起來,在保證穩定性的前提下,讓服務更加高效強大。最後,感謝王強、來曉賓、王華健、伍思磊等同學的幫助和建議。
※設計師告訴你,乾濕分離衛生間的15種裝法
※多年不明原因不孕,微刺激試管嬰兒一舉成功
TAG:全球大搜羅 |