QQ18年
1999年QQ介面
2000年,OICQ正式更名為QQ,釋出視訊聊天功能、QQ群和QQ秀等功能。
2004年,QQ新增個人網路硬碟、遠端協助和QQ小祕書功能。
···
幾經更迭,QQ版本也產生許多變化,很多操作方式都變了,也讓QQ更有現代感了。如今的QQ越來越精美,越來越簡潔,如你所見。
如此龐大的使用者群的任何行為,都會產生巨大的影響。
2017年春節,QQ推出AR紅包加入紅包大戰,經調查手機QQ的紅包全網滲透率達到52.9%。
現在說說分set的背景:2013年的某一天,某個業務的小朋友在申請正式環境的DB接入許可權後,使用正式環境來驗證剛寫完的測試程式,迴圈向DB介面機傳送請求包,但因為這個包格式非法,觸發了DB解包的一個bug,導致收到這些請求包的伺服器群體core dump,無一倖免。。。。整個DB系統的服務頓時進入癱瘓狀態。
因此有了故障隔離的需求,2014年初,我們著手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來解決訪問衝突。
那麼,讀的時候,只要保證修改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收包的時候,把這個時間戳跟當前時間進行對比。
這樣能更有效的做到防滾雪球控制,因為我們把這個包在前面的環節裡面經歷的時間都考慮進來了,用圖形描述可能更清楚一點。
最後,分set的這個版本已經正式上線執行一段時間了,目前狀態穩定。