API 性能優化(一)
項目使用 djangorestframework 開發了一組 restful 服務,調用方包括 PC 網站、移動網站和後台數據系統。得益於 Django 的成熟和 djangorestframework 的合理抽象,開發進度一直很順利和快速。哦,我愛 Python, 也愛 Django。
進一步深入此次優化過程之前,讓我先講下優化前的整體架構,便於後續討論。
一個 request 首先進入 nginx 處理,然後轉給 Gunicorn,Django 1.11運行於 Gunicorn 中,Django 則進一步訪問 Postgresql。就是這樣了。
隨著時間的流逝,我們快速開發了一系列服務,API 的調用次數也迅速上升,其相應也越來越慢,後台數據系統慢尚能忍受(都是自己人),PC 網站的訪問者可是我們親愛的用戶。平均三四秒的龜速,我們應該視為恥辱!於是乎,我們決定休養生息一段時間,對整體優化一下。
我們的優化目標是使得 API 的平均響應時間在500ms 以內,原則是不改變硬體??,方法不限。那麼用什麼方法呢?調整架構?優化代碼?優化 SQL?增加前後端緩存?我們決定從最簡單的方法入手:增加緩存。
Django 提供了裝飾器django.views.decorators.cache.cache_page()支持緩存一個視圖的響應,中間件django.middleware.cache.UpdateCacheMiddleware和django.middleware.cache.FetchFromCacheMiddleware對整體進行緩存。我們的 有些 API 變化非常快,這部分緩存時間只有1分鐘;有些則基本不會修改,可以儘可能長時間緩存,於是我們根據數據的業務屬性對 API 進行了讀寫分離的優化,之後根據 API 的變化頻率和對業務的影響程度對 API 進行了分類,比如有些是可以緩存15秒的,有些是1分鐘、30分鐘、24小時、1個月等等。根據這些分析,我們決定不使用中間件,只使用cache_page()。
然而cache_page()是不能支持 djangorestframework的響應的!幸好我們發現了drf-extensions,其提供了類似的裝飾器,叫 rest_framework_extensions.cache.decorators import cache_response,用法和 cache_page()類似。於是我們根據對 API 的分析結果很快將代碼修改完畢,cache 服務我們選擇了 memcached(比 redis 快、部署簡單)。上線,觀察響應的性能,其性能的提升非常顯著:平均達到了200ms。我們很滿意!
我們的優化使得對資料庫的壓力迅速減小,然而對 gunicorn 的訪問頻率並沒有改變。於是我們提出了新問題,能不能讓 Gunicorn 的訪問壓力也降下來?答案是 nginx的memcached_module。它能讓 nginx 先從 memcached 中獲得響應結果而不必轉發 request 給 Gunicorn,而我們已經做到將 response 緩存至 memcached 了,只要配置 nginx 即可,這事兒很簡單。增加的配置如下:
upstream memcached_products {n server 127.0.0.1:11211;n}nnlocation /products {n set $memcached_key $request_uri;n memcached_pass memcached_products;n error_page 400 403 404 405 500 501 502 503 504 = @memcached_products;n}n
做好相關配置,讓我們重啟一下 nginx 和 gunicorn。OK,啟動沒錯。再看看網站效果,嗯,還不錯,200ms 左右,雖然沒有提高多少,但對比 nginx 的access_log 和 Gunicorn 的 access_log發現,request 確實沒有從 nginx 轉發到 Gunicorn。
網站、後台提升效果明顯,我們也很滿意,優化可暫告一段落。兄弟們,下樓喝杯咖啡去!
------------------------------------------噩耗的分界線------------------------------------------------
沒過多久,就從公司傳來噩耗,多個頁面打不開!我們的神經繃緊了:哪裡出錯了?趕緊看日誌。nginx 正常!gunicorn 正常!那是哪裡出錯了?我們手動測試了線上 API,本來該返回 json 數據的,結果卻是返回一個二進位文件!why?一時確定不了原因,就先把 nginx 的memcached_module配置刪除。
後來分析出來原因:首先,從 nginx 日誌和 Gunicorn 日誌看,nginx從 memcached 取迴響應是沒錯的,那就只能從後端定位了。去掉memcached_module之後也是正常的,說明cache_response()也是對的。那問題出在哪裡?我們能直接看出來的是這個 memcached 緩存響應能被 Gunicorn 使用,不能被 nginx 使用。這說明緩存內容只能被 Django 使用。再去看cache_response的代碼,這次終於理解為什麼出錯了。我們知道,Django的中間件配置的執行順序,request 處理階段,從上到下依次調用中間件的 process_request;再執行視圖方法,然後對視圖的響應從下到上依次調用中間件的 process_response方法。這些process_response會對 response 做各種操作,最後返回的response 才是最終的,而cache_response()在視圖返回後立刻就將 response 緩存,這個 response 則在執行完一系列process_response之後,才返回 nginx 中。也就是說,被緩存的 response 是未經中間件的process_response處理的, nginx從 memcached 中拿回這個響應,當然是錯的!
如果我們還想使用nginx 的memcached_module,那該怎麼辦呢?思路有兩個:讓Gunicorn 緩存正確的 response,或者讓 nginx 來緩存 response。
推薦閱讀: