Instagram基于Python語言的Web Service效率提升之道
Instagram 目前部署了世界上最大規(guī)模的 Django Web 框架(該框架完全使用 Python 編寫)。我們最初選用 Python 是因?yàn)樗秘?fù)盛名的簡潔性與實(shí)用性,這非常符合我們的哲學(xué)思想——“先做簡單的事情”。但簡潔性也會(huì)帶來效率方面的折衷。Instagram 的規(guī)模在過去兩年中已經(jīng)翻番,并且最近已突破 5 億用戶,所以急需最大程度地提升 web 服務(wù)效率以便我們的平臺(tái)能夠繼續(xù)順利地?cái)U(kuò)大。在過去的一年,我們已經(jīng)將效率計(jì)劃efficiency program提上日程,并在過去的六個(gè)月,我們已經(jīng)能夠做到無需向我們的 Django 層Django tiers添加新的容量來維持我們的用戶增長。我們將在本文分享一些由我們構(gòu)建的工具以及如何使用它們來優(yōu)化我們的日常部署流程。
為何需要提升效率?
Instagram,正如所有的軟件,受限于像服務(wù)器和數(shù)據(jù)中心能源這樣的物理限制。鑒于這些限制,在我們的效率計(jì)劃中有兩個(gè)我們希望實(shí)現(xiàn)的主要目標(biāo):
- Instagram 應(yīng)當(dāng)能夠利用持續(xù)代碼發(fā)布正常地提供通信服務(wù),防止因?yàn)樽匀粸?zāi)害、區(qū)域性網(wǎng)絡(luò)問題等造成某一個(gè)數(shù)據(jù)中心區(qū)丟失。
- Instagram 應(yīng)當(dāng)能夠自由地滾動(dòng)發(fā)布新產(chǎn)品和新功能,不必因容量而受阻。
想要實(shí)現(xiàn)這些目標(biāo),我們意識(shí)到我們需要持續(xù)不斷地監(jiān)控我們的系統(tǒng)并與回歸regressions進(jìn)行戰(zhàn)斗。
定義效率
Web services 的瓶頸通常在于每臺(tái)服務(wù)器上可用的 CPU 時(shí)間。在這種環(huán)境下,效率就意味著利用相同的 CPU 資源完成更多的任務(wù),也就是說,每秒處理更多的用戶請(qǐng)求requests per second,RPS。當(dāng)我們尋找優(yōu)化方法時(shí),我們面臨的第一個(gè)最大的挑戰(zhàn)就是嘗試量化我們當(dāng)前的效率。到目前為止,我們一直在使用“每次請(qǐng)求的平均 CPU 時(shí)間”來評(píng)估效率,但使用這種指標(biāo)也有其固有限制:
- 設(shè)備多樣性。使用 CPU 時(shí)間來測量 CPU 資源并非理想方案,因?yàn)樗瑫r(shí)受到 CPU 型號(hào)與 CPU 負(fù)載的影響。
- 請(qǐng)求影響數(shù)據(jù)。測量每次請(qǐng)求的 CPU 資源并非理想方案,因?yàn)樵谑褂妹看握?qǐng)求測量per-request measurement方案時(shí),添加或移除輕量級(jí)或重量級(jí)的請(qǐng)求也會(huì)影響到效率指標(biāo)。
相對(duì)于 CPU 時(shí)間來說,CPU 指令是一種更好的指標(biāo),因?yàn)閷?duì)于相同的請(qǐng)求,它會(huì)報(bào)告相同的數(shù)字,不管 CPU 型號(hào)和 CPU 負(fù)載情況如何。我們選擇使用了一種叫做”每個(gè)活動(dòng)用戶per active user“的指標(biāo),而不是將我們所有的數(shù)據(jù)關(guān)聯(lián)到每個(gè)用戶請(qǐng)求上。我們最終采用“每個(gè)活動(dòng)用戶在高峰期間的 CPU 指令CPU instruction per active user during peak minute”來測量效率。我們建立好新的度量標(biāo)準(zhǔn)后,下一步就是通過對(duì) Django 的分析來更多的了解一下我們的回歸。
Django web services 分析
通過分析我們的 Django web services,我們希望回答兩個(gè)主要問題:
- CPU 回歸會(huì)發(fā)生嗎?
- 是什么導(dǎo)致了 CPU 回歸發(fā)生以及我們?cè)撛鯓有迯?fù)它?
想要回答第一個(gè)問題,我們需要追蹤“每個(gè)活動(dòng)用戶的 CPU 指令CPU-instruction-per-active-user”指標(biāo)。如果該指標(biāo)增加,我們就知道已經(jīng)發(fā)生了一次 CPU 回歸。
我們?yōu)榇藰?gòu)建的工具叫做 Dynostats。Dynostats 利用 Django 中間件以一定的速率采樣用戶請(qǐng)求,記錄關(guān)鍵的效率以及性能指標(biāo),例如 CPU 總指令數(shù)、端到端請(qǐng)求時(shí)延、花費(fèi)在訪問內(nèi)存緩存(memcache)和數(shù)據(jù)庫服務(wù)的時(shí)間等。另一方面,每個(gè)請(qǐng)求都有很多可用于聚合的元數(shù)據(jù)metadata,例如端點(diǎn)名稱、HTTP 請(qǐng)求返回碼、服務(wù)該請(qǐng)求的服務(wù)器名稱以及請(qǐng)求中最新提交的哈希值hash。對(duì)于單個(gè)請(qǐng)求記錄來說,有兩個(gè)方面非常強(qiáng)大,因?yàn)槲覀兛梢栽诓煌木S度上進(jìn)行切割,那將幫助我們減少任何導(dǎo)致 CPU 回歸的原因。例如,我們可以根據(jù)它們的端點(diǎn)名稱聚合所有請(qǐng)求,正如下面的時(shí)間序列圖所示,從圖中可以清晰地看出在特定端點(diǎn)上是否發(fā)生了回歸。
CPU 指令對(duì)測量效率很重要——當(dāng)然,它們也很難獲得。Python 并沒有支持直接訪問 CPU 硬件計(jì)數(shù)器(CPU 硬件計(jì)數(shù)器是指可編程 CPU 寄存器,用于測量性能指標(biāo),例如 CPU 指令)的公共庫。另一方面,Linux 內(nèi)核提供了 perf_event_open 系統(tǒng)調(diào)用。通過 Python ctypes 橋接技術(shù)能夠讓我們調(diào)用標(biāo)準(zhǔn) C 庫的系統(tǒng)調(diào)用函數(shù)syscall,它也為我們提供了兼容 C 的數(shù)據(jù)類型,從而可以編程硬件計(jì)數(shù)器并從它們讀取數(shù)據(jù)。
使用 Dynostats,我們已經(jīng)可以找出 CPU 回歸,并探究 CPU 回歸發(fā)生的原因,例如哪個(gè)端點(diǎn)受到的影響最多,誰提交了真正會(huì)導(dǎo)致 CPU 回歸的變更等。然而,當(dāng)開發(fā)者收到他們的變更已經(jīng)導(dǎo)致一次 CPU 回歸發(fā)生的通知時(shí),他們通常難以找出問題所在。如果問題很明顯,那么回歸可能就不會(huì)一開始就被提交!
這就是為何我們需要一個(gè) Python 分析器,(一旦 Dynostats 發(fā)現(xiàn)了它)從而使開發(fā)者能夠使用它找出回歸發(fā)生的根本原因。不同于白手起家,我們決定對(duì)一個(gè)現(xiàn)成的 Python 分析器 cProfile 做適當(dāng)?shù)男薷?。cProfile 模塊通常會(huì)提供一個(gè)統(tǒng)計(jì)集合來描述程序不同的部分執(zhí)行時(shí)間和執(zhí)行頻率。我們將 cProfile 的定時(shí)器timer替換成了一個(gè)從硬件計(jì)數(shù)器讀取的 CPU 指令計(jì)數(shù)器,以此取代對(duì)時(shí)間的測量。我們?cè)诓蓸诱?qǐng)求后產(chǎn)生數(shù)據(jù)并把數(shù)據(jù)發(fā)送到數(shù)據(jù)流水線。我們也會(huì)發(fā)送一些我們?cè)?Dynostats 所擁有的類似元數(shù)據(jù),例如服務(wù)器名稱、集群、區(qū)域、端點(diǎn)名稱等。
在數(shù)據(jù)流水線的另一邊,我們創(chuàng)建了一個(gè)消費(fèi)數(shù)據(jù)的尾隨者tailer。尾隨者的主要功能是解析 cProfile 的統(tǒng)計(jì)數(shù)據(jù)并創(chuàng)建能夠表示 Python 函數(shù)級(jí)別的 CPU 指令的實(shí)體。如此,我們能夠通過 Python 函數(shù)來聚合 CPU 指令,從而更加方便地找出是什么函數(shù)導(dǎo)致了 CPU 回歸。
監(jiān)控與警報(bào)機(jī)制
在 Instagram,我們每天部署 30-50 次后端服務(wù)。這些部署中的任何一個(gè)都能發(fā)生 CPU 回歸的問題。因?yàn)槊看伟l(fā)生通常都包含至少一個(gè)差異diff,所以找出任何回歸是很容易的。我們的效率監(jiān)控機(jī)制包括在每次發(fā)布前后都會(huì)在 Dynostats 中掃描 CPU 指令,并且當(dāng)變更超出某個(gè)閾值時(shí)發(fā)出警告。對(duì)于長期會(huì)發(fā)生 CPU 回歸的情況,我們也有一個(gè)探測器為負(fù)載最繁重的端點(diǎn)提供日常和每周的變更掃描。
部署新的變更并非觸發(fā)一次 CPU 回歸的唯一情況。在許多情況下,新的功能和新的代碼路徑都由全局環(huán)境變量global environment variables,GEV控制。 在一個(gè)計(jì)劃好的時(shí)間表上,給一部分用戶發(fā)布新功能是很常見事情。我們?cè)?Dynostats 和 cProfile 統(tǒng)計(jì)數(shù)據(jù)中為每個(gè)請(qǐng)求添加了這個(gè)信息作為額外的元數(shù)據(jù)字段。按這些字段將請(qǐng)求分組可以找出由全局環(huán)境變量(GEV)改變導(dǎo)致的可能的 CPU 回歸問題。這讓我們能夠在它們對(duì)性能造成影響前就捕獲到 CPU 回歸。
接下來是什么?
Dynostats 和我們定制的 cProfile,以及我們建立的支持它們的監(jiān)控和警報(bào)機(jī)制能夠有效地找出大多數(shù)導(dǎo)致 CPU 回歸的元兇。這些進(jìn)展已經(jīng)幫助我們恢復(fù)了超過 50% 的不必要的 CPU 回歸,否則我們就根本不會(huì)知道。
我們?nèi)匀贿€有一些可以提升的方面,并很容易將它們地加入到 Instagram 的日常部署流程中:
- CPU 指令指標(biāo)應(yīng)該要比其它指標(biāo)如 CPU 時(shí)間更加穩(wěn)定,但我們?nèi)匀挥^察了讓我們頭疼的差異。保持“信噪比signal:noise ratio”合理地低是非常重要的,這樣開發(fā)者們就可以集中于真實(shí)的回歸上。這可以通過引入置信區(qū)間confidence intervals的概念來提升,并在信噪比過高時(shí)發(fā)出警報(bào)。針對(duì)不同的端點(diǎn),變化的閾值也可以設(shè)置為不同值。
- 通過更改 GEV 來探測 CPU 回歸的一個(gè)限制就是我們要在 Dynostats 中手動(dòng)啟用這些比較的日志輸出。當(dāng) GEV 的數(shù)量逐漸增加,開發(fā)了越來越多的功能,這就不便于擴(kuò)展了。相反,我們能夠利用一個(gè)自動(dòng)化框架來調(diào)度這些比較的日志輸出,并對(duì)所有的 GEV 進(jìn)行遍歷,然后當(dāng)檢查到回歸時(shí)就發(fā)出警告。
- cProfile 需要一些增強(qiáng)以便更好地處理封裝函數(shù)以及它們的子函數(shù)。
鑒于我們?cè)跒?Instagram 的 web service 構(gòu)建效率框架中所投入的工作,所以我們對(duì)于將來使用 Python 繼續(xù)擴(kuò)展我們的服務(wù)很有信心。我們也開始向 Python 語言本身投入更多,并且開始探索從 Python 2 轉(zhuǎn)移 Python 3 之道。我們將會(huì)繼續(xù)探索并做更多的實(shí)驗(yàn)以繼續(xù)提升基礎(chǔ)設(shè)施與開發(fā)者效率,我們期待著很快能夠分享更多的經(jīng)驗(yàn)。
本文作者 Min Ni 是 Instagram 的軟件工程師。