馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

馬蜂窩技術發表於2019-03-29

搜尋業務是馬蜂窩流量分發的重要入口。很多使用者在使用馬蜂窩時,都會有目的性地主動搜尋與自己旅行需求相關的各種資訊,衣食住行,事無鉅細,從而做出最符合需求的旅行決策。

因此在馬蜂窩,搜尋業務互動的下游模組非常多,主要有目的地、POI、熱門景點、美食、商場、酒店、問答、攻略、機票火車票等等,通過實時、精準地返回搜尋結果,幫助使用者做出個性化旅行決策。

面對越來越高的流量,馬蜂窩技術團隊積極嘗試對搜尋架構進行優化和升級,來保證搜尋業務的穩定和效能。

方案背景

由於歷史原因,優化前的搜尋服務與下游模組交的互方式主要為呼叫各下游模組提供的函式,並且採用序列呼叫。

馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

圖 1: 馬蜂窩搜尋業務架構和技術體系

搜尋技術體系

  • 儲存——MySQL、Memcache

  • 模組互動——Function Call

  • 檢索——Elasticsearch

搜尋業務架構

我們將搜尋業務抽象為三個功能模組:

1. 決策系統

負責根據使用者意圖、運營策略、點選日誌等資料,結合決策系統相關演算法和模型,決策應該展示哪些模組(遊記、商品等)及各模組展示順序。

2. Agent

負責根據決策系統確定要展示的模組,從 Elasticsearch 和業務方獲取模組(如遊記、商品等)資料。

3. Format

負責根據不同模組的 UI 互動定義格式化資料,補充 UI 互動缺失資料。

序列的函式級呼叫方式,使之前的搜尋服務架構存在一系列問題:

  • 業務間耦合度高。隨著互動模組越來越多,導致搜尋服務耗時變得很長,平均達到 400-500 ms;

  • 由於與各業務間互動的方式是 Function Call,使上游很難控制下游模組阻塞時間;

  • 下游呼叫增加響應時間相應呈線性增長,使其很難再疊加新的功能,可擴充套件性差;

  • 如果下游模組出現故障,會由於介面阻塞引起超時,導致搜尋服務整體都受到影響,表現出白頁,使用者體驗嚴重下降。

馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

圖 2:問題分析

因此,我們需要找到一種方式來降低搜尋服務對於下游模組的依賴,以及模組間的耦合,從而提升架構的整體可用性和效能。

基於 Golang 的併發代理實現

經過調研,我們開發了基於 Golang 協程實現的併發請求代理工具,將之前函式級呼叫的方式變為基於 TCP/IP 的 HTTP 介面呼叫來與下游模組解耦,同時將序列呼叫變為併發,實現超時控制和異常容錯處理。

主要技術選型——協程(Goroutine)

Goroutine 是 Golang 輕量級執行緒實現,由 Go runtime 管理。它是 Go 並行設計的核心,也是 Golang 最重要的特性之一,相比於程式、執行緒任務的搶佔式排程,需要頻繁進行上下文資訊的核心和使用者空間切換,Goroutine 可以由程式控制,使得它更易用、更高效、更輕便。

Goroutine 維護了一組資料結構和多個執行緒,任務放在一個待執行佇列中,由 Goroutine 維護的執行緒來拉取執行。當任務執行了作業系統的 IO 操作等需要等待時,Goroutine 利用 Linux IO 多路複用技術 (Epoll、Select) 進行執行佇列的任務切換來實現併發。

相比於其他語言的執行緒,其預設佔用記憶體為 2KB, 遠小於其他語言的 M 級別。在效能開銷方面,由於任務排程基本有程式控制,開銷也遠小於執行緒。

選型的過程中,我們對比了 PHP 的 Swoole、Java 多執行緒並行處理方案,它們的 CPU 和記憶體消耗比 Golang 的 Goroutine 要高出很多,並且並行請求數量會受到資源的限制,在高併發的情況下如果控制不當會導致服務崩潰。而使用 Goroutine 實現的併發代理,可以輕鬆支援千萬級別的併發請求。

馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

圖 3:並行與併發

Golang 併發代理實現

代理服務按請求的處理流程,可以劃分為 HTTP Server ——> 引數處理——> 並行請求 (協程排程)——> HTTP 模組 ——> API 層。目前我們的方案支援 HTTP/HTTPS 協議的請求。

馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

圖 4:併發代理架構圖

各模組功能概要

  1. HTTP Sever:使用 Go 語言 httpserver package 實現,用於接收和處理有代理需求的上游模組的 HTTP 請求;

  2. 引數處理:根據定義好的互動協議,將上游模組的請求解析為並行請求商品、遊記等下游模組的請求任務;

  3. 協程排程:使用 Go 語言的 Goroutine 實現,負責執行對下游模組的併發請求任務;

  4. HTTP 模組:使用 Go 語言的 ioutil/http package 實現,負責與下游 API 模組以 HTTP 協議形式互動;

  5. API 模組:將下游模組的函式呼叫封裝為 TCP/IP介面,將函式形式互動變為 HTTP 介面形式互動。

搜尋業務應用代理後,整體架構變化為:

馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

圖 5:併發代理在搜尋業務中的應用

小結與後續規劃

基於 Golang 的併發代理在馬蜂窩搜尋業務中已經使用了一段時間,很好地解決了之前存在的一些問題。目前,搜尋服務平均耗時已經降低到240ms 左右,架構的可用性和可擴充套件性也得到很大提升,並且有效提高了系統資源的利用率。

現在併發代理只支援 HTTP,後續會增加 RPC,來更好地支援整體的服務化改造。在推進和實施搜尋架構升級的過程中,我們也會把更多的經驗分享出來,希望大家持續關注。

本文作者:王江濤,馬蜂窩搜尋推薦研發工程師。

關注馬蜂窩技術,找到更多你想要的內容

馬蜂窩搜尋基於 Golang 併發代理的一次架構升級

相關文章