用 Go 跑的更快:使用 Golang 為機器學習服務(wù)

# 用 Go 跑的更快:使用 Golang 為機器學習服務(wù) 因此,我們的要求是用盡可能少的資源完成每秒300萬次的預(yù)測迂苛。值得慶幸的是沟绪,這是一種比較簡單的推薦系統(tǒng)模型,即多臂老虎機(MAB)讹蘑。多臂老虎機通常涉及從 [Beta 分布](https://en.wikipedia.org/wiki/Beta_distribution) 等分布中取樣末盔。這也是花費時間最多的地方。如果我們能同時做盡可能多的采樣座慰,我們就能很好地利用資源陨舱。最大限度地提高資源利用率是減少模型所需總體資源的關(guān)鍵。 我們目前的預(yù)測服務(wù)是用 Python 編寫的微服務(wù)版仔,它們遵循以下一般結(jié)構(gòu): > 請求->功能獲取->預(yù)測->后期處理->返回 一個請求可能需要我們對成千上萬的用戶隅忿、內(nèi)容對進行評分。帶有 GIL 和多進程的 Python 處理性能很雞肋邦尊,我們已經(jīng)實現(xiàn)了基于 cython 和 C++ 的批量采樣方法,繞過了GIL优烧,我們使用了許多基于內(nèi)核數(shù)量的 workers 來并發(fā)處理請求蝉揍。 目前單節(jié)點的 Python 服務(wù)可以做192個 RPS ,每個大約400對畦娄。平均 CPU 利用率只有20%左右∮终矗現(xiàn)在的限制因素是語言、服務(wù)框架和對存儲功能的網(wǎng)絡(luò)調(diào)用熙卡。 ## 為什么是 Golang? Golang 是一種靜態(tài)類型的語言杖刷,具有很好的工具性。這意味著錯誤會被及早發(fā)現(xiàn)驳癌,而且很容易重構(gòu)代碼滑燃。Golang 的并發(fā)性是原生的,這對于可以并行運行的機器學習算法和對 Featurestore 的并發(fā)網(wǎng)絡(luò)調(diào)用非常重要颓鲜。它是 [這里](https://www.techempower.com/benchmarks/) 基準最快的服務(wù)語言之一表窘。它也是一種編譯語言,所以它在編譯時可以進行很好的優(yōu)化甜滨。 ## 移植現(xiàn)有的 MAB 到 Golang 上 基本思路乐严,將系統(tǒng)分為3個部分: - 用于預(yù)測和健康的基本 REST API 與存根 - Featurestore 的獲取,為此實現(xiàn)一個模塊 - 使用 [cgo](https://pkg.go.dev/cmd/cgo) 提升和轉(zhuǎn)移 c++ 的采樣代碼 第一部分很容易衣摩,我選擇了 [Fiber](https://gofiber.io/) 框架用于REST API昂验。它似乎是最受歡迎的,有很好的文檔艾扮,類似 Expressjs 的API既琴。而且它在基準測試中的表現(xiàn)也相當出色。 早期代碼: ``` func main() { // setup fiber app := fiber.New() // catch all exception app.Use(recover.New()) // load model struct ctx := context.Background() md, err := model.NewModel(ctx) if err != nil { fmt.Println(err) } defer md.Close() // health API app.Get("/health", func(c *fiber.Ctx) error { if err != nil { return fiber.NewError( fiber.StatusServiceUnavailable, fmt.Sprintf("Model couldn't load: %v", err)) } return c.JSON(&fiber.Map{ "status": "ok", }) }) // predict API app.Post("/predict", func(c *fiber.Ctx) error { var request map[string]interface{} err := json.Unmarshal(c.Body(), &request) if err != nil { return err } return c.JSON(md.Predict(request)) }) ``` 就這樣栏渺,任務(wù)一完成了呛梆。花了不到一個小時磕诊。 在第二部分中填物,需要稍微學習一下如何編寫 [帶方法的結(jié)構(gòu)](https://gobyexample.com/methods) 和 [goroutines](https://gobyexample.com/channels) 纹腌。與 C++ 和 Python 的主要區(qū)別之一是,Golang 不支持完全的面向?qū)ο缶幊讨突牵饕遣恢С掷^承升薯。它在結(jié)構(gòu)體上的方法的定義方式也與我遇到的其他語言完全不同。 我們使用的 Featurestore 有 [Golang 客戶端](https://cloud.google.com/go/docs/reference/cloud.google.com/go/aiplatform/latest/apiv1#cloud_google_com_go_aiplatform_apiv1_FeaturestoreOnlineServingClient) 击困,我所要做的就是在它周圍寫一個封裝器來讀取大量并發(fā)的實體涎劈。 我想要的基本結(jié)構(gòu)是: ``` type VertexFeatureStoreClient struct { //client reference to gcp's client } func NewVertexFeatureStoreClient(ctx context.Context,) (*VertexFeatureStoreClient, error) { // client creation code } func (vfs *VertexFeatureStoreClient) GetFeaturesByIdsChunk(ctx context.Context, featurestore, entityName string, entityIds []string, featureList []string) (map[string]map[string]interface{}, error) { // fetch code for 100 items } func (vfs *VertexFeatureStoreClient) GetFeaturesByIds(ctx context.Context, featurestore, entityName string, entityIds []string, featureList []string) (map[string]map[string]interface{}, error) { const chunkSize = 100 // limit from GCP // code to run each fetch concurrently featureChannel := make(chan map[string]map[string]interface{}) errorChannel := make(chan error) var count = 0 for i := 0; i < len(entityIds); i += chunkSize { end := i + chunkSize if end > len(entityIds) { end = len(entityIds) } go func(ents []string) { features, err := vfs.GetFeaturesByIdsChunk(ctx, featurestore, entityName, ents, featureList) if err != nil { errorChannel <- err return } featureChannel <- features }(entityIds[i:end]) count++ } results := make(map[string]map[string]interface{}, len(entityIds)) for { select { case err := <-errorChannel: return nil, err case res := <-featureChannel: for k, v := range res { results[k] = v } } count-- if count < 1 { break } } return results, nil } func (vfs *VertexFeatureStoreClient) Close() error { //close code } ``` #### 關(guān)于 Goroutine 的提示 盡量多使用通道,有很多教程使用 Goroutine 的 sync workgroups阅茶。那些是較低級別的 API蛛枚,在大多數(shù)情況下你都不需要。通道是運行Goroutine 的優(yōu)雅方式脸哀,即使你不需要傳遞數(shù)據(jù)蹦浦,你可以在通道中發(fā)送標志來收集。goroutines 是廉價的虛擬線程撞蜂,你不必擔心制造太多的線程并在多個核心上運行盲镶。最新的 golang 可以為你跨核心運行。 關(guān)于第三部分蝌诡,這是最難的部分溉贿。花了大約一天的時間來調(diào)試它浦旱。所以宇色,如果你的用例不需要復(fù)雜的采樣和 C++,我建議直接使用 [Gonum](https://www.gonum.org/) 闽寡,你會為自己節(jié)省很多時間代兵。 我沒有意識到,從 cython 來的時候爷狈,我必須手動編譯 C++ 文件植影,并將其加載到 cgo include flags 中。 頭文件: ``` #ifndef BETA_DIST_H #define BETA_DIST_H #ifdef __cplusplus extern "C" { #endif double beta_sample(double, double, long); #ifdef __cplusplus } #endif #endif ``` 注意 extern C 涎永,這是 C++ 代碼在 go 中使用的需要思币,由于 [mangling](https://en.wikipedia.org/wiki/Name_mangling) ,C 不需要羡微。另一個問題是谷饿,我不能在頭文件中做任何#include語句,在這種情況下 cgo 鏈接失斅杈蟆(原因不明)博投。所以我把這些語句移到 .cpp 文件中。 編譯它: ``` g++ -fPIC -I/usr/local/include -L/usr/local/lib betadist.cpp -shared -o libbetadist.so ``` 一旦編譯完成盯蝴,你就可以使用它的 cgo毅哗。 cgo 包裝文件: ``` /* #cgo CPPFLAGS: -I${SRCDIR}/cbetadist #cgo CPPFLAGS: -I/usr/local/include #cgo LDFLAGS: -Wl,-rpath,${SRCDIR}/cbetadist #cgo LDFLAGS: -L${SRCDIR}/cbetadist #cgo LDFLAGS: -L/usr/local/lib #cgo LDFLAGS: -lstdc++ #cgo LDFLAGS: -lbetadist #include */ import "C" func Betasample(alpha, beta float64, random int) float64 { return float64(C.beta_sample(C.double(alpha), C.double(beta), C.long(random))) } ``` 注意 LDFLAGS 中的 -lbetadist 是用來鏈接 libbetadist.so 的听怕。你還必須運行 export DYLD_LIBRARY_PATH=/fullpath_to/folder_containing_so_file/ 。然后我可以運行 go run . 虑绵,它能夠像 go 項目一樣工作尿瞭。 將它們與簡單的模型結(jié)構(gòu)和預(yù)測方法整合在一起是很簡單的,而且相對來說花費的時間更少翅睛。 ## 結(jié)果 ![](https://upload-images.jianshu.io/upload_images/28199768-db5a5d736fb77f1d.png) | Metric | Python | Go | | :--------- | :-------: | --------: | | Max RPS | 192 | 819 | | Max latency | 78ms | 110ms | | Max CPU util. | ~20% | ~55% | 這是對 RPS 的**4.3倍**的提升声搁,這使我們的最低節(jié)點數(shù)量從80個減少到19個,這是一個巨大的成本優(yōu)勢捕发。最大延遲略高疏旨,但這是可以接受的,因為 python 服務(wù)在192點時就已經(jīng)飽和了扎酷,如果流量超過這個數(shù)字充石,就會明顯下降。 ## 我應(yīng)該把我所有的模型轉(zhuǎn)換為 Golang 嗎霞玄? 簡短的答案:不用。 長答案拉岁。Go 在服務(wù)方面有很大的優(yōu)勢坷剧,但 Python 仍然是實驗的王道。我只建議在模型簡單且長期運行的基礎(chǔ)模型中使用 Go喊暖,而不是實驗惫企。Go 對于復(fù)雜的 ML 用例來說 [尚](https://github.com/josephmisiti/awesome-machine-learning#go) 不是很成熟。 ## 所以房間里的大象陵叽,為什么不是 Rust 狞尔? 嗯,[希夫做到了](http://shvbsle.in/serving-ml-at-the-speed-of-rust/) 巩掺∑颍看看吧。它甚至比 Go 還快胖替。 ![](https://upload-images.jianshu.io/upload_images/28199768-4f83dc96231c4cd4.png) 本文由[mdnice](https://mdnice.com/?platform=6)多平臺發(fā)布
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末研儒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子独令,更是在濱河造成了極大的恐慌端朵,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燃箭,死亡現(xiàn)場離奇詭異冲呢,居然都是意外死亡,警方通過查閱死者的電腦和手機招狸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門敬拓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邻薯,“玉大人,你說我怎么就攤上這事恩尾〕谒担” “怎么了?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵翰意,是天一觀的道長木人。 經(jīng)常有香客問我,道長冀偶,這世上最難降的妖魔是什么醒第? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮进鸠,結(jié)果婚禮上稠曼,老公的妹妹穿的比我還像新娘。我一直安慰自己客年,他們只是感情好霞幅,可當我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著量瓜,像睡著了一般司恳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绍傲,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天扔傅,我揣著相機與錄音,去河邊找鬼烫饼。 笑死猎塞,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的杠纵。 我是一名探鬼主播荠耽,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼比藻!你這毒婦竟也來了议泵?” 一聲冷哼從身側(cè)響起醉冤,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤平痰,失蹤者是張志新(化名)和其女友劉穎藕坯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體群凶,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡插爹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赠尾。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡力穗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出气嫁,到底是詐尸還是另有隱情当窗,我是刑警寧澤,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布寸宵,位于F島的核電站崖面,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏梯影。R本人自食惡果不足惜巫员,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甲棍。 院中可真熱鬧简识,春花似錦、人聲如沸感猛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陪白。三九已至戳寸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拷泽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工袖瞻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留司致,地道東北人。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓聋迎,卻偏偏與公主長得像脂矫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子霉晕,可洞房花燭夜當晚...
    茶點故事閱讀 43,566評論 2 349

推薦閱讀更多精彩內(nèi)容