成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

B站搜索建庫(kù)架構(gòu)優(yōu)化實(shí)踐

開(kāi)發(fā) 架構(gòu)
通過(guò)這一系列的技術(shù)更新和流程優(yōu)化,我們的索引構(gòu)建架構(gòu)最終從早期的單機(jī)構(gòu)建發(fā)展到分布式的數(shù)據(jù)存儲(chǔ)和建庫(kù)任務(wù),能力更強(qiáng)的同時(shí)也更易維護(hù)和迭代,索引構(gòu)建周期實(shí)現(xiàn)了從天級(jí)到小時(shí)級(jí)別的飛躍式進(jìn)步,為業(yè)務(wù)的未來(lái)發(fā)展奠定了堅(jiān)實(shí)基礎(chǔ)。

前言

搜索是B站的重要基礎(chǔ)功能,需要對(duì)包括視頻、評(píng)論、圖文等海量的站內(nèi)優(yōu)質(zhì)資源建立索引,處理來(lái)自用戶每日數(shù)億的檢索請(qǐng)求。離線索引數(shù)據(jù)的正確、高效產(chǎn)出是搜索業(yè)務(wù)的基礎(chǔ)。我們?cè)谶@里分享搜索離線架構(gòu)整體的改造實(shí)踐:從周期長(zhǎng),流程復(fù)雜的手工構(gòu)建流程,改造為高容量、高性能、易迭代的分布式建庫(kù)架構(gòu)的過(guò)程。

業(yè)務(wù)背景

B站是一個(gè)典型的多資源搜索場(chǎng)景,除了視頻外,還接入了包括UP主、番劇影視(PGC)、直播等幾十種不同類型的資源。除了資源類型多以外,各種資源的數(shù)據(jù)源的形式也多種多樣,包括數(shù)據(jù)庫(kù)、上游業(yè)務(wù)接口、Hive表等等。這些數(shù)據(jù)通過(guò)離線近線的聚合和構(gòu)建,以全量和增量實(shí)時(shí)流兩種方式生產(chǎn)出索引,在線上的服務(wù)中生效。

圖片圖片

實(shí)際業(yè)務(wù)中,除了搜索業(yè)務(wù)自己維護(hù)的視頻MySQL數(shù)據(jù)庫(kù)外,還接入了不同形式的數(shù)據(jù)來(lái)源,一并添加到全量/增量索引數(shù)據(jù)中構(gòu)建。這些數(shù)據(jù)有的僅T+1更新全量;有的則會(huì)提供增量數(shù)據(jù)流和接口,接入時(shí)也希望數(shù)據(jù)變更能在搜索索引中實(shí)時(shí)生效。

全量(base)索引:以文件形式提供,包含某一特定時(shí)間點(diǎn)之前的全部數(shù)據(jù),通常每天產(chǎn)出、更新一次。

增量(delta)索引:以數(shù)據(jù)流(消息隊(duì)列)的形式提供,每條消息對(duì)應(yīng)一份稿件的完整信息。增量索引能實(shí)時(shí)同步更新,適用于時(shí)效性要求高的場(chǎng)景,滿足用戶對(duì)新稿件的檢索需求。

這些第三方數(shù)據(jù)由于各種原因(MySQL容量或性能限制,數(shù)據(jù)維護(hù)團(tuán)隊(duì)不同等),不能直接集成到搜索的MySQL數(shù)據(jù)庫(kù)中。這些數(shù)據(jù)的引入在當(dāng)前的離線和近線建庫(kù)流程中是獨(dú)立的,隨著數(shù)據(jù)的種類增加,搜索離線建庫(kù)流程也逐漸復(fù)雜起來(lái)。

索引數(shù)據(jù)產(chǎn)出流程

以視頻索引為例,構(gòu)建視頻索引時(shí)除了搜索自己維護(hù)的MySQL中視頻信息外,還需要接入多種第三方數(shù)據(jù)到索引中,如接入以下三類數(shù)據(jù):

  • 數(shù)據(jù)A:第三方數(shù)據(jù)庫(kù)+binlog
  • 數(shù)據(jù)B:不開(kāi)放DB直接訪問(wèn),以導(dǎo)出的全量snapshot+接口+數(shù)據(jù)流形式間接提供
  • 數(shù)據(jù)C:Hive表

全量數(shù)據(jù)產(chǎn)出

為了獲取稿件信息,全量建庫(kù)流程需要先后將這些不同的數(shù)據(jù)源獲取到本地,合并為新的全量數(shù)據(jù)集,再構(gòu)建出二進(jìn)制全量索引。

圖片圖片

增量數(shù)據(jù)產(chǎn)出

對(duì)于增量索引,我們希望將每條稿件的完整字段(即包含搜索MySQL+數(shù)據(jù)源A/B/C的所有字段信息)聚合為一條增量索引消息下發(fā)。

我們監(jiān)聽(tīng)binlog A和數(shù)據(jù)流B,但只關(guān)心其中有變化的稿件id,通過(guò)變更搜索MySQL中相應(yīng)稿件的一個(gè)標(biāo)志位,觸發(fā)該稿件的一條binlog及后續(xù)的增量建庫(kù)流程。

在后續(xù)的建庫(kù)流程中,處理binlog時(shí)增加根據(jù)ID查詢MySQL A和接口B的邏輯,這樣就能從MySQL A的查詢結(jié)果,接口B的響應(yīng)和本地的文件C中拿到稿件的對(duì)應(yīng)數(shù)據(jù),寫入到增量索引數(shù)據(jù)流中。

圖片圖片

注意這里對(duì)MySQL A和接口B有著嚴(yán)格的時(shí)效性要求,即查詢得到的數(shù)據(jù)不能比數(shù)據(jù)流下發(fā)的數(shù)據(jù)版本更舊,否則無(wú)法保證數(shù)據(jù)的最終一致性。

舉個(gè)例子,假如MySQL A的binlog來(lái)自主庫(kù),而按ID查詢時(shí)訪問(wèn)了MySQL A的從庫(kù)。

當(dāng)A中某個(gè)稿件的字段發(fā)生變化時(shí),在處理主庫(kù)的對(duì)應(yīng)binlog時(shí)會(huì)從從庫(kù)中查詢當(dāng)前值。由于主庫(kù)從庫(kù)之間的同步有一定延遲,這時(shí)有可能查詢到舊值而不是主庫(kù)中的新值,導(dǎo)致新值不會(huì)在索引中生效,除非該稿件有其他變更再次觸發(fā)binlog處理。

將MySQL A的binlog調(diào)整為由對(duì)應(yīng)從庫(kù)導(dǎo)出則可避免該問(wèn)題發(fā)生。

索引生效流程

B站的檢索引擎和正排服務(wù)加載索引數(shù)據(jù)的過(guò)程如下:

  1. 更新全量索引:服務(wù)周期性地獲取新的全量索引文件,解析元數(shù)據(jù),將索引加載到內(nèi)存中。
  2. 消費(fèi)增量數(shù)據(jù)流:加載了全量索引后,服務(wù)會(huì)從元數(shù)據(jù)指定的時(shí)間點(diǎn)開(kāi)始消費(fèi)增量索引數(shù)據(jù)流,直到消費(fèi)進(jìn)度接近當(dāng)前的最新產(chǎn)出。消費(fèi)到的數(shù)據(jù)會(huì)在服務(wù)側(cè)實(shí)時(shí)地建立增量索引,以支持查詢。當(dāng)一份稿件同時(shí)出現(xiàn)在增量索引和全量索引時(shí),全量索引中的數(shù)據(jù)被覆蓋;稿件在數(shù)據(jù)流中出現(xiàn)多次時(shí),增量索引中的舊值也會(huì)被新值覆蓋。
  3. 提供服務(wù):當(dāng)增量數(shù)據(jù)流的產(chǎn)出進(jìn)度被消費(fèi)追平后,服務(wù)進(jìn)入就緒狀態(tài),開(kāi)始處理請(qǐng)求。

問(wèn)題與挑戰(zhàn)

隨著搜索業(yè)務(wù)復(fù)雜度的增加和數(shù)據(jù)規(guī)模的增長(zhǎng),現(xiàn)有建庫(kù)邏輯在效率和資源層面逐漸難以為繼。

性能

新投稿的增多和歷史投稿的積累,讓搜索MySQL在建庫(kù)過(guò)程中的負(fù)載越來(lái)越大,開(kāi)始出現(xiàn)性能瓶頸。

造成MySQL高負(fù)載的主要場(chǎng)景有:

1. 表結(jié)構(gòu)變更:需要復(fù)制全表數(shù)據(jù)。數(shù)據(jù)復(fù)制期間,數(shù)據(jù)庫(kù)負(fù)載顯著增加。

2. 新增字段并批量導(dǎo)入:將新字段導(dǎo)入數(shù)據(jù)庫(kù)時(shí)需要大量寫入,增加數(shù)據(jù)庫(kù)壓力。

  • 緩解措施:控制寫入過(guò)程,在負(fù)載低峰期小批量執(zhí)行。

3. 全量索引構(gòu)建時(shí)的掃庫(kù)操作:索引構(gòu)建流程每次需要先查詢搜索MySQL數(shù)據(jù)庫(kù)獲取全量稿件信息,dump到文件中,再進(jìn)行索引構(gòu)建。當(dāng)在基線外另行構(gòu)建索引以支持AB實(shí)驗(yàn)時(shí),數(shù)據(jù)庫(kù)的查詢負(fù)載也會(huì)相應(yīng)地倍增。

  • 緩解措施:錯(cuò)開(kāi)不同構(gòu)建任務(wù)的執(zhí)行時(shí)間;將掃庫(kù)結(jié)果和元數(shù)據(jù)保存下來(lái),供多個(gè)建庫(kù)任務(wù)復(fù)用。

當(dāng)MySQL負(fù)載高時(shí),主從數(shù)據(jù)庫(kù)同步會(huì)出現(xiàn)延遲,導(dǎo)致索引數(shù)據(jù)更新不及時(shí)。用戶不能及時(shí)看到最新的投稿和變更,體驗(yàn)受損害。

我們?cè)谌粘?shí)驗(yàn)和迭代時(shí),都必須時(shí)刻考慮對(duì)數(shù)據(jù)庫(kù)的性能影響,而緩解數(shù)據(jù)庫(kù)壓力的措施往往導(dǎo)致迭代周期變長(zhǎng),不同的需求上的索引迭代也不能并行推進(jìn)。性能瓶頸和穩(wěn)定性風(fēng)險(xiǎn)成為了索引迭代的主要障礙。

維護(hù)成本

  1. 搜索索引原始數(shù)據(jù)沒(méi)有統(tǒng)一的存儲(chǔ)承載,數(shù)據(jù)可能來(lái)自多個(gè)數(shù)據(jù)庫(kù)、文件、甚至接口。離線和近線需要各自維護(hù)復(fù)雜的拼接邏輯,導(dǎo)致迭代困難,開(kāi)發(fā)周期長(zhǎng)。數(shù)據(jù)不符合預(yù)期時(shí),難以定位原因。
  2. 全量數(shù)據(jù)和增量數(shù)據(jù)的產(chǎn)出邏輯差異大,沒(méi)有機(jī)制能保證兩套鏈路上最終的產(chǎn)出結(jié)果一致。數(shù)據(jù)的不一致可能影響業(yè)務(wù)效果。
  3. 全量索引是單機(jī)構(gòu)建,構(gòu)建周期,實(shí)例部署和數(shù)據(jù)分片都需要人工維護(hù),每次迭代都消耗大量人力成本。
  4. 增量數(shù)據(jù)接入新的實(shí)時(shí)數(shù)據(jù)時(shí),數(shù)據(jù)提供方需要同時(shí)提供全量、增量數(shù)據(jù)和查詢接口。搜索業(yè)務(wù)和數(shù)據(jù)提供方的對(duì)接、開(kāi)發(fā)成本都較高。

資源

每次構(gòu)建索引時(shí),都需要對(duì)原始數(shù)據(jù)重新切詞分析,構(gòu)建正排、倒排索引。即使數(shù)據(jù)和策略沒(méi)有變化也需要重復(fù)計(jì)算。這些計(jì)算會(huì)消耗大量資源,并且增加索引構(gòu)建所需的時(shí)間。

設(shè)計(jì)目標(biāo)

反思索引構(gòu)建流程中的問(wèn)題,不管是復(fù)雜的多數(shù)據(jù)源合并流程、還是MySQL的性能壓力延遲風(fēng)險(xiǎn),歸根結(jié)底都是存儲(chǔ)設(shè)施能力不足,不能直接承載全部索引數(shù)據(jù)導(dǎo)致的問(wèn)題復(fù)雜化。我們期望將索引依賴的全部數(shù)據(jù)聚合到統(tǒng)一的存儲(chǔ)設(shè)施中,作為基準(zhǔn)數(shù)據(jù)源,直接基于該數(shù)據(jù)源導(dǎo)出全量數(shù)據(jù)和增量數(shù)據(jù)流,并將后續(xù)的建庫(kù)任務(wù)徹底分布式化,達(dá)到數(shù)據(jù)統(tǒng)一、可擴(kuò)展性大幅增強(qiáng)的目的。

預(yù)期新的架構(gòu)設(shè)計(jì)可解決當(dāng)前的痛點(diǎn):

  1. 用分布式的存儲(chǔ)設(shè)施承接搜索數(shù)據(jù),后續(xù)的構(gòu)建任務(wù)也進(jìn)行分布式化改造,讓建庫(kù)鏈路中不再有單點(diǎn)的性能瓶頸。
  2. 全量和增量都從統(tǒng)一的存儲(chǔ)設(shè)施中獲取,完全屏蔽原有不同數(shù)據(jù)來(lái)源對(duì)建庫(kù)過(guò)程的影響,可以讓建庫(kù)的流程簡(jiǎn)單化,降低開(kāi)發(fā)維護(hù)的成本。
  3. 有了足夠的存儲(chǔ)能力之后,我們可以將稿件切詞這類較為穩(wěn)定的中間計(jì)算結(jié)果保存下來(lái),只在有數(shù)據(jù)或策略有變化時(shí)重新計(jì)算。節(jié)省計(jì)算資源的同時(shí)進(jìn)一步縮短全量構(gòu)建周期。

方案設(shè)計(jì)

既然MySQL不能滿足我們的需求,我們的人力也不允許從零開(kāi)始造一個(gè)輪子來(lái)維護(hù)索引數(shù)據(jù),首要的問(wèn)題是找到一個(gè)合適的存儲(chǔ)設(shè)施來(lái)作為基礎(chǔ),在其之上開(kāi)發(fā)數(shù)據(jù)處理和建庫(kù)邏輯。

存儲(chǔ)選型

我們希望新的存儲(chǔ)方案可以具有以下特性:

  • 高容量:可以存入目前全部的索引數(shù)據(jù)。易于水平拓展。
  • 高吞吐量:允許大批量的數(shù)據(jù)寫入和導(dǎo)出。
  • 低延遲:可以快速隨機(jī)讀寫單個(gè)稿件數(shù)據(jù)。
  • 易于迭代:數(shù)據(jù)的結(jié)構(gòu)可以靈活迭代,無(wú)需Online DDL操作(https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html)。

我們從現(xiàn)有的數(shù)據(jù)庫(kù)系統(tǒng)中尋求合適的選型:對(duì)于數(shù)據(jù)靈活迭代的需求排除了MySQL,TiDB等關(guān)系型數(shù)據(jù)庫(kù);批量更新稿件部分字段的需求對(duì)文檔存儲(chǔ)性的NoSQL數(shù)據(jù)庫(kù)不友好;HBase,Cassandra等列族存儲(chǔ)的數(shù)據(jù)模型比較合適,但簡(jiǎn)單寫入查詢延遲較高;KV(鍵值)存儲(chǔ)延遲低,但數(shù)據(jù)模型不太匹配。

最終我們選擇在KV存儲(chǔ)的基礎(chǔ)上封裝行式(Row-Oriented)數(shù)據(jù)庫(kù)的形式,以達(dá)到兼顧吞吐與延遲的效果,并選擇了b站內(nèi)部存儲(chǔ)組件Taishan作為底層KV存儲(chǔ)。

Taishan是B站內(nèi)部的高性能分布式KV存儲(chǔ),具備多分片水平拓展能力,可應(yīng)對(duì)大規(guī)模存儲(chǔ)需求,簡(jiǎn)單讀寫的延遲也較低,此外還具有以下特性:

  • 有序映射:key是有序的,支持scan(范圍查詢)。
  • 支持 CompareAndSwap 操作:基于CAS原子操作可實(shí)現(xiàn)樂(lè)觀鎖,在并發(fā)的數(shù)據(jù)更新時(shí)防止沖突。
  • 高效的數(shù)據(jù)導(dǎo)出:Taishan 支持快速將數(shù)據(jù)全量導(dǎo)出到對(duì)象存儲(chǔ)中,而不需要全表scan。

架構(gòu)設(shè)計(jì)

我們以Taishan為基礎(chǔ)存儲(chǔ)設(shè)施,在其上建立了強(qiáng)大的數(shù)據(jù)存儲(chǔ)層(基于表格存儲(chǔ)模型)和統(tǒng)一的數(shù)據(jù)接入和導(dǎo)出層。

圖片圖片

引入存儲(chǔ)層后,我們將原始數(shù)據(jù)源跟具體的建庫(kù)邏輯隔離,并抽象出可高度復(fù)用的數(shù)據(jù)導(dǎo)入導(dǎo)出邏輯,顯著降低了維護(hù)成本和后續(xù)開(kāi)發(fā)成本。

離線和近線使用相同的數(shù)據(jù)來(lái)源并復(fù)用導(dǎo)出邏輯,從根本上消除了全量索引和增量索引數(shù)據(jù)不一致的可能。

高容量的存儲(chǔ)層允許我們以增量計(jì)算形式來(lái)源空間換取時(shí)間:將一些中間計(jì)算結(jié)果(如切詞和Embedding等)也一并保存,僅在相關(guān)稿件屬性有變化時(shí)觸發(fā)計(jì)算并更新;建庫(kù)時(shí)直接使用保存下來(lái)的結(jié)果,計(jì)算量大幅減少。

在全部數(shù)據(jù)都經(jīng)Taishan聚合后,離線近線的流程變得清晰直觀起來(lái),同時(shí)在根本上消除了全量和增量數(shù)據(jù)不一致的可能性。增量計(jì)算的引入也減少了大量重復(fù)的計(jì)算量,節(jié)省了資源。新的數(shù)據(jù)流程使得迭代和維護(hù)都大為簡(jiǎn)化。

數(shù)據(jù)存儲(chǔ)層

表格模型

基于KV存儲(chǔ)之上封裝出表格存儲(chǔ)模型:

  • 行:每一行代表一個(gè)稿件,通過(guò)稿件ID標(biāo)識(shí)。
  • 列:每一列存儲(chǔ)稿件的若干相關(guān)字段,通過(guò)字段族名(CF,column family)標(biāo)識(shí)。
  • 單元格:行和列的組合確定一個(gè)單元格,對(duì)應(yīng)KV存儲(chǔ)中的一條記錄。

圖片圖片

如上表所示,稿件ID和行一一對(duì)應(yīng)。而稿件的具體字段和列(CF)是多對(duì)多的關(guān)系:

  • 同一稿件的若干相關(guān)字段可以打包保存在一列(如title和uname),這樣會(huì)大幅減少KV的訪問(wèn)次數(shù),提高效率。
  • 表格中不同的列可以保存相同的字段。例如:
  • 這里eb,eb1兩列分別對(duì)應(yīng)doc_embedding字段的不同版本。指定不同的列組合(fs,seg,eb)/(fs,seg,eb1),可以構(gòu)建出兩版包含相同(title,uname,title_term,uname_term),以及不同版本doc_embedding的索引數(shù)據(jù)。

支持并發(fā)寫入

Taishan的CAS支持允許我們?cè)诓煌瑢懭敕街g通過(guò)樂(lè)觀鎖來(lái)同步,避免多個(gè)寫入方同時(shí)更新一個(gè)單元格時(shí)發(fā)生寫入丟失。

如果一列的寫入方唯一,也可以不使用CAS直接寫入。

支持并發(fā)寫入消除了將多個(gè)字段放進(jìn)同一CF的后續(xù)維護(hù)風(fēng)險(xiǎn)。即使同一列后續(xù)要增加新的寫入方,也無(wú)需對(duì)該列進(jìn)行拆分改造。

Key設(shè)計(jì)

Key由ID和字段族名組成。下圖是稿件ID1234567890的seg列對(duì)應(yīng)的實(shí)際Key內(nèi)容。ID和列名間用 “:”來(lái)分隔。

圖片

Data Orientation

Key的設(shè)計(jì)決定了數(shù)據(jù)排列方式。底層Taishan存儲(chǔ)中,數(shù)據(jù)按Key的字符串順序連續(xù)排列。

ID1:CF1

ID1:CF2

ID2:CF1

ID2:CF2

我們將稿件ID放在列名稱前,這樣表格數(shù)據(jù)在Taishan中實(shí)際存放形式如下:

ID1:CF1

ID1:CF2

ID2:CF1

ID2:CF2

實(shí)際使用場(chǎng)景中我們主要按行掃描(獲取相同ID的所有字段值),較少按列掃描(獲取某一字段下所有取值)。同一ID下所有列連續(xù)分布的排列使得按ID(行)讀取時(shí)有更好的訪問(wèn)局部性和Cache命中率。

同時(shí),ID使用大端序int64保存。大端序的特點(diǎn)是作為字符串看待時(shí)的順序和數(shù)字的升序一致,讓遍歷過(guò)程總是按ID升序執(zhí)行,符合人的直覺(jué)與習(xí)慣。

Value設(shè)計(jì)

Value總是以一個(gè)varint N作為header,該varint標(biāo)識(shí)隨后的N個(gè)字節(jié)保存的是元數(shù)據(jù),其余的字節(jié)則用于存儲(chǔ)實(shí)際數(shù)據(jù)。

下圖是一個(gè)包含header、元數(shù)據(jù)(大小為8字節(jié))和數(shù)據(jù)(大小為5字節(jié))的value。

圖片圖片

通過(guò)這種設(shè)計(jì),我們得到了一個(gè)高效、靈活且近似于傳統(tǒng)表格存儲(chǔ)的系統(tǒng),可以為索引建庫(kù)提供強(qiáng)大的支持。

序列化

拋棄JSON

在最初的建庫(kù)流程中,我們使用JSON Lines文件格式保存原始的稿件數(shù)據(jù),例如:video.jsonl

{"id": 81403056, "title": "高燃舞臺(tái)演繹B站最美的夜", "uname": "嗶哩嗶哩晚會(huì)", "doc_embedding": [0.168322, 0.015824, 0.091791, -0.2059]}
{"id": 613621262, "title": "【觸手猴】「強(qiáng)風(fēng)オールバック」を弾いてみた【Piano】", "uname": "marasy_觸手猴", "doc_embedding": [0.007262, 0.040466, 0.028768, 0.161083]}

JSON格式具有良好的可讀性,但效率和性能不足,主要體現(xiàn)在:

  • 存儲(chǔ)效率低:每條記錄中都會(huì)重復(fù)保存字段名及符號(hào)(如 {} 和 ""),浪費(fèi)大量存儲(chǔ)空間。
  • 序列化性能低:文本格式的解析性能較差。

如果將完整的稿件字段拆分到多個(gè)列中分別保存,導(dǎo)出和查詢時(shí)的序列化消耗會(huì)更加嚴(yán)重。

轉(zhuǎn)向protobuf

為了解決上述問(wèn)題,我們選擇了Protocol Buffers(protobuf)格式來(lái)序列化稿件字段。以下是簡(jiǎn)化的Video消息的protobuf定義:video.proto

message Video {
    int64 id = 1;
    string title = 2;
    string uname = 3;
    repeated float doc_embedding = 4;
}

protobuf的優(yōu)勢(shì)主要有:

  • 存儲(chǔ)空間優(yōu)化:protobuf使用field number(以varint形式存儲(chǔ))來(lái)區(qū)分字段,相比于JSON中存儲(chǔ)完整字段名,大大節(jié)省了存儲(chǔ)空間。
  • 性能提升:protobuf的反序列化速度優(yōu)于JSON。
  • 類型安全:和JSON相比,protobuf為數(shù)據(jù)字段提供了強(qiáng)類型保證,增強(qiáng)了我們對(duì)數(shù)據(jù)的信心。嚴(yán)格的數(shù)據(jù)類型也從源頭上消除了JSON 固有的最大安全整數(shù)(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)問(wèn)題。

高效地合并

對(duì)于protobuf消息,多個(gè)序列化后的數(shù)據(jù)塊(buffer)可以直接拼接來(lái)達(dá)到合并的效果:

Video v;
v.ParseFromString(buf1 + buf2);

這與將其分布反序列化后再執(zhí)行合并是等效的:

Video v1, v2;
v1.ParseFromString(buf1);
v2.ParseFromString(buf2);
v1.MergeFrom(v2);

假設(shè)稿件的標(biāo)題保存在列1,稿件Embedding信息保存在列2,buf1/buf2分別是對(duì)應(yīng)單元格查詢的數(shù)據(jù)。我們只需要直接concate未經(jīng)反序列化的兩段buffer即可拼接出稿件完整數(shù)據(jù)。

在使用Cord(https://protobuf.dev/reference/cpp/cpp-generated/#cord)或zero_copy_stream(https://protobuf.dev/reference/cpp/api-docs/google.protobuf.io.zero_copy_stream/)的情況下,連這一次拼接也可以省去。

變更數(shù)據(jù)流

Taishan原生支持binlog導(dǎo)出,可以將變更的Key和Value導(dǎo)出到數(shù)據(jù)流。但直接使用Taishan的binlog會(huì)有以下問(wèn)題:

  • 大批量寫入數(shù)據(jù)時(shí),會(huì)產(chǎn)出大量消息,對(duì)后續(xù)的整個(gè)近線鏈路乃至線上服務(wù)造成壓力。
  • Value只包含變更的列,獲取完整的數(shù)據(jù)仍需按ID掃描Taishan表。

為了靈活控制變處理,我們封裝了寫入層,在數(shù)據(jù)寫入Taishan完成后另外輸出一條變更消息,示例如下:

{"id": 613621262, "cf_changed": ["eb"]} // 稿件av613621262的eb列發(fā)生了變更

數(shù)據(jù)變更的場(chǎng)合是否輸出對(duì)應(yīng)變更消息到流中是可指定的,批量寫入數(shù)據(jù)時(shí)我們選擇不觸發(fā)。

我們也省略了變更后的值,需要的消費(fèi)者可以直接查詢Taishan表的主節(jié)點(diǎn)獲取最新的字段。

查詢從節(jié)點(diǎn)可能得到更新前的舊值,破環(huán)數(shù)據(jù)的最終一致性。

數(shù)據(jù)接入層

存儲(chǔ)方案和序列化方式的確定后,我們開(kāi)始將現(xiàn)有的數(shù)據(jù)統(tǒng)一接入到Taishan中。具體而言是將原有的數(shù)據(jù)庫(kù)全量增量以及T+1更新的數(shù)據(jù)全部寫入上文所說(shuō)的Taishan表格,并按需同步到變更數(shù)據(jù)流。

T+1數(shù)據(jù)接入

T+1更新的數(shù)據(jù)沒(méi)有實(shí)時(shí)的增量更新,只需要定時(shí)寫入全量。

寫入的邏輯是高度復(fù)用的,同過(guò)指定配置將hive表/TSV/CSV/JSON Lines文件映射到Taishan中的對(duì)應(yīng)列。

圖片圖片

從新版全量中被刪除的數(shù)據(jù)需要額外的處理:我們需要跟上一版數(shù)據(jù)對(duì)比,找出這些被刪除的數(shù)據(jù),將這些數(shù)據(jù)同步從Taishan中刪除。

實(shí)時(shí)數(shù)據(jù)接入

實(shí)時(shí)數(shù)據(jù)接入需要將數(shù)據(jù)實(shí)時(shí)寫入Taishan,并同步到變更數(shù)據(jù)流。寫入的順序必須為先寫Taishan再寫變更數(shù)據(jù)流,保證后續(xù)處理變更流時(shí)能從Taishan主節(jié)點(diǎn)查到最新取值。

圖片圖片

由于我們不能對(duì)上游提供的變更數(shù)據(jù)流的字段定義做任何的限制和假設(shè),處理增量的Worker需要開(kāi)發(fā)少量的解析邏輯。

增量寫入只寫入了最近有變更的數(shù)據(jù),為了讓Taishan保存全部的歷史數(shù)據(jù),在實(shí)時(shí)數(shù)量接入后,我們還需要再獲取一份全量數(shù)據(jù)(產(chǎn)出時(shí)間在增量接入后),將這些數(shù)據(jù)也寫入Taishan中。

這的全量寫入過(guò)程和T+1數(shù)據(jù)接入類似,區(qū)別在于只需要初始化時(shí)執(zhí)行一次,后續(xù)的變更都可以通過(guò)增量來(lái)獲取。另外T+1全量寫入時(shí)需要考慮和增量寫入沖突的情況,具體在后文介紹。

寫入沖突處理

全量vs增量

包含實(shí)時(shí)數(shù)據(jù)的數(shù)據(jù)在導(dǎo)入時(shí)往往也需要刷入一份全量數(shù)據(jù)做初始化。

一般來(lái)說(shuō),全量中的數(shù)據(jù)會(huì)比來(lái)自數(shù)據(jù)流的舊,直接寫入會(huì)將來(lái)自數(shù)據(jù)流的新值覆蓋。

因此寫入時(shí)需要利用CAS,僅當(dāng)滿足Precondition:值不存在時(shí),才會(huì)寫入。

增量vs增量

同一列可以有多個(gè)寫入方,比如兩個(gè)worker消費(fèi)兩條不同的數(shù)據(jù)流寫入同一列中的兩個(gè)不同字段。此時(shí)需要先讀取稿件的該列的舊值,更新其中的新字段后,將完整的新值寫入。

當(dāng)兩個(gè)worker同時(shí)運(yùn)行時(shí),可能發(fā)生寫寫沖突(Write-Write Conflict),導(dǎo)致一個(gè)worker寫入的新值,被另外的worker覆蓋而丟失。

在這種場(chǎng)景下,我們同樣使用CAS,在計(jì)算出新值后,僅當(dāng)滿足Precondition:值==舊值時(shí),才會(huì)寫入新值,當(dāng)Precondition不滿足時(shí),必須重試。

增量計(jì)算

我們將一些中間計(jì)算結(jié)果也保存在Taishan中。典型的場(chǎng)景如稿件標(biāo)題的切詞和向量化的結(jié)果。具體而言,如果使用的計(jì)算策略和稿件標(biāo)題沒(méi)有變化,切詞和向量化的結(jié)果可以認(rèn)為是穩(wěn)定的。因此我們可以保存這些結(jié)果來(lái)避免重復(fù)計(jì)算,只在稿件的屬性有變化時(shí)更新計(jì)算結(jié)果。

增量更新切詞結(jié)果的工作流如下圖所示:

圖片圖片

和實(shí)時(shí)數(shù)據(jù)接入類似,在首次接入時(shí)也需要對(duì)全部已有數(shù)據(jù)進(jìn)行一次切詞,讓歷史稿件也有切詞結(jié)果。初始化過(guò)程同樣需要使用CAS來(lái)規(guī)避寫寫沖突。

數(shù)據(jù)導(dǎo)出層

數(shù)據(jù)進(jìn)入Taishan后,我們需要從中導(dǎo)出需要的數(shù)據(jù)來(lái)構(gòu)建全量和增量索引。

對(duì)于一份索引,其全量和增量構(gòu)建任務(wù)的配置是共用的,獲取到實(shí)際數(shù)據(jù)后的處理邏輯也是一致的。

Taishan中不同的列可以保存字段的多個(gè)版本,具體構(gòu)建全量和增量索引時(shí)選用哪些列中的字段,需要在配置中指定。配置中以白名單的形式指定列,無(wú)需擔(dān)心新增列對(duì)已有構(gòu)建任務(wù)產(chǎn)生影響。

不同版本的索引大部分情況下只需要調(diào)整配置再另行部署即可產(chǎn)出。

圖片圖片

全量數(shù)據(jù)導(dǎo)出

Taishan會(huì)每日定期將全量數(shù)據(jù)備份到公司內(nèi)部的對(duì)象存儲(chǔ)。我們通過(guò)備份數(shù)據(jù)好的數(shù)據(jù),遍歷其中的全部KV,也就是按行遍歷表格,取出指定的列來(lái)獲取全量索引數(shù)據(jù)。Taishan備份效率是非常高的,通常在分鐘級(jí),這也大幅地減少了掃庫(kù)的時(shí)間開(kāi)銷。

增量數(shù)據(jù)導(dǎo)出

增量數(shù)據(jù)的導(dǎo)出通過(guò)消費(fèi)變更數(shù)據(jù)流實(shí)現(xiàn)。消費(fèi)后對(duì)每條消息(ID+變更的CF)反查Taishan,獲取所需的完整字段。

數(shù)據(jù)迭代

新的存儲(chǔ)機(jī)制簡(jiǎn)化了索引的迭代流程,只需要將新數(shù)據(jù)寫入Taishan,然后調(diào)整構(gòu)建任務(wù)關(guān)注的列即可。迭代的開(kāi)發(fā)量大幅下降,基本只需復(fù)用現(xiàn)有流程,調(diào)整配置后部署新任務(wù)。

圖片圖片

隨著新存儲(chǔ)方案和增量計(jì)算等優(yōu)化的落地,索引構(gòu)建的周期縮短了一半以上。迭代周期縮短的同時(shí),消除了鏈路上的性能瓶頸和延遲風(fēng)險(xiǎn)。

分布式構(gòu)建

搜索索引最早都是通過(guò)物理機(jī)crontab定時(shí)執(zhí)行腳本實(shí)現(xiàn)。腳本執(zhí)行各種掃庫(kù)操作,將數(shù)據(jù)加載到內(nèi)存中,并產(chǎn)出一份完整的JSON Lines文本文件作為原始索引數(shù)據(jù)集,然后調(diào)用indexer進(jìn)行全量切詞和構(gòu)建正排/倒排索引。物理機(jī)部署穩(wěn)定性沒(méi)有保證且難以維護(hù),我們首先把構(gòu)建遷移到K8S集群上,嘗試以服務(wù)形式部署。這樣雖然保證了建庫(kù)任務(wù)的穩(wěn)定性,但是建庫(kù)任務(wù)資源利用率很低。建庫(kù)服務(wù)構(gòu)建時(shí)需要大量資源,但在大多數(shù)時(shí)間里是不消耗任何資源的,容器依然需要占據(jù)相應(yīng)的CPU/內(nèi)存資源。雖然通過(guò)超配(低軟限高硬限)并錯(cuò)開(kāi)任務(wù)觸發(fā)時(shí)間可以來(lái)減少資源空置的情況,但維護(hù)較為繁瑣。最終我們選擇將建庫(kù)遷移到業(yè)界廣泛使用的分布式計(jì)算框架Spark上,利用Spark潮汐資源進(jìn)行索引構(gòu)建,一方面可以加大構(gòu)建并發(fā),另一方面也可以對(duì)資源進(jìn)行更充分的利用。

為了能夠盡可能的復(fù)用及降低維護(hù)成本,從流程上進(jìn)行抽象,將索引構(gòu)建流程分以下主要步驟:

  1. 讀取數(shù)據(jù):從配置中加載并進(jìn)行數(shù)據(jù)讀取,Taishan源在對(duì)象存儲(chǔ)的導(dǎo)出文件為sst格式,使用官方開(kāi)源的JNI庫(kù)進(jìn)行二次封裝。
  2. 解碼 :Taishan非Spark原生支持的數(shù)據(jù)源,需要額外開(kāi)發(fā)解析邏輯。
  3. 再分片:由于導(dǎo)出的單一文件分片數(shù)據(jù)量較大,一次性讀取將占用大量?jī)?nèi)存,甚至OOM。為解決上述問(wèn)題,參考Spark的cache機(jī)制,在讀取并解碼文件數(shù)據(jù)流的過(guò)程時(shí)先將文件載入到指定內(nèi)存buffer中,若buffer裝滿則生成一個(gè)分片(partition)并寫入hdfs中。以此將較少的文件分片(如128個(gè))拆分為較多的hdfs文件分片(如5000個(gè)),便于Spark的后續(xù)處理。
  4. 稿件處理:通過(guò)配置對(duì)稿件數(shù)據(jù)進(jìn)行一系列的處理操作,如過(guò)濾、字段映射等。
  5. 編碼 & 索引構(gòu)建:將稿件內(nèi)容轉(zhuǎn)化為索引需要的特定編碼形式,如Flatbuffer、Protobuffer。并根據(jù)索引類型和Meta信息,產(chǎn)出索引。
  6. 壓縮打包:對(duì)構(gòu)建出的索引及Meta數(shù)據(jù)進(jìn)行打包,產(chǎn)出最終索引文件。 

除更省資源外,Spark構(gòu)建的任務(wù)并發(fā)度也更高,進(jìn)一步縮短了構(gòu)建時(shí)間,最終達(dá)到小時(shí)級(jí)別。

總結(jié)與展望

通過(guò)這一系列的技術(shù)更新和流程優(yōu)化,我們的索引構(gòu)建架構(gòu)最終從早期的單機(jī)構(gòu)建發(fā)展到分布式的數(shù)據(jù)存儲(chǔ)和建庫(kù)任務(wù),能力更強(qiáng)的同時(shí)也更易維護(hù)和迭代,索引構(gòu)建周期實(shí)現(xiàn)了從天級(jí)到小時(shí)級(jí)別的飛躍式進(jìn)步,為業(yè)務(wù)的未來(lái)發(fā)展奠定了堅(jiān)實(shí)基礎(chǔ)。

參考

  • https://protobuf.dev/programming-guides/encoding/#varints
  • https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html
  • https://research.google/pubs/large-scale-incremental-processing-using-distributed-transactions-and-notifications/
  • https://protobuf.dev/programming-guides/encoding/#last-one-wins
責(zé)任編輯:武曉燕 來(lái)源: 嗶哩嗶哩技術(shù)
相關(guān)推薦

2022-07-05 15:08:52

機(jī)房架構(gòu)

2022-11-22 08:42:38

數(shù)據(jù)庫(kù)

2024-04-26 12:13:45

NameNodeHDFS核心

2022-09-15 15:18:23

計(jì)算實(shí)踐

2024-02-28 07:50:36

大數(shù)據(jù)標(biāo)簽系統(tǒng)AB 實(shí)驗(yàn)

2022-07-29 14:53:09

數(shù)據(jù)實(shí)踐

2025-03-05 00:00:55

2020-04-28 08:15:55

高可用架構(gòu)系統(tǒng)

2023-02-16 07:24:27

VPA技術(shù)

2023-02-09 07:38:39

配置中心架構(gòu)組件

2023-07-19 08:58:00

數(shù)據(jù)管理數(shù)據(jù)分析

2023-02-28 12:12:21

語(yǔ)音識(shí)別技術(shù)解碼器

2023-02-13 09:48:00

PRESTO 集群緩存優(yōu)化

2022-10-08 15:41:08

分布式存儲(chǔ)

2023-10-26 06:43:25

2012-07-03 08:57:42

ASO百度移動(dòng)搜索手機(jī)站優(yōu)化

2023-03-31 13:31:45

2022-04-07 16:50:28

FlinkB站Kafka

2023-11-03 12:54:00

KAFKA探索中間件

2015-05-07 09:32:55

APP開(kāi)源
點(diǎn)贊
收藏

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

主站蜘蛛池模板: 精品一区二区三区在线观看国产 | 亚洲欧美另类在线 | 天天综合久久 | 武道仙尊动漫在线观看 | 亚洲网站观看 | 国产成人精品久久二区二区91 | 一区二区三区视频播放 | 亚洲精品乱码 | 久久亚洲欧美日韩精品专区 | 欧美美女爱爱视频 | 亚洲综合伊人 | 伊人网综合 | 狠狠色综合网站久久久久久久 | 欧美日韩免费一区二区三区 | 国产精品一区在线观看你懂的 | 久久久久久久久国产成人免费 | 亚洲国产精品一区二区www | 国产专区在线 | 中文字幕亚洲一区二区va在线 | 国产精品精品视频一区二区三区 | av永久免费| 亚洲最大的成人网 | 久久久久久99 | 欧美久久久久久 | 国产精品欧美大片 | 国产日韩欧美一区二区 | 日韩毛片视频 | 久久久一区二区 | 国产精品一区二区av | 91九色婷婷 | 91天堂网| 一区二区在线不卡 | 成人一区二区三区在线观看 | 欧美激情一区二区 | 在线观看www| av在线视 | 久久久国产一区二区三区 | 尤物在线精品视频 | 91av视频在线 | 亚洲成人av一区二区 | 欧美综合网 |