記一次tornado QPS 優化

本文最先發表於我的博客:Pylixm'Wikihtml

應項目的需求,咱們使用tornado開發了一個api系統,系統開發完後,在8核16G的虛機上通過壓測qps只有200+。與咱們當初定的QPS 大於2k差了一個數量級,因而便開始了漫長的優化之路。在優化過程當中,學了許多東西,有必要整理記錄下備查。python

咱們的技術選型:mysql

  • python2.7
  • tornado4.4.3
  • sqlalchemy1.1.5
  • mysql5.6
  • rabbitmq

當初技術選型的時候選擇tornado,即是由於其優秀的性能,這麼低的QPS天然是不甘心。究竟tornado能夠達到多少QPS呢?因而編寫了簡單的hello world,在上邊的虛擬機中起16個進程下,使用ab壓測QPS居然達到了驚人的6K,平均響應時間在毫秒級。這下有信心將api的QPS繼續優化了。web

初步分析

提高QPS, 可從兩方面入手,一個是增長併發數,其二是減小平均響應時間。從目前狀況看,增長進程併發數是最直接的手段,但當達到機器資源的瓶頸時,可靠堆疊機器來解決。那麼
相比較下,減少平均響應更爲重要。初步分析了咱們開發的api,平均響應時間在幾百毫秒級別。大部分的時間花在系統與數據庫的交互上,到這,便有了一個優化的主題思路:最大限度的下降平均響應時間。sql

咱們API完成的功能爲,接受請求參數作一些列的認證判斷(與數據庫交互),將消息以廣播的形式發送到rabbitmq供消費者消費,最後返回給客戶端發送結果。根據此邏輯,影響響應時間的地方,分析以下:數據庫

  • 與mysql 數據庫的交互
  • 使用rabbitmq廣播消息時的時間耗費
  • 耗時的業務邏輯代碼片斷

優化思路

根據上邊的問題,從如下幾個方面入手:api

  • 增長tornado的異步特性
  • 分析與數據庫的交互,減小與數據庫的交互時間
  • 分析rabbitmq的時間耗費,減小發送信息時間
  • 優化業務代碼邏輯

具體實施

tornado 的異步特性

開發api時,由於對tornado 的異步特性不是很熟悉,便沒有使用。後來隨着測試的深刻,發現須要使用後,開始瞭解。
隨着瞭解的深刻,發現tornado是並無很好的支持數據庫的異步特性,更可能是對網絡的異步,官網上也是寫的」網絡非阻塞框架「。
查閱官方文檔,tornado的異步實現,見官方文檔
總的來講,使程序異步的方式有3種,參考這裏。以下:websocket

  • 第一種,使用tornado 的 gen.coruntine。網絡

    使用此種方式,須要異步數據庫的驅動庫,經查找現階段並無很好的成熟的支持異步查詢mysql的python驅動,放棄此種方案。
  • 第二種,使用tornado 的線程模塊。併發

    此種方式比較方便,只須要在耗時的函數上添加裝飾器便可,簡單方便,能夠說是一種萬能方案,但此方案耗費系統資源。
    系統資源並非咱們的瓶頸,咱們最後採納了此種方式。
  • 第三種,使用外部隊列,單獨其worker 進程或線程去處理。例如,celery 等。

    此種方式增長了外部的依賴,增長了系統的複雜性和後期的維護難度,放棄此種方案。

增長了異步特性外有顯著的提高。

mysql 數據庫的優化

數據庫方便,咱們適用的是SQLAlchemy。使用ORM時,在減小裸sql帶來的查詢複雜度的同時,必然會增長查詢數據庫的耗時。咱們也作過測試,
使用pymsql連接mysql,直接使用裸sql查詢與使用sqlalcemy 的對象查詢的耗時差異有七、8個毫秒的時差,與sqlalchemy的裸sql方式執行時間幾乎一致。
可見,sqlalchemy的orm方式是有必定時間耗損的。stackoverflow的一個問題,也驗證了個人想法,見Why is loading SQLAlchemy objects via the ORM 5-8x slower than rows via a raw MySQLdb cursor?

針對數據庫方面,咱們作了以下優化:

  • 將SQLAlchemy 查詢改成核心裸sql方式,可參考這裏
  • 優化數據庫,增長必要的索引。
  • 將邏輯中的過濾條件,儘可能的移到sql中,減小sql結果集的大小,加快查詢速度。
  • 將能夠單詞查詢出的數據集放到一次查詢中,減小連接數據庫的次數。

分析rabbitmq的時間耗費,減小發送信息時間

rabbitmq 方面,使用的是pika 做爲驅動庫鏈接的,使用方式是每次發送數據的時候建立連接和通道,發送完畢後當即關閉連接。考慮到是否可使用長連接,建立連接後不關閉,只關閉channel。修改後發現報錯,具體代碼以下:

# -*- coding:utf-8 -*-
import pika
from settings import settings


class Client(object):
    def __init__(self, host, port, username, pwd):
        self.host = host
        self.port = port
        self.username = username
        self.pwd = pwd
        self.init_connection()

    def init_connection(self):
        user_pwd = pika.PlainCredentials(self.username, self.pwd)
        self.connection = pika.BlockingConnection(pika.ConnectionParameters(
            host=self.host, port=self.port, credentials=user_pwd))

    # ...

補充錯誤材料分析 - todo

翻閱了pika的文檔,發現其有異步的使用方式,且有與tornando 框架的結合的實例,見文檔
pika的異步方式,使用了和tornado 相同的基於epull的事件循環模型,如何將其與tornado 的IOloop結合是個問題,
其有個tornado的連接適配器,翻看其代碼仍是有些不太明確如何使用,有時間的時候再繼續研究下。

針對rabbitmq的優化咱們放棄了,但優化過程當中有些值得分析的文章,整理以下:

優化業務代碼邏輯

代碼邏輯方便的優化,以下:

  • 減小循環
  • review 邏輯,去除冗餘邏輯
  • 提取公共變量,賦值一次,減小查詢數據庫。

總結

通過以上的優化,咱們的api 的 QPS 提高到了1200+, 因爲時間問題,咱們暫停了繼續的優化。經過本次QPS的優化過程,有幾點感悟:

  • 使用一項新技術時,必定要認真閱讀官方文檔,瞭解清楚後,再使用。
  • 不要輕易否認一項公認的「技術真理」,要拿數聽說話。

我的工做總結,歡迎留言交流!

相關文章
相關標籤/搜索