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

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

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

前言

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

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

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

圖片圖片

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

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

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

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

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

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

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

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

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

圖片圖片

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

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

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

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

圖片圖片

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

舉個例子,假如MySQL A的binlog來自主庫,而按ID查詢時訪問了MySQL A的從庫。

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

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

索引生效流程

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

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

問題與挑戰(zhàn)

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

性能

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

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

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

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

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

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

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

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

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

維護成本

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

資源

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

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

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

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

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

方案設(shè)計

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

存儲選型

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

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

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

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

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

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

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

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

圖片圖片

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

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

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

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

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

表格模型

基于KV存儲之上封裝出表格存儲模型:

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

圖片圖片

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

  • 同一稿件的若干相關(guān)字段可以打包保存在一列(如title和uname),這樣會大幅減少KV的訪問次數(shù),提高效率。
  • 表格中不同的列可以保存相同的字段。例如:
  • 這里eb,eb1兩列分別對應(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支持允許我們在不同寫入方之間通過樂觀鎖來同步,避免多個寫入方同時更新一個單元格時發(fā)生寫入丟失。

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

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

Key設(shè)計

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

圖片

Data Orientation

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

ID1:CF1

ID1:CF2

ID2:CF1

ID2:CF2

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

ID1:CF1

ID1:CF2

ID2:CF1

ID2:CF2

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

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

Value設(shè)計

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

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

圖片圖片

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

序列化

拋棄JSON

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

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

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

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

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

轉(zhuǎn)向protobuf

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

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

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

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

高效地合并

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

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分別是對應(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會有以下問題:

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

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

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

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

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

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

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

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

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

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

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

圖片圖片

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

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

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

圖片圖片

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

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

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

寫入沖突處理

全量vs增量

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

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

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

增量vs增量

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

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

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

增量計算

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

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

圖片圖片

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

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

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

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

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

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

圖片圖片

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

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

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

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

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

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

圖片圖片

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

分布式構(gòu)建

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

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

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

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

總結(jié)與展望

通過這一系列的技術(shù)更新和流程優(yōu)化,我們的索引構(gòu)建架構(gòu)最終從早期的單機構(gòu)建發(fā)展到分布式的數(shù)據(jù)存儲和建庫任務(wù),能力更強的同時也更易維護和迭代,索引構(gòu)建周期實現(xiàn)了從天級到小時級別的飛躍式進步,為業(yè)務(wù)的未來發(fā)展奠定了堅實基礎(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é)任編輯:武曉燕 來源: 嗶哩嗶哩技術(shù)
相關(guān)推薦

2022-07-05 15:08:52

機房架構(gòu)

2022-11-22 08:42:38

數(shù)據(jù)庫

2024-04-26 12:13:45

NameNodeHDFS核心

2022-09-15 15:18:23

計算實踐

2024-02-28 07:50:36

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

2025-03-05 00:00:55

2022-07-29 14:53:09

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

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-02-28 12:12:21

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

2023-07-19 08:58:00

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

2023-02-13 09:48:00

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

2023-10-26 06:43:25

2022-10-08 15:41:08

分布式存儲

2012-07-03 08:57:42

ASO百度移動搜索手機站優(yōu)化

2015-05-07 09:32:55

APP開源

2022-04-07 16:50:28

FlinkB站Kafka

2023-11-03 12:54:00

KAFKA探索中間件

2023-03-31 13:31:45

點贊
收藏

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