咱們負責的一個業務平臺,有次在發現設置頁面的加載特別特別地慢,簡直就是使人髮指html
讓用戶等待 36s 確定是不可能的,因而咱們就要開啓優化之旅了。前端
既然是網站的響應問題,能夠經過 Chrome 這個強大的工具幫助咱們快速找到優化方向。python
經過 Chrome 的 Network 除了能夠看到接口請求耗時以外,還能看到一個時間的分配狀況,選擇一個配置沒有那麼多的項目,簡單請求看看:
web
雖然只是一個只有三條記錄的項目,加載項目設置都須要 17s,經過 Timing, 能夠看到總的請求共耗時 17.67s ,但有 17.57s 是在 Waiting(TTFB) 狀態。算法
TTFB 是 Time to First Byte 的縮寫,指的是瀏覽器開始收到服務器響應數據的時間(後臺處理時間+重定向時間),是反映服務端響應速度的重要指標。
那麼大概能夠知道優化的大方向是在後端接口處理上面,後端代碼是 Python + Flask 實現的,先不盲猜,直接上 Profile:sql
說實話看到這段代碼是絕望的:徹底看不出什麼?只是看到不少 gevent 和 Threading,由於太多協程或者線程?數據庫
這時候必定要結合代碼來分析(爲了簡短篇幅,參數部分用 「...」 代替):segmentfault
def get_max_cpus(project_code, gids): """ """ ... # 再定義一個獲取 cpu 的函數 def get_max_cpu(project_setting, gid, token, headers): group_with_machines = utils.get_groups(...) hostnames = get_info_from_machines_info(...) res = fetchers.MonitorAPIFetcher.get(...) vals = [ round(100 - val, 4) for ts, val in res['series'][0]['data'] if not utils.is_nan(val) ] max_val = max(vals) if vals else float('nan') max_cpus[gid] = max_val # 啓動線程批量請求 for gid in gids: t = Thread(target=get_max_cpu, args=(...)) threads.append(t) t.start() # 回收線程 for t in threads: t.join() return max_cpus
經過代碼能夠看到,爲了更加快速獲取 gids 全部的 cpu_max 數據,爲每一個 gid 分配一個線程去請求,最終再返回最大值。後端
這裏會出現兩個問題:api
既然知道問題,那就有針對性的方案:
再看第一波優化後的火焰圖:
此次看的火焰圖雖然還有很大的優化空間,但起碼看起來有點正常的樣子了。
咱們再從頁面標記處(接口邏輯處)放大火焰圖觀察:
看到好大一片操做都是由 utils.py:get_group_profile_settings
這個函數引發的數據庫操做熱點。
同理,也是須要經過代碼分析:
def get_group_profile_settings(project_code, gids): # 獲取 Mysql ORM 操做對象 ProfileSetting = unpurview(sandman.endpoint_class('profile_settings')) session = get_postman_session() profile_settings = {} for gid in gids: compound_name = project_code + ':' + gid result = session.query(ProfileSetting).filter( ProfileSetting.name == compound_name ).first() if result: result = result.as_dict() tag_indexes = result.get('tag_indexes') profile_settings[gid] = { 'tag_indexes': tag_indexes, 'interval': result['interval'], 'status': result['status'], 'profile_machines': result['profile_machines'], 'thread_settings': result['thread_settings'] } ...(省略) return profile_settings
看到 Mysql ,第一個反應就是 索引問題,因此優先去看看數據庫的索引狀況,若是有索引的話應該不會是瓶頸:
很奇怪這裏明明已經有了索引了,爲何速度仍是這個鬼樣子呢!
正當毫無頭緒的時候,忽然想起在 第一波優化 的時候, 發現 gid(羣組)越多的影響越明顯,而後看回上面的代碼,看到那句:
for gid in gids: ...
我彷彿明白了什麼。
這裏是每一個 gid 都去查詢一次數據庫,而項目常常有 20 ~ 50+ 個羣組,那確定直接爆炸了。
其實 Mysql 是支持單字段多值的查詢,並且每條記錄並無太多的數據,我能夠嘗試下用 Mysql 的 OR 語法,除了避免屢次網絡請求,還能避開那該死的 for
正當我想事不宜遲直接搞起的時候,餘光瞥見在剛纔的代碼還有一個地方能夠優化,那就是:
看到這裏,熟悉的朋友大概會明白是怎麼回事。
GetAttr 這個方法是Python 獲取對象的 方法/屬性 時候會用到,雖然不可不用,可是若是在使用太過頻繁也會有必定的性能損耗。
結合代碼一塊兒來看:
def get_group_profile_settings(project_code, gids): # 獲取 Mysql ORM 操做對象 ProfileSetting = unpurview(sandman.endpoint_class('profile_settings')) session = get_postman_session() profile_settings = {} for gid in gids: compound_name = project_code + ':' + gid result = session.query(ProfileSetting).filter( ProfileSetting.name == compound_name ).first() ...
在這個遍歷不少次的 for 裏面,session.query(ProfileSetting) 被反覆無效執行了,而後 filter 這個屬性方法也被頻繁讀取和執行,因此這裏也能夠被優化。
總結下的問題就是:
1. 數據庫的查詢沒有批量查詢; 2. ORM 的對象太多重複的生成,致使性能損耗; 3. 屬性讀取後沒有複用,致使在遍歷次數較大的循環體內頻繁 getAttr,成本被放大;
那麼對症下藥就是:
def get_group_profile_settings(project_code, gids): # 獲取 Mysql ORM 操做對象 ProfileSetting = unpurview(sandman.endpoint_class('profile_settings')) session = get_postman_session() # 批量查詢 並將 filter 提到循環以外 query_results = query_instance.filter( ProfileSetting.name.in_(project_code + ':' + gid for gid in gids) ).all() # 對所有的查詢結果再單條處理 profile_settings = {} for result in query_results: if not result: continue result = result.as_dict() gid = result['name'].split(':')[1] tag_indexes = result.get('tag_indexes') profile_settings[gid] = { 'tag_indexes': tag_indexes, 'interval': result['interval'], 'status': result['status'], 'profile_machines': result['profile_machines'], 'thread_settings': result['thread_settings'] } ...(省略) return profile_settings
優化後的火焰圖:
對比下優化前的相同位置的火焰圖:
明顯的優化點: 優化前的,最底部的 utils.py:get_group_profile_settings 和 數據庫相關的熱點大大縮減。
同一個項目的接口的響應時長從 37.6 s 優化成 1.47s,具體的截圖:
如同一句名言:
若是一個數據結構足夠優秀,那麼它是不須要多好的算法。
在優化功能的時候,最快的優化就是:去掉那個功能!
其次快就是調整那個功能觸發的 頻率 或者 複雜度!
從上到下,從用戶使用場景去考慮這個功能優化方式,每每會帶來更加簡單高效的結果,嘿嘿!
固然不少時候咱們是沒法那麼幸運的,若是咱們實在沒法去掉或者調整,那麼就發揮作程序猿的價值咯:Profile
針對 Python 能夠嘗試: cProflile + gprof2dot
而針對 Go 可使用: pprof + go-torch
不少時候看到的代碼問題都不必定是真正的性能瓶頸,須要結合工具來客觀分析,這樣纔能有效直擊痛點!
其實這個 1.47s,其實還不是最好的結果,還能夠有更多優化的空間,好比:
可是這些優化已經不是那麼迫切了,由於這個 1.47s 是比較大型項目的優化結果了,絕大部分的項目其實不到 1s 就能返回
再優化可能付出更大成本,而結果可能也只是從 500ms 到 400ms 而已,結果並不那麼高性價比。
因此咱們必定要時刻清晰本身優化的目標,時刻考慮 投入產出比,在有限的時間作出比較高的價值(若是有空閒時間固然能夠盡情幹到底)
歡迎各位大神指點交流, QQ討論羣: 258498217
轉載請註明來源: http://www.javashuo.com/article/p-zsagomam-do.html