馬蜂窩搜索基于Golang并發(fā)代理的一次架構(gòu)升級(jí)
搜索業(yè)務(wù)是馬蜂窩流量分發(fā)的重要入口。很多用戶在使用馬蜂窩時(shí),都會(huì)有目的性地主動(dòng)搜索與自己旅行需求相關(guān)的各種信息,衣食住行,事無(wú)巨細(xì),從而做出***需求的旅行決策。
因此在馬蜂窩,搜索業(yè)務(wù)交互的下游模塊非常多,主要有目的地、POI、熱門景點(diǎn)、美食、商場(chǎng)、酒店、問答、攻略、機(jī)票火車票等等,通過實(shí)時(shí)、精準(zhǔn)地返回搜索結(jié)果,幫助用戶做出個(gè)性化旅行決策。
面對(duì)越來(lái)越高的流量,馬蜂窩技術(shù)團(tuán)隊(duì)積極嘗試對(duì)搜索架構(gòu)進(jìn)行優(yōu)化和升級(jí),來(lái)保證搜索業(yè)務(wù)的穩(wěn)定和性能。
方案背景
由于歷史原因,優(yōu)化前的搜索服務(wù)與下游模塊交的互方式主要為調(diào)用各下游模塊提供的函數(shù),并且采用串行調(diào)用。
圖 1: 馬蜂窩搜索業(yè)務(wù)架構(gòu)和技術(shù)體系
搜索技術(shù)體系
- 存儲(chǔ)——MySQL、Memcache
- 模塊交互——Function Call
- 檢索——Elasticsearch
搜索業(yè)務(wù)架構(gòu)
我們將搜索業(yè)務(wù)抽象為三個(gè)功能模塊:
1. 決策系統(tǒng)
負(fù)責(zé)根據(jù)用戶意圖、運(yùn)營(yíng)策略、點(diǎn)擊日志等數(shù)據(jù),結(jié)合決策系統(tǒng)相關(guān)算法和模型,決策應(yīng)該展示哪些模塊(游記、商品等)及各模塊展示順序。
2. Agent
負(fù)責(zé)根據(jù)決策系統(tǒng)確定要展示的模塊,從 Elasticsearch 和業(yè)務(wù)方獲取模塊(如游記、商品等)數(shù)據(jù)。
3. Format
負(fù)責(zé)根據(jù)不同模塊的 UI 交互定義格式化數(shù)據(jù),補(bǔ)充 UI 交互缺失數(shù)據(jù)。
串行的函數(shù)級(jí)調(diào)用方式,使之前的搜索服務(wù)架構(gòu)存在一系列問題:
- 業(yè)務(wù)間耦合度高。隨著交互模塊越來(lái)越多,導(dǎo)致搜索服務(wù)耗時(shí)變得很長(zhǎng),平均達(dá)到 400-500 ms;
- 由于與各業(yè)務(wù)間交互的方式是 Function Call,使上游很難控制下游模塊阻塞時(shí)間;
- 下游調(diào)用增加響應(yīng)時(shí)間相應(yīng)呈線性增長(zhǎng),使其很難再疊加新的功能,可擴(kuò)展性差;
- 如果下游模塊出現(xiàn)故障,會(huì)由于接口阻塞引起超時(shí),導(dǎo)致搜索服務(wù)整體都受到影響,表現(xiàn)出白頁(yè),用戶體驗(yàn)嚴(yán)重下降。
圖 2:問題分析
因此,我們需要找到一種方式來(lái)降低搜索服務(wù)對(duì)于下游模塊的依賴,以及模塊間的耦合,從而提升架構(gòu)的整體可用性和性能。
基于 Golang 的并發(fā)代理實(shí)現(xiàn)
經(jīng)過調(diào)研,我們開發(fā)了基于 Golang 協(xié)程實(shí)現(xiàn)的并發(fā)請(qǐng)求代理工具,將之前函數(shù)級(jí)調(diào)用的方式變?yōu)榛?TCP/IP 的 HTTP 接口調(diào)用來(lái)與下游模塊解耦,同時(shí)將串行調(diào)用變?yōu)椴l(fā),實(shí)現(xiàn)超時(shí)控制和異常容錯(cuò)處理。
主要技術(shù)選型——協(xié)程(Goroutine)
Goroutine 是 Golang 輕量級(jí)線程實(shí)現(xiàn),由 Go runtime 管理。它是 Go 并行設(shè)計(jì)的核心,也是 Golang 最重要的特性之一,相比于進(jìn)程、線程任務(wù)的搶占式調(diào)度,需要頻繁進(jìn)行上下文信息的內(nèi)核和用戶空間切換,Goroutine 可以由程序控制,使得它更易用、更高效、更輕便。
Goroutine 維護(hù)了一組數(shù)據(jù)結(jié)構(gòu)和多個(gè)線程,任務(wù)放在一個(gè)待執(zhí)行隊(duì)列中,由 Goroutine 維護(hù)的線程來(lái)拉取執(zhí)行。當(dāng)任務(wù)執(zhí)行了操作系統(tǒng)的 IO 操作等需要等待時(shí),Goroutine 利用 Linux IO 多路復(fù)用技術(shù) (Epoll、Select) 進(jìn)行執(zhí)行隊(duì)列的任務(wù)切換來(lái)實(shí)現(xiàn)并發(fā)。
相比于其他語(yǔ)言的線程,其默認(rèn)占用內(nèi)存為 2KB, 遠(yuǎn)小于其他語(yǔ)言的 M 級(jí)別。在性能開銷方面,由于任務(wù)調(diào)度基本有程序控制,開銷也遠(yuǎn)小于線程。
選型的過程中,我們對(duì)比了 PHP 的 Swoole、Java 多線程并行處理方案,它們的 CPU 和內(nèi)存消耗比 Golang 的 Goroutine 要高出很多,并且并行請(qǐng)求數(shù)量會(huì)受到資源的限制,在高并發(fā)的情況下如果控制不當(dāng)會(huì)導(dǎo)致服務(wù)崩潰。而使用 Goroutine 實(shí)現(xiàn)的并發(fā)代理,可以輕松支持***別的并發(fā)請(qǐng)求。
圖 3:并行與并發(fā)
Golang 并發(fā)代理實(shí)現(xiàn)
代理服務(wù)按請(qǐng)求的處理流程,可以劃分為 HTTP Server ——> 參數(shù)處理——> 并行請(qǐng)求 (協(xié)程調(diào)度)——> HTTP 模塊 ——> API 層。目前我們的方案支持 HTTP/HTTPS 協(xié)議的請(qǐng)求。
圖 4:并發(fā)代理架構(gòu)圖
各模塊功能概要:
- HTTP Sever:使用 Go 語(yǔ)言 httpserver package 實(shí)現(xiàn),用于接收和處理有代理需求的上游模塊的 HTTP 請(qǐng)求;
- 參數(shù)處理:根據(jù)定義好的交互協(xié)議,將上游模塊的請(qǐng)求解析為并行請(qǐng)求商品、游記等下游模塊的請(qǐng)求任務(wù);
- 協(xié)程調(diào)度:使用 Go 語(yǔ)言的 Goroutine 實(shí)現(xiàn),負(fù)責(zé)執(zhí)行對(duì)下游模塊的并發(fā)請(qǐng)求任務(wù);
- HTTP 模塊:使用 Go 語(yǔ)言的 ioutil/http package 實(shí)現(xiàn),負(fù)責(zé)與下游 API 模塊以 HTTP 協(xié)議形式交互;
- API 模塊:將下游模塊的函數(shù)調(diào)用封裝為 TCP/IP接口,將函數(shù)形式交互變?yōu)?HTTP 接口形式交互。
搜索業(yè)務(wù)應(yīng)用代理后,整體架構(gòu)變化為:
圖 5:并發(fā)代理在搜索業(yè)務(wù)中的應(yīng)用
小結(jié)與后續(xù)規(guī)劃
基于 Golang 的并發(fā)代理在馬蜂窩搜索業(yè)務(wù)中已經(jīng)使用了一段時(shí)間,很好地解決了之前存在的一些問題。目前,搜索服務(wù)平均耗時(shí)已經(jīng)降低到240ms 左右,架構(gòu)的可用性和可擴(kuò)展性也得到很大提升,并且有效提高了系統(tǒng)資源的利用率。
現(xiàn)在并發(fā)代理只支持 HTTP,后續(xù)會(huì)增加 RPC,來(lái)更好地支持整體的服務(wù)化改造。在推進(jìn)和實(shí)施搜索架構(gòu)升級(jí)的過程中,我們也會(huì)把更多的經(jīng)驗(yàn)分享出來(lái),希望大家持續(xù)關(guān)注。
本文作者:王江濤,馬蜂窩搜素推薦研發(fā)工程師。
【本文是51CTO專欄作者馬蜂窩技術(shù)的原創(chuàng)文章,作者微信公眾號(hào)馬蜂窩技術(shù)(ID:mfwtech)】