鬥魚:如何打造一個高性能、高可用直播系統架構

近幾年來,國內直播行業發展迅猛,網絡直播平臺也成爲了一種嶄新的社交媒體。直播火熱的同時,也給直播平臺的技術與架構帶來了諸多挑戰。作爲行業領頭人,鬥魚在架構上不斷迭代、改造與優化,形成當前能支撐千萬級用戶同時在線觀看的架構平臺。在這個過程中,鬥魚的直播系統架構踩過哪些坑,演進出哪些特性呢?本文整理自鬥魚房間中臺負責人彭友順近日在TGO鯤鵬會武漢分會活動《大規模互聯網系統架構設計與實現》上的演講,內容如下。

目前鬥魚直播系統每天有20億+的請求量,並且在今年3月份,PDD入駐鬥魚的時候還曾達到40萬+的瞬時QPS,同時我們的服務增長到了上千實例的規模,在全國不同地區和不同機房進行了部署,提供給上百個內部業務方使用。總結下來,鬥魚直播系統有三個特點——流量大、服務多、架構複雜。針對這些問題,要確保整個系統的高性能和高可用,對於我們的技術開發團隊來說也是一個比較大的挑戰。

高性能業務架構演進中的挑戰與解決方案

三大挑戰

早在5年前,我們和其他公司一樣處於單體應用時期,主要使用“Nginx+PHP+Memcache+MySQL”,當時遇到最大的一個問題,是如果一個用戶進入到直播間訪問Memcache的時候,如果剛好Memcache裏面緩存數據失效了,那麼請求就會穿透到MySQL,會對服務造成很大的壓力。

所以從2016年開始,我們將Memcache換成了Redis,將全量的直播間的數據緩存到Redis的內存緩存,解除服務對MySQL的直接依賴,同時還做了一些業務隔離:將業務進行了垂直拆分。保證了那個時期的服務穩定。

但是隨着鬥魚體量的日益增長,請求量越來越大,我們沒想到Redis也會成爲一個新的瓶頸。所以從去年開始鬥魚着手對系統做了一些改造,將PHP換成了Golang,然後在我們Golang裏面做了一些內存緩存池和Redis連接池優化,經過這一系列的改造,目前鬥魚的直播系統無論是在性能上還是可用性上都有很顯著的一個提升。

首先給大家介紹一下我們鬥魚的Memcache時期。在理想的情況下,當一位用戶進入到我們直播間頁面,請求會先到Memcache,如果Memcache裏面有緩存數據,那麼就會直接將數據返給用戶。但是通常情況下並不總是理想狀況,我們現在舉一個具體案例進行介紹。

假設有一位大主播開播了,他就會發開播信息給他的粉絲們,這些粉絲會在短時間內進入到該主播的直播間,這樣就帶來第一個問題——瞬時流量。 緊接着大量請求會併發到Memcache裏,由於Memcache採用是一致性hash的算法,所以同一個直播間的緩存key會落在同一個Memcache節點上,這就會造成某個Memcache節點在短時間內負載過高,導致第二個問題——熱點房間問題。同時又由於直播間剛開播,之前沒有人訪問過這個頁面,Memcache裏是沒有這個直播間信息,那麼又會有大量的請求穿透到MySQL,對MySQL造成性能影響,所以就造成了第三個問題——緩存穿透問題。

解決之道

針對上述三大挑戰,我們做了一些優化。

首先要解決的是緩存穿透問題。鬥魚直播系統裏寫操作都是主播進行觸發,而大量讀服務接口則是用戶進行觸發,所以我們在業務層面上做了一個讀寫分離,規定:只有主播接口纔可以對MySQL和Redis做寫入操作;並且將直播間基礎數據全量且不過期的緩存到Redis裏。這樣就去除了用戶請求對MySQL的依賴。也就是說用戶請求無論如何都不會穿透到MySQL。

針對於第二個問題——瞬時流量,我們主要在Nginx的配置上面做了一些優化。首先,將直播系統單獨分配一個nginx的proxy cache,做一些隔離,避免非核心業務的cache佔用了核心業務cache的空間;其次,對突發流量,在服務返回502,504狀態碼(服務容量不夠了)時,返回上一次緩存的數據,避免直播間白屏,保證其可用性;最後,做了proxy lock on的配置,確保瞬時同樣的請求,只會有一個請求到後端,降低後端和數據源的負載。

最後針對熱點房間問題,我們將一致性hash的Memcache換成了主從結構的Redis。多從庫的Redis,每個裏面都有大主播的數據信息,可以有效的分擔大主播流量,從而使得後端數據源的負載均衡,不會出現單節點的性能問題。

新的問題

優化之後,大家都覺得還挺不錯的,但是,我們發現把Memcache換成Redis之後又帶來了新問題:

帶寬過高

首先我們遇到的是內網帶寬過高的問題。以前Memcache把響應數據給客戶端的時候,它會將這些數據做一個壓縮。我們將Memcache遷移到Redis後,由於Redis沒有這個能力,內網帶寬翻了4到5倍,引起了網卡負載較高,出現一些丟包和性能問題。

使用不當

隨着我們鬥魚的體量越來越大,我們的請求量越來越大,Redis逐漸成爲了我們的性能瓶頸,大量的業務共用同一組Redis,如果某個業務方出現Redis用法不當,阻塞了單線程的Redis,會影響其他業務。同時由於Redis沒有限流功能,很容易Redis被打垮,導致所有業務全部癱瘓。

時延過高

因爲業務需求的升級,我們想帶給用戶更好的體驗,將許多曾經的緩存接口變成了實時接口。如何降低我們請求的時延,考驗着我們開發的能力。

微服務化

爲了解決這三個問題,我們步入了一個新的時期——微服務。首先是在架構上做了一些改變:PHP換成了Golang、 將Redis主從結構變成Redis分片的主從結構。其次我們針對業務做了一些區分:大主播和小主播採用不同的存儲策略,小主播的信息我們是通過hash算法放到一個節點裏,這樣是爲了降低Redis內存開銷;大主播的信息在所有分片均會存放,保證大主播在突發流量時有很多節點進行分擔。

架構改變說完後,我們再聊下,我們鬥魚對Redis的一些理解和經驗。

最佳實踐

剛纔也說了,我們之前業務上共用Redis造成了一些問題,爲了解決這些問題,首先要做的是對Redis做好隔離;其次針對帶寬過高的問題,一方面將Redis裏的key做了精簡,降低流量和存儲大小;另一方面從業務上規範了房間字段的按需索取。最後從代碼層次規範了Redis的使用方法:我們根據線上全鏈路壓測的壓測結果,調整了Redis連接池空閒連接數,這樣可以解決線上的突發流量問題;並且規範了獲取Redis數據的方式,嚴格限定了獲取Redis的每次包儘量不要超過1KB,並且儘量使用pipeline,減少網絡io。

再快一點

剛纔說過我們後來有許多接口變成了實時性接口,怎麼讓服務更快一點呢。我們就把一些熱門主播通過任務提前算好,服務通過異步獲取熱門主播名單緩存這些直播間數據,用戶再看這些熱門主播的時候速度可以更快。同時對於獲取批量直播間數據,通過計算redis從庫個數,併發去拉取直播間數據,來降低業務方的時延。

實驗數據

爲了讓大家有個直觀對優化有個直觀的理解,下面第一個圖,是我們鬥魚在對批量直播間數據做的對比試驗。大家可以看到精簡key和按需索取,其性能要好很多。下面第二個圖,是官方做的Redis包大小的對比圖。當獲取Redis數據包大小在1KB以下,性能比較平穩,但超過1KB,性能急劇下降。所以我們業務場景在使用Redis的時候,一定要注意些這些細節。才能使得我們系統性能更好。

高可用技術架構

爲了讓大家知道高可用的必要性,我們先來看下鬥魚的技術架構圖。這是一個多機房架構的部署圖。我們先來看一個服務區域,首先必不可少的是我們的應用(客戶端和服務端),服務端和客戶端靠etcd來發現彼此。不同的服務區域則是通過我們自己寫的sider來相互通信和感知。然後prometheus和日誌都會把數據採集到微服務管理平臺,一個服務從開始部署上線、到運行監控、故障定位及修復操作都可以在微服務管理平臺進行。

高可用

從剛纔介紹大家應該可以知道目前鬥魚的架構和部署的複雜性。如果沒有好的高可用的解決方案,那麼勢必會造成系統的不可靠,導致嚴重的線上問題。

因此我們提出了高可用的一些解決方案。我們將高可用分爲兩個層次。第一層是自動擋,比如像負載均衡、故障轉移、超時傳遞、彈性擴容、限流熔斷等,這些都是可以通過代碼層面或者運維自動化工具,不需要人工干預,做到系統的自愈。第二層是手動檔,這些主要就是監控報警、全鏈路壓測、混沌工程、sop等,我們能通過一些手段提前預知可能存在的問題,或者線上出現問題無法自愈,我們怎麼快速發現,快速解決。

因爲高可用的內容比較多,限於篇幅有限。我們今天就主要講兩個內容負載均衡和監控報警。

負載均衡

通常情況微服務都會採用roundrobin的負載均衡算法,它實現起來比較簡單,但是它的問題也不小。在這裏給大家看一個負載均衡roundrobin的案例。爲了方便大家理解這個算法,我們對模型做了一些簡化。大家可以看這個圖,有個定時任務,每秒都會先調用一個消耗低cpu的單個房間信息接口,然後再調用一個消耗高cpu的批量房間信息接口。如果剛好服務端就兩臺實例,根據roundrobin算法,這個請求的調用方式就是:奇數次單個房間信息接口都調用到a節點,偶數次批量房間信息接口都調用到b節點。顯然易見的就是a節點負載會遠遠低於b節點。

爲了解決以上問題,我們可不可以根據系統的負載動態的調度呢。於是我們會在客戶端用gRPC調用服務端的時候,讓服務端不僅返回業務數據,同時在header頭裏會將服務端cpu的load返給客戶端。客戶端根據服務端的cpu load和請求的耗時,異步算出下次客戶端需要請求的服務端實例。這個調度算法看似挺美好,但是他會帶來“馬太效應”。假設服務端某個節點此時負載比較小,客戶端可能會因爲算法一窩蜂去請求他,使它負載變高。這樣服務端節點cpu會變得忽低忽高,這也不是我們想要的結果。

所以這個時候,我們通過調研採用了業界流行的p2c算法。我們在客戶端在做調度的時候做了一個隨機數。 如圖左邊所示。可以看到加了隨機數後,可以避免這種“馬太效應”的調度問題,負載較低的多調度點,負載較高的少調度點。當然如果服務端有問題,是直接剔除的。

通過優化負載均衡的算法,我們平滑了服務端的cpu負載,同時可以快速剔除有問題的服務節點。當然還有個最關鍵的成效,就是有了這個算法,我們纔可以把多機房的etcd註冊數據進行同步,做多數據中心,然後客戶端根據算法自動選擇不同機房的節點,實現同城雙活。

這個圖就是我們通過wrr算法切到p2c算法後的對照圖,可以看到在這個圖的前半部分,每個實例之間的負載差距都比較大,並且同一個實例的負載波動也比較大。換成p2c算法後,趨勢圖看起來就比較穩定了。

監控報警

先給大家看一個線上報警的架構圖。通常線上出現問題後,我們的日誌系統和監控系統,會將對應的數據上報到報警系統,但是報警的信息可能五花八門,需要一個人工過濾器系統去篩選這些信息,才能找到真正負責業務的人員。

我們之前在沒有進行服務錯誤收斂的時候,經常會出現各種報警狂轟亂炸,出現陌生的報警信息,不知道該如何處理,業務報警和系統報警交織在一起,很難分辨出報警的輕重緩急。不得已需要有專門幾個人處理和篩選報警信息。但是人工爲什麼要成爲報警的一個系統?

這是因爲我們沒有從源頭做好,沒有很好的對服務進行錯誤收斂,導致錯誤信息氾濫,報警轟炸。

錯誤收斂

要想將錯誤收斂做好,首先要制定好的規範。我們規範了系統和業務的統一錯誤碼,監控,日誌應用同一套錯誤碼,方便關聯和查看。並將監控,日誌索引做了統一規範,例如之前redis的命令叫command,mysql語句叫sql,http請求叫url,這樣導致我們的日誌很難收斂做分析和發現問題,我們後來將一些共性的指標統一成一個名字,例如將剛纔所說的在日誌和監控都取名叫method。

做好規範後,接下來,我們就開始治理我們的服務。我們認爲系統錯誤的重要程度要高於業務錯誤。監控應該更多的區分系統級別的錯誤。所以右圖第二個,因爲我們之前做了統一錯誤碼規範,code小於10000的系統錯誤碼全量記錄到prometheus裏,對於業務錯誤碼,我們都收斂成biz err。如果出現biz err,業務方應該通過這個狀態碼,去日誌裏看詳細信息。

在右邊第三個圖,介紹的是我們收斂redis的一個代碼片段,像redis,mysql也有很多錯誤碼,例如查不到信息非系統級別的錯誤,我們統一收斂到unexpected err,做一個warning告警。但對於像圖中上面的read timeout,write timeout等則是記錄error,做一個高級別的報警。
還有個問題,雖然我們統一了錯誤碼,但如果出現錯誤碼的時候,沒有文檔和解決方案,我們還是一籌莫展。

所以我們提出代碼既是文檔的解決方案。通過自動化的語法樹解析代碼裏的狀態碼和註釋,自動的生成對應的錯誤碼文檔,告訴我們錯誤碼在代碼哪個地方,可能存在的問題。

有了錯誤收斂後,我們才很好的去做我們的監控。

監控指標

我再提出一個問題,有了錯誤收斂,我們監控就可以很好的發現服務問題嗎?

這裏再舉個例子,這裏有個汽車,大家看這個汽車,覺得這個汽車是可以使用的嗎?如果僅從一個平面去觀察這個汽車,有可能你覺得汽車是正常的。但是如果你從多個維度去觀察這個汽車,你可能會發現他尾部存在問題。這個和我們服務監控一樣,我們如果要做監控,那麼這個監控指標一定要立體。

所以我們將監控維度上做了一些要求。從系統維度上,我們需要知道服務的上下游的關聯監控;從應用維度上,我們要能夠看到應用實例監控、應用大盤監控、全部應用大盤監控、各種不同指標的top榜。

在指標上,我們按照SRE文檔裏要求,對四個黃金指標延遲、流量、錯誤和飽和度做了很好的落實。

下圖是我們一個服務,上面展示了一些基礎監控數據,中間的一部分是我們服務的關鍵指標,分別是服務的錯誤碼分佈,服務的qps,服務的p99耗時,這三個剛好對應的就是黃金指標的錯誤、流量、延遲。流量和延遲做開發的其實接觸的比較多,對這些指標還是比較敏感的,我就不詳細介紹。主要來說下黃金指標的錯誤和飽和度。

監控的錯誤識別,上面我已經介紹了錯誤收斂怎麼處理的,如果服務做到很好的錯誤收斂當發現問題的時候,可以很快發現和解決,如下所示,我們可以很快知道我們錯誤日誌和慢日誌的一些情況。但是這個並不能很好的預防不必要的錯誤發生。

所以爲了更好的識別錯誤,預防錯誤發生,可以使用混沌工程進行一些故障注入。一方面可以儘早的識別一些錯誤,另一方面也可以讓我們開發人員積累一些錯誤經驗,方便後續處理線上故障問題。

最後在介紹下,可能容易被大家忽視的一個監控指標–飽和度。通常有好多業務方,容易忽視飽和度的監控和報警,導致服務達到了性能瓶頸之後被動的去做一些應急方案。

並且就算知道這個指標,也很難預估出來服務的飽和度。需要研發人員具有一定的經驗、一定的能力,很多時候我在線下壓測後發現線下壓測和線上壓測完全不一樣。因爲線下是很簡單的,線上環境是很複雜的,所以飽和度最好是通過線上全鏈路壓側。這樣就可以知道線上的水位是多少,知道一個核心數可以提供多少的qps,什麼時候會達到性能瓶頸。下圖就是我們鬥魚做全鏈路壓測之後,得到各個機房某個服務的qps飽和度圖

和時間賽跑

無論系統如何高可用,但系統還是會出現線上故障。如果出現了問題,我們就要想辦法如何快速解決。

系統聚合

我們鬥魚之前微服務系統都是按系統拆分,監控系統、日誌系統、報警系統等,如果線上如果出現問題,那我首先去需要打開多個系統的頁面,不停去登錄賬號,非常影響排查效率。所以後來就是對微服務做聚合的一個操作,把所有的系統按應用維度聚合到一起。我們查看一個應用,可以很快在裏面看到監控、註冊、配置、性能和日誌等信息,這個小小的改動,其實能夠很快的幫助業務方進行排查問題。

數據分析

我們要對數據做分析,雜亂無章的數據對排查速度影響很大。避免人工的一些檢索和判斷。我們根據之前所說的收斂工作和監控的立體維度。可以很快的查看各個應用各種指標的top榜,並且能夠知道報警的錯誤碼是多少,索引到確切的解決方案。

SOP手冊

我們需要通過混沌工程,全鏈路壓測等方式,提前想好可能出現的嚴重問題,做好sop操作手冊,這樣當問題真正來領的時候,我們不會慌張,可以很從容的處理。就像這個圖裏的一樣。

嘉賓介紹:
彭友順,鬥魚房間中臺負責人,2015年加入鬥魚,跟隨着鬥魚成長,經歷了鬥魚直播系統架構的演進歷程,積累了大量高併發、高可用的項目經驗,並主導GO微服務的架構建設,見證了鬥魚直播系統微服務的發展成果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章