QQ18年,解密8億月活的QQ后台服務接口隔離技術


作者:shane,騰訊后台開發高級工程師

QQ18年

1999年2月10日,騰訊QQ橫空出世。光陰荏苒,那個在你屏幕右下角頻頻閃動的企鵝已經度過了18個年頭。隨着QQ一同成長的你,還記得它最初的摸樣嗎?
 

1999年:騰訊QQ的前身OICQ誕生,該版本具備中文網絡尋呼機、公共聊天室以及傳輸文件功能。

1999年QQ界面

 

2000年,OICQ正式更名為QQ,發布視頻聊天功能、QQ群和QQ秀等功能。

 

 

2003年版本,QQ發布聊天場景、捕捉屏幕、給好友播放錄影及QQ炫鈴等功能。  

2004年,QQ新增個人網絡硬盤、遠程協助和QQ小秘書功能。

 ···

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

 

 

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

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

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

 


 

在此期間,后台想必又一次承受了海量的壓力,年后第一波推送,來看看騰訊內部對QQ后台的接口處理的相關技術干貨,或許可以給到你答案。


 

一、背景

QQ后台提供了一套內部訪問的統一服務接口,對騰訊各業務部門提供統一的資料關系鏈訪問服務,后面我們把這套接口簡稱為DB。

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

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

 

二、總體方案

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

分set之前:

 


 

 分set之后:

 


 從圖中可以看出,實現方式其實非常簡單,就是多啟動一個proxy進程根據IP到set的映射關系分發請求包到對應set的進程上。

 

三、分set嘗試

很多事情往往看起來非常簡單,實現起來卻十分復雜,DB分set就是一個典型的例子。怎么說呢?先看看我們剛開始實現的分set方案。

 

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

 

這個方案剛發布幾台后就發現問題:

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

 

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

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

 

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

 

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

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

 

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

 

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

 

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

 

四、最終實現方案

做互聯網服務,最大的一個特點就是,任何一項需求,做與不做,都必須在投入、產出、時間、質量之間做一個取舍。

 

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

 

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

 

舉個簡單的實現方法:安裝一個內核模塊,掛個netfilter鈎子,直接在網絡層進行分set,再把回包改一下發送端口。

 

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

1,不是所有同事都懂內核代碼

2,運營環境的機器不支持動態加載內核模塊,只能重新編譯內核

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

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

 

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

 


方案描述:

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來解決訪問沖突。

 


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

 

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

 

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

 


工作方式如下:

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收包的時候,把這個時間戳跟當前時間進行對比。

 

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

 

 

 

五、性能驗證

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

 

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

 


 

對許多企業而言,雖不一定經歷月活8億用戶,但為了能夠面對蜂擁而來的用戶游刃有余,時刻了解並保持自己的最優狀態迎接用戶,一定要在上線之前對自己的網站承載能力進行一個測試。如果自己沒有服務器,沒有人力,沒有錢,都沒有關系。。。


騰訊提供了一個可以自主進行服務器性能測試的環境,用戶只需要填寫域名和簡單的幾個參數就可以獲知自己的服務器性能情況,目前在騰訊WeTest平台可以免費使用。


騰訊WeTest服務器性能測試運用了沉淀十多年的內部實踐經驗總結,通過基於真實業務場景和用戶行為進行壓力測試,幫助游戲開發者發現服務器端的性能瓶頸,進行針對性的性能調優,降低服務器采購和維護成本,提高用戶留存和轉化率。

 

功能目前免費對外開放中,點擊 http://WeTest.qq.com/gaps 即可體驗!

如果對使用當中有任何疑問,歡迎聯系騰訊WeTest企業qq:800024531


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM