做者:shane,騰訊後臺開發高級工程師前端
QQ18年後端
1999年:騰訊QQ的前身OICQ誕生,該版本具有中文網絡尋呼機、公共聊天室以及傳輸文件功能。服務器
1999年QQ界面網絡
2000年,OICQ正式改名爲QQ,發佈視頻聊天功能、QQ羣和QQ秀等功能。運維
2003年版本,QQ發佈聊天場景、捕捉屏幕、給好友播放錄影及QQ炫鈴等功能。 socket
2004年,QQ新增我的網絡硬盤、遠程協助和QQ小祕書功能。函數
···性能
幾經更迭,QQ版本也產生許多變化,不少操做方式都變了,也讓QQ更有現代感了。現在的QQ愈來愈精美,愈來愈簡潔,如你所見。測試
據不徹底統計,騰訊QQ月活用戶達到8.7億左右,而這個數字還在不斷增長。。。網站
如此龐大的用戶羣的任何行爲,都會產生巨大的影響。
2017年春節,QQ推出AR紅包加入紅包大戰,經調查手機QQ的紅包全網滲透率達到52.9%。
在此期間,後臺想必又一次承受了海量的壓力,年後第一波推送,來看看騰訊內部對QQ後臺的接口處理的相關技術乾貨,或許能夠給到你答案。
1、背景
2、整體方案
爲了更清楚描述分set的方案,咱們經過兩個圖進行分set先後的對比。
分set以前:
分set以後:
從圖中能夠看出,實現方式其實很是簡單,就是多啓動一個proxy進程根據IP到set的映射關係分發請求包到對應set的進程上。
3、分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 -- 這也正是咱們決定必定要作無鎖隊列的緣由。
4、最終實現方案
作互聯網服務,最大的一個特色就是,任何一項需求,作與不作,都必須在投入、產出、時間、質量之間作一個取捨。
前面的嘗試選擇了最簡單的實現方式,目的就是爲了可以儘快上線,減小羣體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收包的時候,把這個時間戳跟當前時間進行對比。
這樣能更有效的作到防滾雪球控制,由於咱們把這個包在前面的環節裏面經歷的時間都考慮進來了,用圖形描述可能更清楚一點。
5、性能驗證
使用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