QQ18年,解密8億月活的QQ后臺服務(wù)接口隔離技術(shù)

QQ18年

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

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

1999年QQ界面

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

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

2004年,QQ新增個人網(wǎng)絡(luò)硬盤、遠程協(xié)助和QQ小秘書功能。

···

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

據(jù)不完全統(tǒng)計,騰訊QQ月活用戶達到8.7億左右,而這個數(shù)字還在不斷增加。。。

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

2017年春節(jié),QQ推出AR紅包加入紅包大戰(zhàn),經(jīng)調(diào)查手機QQ的紅包全網(wǎng)滲透率達到52.9%。

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

背景

QQ后臺提供了一套內(nèi)部訪問的統(tǒng)一服務(wù)接口,對騰訊各業(yè)務(wù)部門提供統(tǒng)一的資料關(guān)系鏈訪問服務(wù),后面我們把這套接口簡稱為DB。

現(xiàn)在說說分set的背景:2013年的某一天,某個業(yè)務(wù)的小朋友在申請正式環(huán)境的DB接入權(quán)限后,使用正式環(huán)境來驗證剛寫完的測試程序,循環(huán)向DB接口機發(fā)送請求包,但因為這個包格式非法,觸發(fā)了DB解包的一個bug,導(dǎo)致收到這些請求包的服務(wù)器群體core

dump,無一幸免。。。。整個DB系統(tǒng)的服務(wù)頓時進入癱瘓狀態(tài)。

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

總體方案

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

分set之前:

分set之后:

從圖中可以看出,實現(xiàn)方式其實非常簡單,就是多啟動一個proxy進程根據(jù)IP到set的映射關(guān)系分發(fā)請求包到對應(yīng)set的進程上。

分set嘗試

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

實現(xiàn)方案一:通過socket轉(zhuǎn)包給分set進程,分set進程直接回包給前端。

這個方案剛發(fā)布幾臺后就發(fā)現(xiàn)問題:

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

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

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

實現(xiàn)方案二:通過socket轉(zhuǎn)包給分set進程,分set進程回包給proxy,由proxy回包。

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

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

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

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

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

最終實現(xiàn)方案

做互聯(lián)網(wǎng)服務(wù),最大的一個特點就是,任何一項需求,做與不做,都必須在投入、產(chǎn)出、時間、質(zhì)量之間做一個取舍。

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

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

舉個簡單的實現(xiàn)方法:安裝一個內(nèi)核模塊,掛個netfilter鉤子,直接在網(wǎng)絡(luò)層進行分set,再把回包改一下發(fā)送端口。

這在內(nèi)核實現(xiàn)是非常非常簡單的事情,但卻帶來很大的風(fēng)險:

1,不是所有同事都懂內(nèi)核代碼

2,運營環(huán)境的機器不支持動態(tài)加載內(nèi)核模塊,只能重新編譯內(nèi)核

3,從運維的角度:動內(nèi)核?==?殺雞取卵?—?內(nèi)核有問題,都不知道找誰了

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

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

方案描述:

1,使用一寫多讀的共享內(nèi)存隊列來分發(fā)數(shù)據(jù)包,每個set創(chuàng)建一個shm_queue,同個set下面的多個服務(wù)進程通過掃描shm_queue進行搶包。

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

3,服務(wù)處理進程回包的時候直接使用Raw Socket回包,把回包的端口寫成proxy收包的端口。

看到這里,各位同學(xué)可能會覺得這個實現(xiàn)非常簡單。。。不可否認,確實也是挺簡單的~~

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

1)這個共享內(nèi)存隊列是一寫多讀的(目前是一個proxy進程對應(yīng)一組set化共享內(nèi)存隊列,proxy的個數(shù)可以配置為多個,但目前只配一個,占單CPU不到10%的開銷),所以共享內(nèi)存隊列的實現(xiàn)必須有效解決讀寫、讀讀沖突的問題,同時必須保證高性能。

2)服務(wù)server需要偵聽后端的回包,同時還要掃描shm_queue中是否有數(shù)據(jù),這兩個操作無法在一個select或者epoll_wait中完成,因此無法及時響應(yīng)前端請求,怎么辦?

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

基于signal通知機制的無鎖共享內(nèi)存隊列

A. 對于第一個問題,解決方法就是無鎖共享內(nèi)存隊列,使用CAS來解決訪問沖突。

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

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

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

工作方式如下:

1)Proxy負責(zé)初始化信號共享內(nèi)存

2)Server進程啟動的時候調(diào)用注冊接口注冊自己的進程ID,并返回進程ID在進程ID列表中的下標(biāo)(sigindex)

3)在Server進入睡眠之前調(diào)用打開通知接口把sigindex對應(yīng)的bitmap置位,然后進入睡眠函數(shù)(pselect)

4)Proxy寫完數(shù)據(jù)發(fā)現(xiàn)共享內(nèi)存隊列中的塊數(shù)達到一定個數(shù)(比如40,可以配置)的時候,掃描進程bitmap,根據(jù)對應(yīng)bit為1的位取出一定個數(shù)(比如8,可以配置為Server進程的個數(shù))的進程ID

5)Proxy遍歷這些進程ID,執(zhí)行kill發(fā)送信號,同時把bitmap對應(yīng)的位置0(防止進程死了,不斷被通知)

6)Server進程收到信號或者超時后從睡眠函數(shù)中醒來,把sigindex對應(yīng)的bit置0,關(guān)閉通知

除了signal通知,其實還有很多通知機制,包括pipe、socket,還有較新的內(nèi)核引入的eventfd、signalfd等等,我們之所以選擇比較傳統(tǒng)的signal通知,主要因為簡單、高效,兼容各種內(nèi)核版本,另外一個原因,是因為signal的對象是進程,我們可以選擇性發(fā)送signal,避免驚群效應(yīng)的發(fā)生。

防滾雪球控制機制

前面已經(jīng)說過,原來的防滾雪球控制機制是基于網(wǎng)卡收包時間戳的。但現(xiàn)在server拿不到網(wǎng)卡收包的時間戳了,只能另尋新路,新的做法是:

Proxy收包的時候把收包時間戳保存起來,跟請求包一起放到隊列里面,server收包的時候,把這個時間戳跟當(dāng)前時間進行對比。

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

性能驗證

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

最后,分set的這個版本已經(jīng)正式上線運行一段時間了,目前狀態(tài)穩(wěn)定。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容