精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

高性能億級錄制列表查詢系統(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ù)源可能也不一樣,接口自然也是需要單獨(dú)設(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ù)的時機(jī)就是緩存的內(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)一個機(jī)制讓命中緩存的直接讀緩存,讓沒有命中緩存的走數(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ī)的搜索條件獨(dú)立去搜,我們的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操作也勉強(qiáng)可以接受。但是大部分情況下我們做一個C端業(yè)務(wù)的話,這個性能是不能接受的!

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

更難以解決的問題還有一個?很可能朋友圈的內(nèi)容庫,關(guān)系鏈庫是來自于兩個獨(dú)立的數(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)定運(yù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ù)的獨(dú)立數(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ù)量都是千萬-億級別的,本身查詢的耗時客觀擺在這里。做了重度工作之后還需要做各種集合運(yùn)算,成本很高。最后出來的效果就是接口的耗時很高,反映到錄制面板體驗上就是用戶需要等待較高的時間才能看到數(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é)程地去并行化四個查詢的動作。最后部署足夠多的機(jī)器,應(yīng)該也能使得整體的穩(wěn)定性提升,但是這樣需要非常高的設(shè)備成本,在“降本增效”的大背景下是無法承受的。這是一種用戰(zhàn)術(shù)勤奮去掩蓋戰(zhàn)略勤奮的做法。
  • 分頁挑戰(zhàn)。如果說通過內(nèi)存聚合還能勉強(qiáng)做到功能的可用。但是引入分頁后這個問題變得幾乎無解,因為在一個分布式系統(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),我們選擇了計算機(jī)領(lǐng)域中典型的以空間換時間的思路。我們需要一個獨(dú)立的數(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è)計去解決。

① 消息可靠性消費(fèi)

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

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

② 對賬

雖然,消息消費(fèi)一側(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ā)生變更的時候,可靠地消費(fèi)這個變更消息,然后更新全部文件的數(shù)據(jù)記錄。

然而字段一致性的困難在于,這種變化點極多,例如標(biāo)題會修改、安全的打擊會修改、權(quán)限值會修改、甚至還有云剪輯后時長都可能會修改。如果真的每個字段都要時刻保持一致,這里幾乎需要改動到原本云錄制系統(tǒng)、權(quán)限系統(tǒng)的方方面面,所有的更新時機(jī)都得對齊,而且還存在并發(fā)消費(fèi)的問題導(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ù)行搜索壓力。現(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)

2025-08-26 04:00:00

2025-06-12 02:22:00

Netflix前端系統(tǒng)

2024-09-02 18:10:20

2020-08-17 08:18:51

Java

2021-06-30 14:23:30

AMD

2016-05-03 16:00:30

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

2021-02-02 08:32:46

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

2022-09-25 21:45:54

日志平臺

2024-08-16 14:01:00

2022-05-12 14:34:14

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

2022-08-15 08:01:35

微服務(wù)框架RPC

2025-01-06 00:00:10

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客戶端
點贊
收藏

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

精品视频在线观看网站| 国产黄a三级三级三级av在线看| 亚洲激情婷婷| 国产亚洲精品综合一区91| 一区二区三区国产免费| 中文字幕中文字幕在线十八区 | av网页在线观看| 国产精成人品2018| 一区二区成人在线| 欧美一进一出视频| 亚洲国产精品欧美久久| 久久国产毛片| 欧美成人精品影院| 在线观看日本中文字幕| 66精品视频在线观看| 欧美综合亚洲图片综合区| 男人天堂新网址| h网站在线免费观看| 成人av中文字幕| 成人精品在线视频| 在线观看日本网站| 欧美色综合网| 国内精品久久久久久久影视简单| 久草视频在线资源站| 18精品爽国产三级网站| av影院在线| 国产精品欧美一级免费| 国产福利久久精品| 又污又黄的网站| 国产欧美欧美| 欧美成人小视频| 东京热无码av男人的天堂| 菁菁伊人国产精品| 日韩亚洲欧美一区| 男生操女生视频在线观看| 蜜桃视频在线观看播放| 一区二区三区久久| 伊人久久99| 国产玉足榨精视频在线观看| 波多野结衣在线一区| 亚洲伊人久久大香线蕉av| 日韩精品久久久久久免费| 一区久久精品| 欧美刺激性大交免费视频| 亚洲一级理论片| 国产免费久久| 亚洲美女黄色片| 亚洲精品久久一区二区三区777| 国产a亚洲精品| 欧洲精品中文字幕| 日韩黄色片在线| 欧美精品videossex少妇| 综合欧美亚洲日本| 一级全黄肉体裸体全过程| 97电影在线看视频| 国产精品色一区二区三区| 日韩国产精品一区二区| 国产高清视频在线| 国产精品美女久久久久久久久 | 91精品国产成人观看| 在线视频欧美日韩| 国产wwwwxxxx| 色一区二区三区四区| 最近2019年日本中文免费字幕| 手机毛片在线观看| 日韩大片在线播放| 久久精品国产电影| 欧美性猛交xxxxx少妇| 欧美黄色大片网站| 欧美激情视频一区二区三区不卡| 久久影院一区二区| 亚洲国产91| 午夜精品国产精品大乳美女| 日本中文字幕第一页| 美女诱惑一区| 国产欧美精品在线播放| 97精品人妻一区二区三区在线| 国产精品主播直播| 国产日韩在线一区二区三区| 欧美女v视频| 国产精品欧美一区喷水| 黑人巨茎大战欧美白妇| 污视频在线看网站| 欧美性猛xxx| 亚洲天堂网一区| 国产精品久久久久久av公交车| 91精品国产综合久久精品| 天堂va欧美va亚洲va老司机| 噜噜噜天天躁狠狠躁夜夜精品| 国产亚洲精品久久久优势| 国产精品视频一区二区三| 亚洲经典三级| 国产精品日韩电影| 亚洲AV午夜精品| 26uuu久久天堂性欧美| 日韩欧美亚洲区| av网址在线看| 欧美日韩激情视频8区| 中文字幕第36页| 亚洲精品一区国产| 国产亚洲精品久久久久久777| 久久久久99精品成人片试看| 国产亚洲欧洲| 成人h片在线播放免费网站| 成人免费观看在线视频| 日本一区二区高清| 老子影院午夜伦不卡大全| 日韩精品第一| 欧美成va人片在线观看| 久久久久久国产免费a片| 欧美日本中文| 国产欧美一区二区三区在线| 日韩在线视频观看免费| 亚洲欧洲精品天堂一级| 少妇高潮喷水久久久久久久久久| 精品国产鲁一鲁****| 亚洲人成自拍网站| 久久精品久久国产| 国产综合久久久久影院| 欧美二区在线| 国产乱妇乱子在线播视频播放网站| 91福利小视频| 午夜一区二区三区免费| 欧美极品一区二区三区| 国产精品丝袜白浆摸在线| 人操人视频在线观看| 一卡二卡欧美日韩| 九九九九九国产| 精品国产一区探花在线观看| 国内偷自视频区视频综合 | 亚洲精品999| 国产盗摄一区二区三区在线| 免费在线看成人av| 秋霞久久久久久一区二区| 国模精品视频| 亚洲成人激情图| 精品爆乳一区二区三区无码av| 久久99蜜桃精品| 日韩精品成人一区二区在线观看| 手机在线观看av| 亚洲福利视频二区| 精品午夜福利在线观看| 国产精品一区不卡| 美女黄色片网站| 24小时成人在线视频| 日韩中文字幕在线播放| 在线观看中文字幕网站| 中文字幕欧美激情| 丰满少妇在线观看| 成人影院在线| 国产免费成人av| 97在线观看免费观看高清| 欧美日韩精品一区二区天天拍小说| 少妇精品无码一区二区免费视频 | 国产乱女淫av麻豆国产| 四季av一区二区凹凸精品| 国产精品狠色婷| 国产小视频在线| 91久久精品网| 人与动物性xxxx| 国产精品一级片| 少妇大叫太大太粗太爽了a片小说| 久久一级大片| 欧美激情一级精品国产| 欧美一区二区三区激情| 亚洲.国产.中文慕字在线| 日本少妇毛茸茸| 久久xxxx精品视频| 香蕉久久免费影视| 91麻豆精品| 欧美国产日韩免费| 深夜福利视频在线观看| 91精品福利视频| 性生交大片免费全黄| 国产揄拍国内精品对白| 欧美一级免费播放| 少妇精品久久久| 成人av电影天堂| 免费在线国产视频| 亚洲毛片在线看| 国产女人高潮毛片| 婷婷中文字幕一区三区| 欧美做受高潮6| 国精产品一区一区三区mba视频| 台湾无码一区二区| 免费视频国产一区| 成人免费福利视频| 九九色在线视频| 亚洲人在线观看| 国产成人精品无码高潮| 欧美性少妇18aaaa视频| 国产又色又爽又高潮免费| 高清av一区二区| 亚洲无吗一区二区三区| 18成人免费观看视频| 色女人综合av| 国语一区二区三区| 国产女精品视频网站免费| www.综合网.com| 日韩视频在线观看免费| 先锋av资源站| 制服丝袜一区二区三区| 国产精品男女视频| 亚洲人成影院在线观看| 爱爱免费小视频| 国产91在线观看| 亚洲天堂av线| 国产精品日韩欧美一区| 成人在线免费观看网址| 国产成人影院| 国产亚洲精品自在久久| 亚洲91在线| 国产成人精品电影久久久| 国产又色又爽又黄刺激在线视频| 视频直播国产精品| 日韩一区av| 亚洲第一精品夜夜躁人人躁| 国产免费久久久| 欧美性大战久久久久久久| 国产成人愉拍精品久久| 一区二区三区自拍| 亚洲精品自拍视频在线观看| 久久亚洲捆绑美女| 国产xxxxxxxxx| 丁香婷婷综合激情五月色| www.超碰97.com| 日本不卡123| 国产91对白刺激露脸在线观看| 欧美精品激情| 米仓穗香在线观看| 久久久久久免费视频| 亚洲精品第一区二区三区| 九九热线有精品视频99| 国产精品一区视频网站| 伊人精品综合| 成人区精品一区二区| 三级欧美日韩| 99久久精品无码一区二区毛片| 精品国产乱码一区二区三区 | 91成品人影院| 欧美人xxxx| 国产又粗又黄又爽的视频| 欧美午夜一区二区三区免费大片| 国产精品视频123| 欧美日韩精品在线播放| 国产又色又爽又黄的| 亚洲福利电影网| 日本一二三区不卡| 亚洲va韩国va欧美va| 日本熟女一区二区| 日韩欧美亚洲范冰冰与中字| 好看的av在线| 色综合久久综合网欧美综合网| 天天干天天干天天干天天| 欧美日韩中文字幕| 男人天堂av在线播放| 色婷婷久久综合| 一级黄色在线观看| 欧美在线看片a免费观看| 中文在线观看免费高清| 欧美日韩视频在线第一区| 亚洲香蕉在线视频| 在线综合视频播放| 精品久久在线观看| 亚洲国产精品成人av| 毛片免费在线播放| 中文字幕日韩欧美在线| 日本在线播放| 欧美大片第1页| 亚洲小少妇裸体bbw| 国产精品久久久久av| 小说区图片区亚洲| 产国精品偷在线| 五月国产精品| 亚洲一区影院| 国语自产精品视频在线看8查询8| 欧美日韩性生活片| 免费美女久久99| 天天干天天曰天天操| 成人av午夜影院| 韩国三级hd中文字幕| 亚洲三级视频在线观看| 精品一区在线视频| 91国偷自产一区二区使用方法| 一区二区三区黄色片| 日韩欧美国产一区二区在线播放| 深夜视频在线免费| 自拍偷拍亚洲欧美| 丁香花在线高清完整版视频| 日本老师69xxx| av日韩在线免费观看| 精品视频一区二区三区四区| 91九色精品| 女人喷潮完整视频| 激情久久久久久久久久久久久久久久| 久久精品aⅴ无码中文字字幕重口| 91蜜桃在线免费视频| 精品国产欧美日韩不卡在线观看| 精品久久久中文| 国产乱人乱偷精品视频a人人澡| 亚洲激情在线观看| 黄色av免费在线| 国产aaa精品| 国产精品99久久免费观看| 亚洲日本精品一区| 西西裸体人体做爰大胆久久久| 亚洲综合123| 国产拍揄自揄精品视频麻豆 | 91成人看片片| 人人妻人人澡人人爽精品日本 | 久久久久人妻一区精品色| 精品女同一区二区三区在线播放| 国产精品欧美激情在线| 亚洲天堂av综合网| 国产不卡人人| 666精品在线| 先锋资源久久| 亚洲老女人av| 久久久精品天堂| 永久免费看片在线播放| 欧美成va人片在线观看| 黄色成人影院| 成人免费网视频| 日韩欧美1区| 国产自偷自偷免费一区| 91色在线porny| 国产午夜精品无码| 日韩免费在线观看| 亚洲七七久久综合桃花剧情介绍| 国产欧美精品va在线观看| 精品久久成人| 欧美一级黄色影院| 久久女同性恋中文字幕| 亚洲黄色三级视频| 欧美精品一区二区三区蜜臀 | 国产999精品久久久| 天天躁日日躁狠狠躁欧美| 大西瓜av在线| 成人永久看片免费视频天堂| 九九在线观看视频| 欧美成人艳星乳罩| 成人超碰在线| 国产精品swag| 黄色一区二区三区四区| 久久精品aⅴ无码中文字字幕重口| 亚洲在线视频网站| 开心激情综合网| 国产+人+亚洲| 日韩av影院| 99蜜桃臀久久久欧美精品网站| 91一区一区三区| 欧美一区二区激情视频| 亚洲老司机av| 日韩不卡视频在线观看| 性欧美.com| 精品一区二区三区在线视频| 四虎884aa成人精品| 日韩精品一区二区三区四区| 精品精品导航| 国产亚洲欧美另类一区二区三区 | 一区二区视频在线| 欧美一级视频免费| 欧美在线国产精品| 精品久久久久久久久久久下田| 午夜宅男在线视频| 亚洲视频免费看| www.久久综合| 91国语精品自产拍在线观看性色 | 亚洲欧美日韩动漫| 国产成人鲁鲁免费视频a| 久久影视一区| 国产乱淫av麻豆国产免费| 精品高清一区二区三区| 久久经典视频| 成人性生交大片免费看视频直播 | 精品国产免费无码久久久| 久久免费国产视频| 欧美美女在线| 亚洲欧美一区二区三区不卡| 午夜不卡av免费| av电影在线观看网址| 亚洲xxxx做受欧美| 亚洲伊人观看| 国产精品嫩草影院俄罗斯 | 看女生喷水的网站在线观看| 成人永久免费| 日韩和欧美一区二区三区| 草视频在线观看| 日韩电影中文字幕在线观看| 国产原创一区| www.99热这里只有精品| 亚洲国产精品高清| 高清国产mv在线观看| 国产精品久久久久一区二区| 欧美区日韩区| 能直接看的av| 亚洲精品电影久久久| 99热这里有精品| 九色porny91| 五月天中文字幕一区二区|