<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          牛逼了!Python 接口優(yōu)化,性能提升25倍!

          共 5746字,需瀏覽 12分鐘

           ·

          2021-01-06 16:19

          點(diǎn)擊上方藍(lán)色小字,關(guān)注Python 知識大全

          重磅干貨,第一時(shí)間送達(dá)

          號外:
          本號免費(fèi)提供 CSDN 資源下載,需要的伙伴公眾號后

          來源:Lin_R ??
          鏈接:https://segmentfault.com/a/1190000020956724

          背景

          我們負(fù)責(zé)的一個(gè)業(yè)務(wù)平臺,有次在發(fā)現(xiàn)設(shè)置頁面的加載特別特別地慢,簡直就是令人發(fā)指

          讓用戶等待 36s 肯定是不可能的,于是我們就要開啟優(yōu)化之旅了。

          投石問路

          既然是網(wǎng)站的響應(yīng)問題,可以通過 Chrome 這個(gè)強(qiáng)大的工具幫助我們快速找到優(yōu)化方向。

          通過 Chrome 的 Network 除了可以看到接口請求耗時(shí)之外,還能看到一個(gè)時(shí)間的分配情況,選擇一個(gè)配置沒有那么多的項(xiàng)目,簡單請求看看:

          雖然只是一個(gè)只有三條記錄的項(xiàng)目,加載項(xiàng)目設(shè)置都需要 17s,通過 Timing, 可以看到總的請求共耗時(shí)?17.67s?,但有?17.57s?是在 Waiting(TTFB) 狀態(tài)。

          TTFB 是 Time to First Byte 的縮寫,指的是瀏覽器開始收到服務(wù)器響應(yīng)數(shù)據(jù)的時(shí)間(后臺處理時(shí)間+重定向時(shí)間),是反映服務(wù)端響應(yīng)速度的重要指標(biāo)。


          Profile 火焰圖 + 代碼調(diào)優(yōu)


          那么大概可以知道優(yōu)化的大方向是在后端接口處理上面,后端代碼是 Python + Flask 實(shí)現(xiàn)的,先不盲猜,直接上 Profile:

          第一波優(yōu)化:功能交互重新設(shè)計(jì)

          說實(shí)話看到這段代碼是絕望的:完全看不出什么?只是看到很多 gevent 和 Threading,因?yàn)樘鄥f(xié)程或者線程?

          這時(shí)候一定要結(jié)合代碼來分析(為了簡短篇幅,參數(shù)部分用 “...” 代替):

          ?def?get_max_cpus(project_code,?gids):
          ????"""
          ????"""

          ????...
          ????#?再定義一個(gè)獲取?cpu?的函數(shù)
          ????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?數(shù)據(jù),為每個(gè) gid 分配一個(gè)線程去請求,最終再返回最大值。

          這里會出現(xiàn)兩個(gè)問題:

          1. 在一個(gè) web api 做線程的?創(chuàng)建 和 銷毀?是有很大成本的,因?yàn)榻涌跁l繁被觸發(fā),線程的操作也會頻繁發(fā)生,應(yīng)該盡可能使用線程池之類的,降低系統(tǒng)花銷;
          2. 該請求是加載某個(gè) gid (群組) 下面的機(jī)器過去 7 天的 CPU 最大值,可以簡單拍腦袋想下,這個(gè)值不是實(shí)時(shí)值也不是一個(gè)均值,而是一個(gè)最大值,很多時(shí)候可能并沒有想象中那么大價(jià)值;

          既然知道問題,那就有針對性的方案:

          1. 調(diào)整功能設(shè)計(jì),不再默認(rèn)加載 CPU 最大值,換成用戶點(diǎn)擊加載(一來降低并發(fā)的可能,二來不會影響整體);
          2. 因?yàn)?1 的調(diào)整,去掉多線程實(shí)現(xiàn);

          再看第一波優(yōu)化后的火焰圖:

          這次看的火焰圖雖然還有很大的優(yōu)化空間,但起碼看起來有點(diǎn)正常的樣子了。

          第二波優(yōu)化:Mysql 操作優(yōu)化處理

          我們再從頁面標(biāo)記處(接口邏輯處)放大火焰圖觀察:

          看到好大一片操作都是由?utils.py:get_group_profile_settings?這個(gè)函數(shù)引起的數(shù)據(jù)庫操作熱點(diǎn)。

          同理,也是需要通過代碼分析:

          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 ,第一個(gè)反應(yīng)就是?索引問題,所以優(yōu)先去看看數(shù)據(jù)庫的索引情況,如果有索引的話應(yīng)該不會是瓶頸:

          很奇怪這里明明已經(jīng)有了索引了,為什么速度還是這個(gè)鬼樣子呢!

          正當(dāng)毫無頭緒的時(shí)候,突然想起在?第一波優(yōu)化?的時(shí)候, 發(fā)現(xiàn) gid(群組)越多的影響越明顯,然后看回上面的代碼,看到那句:

          for?gid?in?gids:?
          ????...

          我仿佛明白了什么。

          這里是每個(gè) gid 都去查詢一次數(shù)據(jù)庫,而項(xiàng)目經(jīng)常有 20 ~ 50+ 個(gè)群組,那肯定直接爆炸了。

          其實(shí) Mysql 是支持單字段多值的查詢,而且每條記錄并沒有太多的數(shù)據(jù),我可以嘗試下用 Mysql 的 OR 語法,除了避免多次網(wǎng)絡(luò)請求,還能避開那該死的?for

          正當(dāng)我想事不宜遲直接搞起的時(shí)候,余光瞥見在剛才的代碼還有一個(gè)地方可以優(yōu)化,那就是:

          看到這里,熟悉的朋友大概會明白是怎么回事。

          GetAttr?這個(gè)方法是Python 獲取對象的?方法/屬性?時(shí)候會用到,雖然不可不用,但是如果在使用太過頻繁也會有一定的性能損耗。

          結(jié)合代碼一起來看:

          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()
          ????????...

          在這個(gè)遍歷很多次的?for?里面,session.query(ProfileSetting)?被反復(fù)無效執(zhí)行了,然后?filter?這個(gè)屬性方法也被頻繁讀取和執(zhí)行,所以這里也可以被優(yōu)化。

          總結(jié)下的問題就是:

          1. 數(shù)據(jù)庫的查詢沒有批量查詢;
          2. ORM 的對象太多重復(fù)的生成,導(dǎo)致性能損耗;
          3. 屬性讀取后沒有復(fù)用,導(dǎo)致在遍歷次數(shù)較大的循環(huán)體內(nèi)頻繁 getAttr,成本被放大;

          那么對癥下藥就是:

          def?get_group_profile_settings(project_code,?gids):
          ????
          ????#?獲取?Mysql?ORM?操作對象
          ????ProfileSetting?=?unpurview(sandman.endpoint_class('profile_settings'))
          ????session?=?get_postman_session()
          ????
          ????
          ????#?批量查詢?并將?filter?提到循環(huán)之外
          ????query_results?=?query_instance.filter(
          ????????ProfileSetting.name.in_(project_code?+?':'?+?gid?for?gid?in?gids)
          ????).all()

          ????#?對全部的查詢結(jié)果再單條處理
          ????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

          優(yōu)化后的火焰圖:

          對比下優(yōu)化前的相同位置的火焰圖:

          明顯的優(yōu)化點(diǎn):優(yōu)化前的,最底部的?utils.py:get_group_profile_settings?和 數(shù)據(jù)庫相關(guān)的熱點(diǎn)大大縮減。

          優(yōu)化效果

          同一個(gè)項(xiàng)目的接口的響應(yīng)時(shí)長從 37.6 s 優(yōu)化成 1.47s,具體的截圖:

          優(yōu)化總結(jié)

          如同一句名言:

          如果一個(gè)數(shù)據(jù)結(jié)構(gòu)足夠優(yōu)秀,那么它是不需要多好的算法。

          在優(yōu)化功能的時(shí)候,最快的優(yōu)化就是:去掉那個(gè)功能!

          其次快就是調(diào)整那個(gè)功能觸發(fā)的?頻率?或者?復(fù)雜度!

          從上到下,從用戶使用場景去考慮這個(gè)功能優(yōu)化方式,往往會帶來更加簡單高效的結(jié)果,嘿嘿!

          當(dāng)然很多時(shí)候我們是無法那么幸運(yùn)的,如果我們實(shí)在無法去掉或者調(diào)整,那么就發(fā)揮做程序猿的價(jià)值咯:Profile

          針對 Python 可以嘗試:cProflile + gprof2dot

          而針對 Go 可以使用: pprof + go-torch

          很多時(shí)候看到的代碼問題都不一定是真正的性能瓶頸,需要結(jié)合工具來客觀分析,這樣才能有效直擊痛點(diǎn)!

          其實(shí)這個(gè) 1.47s,其實(shí)還不是最好的結(jié)果,還可以有更多優(yōu)化的空間,比如:

          1. 前端渲染和呈現(xiàn)的方式,因?yàn)檎麄€(gè)表格是有很多數(shù)據(jù)組裝后再呈現(xiàn)的,響應(yīng)慢的單元格可以默認(rèn)先顯示?菊花,數(shù)據(jù)返回再更新;
          2. 火焰圖看到還有挺多細(xì)節(jié)可以優(yōu)化,可以替換請求數(shù)據(jù)的外部接口,比如再優(yōu)化徹底?GetAttr?相關(guān)的邏輯;
          3. 更極端就是直接 Python 轉(zhuǎn) GO;

          但是這些優(yōu)化已經(jīng)不是那么迫切了,因?yàn)檫@個(gè) 1.47s 是比較大型項(xiàng)目的優(yōu)化結(jié)果了,絕大部分的項(xiàng)目其實(shí)不到 1s 就能返回

          再優(yōu)化可能付出更大成本,而結(jié)果可能也只是從?500ms?到?400ms?而已,結(jié)果并不那么高性價(jià)比。

          所以我們一定要時(shí)刻清晰自己優(yōu)化的目標(biāo),時(shí)刻考慮?投入產(chǎn)出比,在有限的時(shí)間做出比較高的價(jià)值(如果有空閑時(shí)間當(dāng)然可以盡情干到底)


          -END-

          推薦閱讀
          手把手教你發(fā)布 Python 項(xiàng)目開源包
          工作群里常見表情的真正含義……

          關(guān)注「Python 知識大全」,做全棧開發(fā)工程師
          歲月有你 惜惜相處

          回復(fù) 【資料】獲取高質(zhì)量學(xué)習(xí)資料


          得本文對你有幫助?請分享給更多人
          點(diǎn)「在看」的人都變好看了哦


          瀏覽 40
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色片免费视频 | 久久夜色精品国产免费观看 | 亚洲色图综合 | 豆花精品视频 | 免费大免费黄在线 |