性能分析: 一個 Go 編寫的簡單 HTTP Web 服務(wù)器的優(yōu)化方法

性能分析和調(diào)優(yōu)是一種很強(qiáng)大的技術(shù),用來驗(yàn)證是否滿足客戶關(guān)注的性能要求。性能分析常常被用來分析一個程序?qū)⒋蟛糠謺r間花在哪里了,并通過一個科學(xué)的方法來測試調(diào)優(yōu)實(shí)踐的效果。這個帖子使用一個 Go 語言編寫的 HTTP 服務(wù)作為一個例子來定義一種性能分析和調(diào)優(yōu)的普遍方法。go 特別適合性能分析和調(diào)優(yōu),因?yàn)樗谒臉?biāo)準(zhǔn)庫中提供了 pprof 剖析工具鏈。

策略

我們先嘗試建立一個簡單的框架來構(gòu)建對程序的分析。我們將要嘗試做的是使用數(shù)據(jù)引導(dǎo)我們得出結(jié)論,而不是基于直覺或者預(yù)感做出決定。為此我們將要:

  • 確定我們要優(yōu)化的維度(要求)
  • 創(chuàng)建一個測試代碼(harness)將事務(wù)負(fù)載到這個系統(tǒng)上
  • 執(zhí)行一個測試——(生成數(shù)據(jù))
  • 觀察
  • 分析——是否滿足要求?
  • 調(diào)優(yōu)——科學(xué)方法——形成一個假說
  • 執(zhí)行實(shí)驗(yàn)代碼來測試這個假說

簡單的 HTTP 服務(wù)器架構(gòu)

關(guān)于這個貼,我們將使用一個 Golang 編寫的小型 HTTP 服務(wù)器。這個貼的所有代碼都可以在 這里 找到。

我們將要分析的這個應(yīng)用是一個每次請求都查詢 PostgreSQL 的 HTTP 服務(wù)器。此外,通過 Prometheus, node_exporter, 和 Grafana 來收集和可視化應(yīng)用和系統(tǒng)級的指標(biāo):

為簡單起見,本文假設(shè)為了橫向擴(kuò)展(并簡化我們的計(jì)算),每個 HTTP 服務(wù)和 Postgres 數(shù)據(jù)庫將一起部署:

確定目標(biāo)(維度)

這一步概述了特定的目標(biāo)。我們將嘗試分析什么?我們?nèi)绾沃牢覀兊呐σ淹瓿桑?/p>

本文中,我們將假設(shè)客戶端均衡在我們的服務(wù)上,每秒為 10000 請求量。

Google SRE Book 在如何選擇和建模上有更深入的探討。在 SRE 的精髓上,我們將建立我們的模型如下:

  • 延遲—— 99% 的請求應(yīng)該在 60 ms 內(nèi)完成。
  • 費(fèi)用——這個服務(wù)應(yīng)該在我們認(rèn)為盡可能合理的最小費(fèi)用內(nèi)完成。為了達(dá)到這個目標(biāo),吞吐量應(yīng)該最大化。
  • 容量規(guī)劃——對要求啟動多少個實(shí)例和記錄通常情況下的縮放能力的理解。我們需要滿足預(yù)期初始負(fù)載要求并實(shí)現(xiàn) n + 1 redundancy 的實(shí)例數(shù)量是多少?

延遲可能需要除了分析之外的優(yōu)化,而吞吐量就只需要分析了。使用 SRE SLO 處理的延遲需求可能來自客戶端或者產(chǎn)品擁有者所代表的事務(wù)。真正值得一說的是,我們的服務(wù)能夠在一開始就滿足這種承諾而不需要任何調(diào)整!

設(shè)置測試代碼(test harness)

這個測試代碼將應(yīng)用一個固定總數(shù)的負(fù)載到我們的系統(tǒng)。為了分析 HTTP 服務(wù)的性能,數(shù)據(jù)將需要它來生成。

交互負(fù)載(transactional load)

這個測試代碼只使用了 Vegeta 以可配置的速率來產(chǎn)生 HTTP 請求直到停止:

$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta report

觀察

在執(zhí)行一個”無止境“的交互負(fù)載期間(負(fù)載測試)。除了應(yīng)用(請求速率,請求延遲)和系統(tǒng)級(內(nèi)存,CPU,IOPS)的指標(biāo)外,這時將通過剖析應(yīng)用來理解它將時間花費(fèi)在哪里了。

剖析(profiling)

profiling 是度量中的一類,讓我們了解應(yīng)用將時間花費(fèi)到哪里了。它能夠報告應(yīng)用將時間花費(fèi)在哪。Profileing 能夠用來確定哪個函數(shù)正在被調(diào)用,并且應(yīng)用在每個函數(shù)上花費(fèi)了多少時間:

這個數(shù)據(jù)可以用來可視化分析程序?qū)r間花在哪些不必要的工作上。Go(pprof)可以用來生成 profiles,并使用 標(biāo)準(zhǔn)工具鏈 將它們可視化為 火焰圖 。在本文后面,我們將通過使用它們來引導(dǎo)調(diào)優(yōu)的結(jié)論。

執(zhí)行、觀察、分析

我們開始執(zhí)行這些實(shí)踐。我們將執(zhí)行,觀察和分析直到我們的性能要求失效。先選擇任意一個低的負(fù)載量來生成第一份觀察報告和分析。如果每次的性能要求能夠 hold 得住,我們就通過一個隨機(jī)縮放因子(random-ish scaling factor)來增加負(fù)載。每次負(fù)載測試通過調(diào)整速率來執(zhí)行:

make load-test LOAD_TEST_RATE=X

50 個請求 / 秒

觀察上面兩張圖。左上角的是我們的應(yīng)用正在處理 50 個請求每秒,而右上角報告每個請求的延遲時間。將它們結(jié)合在一起來幫助我們觀察和分析我們的性能要求是否滿足。HTTP Request Latency 上的紅線在 SLO 的 60ms 上。這個表示我們最大響應(yīng)時間遠(yuǎn)遠(yuǎn)低于它。

在成本方面:

10k 請求量 / 秒 / 50 請求量 / 機(jī)器 = 200 臺機(jī)器 + 1

我們就可以很好的支持了。

500 請求量 / 秒

當(dāng)我們的請求數(shù)達(dá)到 500 請求量每秒時,事情開始變得有趣:

左上圖再一次展示了應(yīng)用的預(yù)期負(fù)載。如果它不是這樣,它可能會在主機(jī)上判定是負(fù)載測試命令的問題或者是應(yīng)用服務(wù)器的問題。右上角的延遲圖展示了應(yīng)對 500 請求量 / 秒時,每個 HTTP 請求的延遲時間在 25-40 ms 之間。99% 的請求仍然保持在 60 ms SLO 以下。

在成本方面:

10k 請求量 / 秒 / 500 請求量 / 機(jī)器 = 20 臺機(jī)器 + 1

就可以很好的支持!

1000 請求量 / 秒

這個太大了!應(yīng)用正在處理 1000 請求量 / 秒,但是延遲時間已經(jīng)超過 SLO 的延遲量。這個可以看右上角(原文是左上角,可能打錯了?)的圖中的 P99 線。而尾部的 p100 max 遠(yuǎn)大于最大限制量的 60ms,P99 線也在 60ms 以上。是時候查看和剖析應(yīng)用實(shí)際上正在做的事情了。

剖析(profile)

為了剖析,我們將使用 1000 請求量每秒的負(fù)載然后使用 pprof 來采樣這些棧獲得我們的程序?qū)⑺臅r間花費(fèi)在哪些地方。這個可以在負(fù)載被使用時,通過 pprof 的 HTTP 端點(diǎn),并用 curl 來跟蹤:

$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > CPU.1000_reqs_sec_no_optimizations.prof

這個跟蹤可以被可視化:

$ Go tool pprof -http=:12345 CPU.1000_reqs_sec_no_optimizations.prof

火焰圖展示了應(yīng)用在哪些地方花費(fèi)時間和在那里花費(fèi)了多少時間!來自 Brendan Gregg 的描述

x 軸展示了棧的橫截數(shù)量(profile population),按照字典序排列(注意,它不是通過調(diào)用時間長短排序的),y 軸表示棧的深度,從頂層以 0 開始計(jì)數(shù)。每個矩形表示一個棧幀。棧幀的寬度越寬,則它在棧中出現(xiàn)的次數(shù)越多。最底層顯示的是正在 CPU 中運(yùn)行的,在它上面的就是它的父函數(shù)。顏色通常沒有意義,隨機(jī)選擇來區(qū)分不同的棧幀。

分析——假說

為了引導(dǎo)優(yōu)化,我們將重點(diǎn)放在查找那些”無用功“。我們將嘗試查找產(chǎn)生這些”無用功“的大部分源碼,并刪除它。因?yàn)槠饰隹梢越衣冻鲞@個服務(wù)把時間花費(fèi)在哪里了,這就需要從中找出潛在的重復(fù)工作,修改代碼來改進(jìn)它, 重新運(yùn)行測試,并觀察性能是否接近目標(biāo)值。

根據(jù) Bredan Gregg 的描述,go pprof 的火焰圖是從上往下讀的。每一行代表一個棧幀(函數(shù)調(diào)用)。第一行是這個程序的入口點(diǎn),它是所有其他調(diào)用的父親(即,所有其他調(diào)用的棧中都有第一行這個函數(shù)地址)。后面的行從這里分支出去:

在火焰圖中的函數(shù)名上面停留會顯示在跟蹤期間,這個函數(shù)在棧中的時間總數(shù)。HTTPServe 在棧中占時為剖析時間的 65%,而各種 Go 運(yùn)行時方法 runtime.mcall, mstart, gc 構(gòu)成了剩下的剖析時間。一個有趣的事情是程序總運(yùn)行時間的 5% 被花費(fèi)在 DNS 的查詢中:

唯一的 IP 地址需要程序解析的是 Postgres 的地址。點(diǎn)擊 FindByAge 顯示:

有趣的是,這個圖顯示了 main 源碼有 3 點(diǎn)造成了這個延遲:關(guān)閉 / 釋放連接,查詢數(shù)據(jù),和連接。基于這個火焰圖, DNS 查詢和連接的關(guān)閉、打開數(shù)量大概占了總的服務(wù)時間的 13%。

假說:使用連接池來重用連接可以減少 HTTP 交互時間,從而有更高的吞吐量和更低的延遲。

應(yīng)用優(yōu)化——實(shí)踐

更新這個應(yīng)用,避免每次 postgres 請求都重建連接。一個解決方法是使用應(yīng)用級的 連接池 。這個實(shí)踐將使用 Go sql 驅(qū)動的池配置選項(xiàng)來配置一個連接池:

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)

if err != nil {
   return nil, err
}

執(zhí)行、觀察、分析

重新運(yùn)行 1000 測試負(fù)載,顯示 99% 的 HTTP 請求延遲都在 60ms SLO 以下!

而成本方面:

10k 請求量 / 秒 / 1000 請求量 / 機(jī)器 = 10 臺機(jī)器 + 1

我們繼續(xù)嘗試,看能不能更好!

2000 請求量 / 秒

雙倍請求顯示也一樣。左上角的圖顯示應(yīng)用正在接收每秒 2000 的請求量,而 p100 max 客戶端延遲在 60 ms 以上,p99 線卻一直在 SLO 以內(nèi)。

這次成本為:

10k 請求量 / 秒 / 2000 請求量 / 機(jī)器 = 5 臺機(jī)器 + 1

3000 請求量 / 秒

此時,這個服務(wù)能夠在 3000 請求量 / 秒 的速率下,p99 延遲 < 60ms,而 p100 從 2000 請求量 / 秒 時的 100-250 ms 到此時的 250-1000 ms。這個 SLO 沒有被違背,可被接受的成本為:

10k 請求量 / 秒 / 3000 請求量 / 機(jī)器 = 4 臺機(jī)器 + 1

嘗試更進(jìn)一步的分析。

分析——假說

生成并可視化 3000 請求量 / 秒 下應(yīng)用的剖析情況如下:

可以看出,FindByAge 6% 的交互時間是由 Dialing 連接造成的!!建立一個連接池提高了性能,但是可以觀察到應(yīng)用還是繼續(xù)做創(chuàng)建新的數(shù)據(jù)庫連接的重復(fù)工作!

假說:即使連接被放到池里了,但是他們一直被回收并清理導(dǎo)致應(yīng)用必須重新連接。調(diào)整空閑連接數(shù)等于池的大小應(yīng)該可以幫助減少延遲時間,最小化應(yīng)用花在創(chuàng)建數(shù)據(jù)庫連接的總時間。

應(yīng)用優(yōu)化——實(shí)踐

我們嘗試設(shè)置 MaxIdleConns 等于池的大小(或者在 這里 查看):

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
   return nil, err
}

執(zhí)行、觀察、分析

3000 請求量 / 秒

p99 總是 < 60ms !而 3000 請求量每秒也有更低的 p100 了!

仔細(xì)觀察下面的火焰圖,連接的 dial 不再出現(xiàn)了!仔細(xì)看 pg(*conn).query 那行,整個 dialing 不再存在了:

結(jié)論

性能分析是理解是否滿足客戶期望和非功能需求的至關(guān)重要的手段。通過符合客戶期望的審查分析性能能夠幫助我們決定哪些是性能可接受的,哪些是不可接受的。Go 在標(biāo)準(zhǔn)庫中提供了強(qiáng)大的組件,讓這個分析的一系列方法變得簡單易用。

我對你閱讀本文表示感謝,并希望你能反饋!


作者:dm03514 譯者:daliny 校對:polaris1119
本文由 GCTT 原創(chuàng)編譯,Go語言中文網(wǎng) 榮譽(yù)推出

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評論 2 370

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