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

轉(zhuǎn)轉(zhuǎn)搜推排序服務(wù)的響應(yīng)對(duì)象序列化優(yōu)化

開發(fā) 前端
本項(xiàng)目旨在解決搜索推薦服務(wù)化過程中因日志傳輸引起的序列化額外耗時(shí)問題。經(jīng)過三次版本迭代和測試,最終方案成功落地。

1 優(yōu)化背景

為了提升搜索推薦系統(tǒng)的整體工程效率和服務(wù)質(zhì)量,搜索推薦工程團(tuán)隊(duì)對(duì)系統(tǒng)架構(gòu)進(jìn)行了調(diào)整。將原本的單一服務(wù)架構(gòu)拆分為多個(gè)專門化的獨(dú)立模塊,分別為中控服務(wù)、召回服務(wù)和排序服務(wù)。

在新的架構(gòu)中,中控服務(wù)負(fù)責(zé)統(tǒng)籌協(xié)調(diào)請求的分發(fā)和流量控制,召回服務(wù)用于搜索意圖和用戶行為分析并計(jì)算返回搜索推薦候選商品集合,而排序服務(wù)則進(jìn)一步對(duì)這些候選商品進(jìn)行精排序,以達(dá)到更好的最終展示結(jié)果的相關(guān)性、點(diǎn)擊率、轉(zhuǎn)化率等目標(biāo)。

在推薦系統(tǒng)接入新的排序服務(wù)過程中,發(fā)現(xiàn)與原有邏輯相比,微詳情頁場景的響應(yīng)時(shí)間顯著增加了大約10毫秒,主要問題出現(xiàn)在接口請求和響應(yīng)的環(huán)節(jié)上。

同樣,搜索系統(tǒng)在接入排序服務(wù)時(shí)也遇到了類似的情況。與原有邏輯相比,響應(yīng)時(shí)間增加了近20毫秒,導(dǎo)致無法達(dá)到上線的性能標(biāo)準(zhǔn)。

例如,搜索排序服務(wù)在本地執(zhí)行時(shí)的時(shí)間為60毫秒,但通過遠(yuǎn)程調(diào)用時(shí),執(zhí)行時(shí)間卻增加到接近80毫秒,兩者之間的差異接近20毫秒。

為了解決這些性能問題,需要對(duì)這些延遲的原因進(jìn)行深入分析和優(yōu)化。

2 問題分析

問題現(xiàn)象是調(diào)用方等待耗時(shí)和服務(wù)方執(zhí)行耗時(shí)相差較大,所以問題主要出現(xiàn)在遠(yuǎn)程調(diào)用過程,接下來就是分析這個(gè)過程

遠(yuǎn)程調(diào)用可以理解為一種實(shí)現(xiàn)遠(yuǎn)程代碼與本地接口調(diào)用相一致體驗(yàn)的開發(fā)模式

遠(yuǎn)程調(diào)用一般通過動(dòng)態(tài)代理實(shí)現(xiàn),通過調(diào)用動(dòng)態(tài)方法將調(diào)用方法標(biāo)識(shí)和調(diào)用參數(shù)序列化為字節(jié)碼,再通過通信協(xié)議請求服務(wù)端

服務(wù)端解析方法標(biāo)識(shí)和反序列化參數(shù)字節(jié)碼到實(shí)體參數(shù)對(duì)象,再反射的方式調(diào)用方法標(biāo)識(shí)對(duì)應(yīng)的方法

該方法返回結(jié)果(即響應(yīng)對(duì)象)序列化,并返回給調(diào)用方,調(diào)用房完成反序列化響應(yīng)對(duì)象,返回給代理調(diào)用者

整個(gè)過程較為耗時(shí)的部分為序列化/反序列化、網(wǎng)絡(luò)IO、本地調(diào)用,一般為ms級(jí)別。

調(diào)用動(dòng)態(tài)方法和反射耗時(shí)相對(duì)不高,一般為us級(jí)別。如下圖所示。

圖片圖片

調(diào)用動(dòng)態(tài)方法耗時(shí)相比于其他過程一般可忽略不計(jì),服務(wù)端本地調(diào)用邏輯與原服務(wù)已經(jīng)對(duì)齊,也不在本次考慮之內(nèi)。

接下來就是要分析序列化和網(wǎng)絡(luò)IO請求開銷在各環(huán)節(jié)的占比。選擇skynet來定位序列化和網(wǎng)絡(luò)環(huán)節(jié)的耗時(shí)

skynet是公司架構(gòu)組提供的分布式鏈路追蹤工具,通過鏈路中攜帶的上下文,可以記錄經(jīng)過的服務(wù)接口的日志信息

skynet的基本概念是一次完整請求鏈路稱為trace,一次遠(yuǎn)程調(diào)用過程稱為span

以推薦微詳情頁某次請求為例,查詢單次請求的調(diào)用過程,可以看到下圖所示,排序服務(wù)調(diào)用過程中的遠(yuǎn)程耗時(shí)與本地耗時(shí)差值約為 4ms

圖片圖片

通過span攜帶的日志,可以看到以下數(shù)據(jù)

指標(biāo)

耗時(shí)

說明

scf.request.serialize.cost

0.242ms

請求序列化耗時(shí)

scf.request.deserialize.cost

0.261ms

請求反序列化耗時(shí)

scf.response.deserialize.cost

0.624ms

響應(yīng)反序列化耗時(shí)

scf.response.serialize.cost

約0.624ms

響應(yīng)序列化耗時(shí),skynet未提供,可以認(rèn)為與反序列化時(shí)間差異不大

可見,耗時(shí)問題主要集中在響應(yīng)過程階段。如果要計(jì)算遠(yuǎn)程耗時(shí)與本地耗時(shí)的差異為20毫秒的開銷情況,可以結(jié)合上述日志數(shù)據(jù)進(jìn)行線性計(jì)算和整理,得出各個(gè)過程的近似耗時(shí)及其占比,如下圖所示。

搜索與推薦的區(qū)別在于,搜索的請求階段不傳輸特征,因此響應(yīng)過程的耗時(shí)占比更高。因此,需要優(yōu)先考慮減少響應(yīng)過程的耗時(shí)。

圖片圖片

在整個(gè)響應(yīng)過程中,序列化和網(wǎng)絡(luò) I/O 的耗時(shí)各占一半。影響 I/O 耗時(shí)的因素之一是網(wǎng)絡(luò)環(huán)境和機(jī)器配置,另一個(gè)因素是序列化后對(duì)象的長度。

通過與運(yùn)維團(tuán)隊(duì)溝通,我們了解到部分機(jī)器使用的是千兆網(wǎng)卡,而我們傳輸?shù)膶?duì)象長度通常都在 MB 級(jí)別,這對(duì)耗時(shí)有一定的影響。

網(wǎng)絡(luò)問題可以統(tǒng)一整理后提交給運(yùn)維團(tuán)隊(duì)調(diào)整機(jī)器配置來解決,而我們的主要精力應(yīng)放在優(yōu)化序列化過程上。

接下來是對(duì)響應(yīng)對(duì)象的序列化過程分析

首先需要了解響應(yīng)對(duì)象數(shù)據(jù)結(jié)構(gòu),響應(yīng)對(duì)象是一個(gè)泛型類,以支持不同類型的ID,如下所示,包括:

  • 狀態(tài)值,判斷結(jié)果正常或異常
  • RankResult對(duì)象

主要存儲(chǔ)約500長度的RankResultItem列表,每個(gè)Item對(duì)象需要返回商品ID,并以Map的形式返回模型日志、模型打分結(jié)果及部分特征

請求攜帶的其他信息,如A/B測試實(shí)際命中分組名的集合等等

  • 異常日志

如以下代碼所示

class RankResponse<T> {
    int status;
    RankResult<T> result;
    String errorMsg;
}
 
class RankResult<T> {
    List<RankResultItem<T>> items;
    Map<String, Object> others;
}
 
class RankResultItem<T> {
    T id;
    Map<String, Object> features; // 回傳特征
    Map<String, String> metric;     // 模型日志
    Map<String, Double> results;  // 模型結(jié)果
}

優(yōu)化前,搜索排序服務(wù)采用了架構(gòu)組提供的 SCFV4 序列化方法。

SCFV4 是一種最終輸出字節(jié)碼的序列化方式,它會(huì)對(duì)所有被 @SCFSerializable 注解標(biāo)注的業(yè)務(wù)數(shù)據(jù)傳輸對(duì)象預(yù)編譯序列化和反序列化方法。對(duì)于 Java 的基礎(chǔ)類(如 List、Map 等)和基本類型(如 Integer 等),SCFV4 也預(yù)設(shè)了相應(yīng)的序列化和反序列化方法。

在序列化執(zhí)行時(shí),SCFV4 根據(jù)對(duì)象的數(shù)據(jù)結(jié)構(gòu)層次逐層遍歷,調(diào)用每個(gè)子成員對(duì)象的序列化方法,最終輸出字節(jié)碼。反序列化的過程與之類似。

SCFV4 序列化的特點(diǎn)如下:

  • 與 JSON 相比,反序列化后的類型較為安全,但不能完全保證類型一致性。例如,不論輸入的 Map 是何種類型,反序列化后都會(huì)變?yōu)?HashMap。
  • 對(duì)泛型類型和基本類型的序列化過程進(jìn)行了優(yōu)化,對(duì)于數(shù)據(jù)大小在 KB 級(jí)別的對(duì)象,性能表現(xiàn)良好。
  • 作為一種與SCF框架緊密集成的序列化方法,能夠與框架中的其他模塊無縫協(xié)作,從而簡化開發(fā)流程,減少開發(fā)者的工作量。
  • 在代碼編寫時(shí),需要通過注解的方式對(duì)序列化對(duì)象類進(jìn)行預(yù)設(shè)定,版本只能向后兼容。
  • 有時(shí)存在異常處理不夠友好的問題,異常信息無法直接反映業(yè)務(wù)代碼中的問題,定位序列化問題時(shí),往往需要依賴經(jīng)驗(yàn)進(jìn)行判斷。

下面回到排序響應(yīng)對(duì)象,分析響應(yīng)對(duì)象的序列化過程,將過程梳理到下圖:

圖片圖片

可以看到,一次序列化過程可能需要對(duì)多達(dá) 500 次的商品日志 Map、商品得分 Map 和商品 ID 進(jìn)行序列化。

由于日志對(duì)象的數(shù)據(jù)規(guī)模遠(yuǎn)大于其他類型的對(duì)象,因此我們可以假設(shè),序列化的主要開銷來自于序列化特征日志 Map 的耗時(shí)。

在相關(guān)的 MapSerializer 類中可以發(fā)現(xiàn),序列化 Map 時(shí)不僅需要解析 Key-Value 的數(shù)據(jù)類型,還大量調(diào)用了 String.getBytes() 方法。

假設(shè)特征日志的總長度約為 1MB,在本地測試中,getBytes 方法的耗時(shí)大約為 10 毫秒,這與我們的預(yù)期一致。因此,我們的優(yōu)化思路應(yīng)重點(diǎn)放在優(yōu)化特征日志的序列化過程中。

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

3.1 優(yōu)化方案一

首先想到的優(yōu)化方案是通過不傳輸日志來完全節(jié)省日志的序列化時(shí)間。針對(duì)這一思路,有兩種具體的實(shí)現(xiàn)方式:

  • 在排序服務(wù)中直接打印日志,并由排序服務(wù)直接將日志上報(bào)到 Kafka。
  • 使用 Redis 緩存日志,從而減少序列化和傳輸?shù)拈_銷。

如果直接打印日志,每個(gè)請求最多需要打印 1000 條日志。經(jīng)過與數(shù)據(jù)團(tuán)隊(duì)的討論,我們得出了以下結(jié)論:

  • 數(shù)據(jù)采集:直接打印日志將導(dǎo)致每天的日志量達(dá)到約 15TB。以 15 臺(tái)機(jī)器的集群計(jì)算,每分鐘需要采集 1GB 的數(shù)據(jù),這會(huì)給日志采集系統(tǒng)和我們的服務(wù) I/O 帶來巨大壓力。
  • 數(shù)據(jù)存儲(chǔ):每天新增的數(shù)據(jù)量將達(dá)到 50-60TB。由于每天生成的 15TB 數(shù)據(jù)需要先采集再清洗,這樣就會(huì)產(chǎn)生兩份數(shù)據(jù)。每份數(shù)據(jù)有 3 個(gè)副本,總共是 6 份數(shù)據(jù)。最終在 Hadoop 集群中還會(huì)有 2 份備份,約 45TB。即使使用 Hive 表和 Parquet+GZ 格式壓縮,數(shù)據(jù)量也大約在 5-10TB 之間。

經(jīng)過分析,我們認(rèn)為改用直接打印日志的方案并不是最優(yōu)選擇,因此沒有實(shí)施。

如果將日志異步寫入Redis,并在重排序時(shí)從Redis中讀取,就能有效地優(yōu)化性能。通過設(shè)置日志緩存的過期時(shí)間為 1 秒,可以滿足排序到重排序之間的時(shí)間間隔要求。

在這種情況下,Redis的預(yù)估使用量為:每請求日志大小 × QPS × 過期時(shí)間 = 2MB × 500 × 1秒 = 1GB。

由于成本相對(duì)可控,因此我們決定嘗試這一方法。

圖片圖片

如上圖所示,本次方案的目標(biāo)是將紅色部分的輸入特征日志處理邏輯提前到模型輸入特征處理之后,并引入 Redis 緩存邏輯,同時(shí)實(shí)現(xiàn)異步執(zhí)行。

然而,這里遇到了一個(gè)挑戰(zhàn):輸入特征集合是由預(yù)測框架生成的,其生成時(shí)間點(diǎn)只有框架內(nèi)部知道。因此,日志處理過程必須在預(yù)測框架內(nèi)部執(zhí)行。

在深入討論之前,先介紹一下預(yù)測框架和排序框架之間的關(guān)系。預(yù)測框架的主要職責(zé)是管理和執(zhí)行算法模型,它生成模型所需的輸入特征,并基于這些特征進(jìn)行預(yù)測。排序框架則利用預(yù)測框架的輸出,對(duì)商品或內(nèi)容進(jìn)行排序,以優(yōu)化最終展示給用戶的結(jié)果。

雖然預(yù)測框架和排序框架在功能上是相互獨(dú)立的,但它們之間密切合作。排序框架依賴預(yù)測框架提供的預(yù)測結(jié)果,而預(yù)測框架則處理來自排序框架的輸入特征。然而,預(yù)測框架的主要任務(wù)是執(zhí)行算法模型,與具體的業(yè)務(wù)邏輯無關(guān)。因此,在預(yù)測框架中引入排序框架的業(yè)務(wù)邏輯會(huì)導(dǎo)致相互依賴,這違背了各自的設(shè)計(jì)初衷,也不利于系統(tǒng)的可維護(hù)性和擴(kuò)展性。

因此,我們需要一種方式來解耦兩者,實(shí)現(xiàn)日志處理邏輯的同時(shí),不破壞架構(gòu)設(shè)計(jì)。

如果在預(yù)測框架內(nèi)部執(zhí)行日志處理,就需要將排序商品列表、排序上下文、日志處理插件、線程池、Redis 客戶端對(duì)象、過期時(shí)間配置等所有組件都傳遞到預(yù)測框架中。這將導(dǎo)致排序框架與預(yù)測框架的相互依賴,而這種雙向依賴是不合理的,因?yàn)轭A(yù)測框架不僅服務(wù)于排序框架,還用于通用推薦和定價(jià)框架。

為了解決這個(gè)問題,我們可以通過傳遞 Consumer 對(duì)象來實(shí)現(xiàn)解耦。預(yù)測框架作為模型輸入特征的生產(chǎn)者,排序框架作為消費(fèi)者。具體來說,排序框架可以實(shí)現(xiàn)一個(gè)指定日志處理邏輯的 Consumer 接口。當(dāng)預(yù)測框架生成輸入特征集合后,調(diào)用 accept 方法來完成日志處理。

為了便于異步處理,我們在排序框架中定義了一個(gè) IFutureConsumer 接口,該接口支持獲取 Future 方法,用于在排序框架中等待日志處理完成。同時(shí),預(yù)測框架接收到的仍然是一個(gè)標(biāo)準(zhǔn)的 Consumer 接口對(duì)象。

這種方法確保了預(yù)測框架和排序框架之間的解耦,明確了各自的職責(zé),避免了雙向依賴,使系統(tǒng)更加靈活和可擴(kuò)展。

interface IFutureConsumer<T> extend Consumer<T> {
    // accpet(T t)
 
    Future<?> getFuture();
}

預(yù)測框架生成輸入特征并傳遞給排序框架。

// 預(yù)測框架生成輸入特征
T inputFeatures = ...;

// 調(diào)用排序框架的日志處理邏輯
IFutureConsumer<T> consumer = ...; // 由排序框架提供
consumer.accept(inputFeatures);

排序框架處理日志。

public class HandleLogConsumer<T> implements IFutureConsumer<T> {
    private Future<?> future;

    @Override
    public void accept(T t) {
        // 異步處理日志邏輯
        this.future = executorService.submit(() -> {
            // 處理日志邏輯
        });
    }

    @Override
    public Future<?> getFuture() {
        return this.future;
    }
}

整個(gè)過程如下圖所示:

圖片

另外本方案使用了Redis的哈希(hash)數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲(chǔ),原因是在搜索服務(wù)中,每次請求都需要刷新結(jié)果緩存,這也意味著需要同時(shí)刷新相關(guān)的日志緩存。然而,搜索服務(wù)并不知道具體有哪些 infoid 已經(jīng)被存儲(chǔ),如果使用字符串(string)結(jié)構(gòu)來存儲(chǔ)這些日志數(shù)據(jù),很難做到全量刷新,因?yàn)闊o法有效地管理和定位所有存儲(chǔ)的鍵值對(duì)。

相比之下,使用哈希(hash)結(jié)構(gòu)存儲(chǔ)日志信息有明顯的優(yōu)勢。我們可以使用 ctr、cvr、info 等作為哈希表的關(guān)鍵字前綴,并以請求的 MD5 值作為后綴。這種方式只需要刷新 2~3 個(gè)哈希鍵(key),就可以覆蓋所有相關(guān)的日志數(shù)據(jù)。這種方法不僅簡化了緩存刷新操作,而且更高效,因?yàn)橹恍璨僮魃倭康墓fI即可完成全量刷新,適合搜索服務(wù)的需求。

推薦側(cè)在找靚機(jī)微詳情頁場景先行接入了此方案,額外耗時(shí)由14ms優(yōu)化至4ms

但隨后發(fā)現(xiàn)了兩個(gè)問題:

隨著首頁推薦等主要場景的接入,后處理過程中獲取日志過程耗時(shí)達(dá)到了7ms以上,原因是寫qps較高(單redis-server節(jié)點(diǎn)近7w qps),日志數(shù)據(jù)又屬于bigkey(1k以上),redis-server極易發(fā)生阻塞,擴(kuò)容后仍在3ms左右水平 搜索測試耗時(shí)并未明顯下降,讀取和刷緩存過期時(shí)間也帶來了額外的成本。

總結(jié)

  • 此方案是對(duì)Redis性能過于樂觀,在處理高qps + bigkey的場景性能無法滿足要求,后續(xù)需要經(jīng)常關(guān)注單點(diǎn)qps并評(píng)估擴(kuò)容,維護(hù)成本也高
  • 業(yè)務(wù)過多的感知框架實(shí)現(xiàn)邏輯,有些操作甚至需要業(yè)務(wù)干預(yù),如手動(dòng)刷新過期時(shí)間等,接入過程體驗(yàn)不夠友好,負(fù)擔(dān)過重

3.2 優(yōu)化方案二

在本地打印日志和緩存日志的方案不可行后,我們只能重新考慮通過響應(yīng)對(duì)象將日志數(shù)據(jù)傳回調(diào)用方的方案。以下是幾種可行的思路:

  • 本地緩存日志數(shù)據(jù):將日志數(shù)據(jù)暫時(shí)緩存到本地,等待重排序完成后,再請求同一臺(tái)機(jī)器取出所需的日志數(shù)據(jù)。
  • 分步返回?cái)?shù)據(jù):先返回排序后的模型得分部分,再異步返回日志數(shù)據(jù)部分。
  • 日志異步轉(zhuǎn)換和壓縮:在預(yù)測階段,將日志數(shù)據(jù)異步轉(zhuǎn)換為字節(jié)數(shù)組并進(jìn)行壓縮,序列化時(shí)直接返回這些字節(jié)數(shù)據(jù)。在整個(gè)搜索推薦流程完成后,再將要下發(fā)的topN商品的日志數(shù)據(jù)解壓并轉(zhuǎn)換回字符串。

經(jīng)過評(píng)估,前兩種方案雖然具有一定的可行性,但需要調(diào)用方進(jìn)行較多的開發(fā)支持,實(shí)施周期較長。此外,這些方案還需考慮更復(fù)雜的容災(zāi)處理設(shè)計(jì),例如應(yīng)對(duì)因重啟或超時(shí)導(dǎo)致的日志丟失,以及緩存引起的 GC 問題。為了規(guī)避這些風(fēng)險(xiǎn),我們決定嘗試第三種方案。

為了能實(shí)現(xiàn)僅在需要時(shí),即取商品列表topN并打印后端日志時(shí),才將日志從bytes轉(zhuǎn)回String,在響應(yīng)RankResultItem增加了LazyMetric數(shù)據(jù)類型,利用延遲加載機(jī)制,減少了不必要的數(shù)據(jù)處理和傳輸開銷,數(shù)據(jù)結(jié)構(gòu)如下:

class RankResultItem {
    Map<String, LazyMetric> lazyMetricMap;
}
 
class LazyMetric {
    byte[] data; // 編碼后的字符串?dāng)?shù)據(jù)
    byte compressMethodCode; // 壓縮類型
 
    LazyMetric(String str){
        // string2bytes
    }
  
    String toString() {
        // bytes2string
    }
}

日志生產(chǎn)及獲取過程調(diào)整為:

圖片圖片

可以看出,String 轉(zhuǎn) bytes 的編碼過程耗時(shí)已經(jīng)在模型執(zhí)行時(shí)并行處理中被優(yōu)化掉了。在從模型預(yù)測模塊到 topN 節(jié)點(diǎn)的整個(gè)執(zhí)行過程中,系統(tǒng)始終攜帶的是 bytes 類型的數(shù)據(jù)。只有在 topN 節(jié)點(diǎn)完成了所有搜索推薦流程需要準(zhǔn)備返回商品時(shí),才會(huì)主動(dòng)調(diào)用 toString 方法將 bytes 轉(zhuǎn)回 String,而其他商品的日志數(shù)據(jù)則會(huì)被直接丟棄。這樣一來,decode 的次數(shù)從 500 次減少到了 10 次(假設(shè) N 一般為 10)。整個(gè)過程對(duì)業(yè)務(wù)側(cè)的集成并不復(fù)雜,開啟功能后,排序框架就會(huì)自動(dòng)將日志數(shù)據(jù)轉(zhuǎn)存到 LazyMetricMap 中。中控服務(wù)隨后可以從每個(gè) Item 的 LazyMetricMap 中取出 LazyMetric 對(duì)象,并在合適的時(shí)機(jī)調(diào)用 toString 方法,提升搜索推薦業(yè)務(wù)整體開發(fā)效率。壓縮過程選擇了java自帶的gzip和zlib兩種方法進(jìn)行測試,測試結(jié)果如下:

方法

序列化時(shí)間

反序列化時(shí)間

總時(shí)間

數(shù)據(jù)大小 (bytes)

壓縮比率

【SCFV4】原方法

1.70ms

1.28ms

2.98ms

1,192,564

0.8336

【SCFV4】metric日志直接轉(zhuǎn)bytes傳輸

1.46ms

0.74ms

2.20ms

1,216,565

0.8504

【SCFV4】metric日志zlib轉(zhuǎn)bytes傳輸

1.15ms

0.73ms

1.88ms

472,065

0.3300

【SCFV4】metric日志gzip轉(zhuǎn)bytes傳輸

1.30ms

0.77ms

2.07ms

490,065

0.3425

【Hessian】原方法

2.33ms

3.45ms

5.78ms

1,165,830

0.8149

【Hessian】metric日志直接轉(zhuǎn)bytes傳輸

1.00ms

3.80ms

4.80ms

1,168,143

0.8165

【Hessian】metric日志zlib轉(zhuǎn)bytes傳輸

0.61ms

1.46ms

2.07ms

422,513

0.2953

【Hessian】metric日志gzip轉(zhuǎn)bytes傳輸

0.59ms

1.44ms

2.03ms

440,509

0.3079

可以看到,在不同的序列化方法下,序列化耗時(shí)都有所減少,性能最高提升至原來的 35%,序列化后的數(shù)據(jù)量也減少到原來的 36%,這也預(yù)示著網(wǎng)絡(luò) I/O 的開銷會(huì)有所下降。

接入情況:

  • 推薦系統(tǒng):在轉(zhuǎn)轉(zhuǎn)首頁推薦場景中接入后,與原 Redis 方案相比,性能沒有顯著提升,但減少了 Redis 中間存儲(chǔ)環(huán)節(jié),從而降低了特征數(shù)據(jù)丟失的風(fēng)險(xiǎn)。
  • 搜索系統(tǒng):在接入并進(jìn)行壓測后,雖然整體耗時(shí)有所減少,但仍然存在大約 13 毫秒的額外耗時(shí)(71ms 對(duì)比 58ms),尚未完全解決性能問題。

總結(jié):

  • 盡管方案在性能上有所提升,但這僅是對(duì)原有序列化框架的修補(bǔ),核心問題在于 SCF 序列化對(duì)特定對(duì)象的執(zhí)行效率仍然不高,因此問題并未徹底解決。初步分析表明,可能的原因在于 Map 的序列化過程本身依然較為耗時(shí),并且在反序列化時(shí),需要為 LazyMetric 的字節(jié)數(shù)組(搜索約 1500 個(gè),推薦約 800 個(gè))分配大量碎片化的內(nèi)存空間,這導(dǎo)致了額外的耗時(shí)。

3.2 優(yōu)化方案三

根據(jù)對(duì) V2 方案的總結(jié),V3 方案的設(shè)計(jì)原則是:放棄使用 SCF 的通用對(duì)象序列化,RPC 層僅通過字節(jié)數(shù)組進(jìn)行交互,而排序框架采用自定義的序列化方法。

思路一:繼續(xù)嘗試接入現(xiàn)有的開源序列化框架,并在此基礎(chǔ)上對(duì)排序響應(yīng)對(duì)象進(jìn)行定制化開發(fā)。常見的開源項(xiàng)目包括 protobuf、Kryo、Hessian 等。

思路二:自行開發(fā)專門適用于排序響應(yīng)對(duì)象的序列化方法。

思路一的優(yōu)勢在于安全性、通用性和高性能方面都表現(xiàn)良好,部分框架也提供一定的定制化能力。然而,這類框架通常為了適應(yīng)多種業(yè)務(wù)場景,會(huì)包含大量通用代碼和復(fù)雜邏輯。以 Kryo 為例,其項(xiàng)目代碼行數(shù)超過 2 萬行,這使得短期內(nèi)很難掌握所有細(xì)節(jié),一旦出現(xiàn)問題可能會(huì)阻礙開發(fā)進(jìn)度,并且不一定能按期解決序列化問題。不過,開源框架技術(shù)成熟,適合作為長期方案。

思路二的優(yōu)勢在于既可以借鑒其他框架的優(yōu)化策略,又可以低成本地針對(duì)特定對(duì)象進(jìn)行定制優(yōu)化,從而實(shí)現(xiàn)更高的序列化效率。雖然在安全性方面,需要通過單元測試來保障,但開發(fā)一個(gè)針對(duì)特定應(yīng)用場景的序列化方法相對(duì)簡單。考慮到排序框架接口的參數(shù)對(duì)象不經(jīng)常更改,這種方法可以做到一次開發(fā)、長期受益。因此,我們傾向于選擇思路二。

整理思路后,序列化開發(fā)可以按照以下步驟進(jìn)行:

定義字節(jié)數(shù)組的序列化數(shù)據(jù)結(jié)構(gòu)

  • 確定如何將對(duì)象數(shù)據(jù)映射到字節(jié)數(shù)組的格式中,包括字段的順序、類型,以及如何處理可變長度數(shù)據(jù)。

定義序列化接口并實(shí)現(xiàn)具體的序列化類:

  • 創(chuàng)建一個(gè)通用的序列化接口,用于定義序列化和反序列化的方法。
  • 為每個(gè)需要序列化的對(duì)象類型實(shí)現(xiàn)具體的序列化類,確保符合接口的要求。

定義序列化過程的數(shù)據(jù)緩沖類:

  • 開發(fā)一個(gè)用于在序列化和反序列化過程中暫存數(shù)據(jù)的緩沖類,以便有效管理字節(jié)數(shù)組的讀寫操作。

實(shí)現(xiàn)各對(duì)象的具體序列化方法:

  • 為每個(gè)對(duì)象類型實(shí)現(xiàn)具體的序列化和反序列化方法,將對(duì)象數(shù)據(jù)轉(zhuǎn)換為字節(jié)數(shù)組或從字節(jié)數(shù)組重構(gòu)對(duì)象。

序列化結(jié)果最終要存儲(chǔ)在字節(jié)數(shù)組(byte[])中,因此定義如何存儲(chǔ)是我們的首要任務(wù)。

一個(gè)排序?qū)ο蟀S多內(nèi)容。為了簡化存儲(chǔ)過程并便于編寫代碼,我們采用了一種類似樹狀的存儲(chǔ)結(jié)構(gòu),與其他序列化方式大致相同。這種結(jié)構(gòu)將排序?qū)ο蟮恼w作為根節(jié)點(diǎn),然后按照對(duì)象的層次結(jié)構(gòu)逐級(jí)展開存儲(chǔ)。

與其他序列化方式不同的是,我們考慮到排序過程中對(duì)所有商品都會(huì)執(zhí)行相同的操作,因此商品類的特征 Map、結(jié)果 Map 和日志 Map 的存儲(chǔ)鍵集合在實(shí)際應(yīng)用中是保持一致的。由于這些鍵是可以復(fù)用的,我們將其提取出來并統(tǒng)一存儲(chǔ)在 items_common 中。這樣一來,Map 的值可以按照固定的順序進(jìn)行鏈?zhǔn)酱鎯?chǔ),這種方法不僅節(jié)省了空間,還提升了存儲(chǔ)效率。

圖片

為了進(jìn)一步降低代碼復(fù)雜度,還需要定義統(tǒng)一的接口,再將各個(gè)成員序列化過程分解到多個(gè)具體實(shí)現(xiàn)類中

自定義序列化方法接口定義如下:

public interface IRankObjSerializer<T> {
 
    int estimateUsage(T obj, RankObjSerializeContext context);
 
    void serialize(T obj, RankObjSerializeContext context) throws Exception;
 
    T deserialize(RankObjDeserializeContext context) throws Exception;
}

方法的含義如下:

estimateUsage:快速評(píng)估序列化對(duì)象的長度。

  • 在序列化過程中,如果字節(jié)數(shù)組的容量不足,就需要?jiǎng)?chuàng)建一個(gè)新數(shù)組,其大小為當(dāng)前大小的兩倍,并復(fù)制已有數(shù)據(jù)。這一過程需要進(jìn)行 log(最終大小) - log(初始大小) 次擴(kuò)容操作。estimateUsage 方法通過快速評(píng)估一個(gè)稍大于或接近最終大小的初始容量,來減少擴(kuò)容次數(shù),提高效率。

serialize:用于序列化對(duì)象。

  • 序列化過程中,context 包含一個(gè) Output 對(duì)象,用于處理字節(jié)數(shù)據(jù)的輸出,同時(shí)還包括 metricMap、resultMap 等公共的 keySet,這些 keySet 用于統(tǒng)一管理序列化過程中的鍵集合。

deserialize:用于反序列化對(duì)象。

  • 反序列化過程中,context 包含一個(gè) Input 對(duì)象,用于處理字節(jié)數(shù)據(jù)的輸入,此外,還包括 metricMap、resultMap 等公共的 keySet,以確保反序列化時(shí)使用的鍵集合與序列化時(shí)一致。

通過這些方法,可以更有效地管理對(duì)象的序列化和反序列化過程,提升整體性能和資源利用率。

根據(jù)響應(yīng)對(duì)象的數(shù)據(jù)層次,序列化過程需要針對(duì)不同的類型進(jìn)行拆解,并為每種具體類型設(shè)計(jì)相應(yīng)的序列化類。以下是各類序列化器的設(shè)計(jì):

GeneralObjSerializer:

  • 負(fù)責(zé)序列化和反序列化 Java 的基本數(shù)據(jù)類型(如 int、float、double 等)以及字符串 (String) 類型。
  • 提供方法將基本數(shù)據(jù)類型和字符串轉(zhuǎn)換為字節(jié)數(shù)組,并在反序列化時(shí)將字節(jié)數(shù)組轉(zhuǎn)換回相應(yīng)的基本類型或字符串。

GeneralMapSerializer(用于基本 Map 類型,按順序存儲(chǔ)鍵值對(duì)):

  • 負(fù)責(zé)序列化和反序列化 Map 對(duì)象。該類按特定順序存儲(chǔ) Map 的鍵值對(duì)(key-value),確保在反序列化時(shí)可以恢復(fù) Map 的原始狀態(tài)。
  • 支持常見的 Map 實(shí)現(xiàn)(如 HashMap、TreeMap 等),并處理可能的空鍵或空值。

GeneralListSerializer(

  • 負(fù)責(zé)序列化和反序列化 List 對(duì)象,能夠?qū)?List 轉(zhuǎn)換為字節(jié)數(shù)組,并在反序列化時(shí)恢復(fù) List 的原始結(jié)構(gòu)和內(nèi)容。
  • 適用于各種 List 實(shí)現(xiàn)(如 ArrayList、LinkedList 等),并處理列表中的空元素。

GeneralSetSerializer:

  • 負(fù)責(zé)序列化和反序列化 Set 對(duì)象,將 Set 中的元素序列化為字節(jié)數(shù)組并按序恢復(fù)。
  • 支持常見的 Set 實(shí)現(xiàn)(如 HashSet、TreeSet 等),并確保在反序列化后保持 Set 的無序性和唯一性。

RankResultSerializer:

  • 負(fù)責(zé)序列化和反序列化 RankResult 對(duì)象,處理與排序結(jié)果相關(guān)的數(shù)據(jù)結(jié)構(gòu)和字段。
  • 該類將 RankResult 的復(fù)雜對(duì)象和嵌套結(jié)構(gòu)序列化為字節(jié)數(shù)組,并在反序列化時(shí)重構(gòu)完整的 RankResult 對(duì)象。

RankResultItemSerializer:

  • 專門用于序列化和反序列化 RankResultItem 對(duì)象,處理單個(gè)排序結(jié)果項(xiàng)的序列化。
  • 負(fù)責(zé)將每個(gè) RankResultItem 的各個(gè)字段(包括特征、結(jié)果和日志等)轉(zhuǎn)換為字節(jié)數(shù)組,并在反序列化時(shí)恢復(fù)其內(nèi)容和結(jié)構(gòu)。

RankResponseSerializer:

  • 用于序列化和反序列化 RankResponse 對(duì)象,管理整個(gè)排序響應(yīng)的序列化過程。
  • 該類負(fù)責(zé)將完整的響應(yīng)數(shù)據(jù),包括所有 RankResultItem,序列化為字節(jié)數(shù)組,并在反序列化時(shí)重建整個(gè) RankResponse 對(duì)象。

序列化過程中依次將寫入到一段足夠長的byte數(shù)組里,序列化完成時(shí)再一次性讀出所有寫入數(shù)據(jù),定義Output類作為序列化過程中的數(shù)據(jù)緩沖(同樣有Input類作用于反序列化,實(shí)現(xiàn)類似)

class Output {
    byte[] data;
    int offset;
 
    Output(int estimateUsage) {
        data = new byte[estimateUsage];
        offset = 0;
    }
     
 
    void writeInt(int);
    void writeLong(long);
    void writeFloat(float);
    void writeBytes(byte[]);
    ...
}

data:作為序列化數(shù)據(jù)的緩沖區(qū),為了寫入效率最高,緩沖區(qū)是連續(xù)且足夠長的byte數(shù)組,足夠長由入?yún)stimateUsage來保證

offset:是下一個(gè)要寫入數(shù)據(jù)的位置,如果offset >= 數(shù)組長度,則需要擴(kuò)容,擴(kuò)容每次按兩倍擴(kuò)容

estimateUsage的準(zhǔn)確性影響了擴(kuò)容次數(shù),進(jìn)而影響序列化效率,經(jīng)測試從以32為起始容量初始化并逐漸擴(kuò)容到所需容量與直接使用estimateUsage初始化,序列化耗時(shí)相差20%左右

writeInt、writeLong:整型和長整型的寫入是可變長的,雖然int和long分別使用了32bit和64bit的空間,但如1、2、8、64等較小的數(shù)字只是用了前8bit的空間,一般可變長序列化采取的做法是將每8bit為一組,低7位存儲(chǔ)真實(shí)數(shù)據(jù),高位存儲(chǔ)標(biāo)識(shí)符,表明更高位是否仍存在更多數(shù)據(jù),可變長編碼下整型需要1~5byte,長整型則需要1~10byte,存儲(chǔ)數(shù)字值越小時(shí),可變長的壓縮效果越好。讀取時(shí)再從低位依次向高位讀取,直到標(biāo)識(shí)符表明數(shù)據(jù)讀取完畢,當(dāng)緩沖區(qū)剩余長度不足可變長的最大長度時(shí),需要調(diào)用readInt_slow或readLong_slow方法,逐個(gè)byte讀取并判斷是否越界

writeFloat、writeDouble:這兩種類型不能直接寫入,需要調(diào)用Float.floatToRawIntBits和Double.doubleToRawLongBits轉(zhuǎn)為Integer型和Long型。我們的特征由于特征默認(rèn)值等原因存在大量0.0、-1.0、1.0等數(shù)值,但在可變長存儲(chǔ)下,轉(zhuǎn)int后實(shí)際占用位數(shù)很長,優(yōu)化方式是轉(zhuǎn)換前先判斷了它是否為整型數(shù)字,如是整型就取整后直接存為整型,可將原本需要5~10位的存儲(chǔ)空間節(jié)省到1位,一個(gè)較為快速的判斷方式為:

void checkDoubleIsIntegerValue(double d) {
    return ((long)d == d);
}

多數(shù)序列化實(shí)現(xiàn)按待序列化的各個(gè)成員類型依次調(diào)用對(duì)應(yīng)序列化方法即可

Item間的共享數(shù)據(jù)處理,是本次序列化優(yōu)化最核心的優(yōu)化點(diǎn),對(duì)序列化效率提升有決定性影響,如特征/結(jié)果/日志Map的keySet的存儲(chǔ)復(fù)用,具體做法是

讀取第一個(gè)Item的所有keySet并保存在序列化上下文中,作為基準(zhǔn)數(shù)據(jù),后續(xù)每個(gè)Item都與第一個(gè)的keySet判斷,完全相同就按第一個(gè)item的相同順序?qū)alues依次取出,按隊(duì)列存儲(chǔ),快速的判斷方式如下:

private static boolean isNotEqualSet(Set<String> set1, Set<String> set2) {
        return set1 == null || set2 == null || (set1.size() != set2.size()) || !set1.containsAll(set2);
 }

當(dāng)任意商品不滿足keySet一致性的要求時(shí),Item序列化方法會(huì)向上拋出異常,排序框架會(huì)捕獲到該異常,并將返回的壓縮響應(yīng)對(duì)象(CompressedRankResponse)退化為普通響應(yīng)對(duì)象(RankResponse)

異常行為會(huì)根據(jù)用戶的選擇上報(bào)給監(jiān)控平臺(tái),或需要排查問題時(shí)選擇打印到本地文件

上游服務(wù)無需關(guān)心排序服務(wù)返回了哪種響應(yīng)對(duì)象,這是因?yàn)槠胀憫?yīng)對(duì)象和序列化后的壓縮響應(yīng)對(duì)象實(shí)現(xiàn)了同一接口

即原RankResponse對(duì)象和新CompressedRankResponse對(duì)象實(shí)現(xiàn)了IRankResponse接口,CompressedRankResponse是RankResponse的裝飾器對(duì)象

CompressedRankResponse對(duì)象在用戶調(diào)用任意方法,且當(dāng)內(nèi)置RankResponse對(duì)象為空時(shí)完成反序列化,如下段代碼中的getStatus方式所示

后續(xù)再調(diào)用其他方法在使用體驗(yàn)上是與未壓縮對(duì)象一致的,這種與直接返回byte數(shù)組相比,業(yè)務(wù)使用更友好,異常時(shí)可以快速降級(jí),也沒有太多帶來額外成本

IRankResponse rank(RankRequest request);
 
class RankResponse implements IRankResponse;
 
class CompressedRankResponse implements IRankResponse {
    byte[] bytes; // 排序服務(wù)返回的數(shù)據(jù)
     
    RankResponse response = null; // 調(diào)用任意方法后反序列化生成的數(shù)據(jù)
     
    public int getStatus() {
        if(response == null) {
            // 執(zhí)行反序列化
            response = this.doDeserilize();
        }
        return response.getStatus();
    }
}

模擬搜索500個(gè)商品,測試2000次,序列化前大小1430640

第一次測試:實(shí)驗(yàn)組為V3優(yōu)化,對(duì)照組為無優(yōu)化

方法

序列化時(shí)間

反序列化時(shí)間

總時(shí)間

數(shù)據(jù)大小 (bytes)

壓縮比率

SCFV4 序列化原方法

1.86ms

1.19ms

3.05ms

1,188,961

0.8310

框架自定義序列化方法

0.32ms

0.17ms

0.49ms

392,127

0.2741

優(yōu)化降低

82.80%

85.71%

83.93%

67.02%

67.02%

第二次測試:實(shí)驗(yàn)組為V3優(yōu)化,對(duì)照組為V2優(yōu)化

方法

序列化時(shí)間

反序列化時(shí)間

總時(shí)間

數(shù)據(jù)大小 (bytes)

壓縮比率

框架自定義序列化方法

0.41ms

0.20ms

0.61ms

393,001

0.274

帶日志 byte 壓縮 + SCF 序列化方法

3.10ms

1.00ms

4.10ms

503,961

0.35

優(yōu)化降低

-

-

85.12%

21.7%

21.7%

可見V3對(duì)序列化過程的執(zhí)行效率提升明顯

以下是業(yè)務(wù)接入情況 

搜索側(cè)接入:測試接入排序服務(wù)耗時(shí)于未服務(wù)化時(shí)持平,滿足上線要求

推薦測接入:序列化過程在2ms左右完成,場景接入后耗時(shí)均有明顯下降,符合預(yù)期

場景

優(yōu)化前

優(yōu)化后

提升時(shí)間

百分比

找靚機(jī)微詳情頁推薦

69ms

64ms

5ms

7.24%

轉(zhuǎn)轉(zhuǎn) B2C 詳情頁

97ms

94ms

3ms

3.09%

轉(zhuǎn)轉(zhuǎn) C2C 詳情頁

92ms

87ms

5ms

5.43%

轉(zhuǎn)轉(zhuǎn)首頁推薦 3C 頁

112ms

106ms

6ms

5.36%

轉(zhuǎn)轉(zhuǎn)首頁推薦默認(rèn)

130ms

128ms

2ms

1.54%

4 總結(jié)

本項(xiàng)目旨在解決搜索推薦服務(wù)化過程中因日志傳輸引起的序列化額外耗時(shí)問題。經(jīng)過三次版本迭代和測試,最終方案成功落地。

結(jié)論

本地測試:

  • 與優(yōu)化前相比,排序響應(yīng)對(duì)象的序列化過程節(jié)省了約 83% 的序列化開銷,網(wǎng)絡(luò)開銷減少了約 67%。搜索:
  • 有效降低了排序服務(wù)響應(yīng)中的序列化過程對(duì)搜索接口整體耗時(shí)的影響,使得新的搜索排序服務(wù)在性能上達(dá)到了上線要求。推薦:
  • 在推薦排序服務(wù)化后,接入本項(xiàng)目方案,在多個(gè)展位實(shí)現(xiàn)了接口整體耗時(shí)絕對(duì)值降低 2ms 到 6ms 的性能提升。

思考

從問題發(fā)現(xiàn)到解決上線,項(xiàng)目歷時(shí)近一個(gè)月。雖然問題定位較為迅速,但在確定最終方案和落地時(shí)經(jīng)歷了較長的周期。方案設(shè)計(jì)過程中有兩點(diǎn)需要注意:

方案評(píng)估要更細(xì)致:

  • 不要急于實(shí)施方案,要及時(shí)暴露性能瓶頸。例如,前期如果發(fā)現(xiàn)日志上報(bào)超出了 Redis 單節(jié)點(diǎn) 10 萬 QPS 的瓶頸,在考慮實(shí)施 V1 方案時(shí)會(huì)更加謹(jǐn)慎。

方案設(shè)計(jì)要更具全局性:

  • 不應(yīng)僅將視角局限于字符串序列化過程,而是從整個(gè)序列化過程的角度出發(fā)。這樣可以更快地跳過 V2 方案,直接進(jìn)入更高效的 V3 方案。

后續(xù)工作

廢棄遺留代碼:

  • 遺留代碼增加了開發(fā)的不確定性和風(fēng)險(xiǎn)性,因此需要廢棄 SCF 原生序列化方法和 V1 方法。

召回框架的序列化優(yōu)化:

  • 召回服務(wù)框架的序列化優(yōu)化尚未啟動(dòng),預(yù)計(jì)也能獲得顯著的性能提升。然而,與排序框架相比,召回框架涉及的傳輸對(duì)象類型更多,優(yōu)化難度更高,因此方案需要在排序優(yōu)化的基礎(chǔ)上進(jìn)一步調(diào)整。
責(zé)任編輯:武曉燕 來源: 轉(zhuǎn)轉(zhuǎn)技術(shù)
相關(guān)推薦

2011-06-01 15:05:02

序列化反序列化

2018-03-19 10:20:23

Java序列化反序列化

2009-06-14 22:01:27

Java對(duì)象序列化反序列化

2012-04-13 10:45:59

XML

2009-09-09 14:45:41

XML序列化和反序列化

2009-09-09 15:47:27

XML序列化和反序列化

2021-08-30 12:25:12

Python序列化函數(shù)

2009-03-10 13:38:01

Java序列化字節(jié)流

2022-08-06 08:41:18

序列化反序列化Hessian

2016-12-05 18:32:08

序列化androidjava

2010-05-14 10:55:04

java對(duì)象序列化

2012-02-14 10:29:02

Java

2009-08-25 15:15:08

C#對(duì)象序列化應(yīng)用

2024-03-05 12:49:30

序列化反序列化C#

2009-08-24 17:14:08

C#序列化

2021-11-18 11:48:46

ObjectInputJava

2011-06-01 14:26:11

序列化

2011-05-18 15:20:13

XML

2023-12-13 13:49:52

Python序列化模塊

2009-08-06 11:16:25

C#序列化和反序列化
點(diǎn)贊
收藏

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

主站蜘蛛池模板: a国产一区二区免费入口 | 999精品在线 | 精品国产视频 | 久久久免费毛片 | 色精品| 亚洲免费精品 | 欧美视频偷拍 | 伊人久久免费 | 九九色综合 | 精品国产青草久久久久96 | 精产国产伦理一二三区 | 国产精品自产拍 | 亚洲精品一| 国产aa | 日本精品久久久一区二区三区 | 中文字幕亚洲精品 | 国产日韩欧美一区 | 久久88| 97国产一区二区精品久久呦 | 成人高潮片免费视频欧美 | 一区二区亚洲 | 蜜桃在线一区二区三区 | 国产精品美女久久久久aⅴ国产馆 | www.夜夜骑 | 国产高清一区二区三区 | 蜜月aⅴ国产精品 | 日韩在线视频一区 | 天天干.com| 午夜视频在线免费观看 | 一级黄色大片 | 久久久视频在线 | 国产精品五月天 | 色橹橹欧美在线观看视频高清 | 国产乱一区二区三区视频 | 久久免费视频网 | 精品亚洲国产成av人片传媒 | 国产精品视频久久久久 | 一级片在线视频 | 日韩高清在线 | 久久久123| 麻豆成人在线视频 |