1. 程式人生 > >如何實現支援數億使用者的長連訊息系統 | Golang高併發案例

如何實現支援數億使用者的長連訊息系統 | Golang高併發案例

此文是根據周洋在【高可用架構群】中的分享內容整理而成,轉發請註明出處。

周洋,360手機助手技術經理及架構師,負責360長連線訊息系統,360手機助手架構的開發與維護。

不知道咱們群名什麼時候改為“Python高可用架構群”了,所以不得不說,很榮幸能在接下來的一個小時裡在Python群裡討論golang....



360訊息系統介紹

360訊息系統更確切的說是長連線push系統,目前服務於360內部多個產品,開發平臺數千款app,也支援部分聊天業務場景,單通道多app複用,支援上行資料,提供接入方不同粒度的上行資料和使用者狀態回撥服務。

目前整個系統按不同業務分成9個功能完整的叢集,部署在多個idc上(每個叢集覆蓋不同的idc),實時線上數億量級。通常情況下,pc,手機,甚至是智慧硬體上的360產品的push訊息,基本上是從我們系統發出的。

關於push系統對比與效能指標的討論

很多同行比較關心go語言在實現push系統上的效能問題,單機效能究竟如何,能否和其他語言實現的類似系統做對比麼?甚至問如果是創業,第三方雲推送平臺,推薦哪個?

其實各大廠都有類似的push系統,市場上也有類似功能的雲服務。包括我們公司早期也有erlang,nodejs實現的類似系統,也一度被公司要求做類似的對比測試。我感覺在討論對比資料的時候,很難保證大家環境和需求的統一,我只能說下我這裡的體會,資料是有的,但這個資料前面估計會有很多定語~

第一個重要指標:單機的連線數指標

做過長連線的同行,應該有體會,如果在穩定連線情況下,連線數這個指標,在沒有網路吞吐情況下對比,其實意義往往不大,維持連線消耗cpu資源很小,每條連線tcp協議棧會佔約4k的記憶體開銷,系統引數調整後,我們單機測試資料,最高也是可以達到單例項300w長連線。但做更高的測試,我個人感覺意義不大。

因為實際網路環境下,單例項300w長連線,從理論上算壓力就很大:實際弱網路環境下,移動客戶端的斷線率很高,假設每秒有1000分之一的使用者斷線重連。300w長連線,每秒新建連線達到3w,這同時連入的3w使用者,要進行註冊,載入離線儲存等對內rpc呼叫,另外300w長連線的使用者心跳需要維持,假設心跳300s一次,心跳包每秒需要1w tps。單播和多播資料的轉發,廣播資料的轉發,本身也要響應內部的rpc呼叫,300w長連線情況下,gc帶來的壓力,內部介面的響應延遲能否穩定保障。這些集中在一個例項中,可用性是一個挑戰。所以線上單例項不會hold很高的長連線,實際情況也要根據接入客戶端網路狀況來決定。

第二個重要指標:訊息系統的記憶體使用量指標

這一點上,使用go語言情況下,由於協程的原因,會有一部分額外開銷。但是要做兩個推送系統的對比,也有些需要確定問題。比如系統從設計上是否需要全雙工(即讀寫是否需要同時進行)如果半雙工,理論上對一個使用者的連線只需要使用一個協程即可(這種情況下,對使用者的斷線檢測可能會有延時),如果是全雙工,那讀/寫各一個協程。兩種場景記憶體開銷是有區別的。

另外測試資料的大小往往決定我們對連線上設定的讀寫buffer是多大,是全域性複用的,還是每個連線上獨享的,還是動態申請的。另外是否全雙工也決定buffer怎麼開。不同的策略,可能在不同情況的測試中表現不一樣。

第三個重要指標:每秒訊息下發量

這一點上,也要看我們對訊息到達的QoS級別(回覆ack策略區別),另外看架構策略,每種策略有其更適用的場景,是純粹推?還是推拉結合?甚至是否開啟了訊息日誌?日誌庫的實現機制、以及緩衝開多大?flush策略……這些都影響整個系統的吞吐量。

另外為了HA,增加了內部通訊成本,為了避免一些小概率事件,提供閃斷補償策略,這些都要考慮進去。如果所有的都去掉,那就是比較基礎庫的效能了。

所以我只能給出大概資料,24核,64G的伺服器上,在QoS為message at least,純粹推,訊息體256B~1kB情況下,單個例項100w實際使用者(200w+)協程,峰值可以達到2~5w的QPS...記憶體可以穩定在25G左右,gc時間在200~800ms左右(還有優化空間)。

我們正常線上單例項使用者控制在80w以內,單機最多兩個例項。事實上,整個系統在推送的需求上,對高峰的輸出不是提速,往往是進行限速,以防push系統瞬時的高吞吐量,轉化成對接入方業務伺服器的ddos攻擊所以對於效能上,我感覺大家可以放心使用,至少在我們這個量級上,經受過考驗,go1.5到來後,確實有之前投資又增值了的感覺。

訊息系統架構介紹

下面是對訊息系統的大概介紹,之前一些同學可能在gopher china上可以看到分享,這裡簡單講解下架構和各個元件功能,額外補充一些當時遺漏的資訊:

架構圖如下,所有的service都 written by golang.


幾個大概重要元件介紹如下:

dispatcher service 根據客戶端請求資訊,將應網路和區域的長連線伺服器的,一組IP傳送給客戶端。客戶端根據返回的IP,建立長連線,連線Room service.

room Service,長連線閘道器,hold使用者連線,並將使用者註冊進register service,本身也做一些接入安全策略、白名單、IP限制等。

register service 是我們全域性session儲存元件,儲存和索引使用者的相關資訊,以供獲取和查詢。

coordinator service 用來轉發使用者的上行資料,包括接入方訂閱的使用者狀態資訊的回撥,另外做需要協調各個元件的非同步操作,比如kick使用者操作,需要從register拿出其他使用者做非同步操作.

saver service是儲存訪問層,承擔了對redis和mysql的操作,另外也提供部分業務邏輯相關的記憶體快取,比如廣播資訊的載入可以在saver中進行快取。另外一些策略,比如客戶端sdk由於被惡意或者意外修改,每次載入了訊息,不回覆ack,那服務端就不會刪除訊息,訊息就會被反覆載入,形成死迴圈,可以通過在saver中做策略和判斷。(客戶端總是不可信的)。

center service 提供給接入方的內部api伺服器,比如單播或者廣播介面,狀態查詢介面等一系列api,包括運維和管理的api。

舉兩個常見例子,瞭解工作機制:比如發一條單播給一個使用者,center先請求Register獲取這個使用者之前註冊的連線通道標識、room例項地址,通過room service下發給長連線Center Service比較重的工作如全網廣播,需要把所有的任務分解成一系列的子任務,分發給所有center,然後在所有的子任務裡,分別獲取線上和離線的所有使用者,再批量推到Room Service。通常整個叢集在那一瞬間壓力很大。

deployd/agent service 用於部署管理各個程序,收集各元件的狀態和資訊,zookeeper和keeper用於整個系統的配置檔案管理和簡單排程

關於推送的服務端架構

常見的推送模型有長輪訓拉取,服務端直接推送(360訊息系統目前主要是這種),推拉結合(推送只發通知,推送後根據通知去拉取訊息).

拉取的方式不說了,現在並不常用了,早期很多是nginx+lua+redis,長輪訓,主要問題是開銷比較大,時效性也不好,能做的優化策略不多。

直接推送的系統,目前就是360訊息系統這種,訊息型別是消耗型的,並且對於同一個使用者並不允許重複消耗,如果需要多終端重複消耗,需要抽象成不同使用者。

推的好處是實時性好,開銷小,直接將訊息下發給客戶端,不需要客戶端走從接入層到儲存層主動拉取.

但純推送模型,有個很大問題,由於系統是非同步的,他的時序性無法精確保證。這對於push需求來說是夠用的,但如果複用推送系統做im型別通訊,可能並不合適。

對於嚴格要求時序性,訊息可以重複消耗的系統,目前也都是走推拉結合的模型,就是隻使用我們的推送系統發通知,並附帶id等給客戶端做拉取的判斷策略,客戶端根據推送的key,主動從業務伺服器拉取訊息。並且當主從同步延遲的時候,跟進推送的key做延遲拉取策略。同時也可以通過訊息本身的QoS,做純粹的推送策略,比如一些“正在打字的”低優先順序訊息,不需要主動拉取了,通過推送直接消耗掉。

哪些因素決定推送系統的效果?

首先是sdk的完善程度,sdk策略和細節完善度,往往決定了弱網路環境下最終推送質量.

SDK選路策略,最基本的一些策略如下:有些開源服務可能會針對使用者hash一個該接入區域的固定ip,實際上在國內環境下不可行,最好分配器(dispatcher)是返回雜湊的一組,而且埠也要參開,必要時候,客戶端告知是retry多組都連不上,返回不同idc的伺服器。因為我們會經常檢測到一些case,同一地區的不同使用者,可能對同一idc內的不同ip連通性都不一樣,也出現過同一ip不同埠連通性不同,所以使用者的選路策略一定要靈活,策略要足夠完善.另外在選路過程中,客戶端要對不同網路情況下的長連線ip做快取,當網路環境切換時候(wifi、2G、3G),重新請求分配器,快取不同網路環境的長連線ip。

客戶端對於資料心跳和讀寫超時設定,完善斷線檢測重連機制

針對不同網路環境,或者客戶端本身訊息的活躍程度,心跳要自適應的進行調整並與服務端協商,來保證鏈路的連通性。並且在弱網路環境下,除了網路切換(wifi切3G)或者讀寫出錯情況,什麼時候重新建立鏈路也是一個問題。客戶端發出的ping包,不同網路下,多久沒有得到響應,認為網路出現問題,重新建立鏈路需要有個權衡。另外對於不同網路環境下,讀取不同的訊息長度,也要有不同的容忍時間,不能一刀切。好的心跳和讀寫超時設定,可以讓客戶端最快的檢測到網路問題,重新建立鏈路,同時在網路抖動情況下也能完成大資料傳輸。

結合服務端做策略

另外系統可能結合服務端做一些特殊的策略,比如我們在選路時候,我們會將同一個使用者儘量對映到同一個roomservice例項上。斷線時,客戶端儘量對上次連線成功的地址進行重試。主要是方便服務端做閃斷情況下策略,會暫存使用者閃斷時例項上的資訊,重新連入的時候,做單例項內的遷移,減少延時與載入開銷.

客戶端保活策略

很多創業公司願意重新搭建一套push系統,確實不難實現,其實在協議完備情況下(最簡單就是客戶端不回ack不清資料),服務端會保證訊息是不丟的。但問題是為什麼在訊息有效期內,到達率上不去?往往因為自己app的pushservice存活能力不高。選用雲平臺或者大廠的,往往sdk會做一些保活策略,比如和其他app共生,互相喚醒,這也是雲平臺的pushservice更有保障原因。我相信很多雲平臺旗下的sdk,多個使用同樣sdk的app,為了實現服務存活,是可以互相喚醒和保證活躍的。另外現在push sdk本身是單連線,多app複用的,這為sdk實現,增加了新的挑戰。

綜上,對我來說,選擇推送平臺,優先會考慮客戶端sdk的完善程度。對於服務端,選擇條件稍微簡單,要求部署接入點(IDC)越要多,配合精細的選路策略,效果越有保證,至於想知道哪些雲服務有多少點,這個群裡來自各地的小夥伴們,可以合夥測測。

go語言開發問題與解決方案

下面講下,go開發過程中遇到挑戰和優化策略,給大家看下當年的一張圖,在第一版優化方案上線前一天截圖~


可以看到,記憶體最高佔用69G,GC時間單例項最高時候高達3~6s.這種情況下,試想一次悲劇的請求,經過了幾個正在執行gc的元件,後果必然是超時... gc照成的接入方重試,又加重了系統的負擔。遇到這種情況當時整個系統最差情況每隔2,3天就需要重啟一次~

當時出現問題,現在總結起來,大概以下幾點

1.散落在協程裡的I/O,Buffer和物件不復用。

當時(12年)由於對go的gc效率理解有限,比較奔放,程式裡大量short live的協程,對內通訊的很多io操作,由於不想阻塞主迴圈邏輯或者需要及時響應的邏輯,通過單獨go協程來實現非同步。這回會gc帶來很多負擔。

針對這個問題,應儘量控制協程建立,對於長連線這種應用,本身已經有幾百萬併發協程情況下,很多情況沒必要在各個併發協程內部做非同步io,因為程式的並行度是有限,理論上做協程內做阻塞操作是沒問題。

如果有些需要非同步執行,比如如果不非同步執行,影響對使用者心跳或者等待response無法響應,最好通過一個任務池,和一組常駐協程,來消耗,處理結果,通過channel再傳回呼叫方。使用任務池還有額外的好處,可以對請求進行打包處理,提高吞吐量,並且可以加入控量策略.

2.網路環境不好引起激增

go協程相比較以往高併發程式,如果做不好流控,會引起協程數量激增。早期的時候也會發現,時不時有部分主機記憶體會遠遠大於其他伺服器,但發現時候,所有主要profiling引數都正常了。

後來發現,通訊較多系統中,網路抖動阻塞是不可免的(即使是內網),對外不停accept接受新請求,但執行過程中,由於對內通訊阻塞,大量協程被建立,業務協程等待通訊結果沒有釋放,往往瞬時會迎來協程暴漲。但這些記憶體在系統穩定後,virt和res都並沒能徹底釋放,下降後,維持高位。

處理這種情況,需要增加一些流控策略,流控策略可以選擇在rpc庫來做,或者上面說的任務池來做,其實我感覺放在任務池裡做更合理些,畢竟rpc通訊庫可以做讀寫資料的限流,但它並不清楚具體的限流策略,到底是重試還是日誌還是快取到指定佇列。任務池本身就是業務邏輯相關的,它清楚針對不同的介面需要的流控限制策略。

3.低效和開銷大的rpc框架

早期rpc通訊框架比較簡單,對內通訊時候使用的也是短連線。這本來短連線開銷和效能瓶頸超出我們預期,短連線io效率是低一些,但埠資源夠,本身吞吐可以滿足需要,用是沒問題的,很多分層的系統,也有http短連線對內進行請求的

但早期go版本,這樣寫程式,在一定量級情況,是支撐不住的。短連線大量臨時物件和臨時buffer建立,在本已經百萬協程的程式中,是無法承受的。所以後續我們對我們的rpc框架作了兩次調整。

第二版的rpc框架,使用了連線池,通過長連線對內進行通訊(複用的資源包括client和server的:編解碼Buffer、Request/response),大大改善了效能。

但這種在一次request和response還是佔用連線的,如果網路狀況ok情況下,這不是問題,足夠滿足需要了,但試想一個room例項要與後面的數百個的register,coordinator,saver,center,keeper例項進行通訊,需要建立大量的常駐連線,每個目標機幾十個連線,也有數千個連線被佔用。

非持續抖動時候(持續逗開多少無解),或者有延遲較高的請求時候,如果針對目標ip連線開少了,會有瞬時大量請求阻塞,連線無法得到充分利用。第三版增加了Pipeline操作,Pipeline會帶來一些額外的開銷,利用tcp的全雙特性,以儘量少的連線完成對各個服務叢集的rpc呼叫。

4.Gc時間過長

Go的Gc仍舊在持續改善中,大量物件和buffer建立,仍舊會給gc帶來很大負擔,尤其一個佔用了25G左右的程式。之前go team的大咖郵件也告知我們,未來會讓使用協程的成本更低,理論上不需要在應用層做更多的策略來緩解gc.

改善方式,一種是多例項的拆分,如果公司沒有埠限制,可以很快部署大量例項,減少gc時長,最直接方法。不過對於360來說,外網通常只能使用80和433。因此常規上只能開啟兩個例項。當然很多人給我建議能否使用SO_REUSEPORT,不過我們核心版本確實比較低,並沒有實踐過。

另外能否模仿nginx,fork多個程序監控同樣埠,至少我們目前沒有這樣做,主要對於我們目前程序管理上,還是獨立的執行的,對外監聽不同埠程式,還有配套的內部通訊和管理埠,例項管理和升級上要做調整。

解決gc的另兩個手段,是記憶體池和物件池,不過最好做仔細評估和測試,記憶體池、物件池使用,也需要對於程式碼可讀性與整體效率進行權衡。

這種程式一定情況下會降低並行度,因為用池內資源一定要加互斥鎖或者原子操作做CAS,通常原子操作實測要更快一些。CAS可以理解為可操作的更細行為粒度的鎖(可以做更多CAS策略,放棄執行,防止忙等)。這種方式帶來的問題是,程式的可讀性會越來越像C語言,每次要malloc,各地方用完後要free,對於物件池free之前要reset,我曾經在應用層嘗試做了一個分層次結構的“無鎖佇列”


上圖左邊的陣列實際上是一個列表,這個列表按大小將記憶體分塊,然後使用atomic操作進行CAS。但實際要看測試資料了,池技術可以明顯減少臨時物件和記憶體的申請和釋放,gc時間會減少,但加鎖帶來的並行度的降低,是否能給一段時間內的整體吞吐量帶來提升,要做測試和權衡…

在我們訊息系統,實際上後續去除了部分這種黑科技,試想在百萬個協程裡面做自旋操作申請複用的buffer和物件,開銷會很大,尤其在協程對執行緒多對多模型情況下,更依賴於golang本身排程策略,除非我對池增加更多的策略處理,減少忙等,感覺是在把runtime做的事情,在應用層非常不優雅的實現。普遍使用開銷理論就大於收益。

但對於rpc庫或者codec庫,任務池內部,這些開定量協程,集中處理資料的區域,可以嘗試改造~

對於有些固定物件複用,比如固定的心跳包什麼的,可以考慮使用全域性一些物件,進行復用,針對應用層資料,具體設計物件池,在部分環節去複用,可能比這種無差別的設計一個通用池更能進行效果評估.

訊息系統的運維及測試

下面介紹訊息系統的架構迭代和一些迭代經驗,由於之前在其他地方有過分享,後面的會給出相關連結,下面實際做個簡單介紹,感興趣可以去連結裡面看

架構迭代~根據業務和叢集的拆分,能解決部分灰度部署上線測試,減少點對點通訊和廣播通訊不同產品的相互影響,針對特定的功能做獨立的優化.

訊息系統架構和叢集拆分,最基本的是拆分多例項,其次是按照業務型別對資源佔用情況分類,按使用者接入網路和對idc布點要求分類(目前沒有條件,所有的產品都部署到全部idc)




系統的測試go語言在併發測試上有獨特優勢。



對於壓力測試,目前主要針對指定的伺服器,選定線上空閒的伺服器做長連線壓測。然後結合視覺化,分析壓測過程中的系統狀態。但壓測早期用的比較多,但實現的統計報表功能和我理想有一定差距。我覺得最近出的golang開源產品都符合這種場景,go寫網路併發程式給大家帶來的便利,讓大家把以往為了降低複雜度,拆解或者分層協作的元件,又組合在了一起。

Q&A

Q1:協議棧大小,超時時間定製原則?

行動網路下超時時間按產品需求通常2g,3G情況下是5分鐘,wifi情況下5~8分鐘。但對於個別場景,要求響應非常迅速的場景,如果連線idle超過1分鐘,都會有ping,pong,來校驗是否斷線檢測,儘快做到重新連線。

Q2:訊息是否持久化?

訊息持久化,通常是先存後發,儲存用的redis,但落地用的mysql。mysql只做故障恢復使用。

Q3:訊息風暴怎麼解決的?

如果是傳送情況下,普通產品是不需要限速的,對於較大產品是有傳送佇列做控速度,按人數,按秒進行控速度發放,傳送成功再發送下一條。

Q4:golang的工具鏈支援怎麼樣?我自己寫過一些小程式千把行之內,確實很不錯,但不知道程式碼量上去之後,配套的debug工具和profiling工具如何,我看上邊有分享說golang自帶的profiling工具還不錯,那debug呢怎麼樣呢,官方一直沒有出debug工具,gdb支援也不完善,不知你們用的什麼?

是這樣的,我們正常就是println,我感覺基本上可以定位我所有問題,但也不排除由於並行性通過println無法復現的問題,目前來看只能靠經驗了。只要常見併發嘗試,經過分析是可以找到的。go很快會推出除錯工具的~

Q5:協議棧是基於tcp嗎?

是否有協議拓展功能?協議棧是tcp,整個系統tcp長連線,沒有考慮擴充套件其功能~如果有好的經驗,可以分享~

Q6:問個問題,這個系統是接收上行資料的吧,系統接收上行資料後是轉發給相應系統做處理麼,是怎麼轉發呢,如果需要給客戶端返回呼叫結果又是怎麼處理呢?

系統上行資料是根據協議頭進行轉發,協議頭裡面標記了產品和轉發型別,在coordinator裡面跟進產品和轉發型別,回撥使用者,如果使用者需要阻塞等待回覆才能後續操作,那通過再發送訊息,路由回用戶。因為整個系統是全非同步的。

Q7:問個pushsdk的問題。pushsdk的單連線,多app複用方式,這樣的情況下以下幾個問題是如何解決的:1)系統流量統計會把所有流量都算到啟動連線的應用吧?而啟動應用的連線是不固定的吧?2)同一個pushsdk在不同的應用中的版本號可能不一樣,這樣暴露出來的介面可能有版本問題,如果用單連線模式怎麼解決?

流量只能算在啟動的app上了,但一般這種安裝率很高的app承擔可能性大,常用app本身被檢測和殺死可能性較少,另外訊息下發量是有嚴格控制的。整體上使用者還是省電和省流量的。我們pushsdk儘量向上相容,出於這個目的,pushsdk本身做的工作非常有限,抽象出來一些常見的功能,純推的系統,客戶端策略目前做的很少,也有這個原因。

Q8:生產系統的profiling是一直開啟的麼?

不是一直開啟,每個叢集都有采樣,但需要開啟哪個可以後臺控制。這個profling是通過介面呼叫。

Q9:面前系統中的訊息消費者可不可以分組?類似於Kafka。

客戶端可以訂閱不同產品的訊息,接受不同的分組。接入的時候進行bind或者unbind操作

Q10:為什麼放棄erlang,而選擇go,有什麼特別原因嗎?我們現在用的erlang?

erlang沒有問題,原因是我們上線後,其他團隊才做出來,經過qa一個部門對比測試,在沒有顯著效能提升下,選擇繼續使用go版本的push,作為公司基礎服務。

Q11:流控問題有排查過網絡卡配置導致的idle問題嗎?

流控是業務級別的流控,我們上線前對於內網的極限通訊量做了測試,後續將請求在rpc庫內,控制在小於內部通訊開銷的上限以下.在到達上限前作流控。

Q12:服務的協調排程為什麼選擇zk有考慮過raft實現嗎?golang的raft實現很多啊,比如Consul和ectd之類的。

3年前,還沒有後兩者或者後兩者沒聽過應該。zk當時公司內部成熟方案,不過目前來看,我們不準備用zk作結合系統的定製開發,準備用自己寫的keeper代替zk,完成配置檔案自動轉資料結構,資料結構自動同步指定程序,同時裡面可以完成很多自定義的發現和控制策略,客戶端包含keeper的sdk就可以實現以上的所有監控資料,profling資料收集,配置檔案更新,啟動關閉等回撥。完全抽象成語keeper通訊sdk,keeper之間考慮用raft。

Q13:負載策略是否同時在服務側與CLIENT側同時做的 (DISPATCHER 會返回一組IP)?另外,ROOMSERVER/REGISTER SERVER連線狀態的一致性|可用性如何保證? 服務側保活有無特別關注的地方?安全性方面是基於TLS再加上應用層加密?

會在server端做,比如重啟操作前,會下發指令型別訊息,讓客戶端進行主動行為。部分訊息使用了加密策略,自定義的rsa+des,另外滿足我們安全公司的需要,也定製開發很多安全加密策略。一致性是通過冷備解決的,早期考慮雙寫,但實時狀態雙寫同步代價太高而且容易有髒資料,比如register掛了,呼叫所有room,通過重新刷入指定register來解決。

Q14:這個keeper有開源打算嗎?

還在寫,如果沒耦合我們系統太多功能,一定會開源的,主要這意味著,我們所有的bind在sdk的庫也需要開源~

Q15:比較好奇lisence是哪個如果開源?

FreeBSD

想和群內專家繼續交流高併發長連相關技術,請關注公眾號後,回覆arch,申請進群。

本文策劃郭軍,內容由劉偉編輯,四正、Tim校對與釋出,其他多位志願者對本文亦有貢獻。更多關於架構方面的內容,讀者可以通過搜尋“ArchNotes”或長按下面圖片,關注“高可用架構”公眾號,檢視更多架構方面內容,獲取通往架構師之路的寶貴經驗。轉載請註明來自“高可用架構(ArchNotes)”微信公眾號。