QQ18年,解密8億月活的QQ後臺服務介面隔離技術

發表於2017-03-07

QQ18年

1999年2月10日,騰訊QQ橫空出世。光陰荏苒,那個在你螢幕右下角頻頻閃動的企鵝已經度過了18個年頭。隨著QQ一同成長的你,還記得它最初的摸樣嗎?
1999年:騰訊QQ的前身OICQ誕生,該版本具備中文網路尋呼機、公共聊天室以及傳輸檔案功能。
11LabImage_4be3a900073d29348797ab40ca76ffea

1999年QQ介面

2000年,OICQ正式更名為QQ,釋出視訊聊天功能、QQ群和QQ秀等功能。

12LabImage_5fc462d5b440dbd79e0ddeea0bf89ca9
2003年版本,QQ釋出聊天場景、捕捉螢幕、給好友播放錄影及QQ炫鈴等功能。

2004年,QQ新增個人網路硬碟、遠端協助和QQ小祕書功能。

···

幾經更迭,QQ版本也產生許多變化,很多操作方式都變了,也讓QQ更有現代感了。如今的QQ越來越精美,越來越簡潔,如你所見。

13LabImage_ccbacec5ca61cc2d0e636f9f3ab7f6e5

14LabImage_77d32b515907f571ba471a4aeb35ed07

據不完全統計,騰訊QQ月活使用者達到8.7億左右,而這個數字還在不斷增加。。。

如此龐大的使用者群的任何行為,都會產生巨大的影響。

2017年春節,QQ推出AR紅包加入紅包大戰,經調查手機QQ的紅包全網滲透率達到52.9%。

15LabImage_67c0e736017bb4412044c4c91bdea8b4
在此期間,後臺想必又一次承受了海量的壓力,年後第一波推送,來看看騰訊內部對QQ後臺的介面處理的相關技術乾貨,或許可以給到你答案。
背景
QQ後臺提供了一套內部訪問的統一服務介面,對騰訊各業務部門提供統一的資料關係鏈訪問服務,後面我們把這套介面簡稱為DB。

現在說說分set的背景:2013年的某一天,某個業務的小朋友在申請正式環境的DB接入許可權後,使用正式環境來驗證剛寫完的測試程式,迴圈向DB介面機傳送請求包,但因為這個包格式非法,觸發了DB解包的一個bug,導致收到這些請求包的伺服器群體core dump,無一倖免。。。。整個DB系統的服務頓時進入癱瘓狀態。

因此有了故障隔離的需求,2014年初,我們著手DB的故障隔離增強改造。實現方法就是分set服務–把不同業務部門的請求定向到不同的服務程式組上,如果某個業務的請求有問題,最多隻影響一個部門,不會影響整個服務系統。

總體方案
為了更清楚描述分set的方案,我們通過兩個圖進行分set前後的對比。

分set之前:

16LabImage_d22b188e78cbb90687bcb977e20f1c19
 分set之後:
17LabImage_e5573677f7d3a9e16de645efc1caa65c
從圖中可以看出,實現方式其實非常簡單,就是多啟動一個proxy程式根據IP到set的對映關係分發請求包到對應set的程式上。
分set嘗試
很多事情往往看起來非常簡單,實現起來卻十分複雜,DB分set就是一個典型的例子。怎麼說呢?先看看我們剛開始實現的分set方案。

 

實現方案一:通過socket轉包給分set程式,分set程式直接回包給前端。

18LabImage_b1d8fc04c188a8ca8a555b7ad61573da
這個方案剛釋出幾臺後就發現問題:

1,有前端業務投訴回包埠不對導致訪問失敗。後來瞭解這些業務會對回包埠進行校驗,如果埠不一致就會把包丟棄。

2,CPU比原來上漲了25%(同樣的請求量,原來是40%,使用這個方案後CPU變成50%)

回包埠改變的問題因為影響業務(業務就是我們的上帝,得罪不起^^),必須馬上解決,於是有了方案二。

 

實現方案二:通過socket轉包給分set程式,分set程式回包給proxy,由proxy回包。

19LabImage_c59753254e161baf66e37ea69ee7de0c
改動很快完成,一切順利,馬上鋪開批量部署。。。。

晚上10點準時迎來第一次高峰,DB出現大量的丟包和CPU告警,運維緊急遷移流量。

第二天全部回滾為未分set的版本。

重新做效能驗證的時候,發現CPU比原來漲了50%,按這個比例,原來600多臺機器,現在需要增加300多臺機器才能撐起同樣請求的容量。(這是寫本文時候的機器數,目前機器數已經翻倍了~)

 

後來分析原因的時候,發現網路卡收發包量都漲了一倍,而CPU基本上都消耗在核心socket佇列的處理上,其中競爭socket資源的spin_lock佔用了超過30%的CPU — 這也正是我們決定一定要做無鎖佇列的原因。

最終實現方案
做網際網路服務,最大的一個特點就是,任何一項需求,做與不做,都必須在投入、產出、時間、質量之間做一個取捨。

前面的嘗試選擇了最簡單的實現方式,目的就是為了能夠儘快上線,減少群體core掉的風險,但卻引入了容量不足的風險。

既然這個方案行不通,那就得退而求其次(退說的是延期,次說的是犧牲一些人力和運維投入),方案是很多的,但是需要以人力作為代價。

舉個簡單的實現方法:安裝一個核心模組,掛個netfilter鉤子,直接在網路層進行分set,再把回包改一下傳送埠。

這在核心實現是非常非常簡單的事情,但卻帶來很大的風險:

1,不是所有同事都懂核心程式碼

2,運營環境的機器不支援動態載入核心模組,只能重新編譯核心

3,從運維的角度:動核心 == 殺雞取卵 — 核心有問題,都不知道找誰了

好吧,我無法說服開發運營團隊,就只能放棄這種想法了–即便很不情願。

。。。跑題了,言歸正傳,這是我們重新設計的方案:

20LabImage_197ec93628059847520b0e4b5a819a0a
方案描述:

1,使用一寫多讀的共享記憶體佇列來分發資料包,每個set建立一個shm_queue,同個set下面的多個服務程式通過掃描shm_queue進行搶包。

2,Proxy在分發的時候同時把收包埠、客戶端地址、收包時間戳(用於防滾雪球控制,後面介紹)一起放到shm_queue中。

3,服務處理程式回包的時候直接使用Raw Socket回包,把回包的埠寫成proxy收包的埠。

 

看到這裡,各位同學可能會覺得這個實現非常簡單。。。不可否認,確實也是挺簡單的~~

不過,在實施的時候,有一些細節是我們不得不考慮的,包括:

1)這個共享記憶體佇列是一寫多讀的(目前是一個proxy程式對應一組set化共享記憶體佇列,proxy的個數可以配置為多個,但目前只配一個,佔單CPU不到10%的開銷),所以共享記憶體佇列的實現必須有效解決讀寫、讀讀衝突的問題,同時必須保證高效能。

2)服務server需要偵聽後端的回包,同時還要掃描shm_queue中是否有資料,這兩個操作無法在一個select或者epoll_wait中完成,因此無法及時響應前端請求,怎麼辦?

3)原來的防滾雪球控制機制是直接取網路卡收包的時間戳和使用者層收包時系統時間的差值,如果大於一定閥值(比如100ms),就丟棄。現在server不再直接收包了,這個策略也要跟著變化。

基於signal通知機制的無鎖共享記憶體佇列

A. 對於第一個問題,解決方法就是無鎖共享記憶體佇列,使用CAS來解決訪問衝突。

21LabImage_b61afb53e00634c08b2c0a4ffea78c0a
這裡順便介紹一下CAS(Compare And Swap),就是一個彙編指令cmpxchg,用於原子性執行CAS(mem, oldvalue, newvalue):如果mem記憶體地址指向的值等於oldvalue,就把newvalue寫入mem,否則返回失敗。

那麼,讀的時候,只要保證修改ReadIndex的操作是一個CAS原子操作,誰成功修改了ReadIndex,誰就獲得對修改前ReadIndex指向元素的訪問權,從而避開多個程式同時訪問的情況。

B. 對於第二個問題,我們的做法就是使用註冊和signal通知機制:

22LabImage_bf5cd2e467d64fff0d9455b1982988b7
工作方式如下:

1)Proxy負責初始化訊號共享記憶體

2)Server程式啟動的時候呼叫註冊介面註冊自己的程式ID,並返回程式ID在程式ID列表中的下標(sigindex)

3)在Server進入睡眠之前呼叫開啟通知介面把sigindex對應的bitmap置位,然後進入睡眠函式(pselect)

4)Proxy寫完資料發現共享記憶體佇列中的塊數達到一定個數(比如40,可以配置)的時候,掃描程式bitmap,根據對應bit為1的位取出一定個數(比如8,可以配置為Server程式的個數)的程式ID

5)Proxy遍歷這些程式ID,執行kill傳送訊號,同時把bitmap對應的位置0(防止程式死了,不斷被通知)

6)Server程式收到訊號或者超時後從睡眠函式中醒來,把sigindex對應的bit置0,關閉通知

除了signal通知,其實還有很多通知機制,包括pipe、socket,還有較新的核心引入的eventfd、signalfd等等,我們之所以選擇比較傳統的signal通知,主要因為簡單、高效,相容各種核心版本,另外一個原因,是因為signal的物件是程式,我們可以選擇性傳送signal,避免驚群效應的發生。

防滾雪球控制機制

前面已經說過,原來的防滾雪球控制機制是基於網路卡收包時間戳的。但現在server拿不到網路卡收包的時間戳了,只能另尋新路,新的做法是:

Proxy收包的時候把收包時間戳儲存起來,跟請求包一起放到佇列裡面,server收包的時候,把這個時間戳跟當前時間進行對比。

這樣能更有效的做到防滾雪球控制,因為我們把這個包在前面的環節裡面經歷的時間都考慮進來了,用圖形描述可能更清楚一點。

23LabImage_0a7fd25298eed51243da4dca33c78cdb
效能驗證
使用shm_queue和raw socket後,DB介面機處理效能基本跟原來未分set的效能持平,新加的proxy程式佔用的CPU一直維持在單CPU 10%以內,但攤分到多個CPU上就變成非常少了(對於8核的伺服器,只是增加了1.25%的平均CPU開銷,完全可以忽略不計)。

最後,分set的這個版本已經正式上線執行一段時間了,目前狀態穩定。

相關文章