自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

高性能億級錄制列表查詢系統(tǒng)設(shè)計實踐

系統(tǒng)
本文介紹了列表系統(tǒng)的性能設(shè)計遇到的一些挑戰(zhàn)點,以及在做緩存優(yōu)化的時候可以采取的三個解決方案。

作者 | jaskeylin

一、背景

在騰訊會議320的APP改版中,我們需要構(gòu)建一個一級TAB,在其中放置“我的錄制”、“最近瀏覽”+“全部文件”的三大列表查詢頁。以下是騰訊會議錄制面板的界面(設(shè)計稿)

我的錄制就是作者本人所生產(chǎn)的錄制文件,而所謂“最近瀏覽”很好理解,就是過去觀看過的錄制的足跡留痕。而所謂全部文件則相對比較復(fù)雜,可以認(rèn)為是“我的錄制”+“最近瀏覽”+“授權(quán)給我的錄制”三個集合的并集。

我的錄制就是作者本人所生產(chǎn)的錄制文件,而所謂“最近瀏覽”很好理解,就是過去觀看過的錄制的足跡留痕。而所謂全部文件則相對比較復(fù)雜,可以認(rèn)為是“我的錄制”+“最近瀏覽”+“授權(quán)給我的錄制”三個集合的并集。

雖然三個TAB的樣式幾乎長一模一樣,但是數(shù)據(jù)集是完全不同的,數(shù)據(jù)源可能也不一樣,接口自然也是需要單獨設(shè)計的。但無論哪個接口,三者均面臨著同樣的3個挑戰(zhàn):

  • 調(diào)用量大。作為一個4億用戶量的APP,一個一級入口的流量足以讓所有后臺設(shè)計者起敬畏之心。
  • 數(shù)據(jù)量大。騰訊會議的錄制的數(shù)據(jù)庫的存量數(shù)據(jù)巨大。未來還將持續(xù)保持高速的增長,存儲的壓力、寫入/查詢的壓力很大。
  • 耗時要求高。作為一級TAB的入口,產(chǎn)品對于其中的體驗要求極高,秒開是必須的,這意味著一次接口調(diào)用查詢一頁的耗時在高峰壓力下也要在百毫秒級別內(nèi)。

面對這些挑戰(zhàn),下面介紹騰訊會議的后臺系統(tǒng)是如何應(yīng)對的。

二、分頁列表類接口設(shè)計挑戰(zhàn)

在講具體設(shè)計之前,無論是“我的錄制”還是“最近瀏覽”,本質(zhì)上都是一個列表類功能。這類功能隨處可見,但是要把這個功能做到高并發(fā)、高可用、低延遲,其實并不是一個簡單的任務(wù)。我們以簡化版的“我的錄制”為例去看這里可能有什么挑戰(zhàn)。

一個列表的每一行記錄實際上元素非常的多(上圖已標(biāo)注出來)。我們姑且先假設(shè)這里的cover,title,duration,meet_code,auth_value,create_time,size是存儲在同一張表中(實際上不是)來看看最簡單的一個列表系統(tǒng),在數(shù)據(jù)量大、并發(fā)量大的時候有哪些挑戰(zhàn)。

1. 深分頁問題

從功能的角度來看,“我的錄制”的實現(xiàn)其實就是一條SQL的事情:

select * from t_records where uid = '{my_uid}' limit X, 30;

以上SQL表示查詢我的錄制列表的內(nèi)容,每頁30條。隨著用戶翻頁的進(jìn)行,X會逐漸增大。

如果這是一個幾萬幾十萬的數(shù)據(jù)表,這樣實現(xiàn)是完全沒有問題的,實現(xiàn)簡單,維護(hù)容易又能實現(xiàn)業(yè)務(wù)需求。

(1) 索引的工作原理

但是在騰訊會議的錄制場景下,非常很多表的數(shù)據(jù)量都是超級大表,而且一張表的字段有30+個。只考慮第一頁的情況下,需要在這樣的的大表中搜出來符合數(shù)據(jù)的用戶就是一件挺消耗性能的事情。即:select * from t_records where uid = '{my_uid}' limit 30;

第一步:在命中索引uid的情況下,先找到uid={my_uid}的索引葉子節(jié)點,找到對應(yīng)表的主鍵id后,回表到主鍵索引中再找到對應(yīng)id的葉子節(jié)點,讀出來足夠一頁的數(shù)據(jù),并且把所有字段的內(nèi)容回傳給業(yè)務(wù)。此過程大約如以下圖所示(圖片來源于網(wǎng)絡(luò),以user_name作為索引,但原理是一樣的):

(2) 深分頁時的索引工作原理

假設(shè)加了分頁 select * from t_records where uid = '{my_uid}' limit X,30;

innodb的工作過程會發(fā)生變化,我們假設(shè)X=6000000

數(shù)據(jù)庫的server層會調(diào)用innodb的接口,由于這次的offset=6000000,innodb會在非主鍵索引中獲取到第0到(6000000 + 30)條數(shù)據(jù),返回給server層之后根據(jù)offset的值挨個拋棄,最后只留下最后面的30條,放到server層的結(jié)果集中,返回給業(yè)務(wù)。這樣看起來就非常的愚蠢。

壞事不單只如此,因為這里命中的索引并不是主鍵索引,而是非主鍵索引,掃描的這6000000數(shù)據(jù)的過程還都需要回表,這里的性能損耗就極大了。而且,如果你在嘗試在一張巨型表中explain如上語句,數(shù)據(jù)庫甚至?xí)趖ype那一欄中顯示“ALL”,也就是全表掃描。這是因為優(yōu)化器,會在執(zhí)行器執(zhí)行sql語句前,判斷下哪種執(zhí)行計劃的代價更小。但優(yōu)化器在看到非主鍵索引的600w次回表之后,直接搖了搖頭,說“還是全表一條條記錄去判斷吧”,于是選擇了全表掃描。

所以,當(dāng)limit offset過大時,非主鍵索引查詢非常容易變成全表掃描,是真·性能殺手。

這是:https://ramzialqrainy.medium.com/faster-pagination-in-mysql-you-are-probably-doing-it-wrong-d9c9202bbfd8

中的一些數(shù)據(jù),可以看到隨著分頁的深入(offset遞增),耗時呈指數(shù)型上升。

2. 深分頁問題的解決思路

要解決深分頁的問題,其中一個思路是減少回表的損耗。網(wǎng)絡(luò)上有不少的分享了,總體歸結(jié)起來就是“延遲join”,和游標(biāo)法。

(1) 延遲join

可以把上面的sql改成一個join語句:

select * from t_records inner join (
   select id from t_records where uid = '{my_uid}' limit X,30; 
) as t2 using (id)

這樣的原理在于join的驅(qū)動表中只需要返回id,是不需要進(jìn)行回表的,然后原表中字段的時候只需要查詢30行數(shù)據(jù)(也僅需要回表這30行數(shù)據(jù))。當(dāng)然,以上語句同樣可以改寫成子查詢,這里就不再贅述。

(2) Seek Method

深分頁的本質(zhì)原因是,偏移量offset越大,需要掃描的行數(shù)越多,然后再丟掉,導(dǎo)致查詢性能下降。如果我們可以精確定位到上次查詢到哪里,然后直接從那里開始查詢,就能省去“回表、丟棄”這兩個步驟了。

我們可以seek method,就像看書一樣,假設(shè)我每天睡覺前需要看30頁書,每天看完我都用書簽記錄了上次看到的位置,那么下次再看30頁的時候直接從書簽位置開始看即可。這樣以上SQL需要做一些業(yè)務(wù)邏輯的修改,例如:

select id from t_records where uid = '{my_uid}' and id> {last_id} limit  30;

這也是我們平時最常用的分頁方法。但是這個方法有幾個弊端,需要我們做一定的取舍:

Seek Method 局限一:無法支持跳頁

例如有些管理后臺需要支持用戶直接跳到第X頁,這種方案則無法支持。但現(xiàn)在大部分的列表產(chǎn)品實際上都很少這樣的述求了,大部分設(shè)計都已經(jīng)是瀑布流產(chǎn)品的設(shè)計,如朋友圈。

少數(shù)PC端的場景即便存在傳統(tǒng)分頁設(shè)計,也不會允許用戶跳到特別大的頁碼。

所以這個限制通常情況是可以和產(chǎn)品溝通而繞過的,一些跳頁的功能實際上也很少人會使用。通常存在于一些PC端的管理后臺,而管理后臺的場景下,并發(fā)量很低,用傳統(tǒng)分頁模式一般也能解決問題。

Seek Method 局限二:排序場景下有限制

大部分的列表頁面的SQL并沒有我們例子中這么簡單,至少會多一個條件:按照創(chuàng)建時間/更新時間等排序(大部分情況還是倒序),以按照錄制創(chuàng)建時間排序為例,這條SQL如下1:

select * from t_records where uid = '{my_uid}'  order by create_time desc limit X, 30;

如果需要改成瀑布流的話,這里大概率需要這樣改:

select * from t_records where uid = '{my_uid}'  and create_time < {last_create_time }order by create_time desc limit 30;

這樣一眼看去沒有什么問題,但是問題是create_time 和 id有一個最大的區(qū)別在于ID肯定能保證全局唯一,但是create_time 不能。萬一全局范圍內(nèi)create_time 出現(xiàn)重復(fù),那么這個方法是有可能出現(xiàn)丟數(shù)據(jù)的。如下圖所示,當(dāng)翻頁的時候直接用create_time>200的話,可能會丟失3條數(shù)據(jù)。

要解決這個問題也有一些方法,筆者嘗試過的有:

  • 主鍵字段設(shè)計上保證和排序字段的單調(diào)性一致。怎么說呢?例如我保證create_time越大的,id一定越大(例如使用雪花算法來計算出ID的值)。那么這樣就依舊可以使用ID字段作為游標(biāo)來改寫SQL了
  • 把<(順排就是>)改成<=/>=,這樣以后,數(shù)據(jù)就不會丟了,但是可能會重復(fù)。然后讓客戶端做去重。這樣做其實還有一個隱患,就是如果相同create_time的數(shù)據(jù)真的太多了,已經(jīng)超過了一頁。那么可能永遠(yuǎn)都翻不了頁了。

(3) 列表接口緩存設(shè)計挑戰(zhàn)

解決完深分頁的問題,不意味著我們的列表就能經(jīng)得起高并發(fā)的沖擊,它最多意味著不會隨著翻頁的進(jìn)行而性能斷崖式下降。但是,一旦請求量很大的話,很可能第一頁的請求不一定扛得住。為了提升整個列表的性能,肯定要做一定的緩存設(shè)計。下面來介紹一下一個最常見、最簡單的緩存手段。

方案一:列表結(jié)果緩存

緩存最簡單的就是緩存首頁/前幾頁的結(jié)果,因為大部分情況下列表80%產(chǎn)品都是只使用第一頁或者少數(shù)的前幾頁。以錄制列表為例,假設(shè)我們緩存第一頁的錄制結(jié)果,那么可以用List去緩存。如下圖所示:

首頁結(jié)果緩存的缺點:

  • 一致性維護(hù)的困難。例如新增、刪除視頻的時候,固然需要維護(hù)這個List的一致性。你能想到可能是新增、刪除的時候,直接對list進(jìn)行遍歷,找到對應(yīng)的視頻進(jìn)行新增或者刪除操作即可。但實際上在讀、寫并發(fā)場景下,動態(tài)維護(hù)緩存是很容易導(dǎo)致不一致的。如果為了更好的一致性考慮,可以考慮有變更的時候便刪除掉整個list緩存。
  • 過于頻繁的維護(hù)緩存。無論走修改策略還是刪除策略,其維護(hù)的時機就是緩存的內(nèi)容發(fā)生變動的時候。緩存整個結(jié)果意味著結(jié)果變動的內(nèi)容可能性非常大。例如錄制的狀態(tài)是經(jīng)常發(fā)生變更的:新建->錄制中、轉(zhuǎn)碼中、轉(zhuǎn)碼完成、完成等等。錄制的標(biāo)題也是可能發(fā)生變動的,錄制的打擊狀態(tài)也是隨時可能變的,權(quán)限也是可能被管理員修改的。這些單一錄制的任意字段都可能需要對整個list緩存進(jìn)行維護(hù)(修改/刪除),如果采取的是刪除策略,那么頻繁的維護(hù)動作會導(dǎo)致緩存經(jīng)常失效而性能提升有限。如果采取更新策略則又維護(hù)困難且有一致性的問題。
  • 緩存擴(kuò)散維護(hù)的困難。這個在“我的錄制”里不存在這樣的問題,但是假設(shè)我們把“我的錄制”的功能范圍擴(kuò)充為:屬于我的錄制以及我看過的錄制。這種情況下用首頁結(jié)果緩存就會有巨大挑戰(zhàn)。因為一個視頻X可能被N個人看到。但是每個人都有自己的首頁緩存(一個人一條list的redis結(jié)構(gòu)),當(dāng)X的某個字段(例如標(biāo)題)發(fā)生變動的時候,我們需要找到這N個人的list結(jié)構(gòu)。你可能會說,可以采取keys的操作找出來這些list去維護(hù)即可。但是keys操作是O(N)時間復(fù)雜度的操作,性能極差。哪怕我們采取scan去替換keys,在N極大的情況下,這里的損耗也是非常巨大的。

方案二:ID查詢+元素緩存

另外一個可行的方案是先查詢出這一頁的ID數(shù)據(jù),然后再針對ID去查詢對應(yīng)頁面所需要的其他詳情數(shù)據(jù)。如下圖所示:

這樣的好處是緩存設(shè)計可以不針對某個用戶的頁面結(jié)果去緩存,而是把元素信息緩存起來,這個方案有3個好處:

  • 查詢數(shù)據(jù)庫只查詢ID的話,可以走聚簇索引,少一次回表。而且select 的字段數(shù)據(jù)也變少,查詢因為搜索的字段變少了,本身查詢的性能也會提升(網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)變少了)。
  • 緩存的維護(hù)很簡單。因為緩存的變化是單一數(shù)據(jù)結(jié)構(gòu)的變化而不是一個集合的變化,維護(hù)起來會輕便很多。
  • 沒有緩存擴(kuò)散的問題。假設(shè)一個錄制被N個人瀏覽過,這個錄制的狀態(tài)變更也僅需要變動一個緩存key。

但是這個方案也有挑戰(zhàn)。假設(shè)一頁查詢30個數(shù)據(jù),實際上是一次數(shù)據(jù)庫操作+30次緩存的操作。這里30次緩存操作可能有一些命中換成有一些沒命中緩存。最簡單粗暴的方案就是把30個ID湊一起例如:1_2_3_4........_30,一批查詢就是一個大緩存結(jié)果。但是這樣就又繞回去方案一里面的緩存缺點里了。所以只能一個緩存一個KEY,但是要實現(xiàn)一個機制讓命中緩存的直接讀緩存,讓沒有命中緩存的走數(shù)據(jù)庫查詢后再回填到緩存中。這里是有一定實現(xiàn)復(fù)雜度的,而且如果30次緩存操作都是串行的話,疊加起來耗時也是個不小的,還需要考慮并行獲取的情況。其示意圖如下所示:

這套方案本身集成到了騰訊會議一個緩存組件FireCache上,業(yè)務(wù)只需要調(diào)用API即可,不需要考慮哪些緩存命中哪些緩存不命中,也不需要考慮并發(fā)的問題。

方案三:ID列表緩存+元素緩存

方案一和方案二的結(jié)合。

對于ID的列表,也采取Redis集合結(jié)構(gòu)體去緩存,這樣查詢ID列表能盡量的命中緩存。當(dāng)查到列表后,元素本身也是像方案二一樣緩存起來的,那么也會大量命中緩存。這里的集合結(jié)構(gòu)體可以采取ZSet,也可以采取List。采取Zset的場景大概率是動態(tài)更新策略的場景,而采取List的場景則更多是動態(tài)刪除策略的場景。兩者的優(yōu)劣前面已經(jīng)介紹過了不再解釋。

以上就是最典型的列表接口的常見優(yōu)化方案,在沒有其他特殊的建設(shè)下可以按照合適的需要選擇。

以下是三個方案的優(yōu)點和缺點:

混合數(shù)據(jù)源列表問題

有些時候,數(shù)據(jù)來源可能不是簡單的一條SQL語句就能拿到數(shù)據(jù)的。例如朋友圈需要展示的數(shù)據(jù)不僅僅是自己發(fā)表過的數(shù)據(jù)。還涉及到朋友發(fā)表的數(shù)據(jù)中權(quán)限可見的數(shù)據(jù)。我們假設(shè)一個類似朋友圈的產(chǎn)品底層也是用關(guān)系型數(shù)據(jù)庫存儲的,下面我們看基于這樣的數(shù)據(jù)庫設(shè)計是實現(xiàn)一個列表查詢有哪些挑戰(zhàn)。

挑戰(zhàn)一:復(fù)雜查詢

如果按照常規(guī)的搜索條件獨立去搜,我們的SQL語句實際上是一條很復(fù)雜的并集語句。這個語句大概是:

select * from t_friends_content where creator_id = {自己} 
union all
select *  from t_friends_content where creator_id in (select friend_id from t_relations where hostid={自己})

最后這兩個語句還需要分頁和排序!

如果不考慮性能的話(一些B端的場景使用量非常低頻),僅僅兩條SQL語句做union操作也勉強可以接受。但是大部分情況下我們做一個C端業(yè)務(wù)的話,這個性能是不能接受的!

挑戰(zhàn)二:跨庫分頁&排序問題

更難以解決的問題還有一個?很可能朋友圈的內(nèi)容庫,關(guān)系鏈庫是來自于兩個獨立的數(shù)據(jù)庫。更有甚者,可能來自于兩個系統(tǒng)。這樣的跨庫場景下分頁、排序?qū)順O大的實現(xiàn)復(fù)雜度和一個指數(shù)級的性能下降。例如,我需要查詢第3頁的朋友圈數(shù)據(jù),實際上我是需要查詢前3頁的“我發(fā)表的朋友圈”+前3頁的“我朋友的朋友圈”之后,在內(nèi)存中混合排序才能得到真實的第三頁數(shù)據(jù)。如果這里的頁碼越來越深,這里的性能會指數(shù)級的下降。

如下圖的一個例子所示:要搜索第三頁的數(shù)據(jù),實際上不能看兩個數(shù)據(jù)源的第三頁數(shù)據(jù),而是要在第二頁中找到的數(shù)據(jù)才是對的。

跨庫分頁的問題在分庫的場景下也會一樣遇到,例如查詢我發(fā)表的朋友圈時候,如果朋友圈的數(shù)據(jù)庫是分庫的(假設(shè)是按照朋友圈ID去分庫),也一樣有類似的問題。

以上是一個列表設(shè)計可能遇到的一些常見挑戰(zhàn)??梢钥吹剑谕庑锌磥矸浅:唵蔚牧斜聿樵?,一旦要求要做成一個高性能的產(chǎn)品,其實現(xiàn)的難度對比就像一倆玩具車和一臺F1方程式跑車的區(qū)別。功能都是能跑,但是面臨的挑戰(zhàn)完全不一樣。這也是優(yōu)秀后臺研發(fā)工程師的價值所在——同樣一模一樣的功能,要以一個高性能、高可用的標(biāo)準(zhǔn)去實現(xiàn)它,這兩者無論從設(shè)計還是實現(xiàn)上根本就不是同一個東西。

介紹完通用的方案和挑戰(zhàn)后,以下介紹騰訊會議的錄制列表是怎么設(shè)計的。

三、騰訊會議“我的錄制”列表優(yōu)化實踐

1. 錄制列表的挑戰(zhàn)

“我的錄制”列表本來是騰訊會議一直存在的功能。功能背后的接口在現(xiàn)網(wǎng)也穩(wěn)定運行了非常久的時間。雖然數(shù)據(jù)庫的數(shù)據(jù)量非常大,但因為這個接口的業(yè)務(wù)功能并不復(fù)雜(就只是查詢屬于自己的錄制文件),而且只開放給了企業(yè)的管理員,調(diào)用量很低,所以從實現(xiàn)上后臺老的實現(xiàn)就是直接使用數(shù)據(jù)庫的查詢來完成的。

但是隨著這次的錄制面板的在APP一級入口開放,會給這里接口性能提出更多地挑戰(zhàn)要求。由于錄制列表的數(shù)據(jù)目前存在于一個龐大的單表MySQL中,底層的壓力是非常巨大的。在我們第一輪的壓測中,在270qps的場景下,成功率僅有94.71%。

為了應(yīng)對這個挑戰(zhàn),在業(yè)務(wù)層,我們需要頂住流量的沖擊,我們的選擇是通過緩存保護(hù)好后面的數(shù)據(jù)接口。

2. 錄制列表的緩存設(shè)計

實際上,我的錄制列表是一個非常經(jīng)典的業(yè)務(wù)場景:業(yè)務(wù)簡單(僅查自己的數(shù)據(jù))但是要求的并發(fā)高,典型的場景如視頻網(wǎng)站中的我的視頻、微博中的我的微博、收件箱里我的消息等等。常用的方案前文已經(jīng)介紹過了。以下介紹在騰訊會議的實踐方案。

(1) “我的錄制”的2層緩存設(shè)計

實際上針對這類型的查詢,如果最簡單粗暴的優(yōu)化,是把數(shù)據(jù)全量緩存到Redis中。直接把Redis當(dāng)存儲使用。這樣性能絕對扛扛的。(目前騰訊會議主面板的列表的存儲設(shè)計就是沒有DB只存Redis的)。但千萬級別的數(shù)據(jù)庫數(shù)據(jù)使用Redis重新緩存一輪需要非常高的成本(RMB成本),而且也會有Big Key的大集合問題(每個用戶的錄制視頻是持續(xù)性單調(diào)遞增,造成list中的數(shù)據(jù)太大)??紤]到用戶的查詢行為絕大部分的場景會集中于首頁,我們可以緩存首頁的數(shù)據(jù)來讓絕大部分的數(shù)據(jù)能命中緩存從而保護(hù)到后臺。

最簡單的思路當(dāng)然是直接緩存某個用戶的首頁的結(jié)果(即前文的方案一),但是這個設(shè)計有以下幾個問題:

  • 首頁返回的數(shù)據(jù)包是比較大的(接口40多個字段),這樣會導(dǎo)致每個用戶需要緩存的數(shù)據(jù)體積較大,所需要的緩存成本較多。
  • 用戶的數(shù)據(jù)發(fā)生變動的時候,會驚擾不必要的緩存數(shù)據(jù)。例如一個用戶的首頁數(shù)據(jù)是id=1到id=30的數(shù)據(jù),假設(shè)我們緩存了1-30這些數(shù)據(jù)的詳細(xì)內(nèi)容,這時候當(dāng)用戶新增了一個id=31的視頻的時候,或者id=5的這個視頻的標(biāo)題發(fā)生變更的時候,我需要整體失效掉id=1-30這個緩存的結(jié)果。但實際上每次的變動其實只是一個數(shù)據(jù),但是我們卻被迫驚擾了30個數(shù)據(jù)。這也是方案一最大的缺點。

最后我們采取了實現(xiàn)最為困難的方案三,但是列表的緩存我們簡化了只存第一頁的數(shù)據(jù)。所以緩存的首頁數(shù)據(jù)是帶緩存,也就是說用一個list緩存了id=1到id=30的id。然后需要查詢這些錄制詳情的時候,我們再走對應(yīng)的批量接口獲取對應(yīng)的詳情。最終形成一個二級的緩存結(jié)構(gòu)。第一級是ID索引,第二級是錄制詳情緩存。

這樣當(dāng)某個視頻的數(shù)據(jù)發(fā)生變更的時候,我只需要失效掉這個list的緩存,而每個背后詳情的緩存是不會失效的。

(2) 性能優(yōu)化后的效果

在這樣的緩存設(shè)計下,我的錄制接口能經(jīng)收住了700+qps的壓力:

同時平均耗時也從原來的308ms降低到了70ms,提升了4倍的性能!

“我的錄制”列表后續(xù)優(yōu)化方向

目前我們實現(xiàn)的一級索引緩存(id緩存),是用list維護(hù)的。一旦有新增錄制、修改錄制,為了緩存的一致性維護(hù)方便,目前的手段都是直接清空緩存,等下次的查詢的時候再重新裝載。這樣的好處是肯定不會出現(xiàn)讀寫一致性的并發(fā)導(dǎo)致數(shù)據(jù)不一致的問題。后續(xù)如果有更高的性能要求,我們可以考慮新增緩存直接往list 中 push 數(shù)據(jù),即有動態(tài)刪除數(shù)據(jù)的策略改成動態(tài)更新的策略。

四、“全部文件”架構(gòu)實現(xiàn)

1. 多數(shù)據(jù)源查詢的挑戰(zhàn)

(1) 數(shù)據(jù)來源復(fù)雜,查找功能開發(fā)難度大

錄制全部文件的功能是包含多種數(shù)據(jù)來源的。如下圖所示,是筆者的一個面板數(shù)據(jù),雖然都是錄制,但是其來源于非常多的可能,可能是自己創(chuàng)建的,也可能是自己瀏覽過的,也可能是自己申請查看的,也可能是別人邀請我看的。以下是測試環(huán)境的一張截圖,數(shù)據(jù)來源比較全,讀者可以從中看到其數(shù)據(jù)源是比較復(fù)雜的。

所以這肯定是一個多數(shù)據(jù)源的查詢場景。屬于前面我們提到的“混合數(shù)據(jù)源列表”的一個典型場景。要完成一個完整的全部文件需求,按照正常思路去開發(fā),需要聚合查詢以下幾種數(shù)據(jù)源:

  • 用戶本身的錄制數(shù)據(jù)。例如創(chuàng)建了一個錄制,就需要出現(xiàn)在面板中。本數(shù)據(jù)目前存儲于媒體應(yīng)用組的云錄制系統(tǒng)后臺。
  • 錄制的授權(quán)數(shù)據(jù)。例如一個錄制假設(shè)是參會者可見,那么我參加了這個會議也要出現(xiàn)在我們的面板中,例如一個錄制被指定給我可見,我也要馬上能看到這份數(shù)據(jù)。這里的數(shù)據(jù)存儲星環(huán)后臺二組的權(quán)限系統(tǒng)。
  • 用戶的瀏覽記錄。如果一個視頻被我瀏覽過,就應(yīng)該馬上能出現(xiàn)在列表中。哪怕這個視頻后續(xù)被刪除/移除了我的權(quán)限(后續(xù)權(quán)限被剝奪,封面圖、標(biāo)題會降級顯示)。這是瀏覽記錄系統(tǒng)維護(hù)的獨立數(shù)據(jù)庫,由星環(huán)后臺一組維護(hù)。
  • 用戶操作的刪除記錄。出現(xiàn)過的錄制,只要一個用戶不想看到,就可以移除,可以認(rèn)為是一個黑名單工作。這部分工作是全新功能,開發(fā)這個需求前沒有這部分?jǐn)?shù)據(jù)。

試想一下,如果要完成一個 my-all-records的接口開發(fā),至少你需要做一下的工作:搜索基于uid=自己,搜索4個數(shù)據(jù)源的數(shù)據(jù)。其中1 2 3 數(shù)據(jù)求并集,最后再對數(shù)據(jù)4求差集。如下圖所示:

但這樣做雖然簡單,但是有幾個問題是極難克服的:

  • 性能查詢負(fù)擔(dān),當(dāng)一個數(shù)據(jù)庫的數(shù)據(jù)量都是千萬-億級別的,本身查詢的耗時客觀擺在這里。做了重度工作之后還需要做各種集合運算,成本很高。最后出來的效果就是接口的耗時很高,反映到錄制面板體驗上就是用戶需要等待較高的時間才能看到數(shù)據(jù),這對于用戶體驗追求極致的團(tuán)隊而言是無法接受的。
  • 穩(wěn)定性挑戰(zhàn)。每次查詢海量數(shù)據(jù)數(shù)據(jù)源本身就是一個“高危動作”,如果一個海量數(shù)據(jù)庫查詢成功率是99%,那么四個數(shù)據(jù)庫都成功的概率就會下降到96%。
  • 成本。為了實現(xiàn)困難且耗時高的查詢,我可以選擇購買足夠高配置的數(shù)據(jù)庫、優(yōu)化數(shù)據(jù)庫的部分配置參數(shù)去抗住查詢數(shù)據(jù)的壓力,提升查詢成功率。同時在服務(wù)器中,通過代碼的一些優(yōu)化,并發(fā)多協(xié)程地去并行化四個查詢的動作。最后部署足夠多的機器,應(yīng)該也能使得整體的穩(wěn)定性提升,但是這樣需要非常高的設(shè)備成本,在“降本增效”的大背景下是無法承受的。這是一種用戰(zhàn)術(shù)勤奮去掩蓋戰(zhàn)略勤奮的做法。
  • 分頁挑戰(zhàn)。如果說通過內(nèi)存聚合還能勉強做到功能的可用。但是引入分頁后這個問題變得幾乎無解,因為在一個分布式系統(tǒng)中,要聚合第N頁的數(shù)據(jù)需要合并所有系統(tǒng)的前N頁數(shù)據(jù)才能計算得出,注意是計算前N頁不是第N頁,相當(dāng)于做一個多路歸并排序!也就是翻頁越深,查找量和計算量越大。而且我們這個場景更復(fù)雜,分頁后還需要剔除一部分刪除記錄,其挑戰(zhàn)如下圖所示:

(2) 查詢性能的挑戰(zhàn)

由于騰訊會議具有海量的C端請求,作為一級TAB的錄制面板,當(dāng)他全量的開放之后講具有很大的查詢量。這對查詢系統(tǒng)的QPS提出了很大的要求。根據(jù)現(xiàn)在峰值的會議列表的QPS來折算,最后目標(biāo)定在了錄制面板列表接口需要承接700qps的查詢的目標(biāo)。按照一頁30個錄制數(shù)據(jù)返回來看,這個目標(biāo)需要一秒返回21000個錄制的數(shù)據(jù),其并發(fā)挑戰(zhàn)是不小的。同時錄制面板作為一個核心功能,我們希望能達(dá)到面板的秒開,那么對于接口耗時也同樣有著要求。我們認(rèn)為,一個頁面要能秒開,接口耗時最多只能到500毫秒。

2. 全部文件的基本架構(gòu)設(shè)計

為了應(yīng)對以上兩個挑戰(zhàn),我們選擇了計算機領(lǐng)域中典型的以空間換時間的思路。我們需要一個獨立的數(shù)據(jù)源,能提前計算好用戶所需的文件,然后搜索的時候只搜索該數(shù)據(jù)源就能得到所篩選的已排序的列表數(shù)據(jù)。設(shè)計圖如下:

(1) 存儲選型的述求

為此,我們需要評估這個數(shù)據(jù)庫的量級以便選擇合適的數(shù)據(jù)庫。(部分?jǐn)?shù)據(jù)已模糊化處理)

錄制數(shù)據(jù)庫存量數(shù)據(jù)是約x000w;授權(quán)數(shù)據(jù)庫存量數(shù)據(jù)月x000w;瀏覽數(shù)據(jù)雖然一開始量級不大,但是隨著瀏覽留痕的上線將以極快的速度增長;刪除記錄表功能是新做的未來預(yù)計數(shù)據(jù)量也很少,估計在十萬級別。故合并數(shù)據(jù)庫的行數(shù)將達(dá)到一個很大的級別(x億)。每日新增的錄制數(shù)據(jù)約xw。按照一條數(shù)據(jù)被x人瀏覽/授權(quán)來初步計算,每天新增面板數(shù)據(jù)量將在x萬的數(shù)據(jù)。一年的增量x億附近,也就是說5年內(nèi),在業(yè)務(wù)體量沒有上升的前提下,數(shù)據(jù)量就將達(dá)到x億級別的量級。

對于數(shù)據(jù)庫要求就是三點:

  • 橫向擴(kuò)展能力好,能存儲x億級別的數(shù)據(jù)
  • 在x億級別的數(shù)據(jù)量下能保持寫入、查詢的穩(wěn)定
  • 查詢性能高,寫入性能較好
  • 成本低

(2) 存儲對比一覽

在此,我們考慮過幾個數(shù)據(jù)庫,考慮了存量量級、查詢性能、寫入性能、成本等因素下,大致對比因素如下表所示:

最終從業(yè)務(wù)適配、擴(kuò)展性和成本等多種維度的考慮,我們選擇了MongoDB作為最終的選型數(shù)據(jù)庫。

(3) 數(shù)據(jù)擴(kuò)展性設(shè)計

這里我們不討論具體的表設(shè)計、索引設(shè)計等的細(xì)節(jié)。撇開這些非常業(yè)務(wù)的設(shè)計外,在整體架構(gòu)的擴(kuò)展性、穩(wěn)定性上,也有很多的挑戰(zhàn)點。下面拋出來其中的比較共性的3點,以便讀者參考。

① 如何讓數(shù)據(jù)存儲是平衡的,也就是說盡可能能讓數(shù)據(jù)均勻的擴(kuò)散,而不是集中在極少數(shù)的分區(qū),即數(shù)據(jù)的分片要具有區(qū)分度。如果某個分片鍵值的數(shù)據(jù)特別多,導(dǎo)致數(shù)據(jù)聚集,就會導(dǎo)致該chunk塊性能下降,即避免jumbo chunk。這是非常容易導(dǎo)致的。假設(shè)我們用用戶的UID作為分區(qū)鍵,那一旦用戶的錄制如果非常多,它的數(shù)據(jù)多到足以占據(jù)分區(qū)一半以上的量,那么這個用戶數(shù)據(jù)所在的分區(qū)就一定會有數(shù)據(jù)傾斜。這是架構(gòu)師設(shè)計數(shù)據(jù)分片的時候特別需要避免的。

均勻分配的分區(qū)數(shù)據(jù)

部分分區(qū)存儲了過多的數(shù)據(jù),即數(shù)據(jù)傾斜。

② 數(shù)據(jù)避免單調(diào)性。即如何讓寫入數(shù)據(jù)的性能能盡可能橫向擴(kuò)展。因為每天新增的數(shù)據(jù)量極大,這意味著除了查詢性能,寫入性能的要求也很高,如果寫入一直是針對某個分片節(jié)點進(jìn)行寫入,這是有寫入單點瓶頸的,也就是所謂的數(shù)據(jù)的增長要規(guī)避單調(diào)性。

③ 如何讓熱點查詢命中分片鍵。這個很好理解,如果不命中分片鍵,再好的存儲也無法提供足夠的查詢性能。

解決思路:

最直觀的解法肯定是基于UID做數(shù)據(jù)分區(qū)的,因為從數(shù)據(jù)查詢的角度看,肯定是需要基于用戶uid查詢的,這意味著核心的查詢肯定能命中uid這個分片鍵。其次uid分片也具有不錯的區(qū)分度,大部分情況下數(shù)據(jù)是比較均衡的。其次uid背后的數(shù)據(jù)并不是單調(diào)遞增的,因為全局范圍內(nèi),所有的用戶都在產(chǎn)生數(shù)據(jù),也就是說寫入操作不會只在某個chunk。

但是uid分片有一個問題就是數(shù)據(jù)傾斜問題,例如有部分的用戶會特別活躍,導(dǎo)致某些用戶的留痕記錄會特別多。假設(shè)我們稱這些用戶叫大V用戶(類似微博的大V很多粉絲,他的粉絲數(shù)據(jù)也很容易造成傾斜)。如果使用uid分片,那么大V用戶的數(shù)據(jù)所處的chunk就可能是jumbo chunk。

為了解決這個問題,我們采取了uid+文件id作為聯(lián)合分片鍵,這樣可以實現(xiàn)大部分小用戶的數(shù)據(jù)只需要集中在同一個chunk種,而數(shù)據(jù)量過大的用戶則會劈開到多個chunk里,從而避免熱點的問題。最后三個挑戰(zhàn)均能解決。

3.數(shù)據(jù)一致性的挑戰(zhàn)

使用空間換時間的方法,最大的問題就是一致性。

一致性來源于兩個地方:

  • 數(shù)據(jù)的數(shù)量是否是一致的。這個很好理解,例如新增了一個錄制,全部文件的數(shù)據(jù)庫需要有這個錄制;剛剛授權(quán)了一個人,全部文件的數(shù)據(jù)庫也需要有這個錄制。同樣道理,刪除錄制后也需要在全部文件數(shù)據(jù)庫中刪除此記錄。
  • 數(shù)據(jù)字段是否是一致的。這個也好理解,因為數(shù)據(jù)本身是一直在發(fā)生變化的,例如錄制的狀態(tài)、權(quán)限、標(biāo)題、封面乃至安全審批狀態(tài),瀏覽/授權(quán)這個視頻的時間都會發(fā)生變化。

(1) 數(shù)據(jù)量一致性的解決方案

要解決一致性的問題,除了分布式事務(wù)這種很重的解決手段外,我們選擇了可靠消息+離線對賬混合處理的設(shè)計去解決。

① 消息可靠性消費

雖然我們有多個數(shù)據(jù)庫的數(shù)據(jù)源需要處理,但是好消息是一個錄制的生產(chǎn)和刪除從路徑上比較收斂的。所以我們可以在錄制的開始、錄制的刪除、權(quán)限點變更的時候,通過監(jiān)聽外部系統(tǒng)的Kafka事件來做數(shù)據(jù)的同步變更。然后通過Kafka 消息的at-least-once特性來保證消費的最終成功。對于消費多次都不成功的消息,依賴消息做可靠消費最難解決的問題有兩點:

  • 消息失敗導(dǎo)致消息丟失:雖然Kafka有at-least-once機制,如果一個消息一直消費失敗,是會阻塞該分區(qū)后面的消息消費的。所以我們實現(xiàn)了一套建議的死信通知的能力。在重試一定次數(shù)還是無法消費成功,我們會投遞這個消息到一個死信主題中,并且告警出來。一旦收到這個告警,就會人工介入去做數(shù)據(jù)的補償,保證數(shù)據(jù)最后能被人為干預(yù)修復(fù)成合適的狀態(tài)。
  • 消息冪等:Kafka在非常多的環(huán)節(jié)都可能導(dǎo)致消息重復(fù)。例如消費者重啟、消費者擴(kuò)容導(dǎo)致的Rebalance、Kafka的主備切換等等。為了保證數(shù)據(jù)不會被重復(fù)寫入,我們自研的一個消費的框架,通過封裝消費邏輯的流程,抽象出了一個冪等Key的概念由消費的代碼去實現(xiàn),當(dāng)發(fā)現(xiàn)相同的冪等Key已經(jīng)存在于Redis則認(rèn)為近期已經(jīng)消費過,直接跳過。

② 對賬

雖然,消息消費一側(cè)我們有把握能保證消息最終能落庫,但是消息生產(chǎn)側(cè)和一些一些產(chǎn)品邏輯的遺漏,是會可能導(dǎo)致數(shù)據(jù)是有丟失的。對于一個擁有大型的數(shù)據(jù)的系統(tǒng),我們必須對自己寫的代碼保持足夠的懷疑,無論是系統(tǒng)內(nèi)部還是外部系統(tǒng),沒有人能保證不寫出bug。

這時候就需要有人能發(fā)現(xiàn)這些數(shù)據(jù)的不一致,而不是等待用戶發(fā)現(xiàn)問題再投訴。為此,我們專門開發(fā)了一個復(fù)雜的對賬系統(tǒng),用來把每個情況的數(shù)據(jù)進(jìn)行增量、全量的對賬。這個過程中,我們也確實發(fā)現(xiàn)了不少問題,例如權(quán)限系統(tǒng)某些場景下會忘記發(fā)送Kafka事件、例如媒體中臺系統(tǒng)對于混合云的場景下會丟失webhook事件導(dǎo)致Kafka消息忘記發(fā)送,再例如產(chǎn)品的審批流設(shè)計中沒有很好的考慮錄制文件權(quán)限已經(jīng)改變的場景,導(dǎo)致文件數(shù)據(jù)依然能被審批通過的場景等。

(2) 數(shù)據(jù)字段一致性的解決方案

如果說數(shù)據(jù)量的一致性還算好解決的話,數(shù)據(jù)字段的一致性則異常復(fù)雜。從理論上說,兩者的解決思路是一樣的,即:在數(shù)據(jù)發(fā)生變更的時候,可靠地消費這個變更消息,然后更新全部文件的數(shù)據(jù)記錄。

然而字段一致性的困難在于,這種變化點極多,例如標(biāo)題會修改、安全的打擊會修改、權(quán)限值會修改、甚至還有云剪輯后時長都可能會修改。如果真的每個字段都要時刻保持一致,這里幾乎需要改動到原本云錄制系統(tǒng)、權(quán)限系統(tǒng)的方方面面,所有的更新時機都得對齊,而且還存在并發(fā)消費的問題導(dǎo)致字段準(zhǔn)確性堪憂,極容易出現(xiàn)各種個月的bug。

“如無必要,勿增實體”,這是我們常說的奧卡姆剃刀定律。我們在其中得到了一些靈感。

如果說新增了全部文件表是因為沒辦法解決多數(shù)據(jù)庫的分頁問題,多數(shù)據(jù)庫的聚合計算問題,是用空間換時間的必要性。那么把詳情字段也冗余存儲就是增加了實體的設(shè)計。

我們最終的設(shè)計決定不冗余可能發(fā)生變更的字段。最終其設(shè)計類似于一個數(shù)據(jù)庫的索引樹一般。我們的全部文件數(shù)據(jù)庫存儲的僅僅是多個數(shù)據(jù)源的索引,數(shù)據(jù)發(fā)生增減,索引自然需要維護(hù),但是數(shù)據(jù)表詳情字段的變更并不需要變更索引的。如下圖所示:

這樣設(shè)計后,整個業(yè)務(wù)系統(tǒng)的架構(gòu)變得簡潔起來。我們僅需要花大力氣去維護(hù)索引的有效性和查詢性能即可。而數(shù)據(jù)的詳情例如標(biāo)題、權(quán)限值、封面等都通過已有的批量接口獲取最真實的值即可,從而從根源上避免了維護(hù)十幾個字段的一致性問題。

最后,這套系統(tǒng)的設(shè)計如下:

(3) 第一版本性能效果

通過這套設(shè)計,我們僅使用了1個月左右的時間便上線了錄制面板的功能。雖然整體的架構(gòu)上具備不錯的擴(kuò)展性,這個版本的性能實際上是無法達(dá)到預(yù)期的,第一版本的壓測數(shù)據(jù)如下:

可以看到,當(dāng)查詢性能的qps達(dá)到200以上時,就出現(xiàn)了成功率的下降,再往后壓測成功率明顯下降,可見已到瓶頸。而且從耗時上看,也較高,接近一秒。

這個也是我們預(yù)期內(nèi)的。因為數(shù)據(jù)庫一次查詢并不能查詢回來所需的所有字段,背后字段的獲取的接口是有瓶頸的。例如一個用戶一頁數(shù)據(jù)是30個錄制視頻,那么除了第一部從MongoDB獲取到這30個視頻的索引外(從我們壓測時候看這一步非常的快,側(cè)面證明了MongoDB的性能以及我們分片、索引設(shè)計的合理性),還需要調(diào)用數(shù)個接口去獲取權(quán)限、詳情、安全狀態(tài)等。這里每次接口調(diào)用實際上是一個批量接口,如果壓測是200qps,那么實際上對于后端的數(shù)據(jù)查詢是200*30=6000/s的數(shù)據(jù)行搜索壓力?,F(xiàn)在無論權(quán)限庫還是云錄制庫,底層都是千萬級的存儲(存儲在MySQL中),這樣的查詢壓力無疑是很有壓力的。

為了應(yīng)對全量后更高的QPS壓力,優(yōu)化點在哪里呢?

優(yōu)化方向:一個數(shù)據(jù)源一個緩存

如果套用前面介紹過列表系統(tǒng)的優(yōu)化,我們這里的優(yōu)化可以算作是方案二的優(yōu)化。所以里面是有能力做數(shù)據(jù)源的緩存的。最后我們的優(yōu)化思路實際上很直接:如果我需要調(diào)用N個批量的接口,如果我能把N個接口都有緩存,是不是就可以大量降低后端的壓力,通過命中緩存來降低獲取數(shù)據(jù)的時間,從而大幅提升性能。

這個思路是可行的,但是挑戰(zhàn)也很大。原因在于這里每個接口實際上都是批量的接口。例如一個用戶A的首頁是id=1、id=2.....id=30的視頻的詳情記錄表,后端提供的批量接口的入?yún)⒕褪沁@30個id。然后返回30個數(shù)據(jù)回來。這時候如果我們要設(shè)計緩存,最直接的設(shè)計就是把id=1到id=30的拼成一個很長的key串,緩存這一批的結(jié)果,下次再查詢的時候就能命中了。

但是這樣設(shè)計有兩個問題:

  • 緩存利用效率很低。例如A用戶的首頁是id=1到id=30被我們緩存了,但是B用戶的首頁可能是id=1到id=29,這時候其實B用戶是無法利用之前緩存的視頻內(nèi)容的,哪怕B用戶看到的數(shù)據(jù)實際上是A用戶的子集。這樣會很大的增加緩存的存儲量,從而提升系統(tǒng)的成本
  • 緩存維護(hù)非常困難。例如id=3的視頻現(xiàn)在發(fā)生了標(biāo)題的變更,這時候我們應(yīng)該去通知A用戶和B用戶的緩存要刷新,否則他看到的就是變更前的臟數(shù)據(jù),但是對于A用戶它的緩存key可能是1234567....30,你現(xiàn)在拿著id=3這個信息是沒辦法找到這個key去更新的,除非我們還要額外維護(hù)一套key和id的關(guān)系。

為此,我們的解決方案是,面對批量查詢接口,我們也會只緩存每一個具體的數(shù)據(jù),一個數(shù)據(jù)一個key。然后把數(shù)據(jù)分為兩波,第一波是數(shù)據(jù)是否已經(jīng)在緩存的,則直接查詢。第二波是緩存查詢不到的,則走批量接口。這樣一來,任何一個數(shù)據(jù)只要被其中一個用戶的查詢行為載入了緩存,它都能被其他用戶復(fù)用。同時,任何一個數(shù)據(jù)的更新都及時地刷新緩存。

當(dāng)然因為我們需要調(diào)用的批量接口非常多,如果每個接口都需要做這樣的數(shù)據(jù)分拆邏輯,無疑是工作量巨大的。為此,我們星環(huán)后臺一組開發(fā)了一個FireCache組件,其中提供了類似的批量緩存能力,使得這一工作變得簡單。

性能優(yōu)化后的效果

壓測數(shù)據(jù):

  • 在這樣大量緩存化的方案之后,我們大量的提升錄制面板獲取全部文件的接口性能,在壓測達(dá)到目標(biāo)700qps時候,平均耗時僅96毫秒。
  • 在并發(fā)提升了3倍壓力的背景下,平均耗時還提升了10倍!可見優(yōu)化效果明顯。也能達(dá)到我們現(xiàn)網(wǎng)流量的壓力以及秒開的后臺性能要求。

現(xiàn)網(wǎng)數(shù)據(jù):現(xiàn)網(wǎng)雖然沒有壓測的峰值數(shù)據(jù),但是數(shù)據(jù)情況更為復(fù)雜,但是表現(xiàn)依舊非常理想。

五、小結(jié)

本文介紹了列表系統(tǒng)的性能設(shè)計遇到的一些挑戰(zhàn)點,以及在做緩存優(yōu)化的時候可以采取的三個解決方案。最后,我們還花了很大的篇幅介紹了騰訊會議錄制系統(tǒng)的實踐思路,希望能幫助大家舉一反三,綜合自己業(yè)務(wù)的特點做出更好的設(shè)計。總的來說,為了應(yīng)對錄制面板的高并發(fā)、海量數(shù)據(jù)的挑戰(zhàn),騰訊會議的錄制系統(tǒng)總共采取了以下的幾個手段,最終達(dá)成了較好的效果。

  • MongoDB的海量數(shù)據(jù)實踐設(shè)計
  • 對賬系統(tǒng)
  • 二級緩存設(shè)計
  • 可靠消息的處理
  • 緩存組件的開發(fā)
責(zé)任編輯:趙寧寧 來源: 騰訊技術(shù)工程
相關(guān)推薦

2024-11-20 19:56:36

2020-07-16 08:06:53

網(wǎng)關(guān)高性能

2020-01-17 11:00:23

流量系統(tǒng)架構(gòu)

2024-09-02 18:10:20

2020-08-17 08:18:51

Java

2021-06-30 14:23:30

AMD

2021-02-02 08:32:46

日志系統(tǒng) 高性能

2016-05-03 16:00:30

Web系統(tǒng)容錯性建設(shè)

2022-09-25 21:45:54

日志平臺

2022-08-15 08:01:35

微服務(wù)框架RPC

2025-01-06 00:00:10

2024-08-16 14:01:00

2022-05-12 14:34:14

京東數(shù)據(jù)

2021-05-24 09:28:41

軟件開發(fā) 技術(shù)

2014-03-19 14:34:06

JQuery高性能

2023-05-08 18:33:55

ES數(shù)據(jù)搜索

2011-04-22 16:23:16

ASP.NET動態(tài)應(yīng)用系統(tǒng)

2022-09-25 22:09:09

大數(shù)據(jù)量技術(shù)HDFS客戶端

2021-06-02 06:49:18

Redis緩存設(shè)計.

2021-03-02 07:54:18

流量網(wǎng)關(guān)設(shè)計
點贊
收藏

51CTO技術(shù)棧公眾號