從0到1,Python Web開發的進擊之路

本文將以我的(開發)的角度,講述如何從零開始,編寫、搭建和部署一個基於Python的Web應用程序。php

從最簡單的出發點來剖析,一個web應用後端要完成的工做抽象出來無非就是3點:html

  1. 接收和解析請求。
  2. 處理業務邏輯。
  3. 生產和返回響應。

對於初學者來講,咱們關心的只需這些步驟就夠了。要檢驗這三個步驟,最簡單的方法是先寫出一個hello world。前端

request->"hello world"->response

python有許多流行的web框架,咱們該如何選擇呢?試着考慮三個因素:python

  • 易用:該框架是面對初學者友好的,並且具備健全的文檔,靈活開發部署。例如flask,bottle。
  • 效率:該框架適合快速開發,擁有豐富的輪子,注重開發效率。例如django。
  • 性能:該框架能承載更大的請求壓力,提升吞吐量。例如falcon,tornado,aiohttp,sanic。

根據場景使用合適的框架能少走許多彎路,固然,你還能本身寫一個框架,這個下面再說。mysql

對於缺少經驗的人來講,易用性無疑是排在第一位的,推薦用flask做爲python web入門的第一個框架,另外也推薦django。ios

首先用virtualenv建立python的應用環境,爲何用virtualenv呢,virtualenv能建立一個純淨獨立的python環境,避免污染全局環境。(順便安利kennethreitz大神的pipenvnginx

mkdir todo
cd todo
virtualenv venv
source venv/bin/activate
pip install flask
touch server.py

代碼未寫,規範先行。在寫代碼以前要定義好一套良好代碼規範,例如PEP8。這樣才能使得你的代碼變的更加可控。git

心中默唸The Zen of Python:github

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

下面用flask來編寫第一個程序:web

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/index')
def index():
    return jsonify(msg='hello world')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

在命令行輸入python server.py

python server.py
* Running on http://0.0.0.0:8000/ (Press CTRL+C to quit)

打開瀏覽器,訪問http://127.0.0.1:8000/index,如無心外,會看到下面的響應。

{
  "msg": "hello world"
}

一個最簡單的web程序就完成了!讓咱們看下過程當中都發生了什麼:

  1. 客戶端(瀏覽器)根據輸入的地址http://127.0.0.1:8000/index找到協議(http),主機(127.0.0.1),端口(8000)和路徑(/index),與服務器(application server)創建三次握手,併發送一個http請求。

  2. 服務器(application server)把請求報文封裝成請求對象,根據路由(router)找到/index這個路徑所對應的視圖函數,調用這個視圖函數。

  3. 視圖函數生成一個http響應,返回一個json數據給客戶端。

    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 27
    Server: Werkzeug/0.11.15 Python/3.5.2
    Date: Thu, 26 Jan 2017 05:14:36 GMT

當咱們輸入python server.py時,會創建一個服務器(也叫應用程序服務器,即application server)來監聽請求,並把請求轉給flask來處理。那麼這個服務器是如何跟python程序打交道的呢?答案就是WSGI(Web Server Gateway Interface)接口,它是server端(服務器)與application端(應用程序)之間的一套約定俗成的規範,使咱們只要編寫一個統一的接口,就能應用到不一樣的wsgi server上。用圖表示它們的關係,就是下面這樣的:

只要application端(flask)和server端(flask內建的server)都遵循wsgi這個規範,那麼他們就可以協同工做了,關於WSGI規範,可參閱Python官方的PEP 333裏的說明。

目前爲止,應用是下面這個樣子的:


一切都很簡單,如今咱們要作一個Todo應用,提供添加todo,修改todo狀態和刪除todo的接口。

先不考慮數據庫,能夠迅速地寫出下面的代碼:

from flask import Flask, jsonify, request, abort, Response
from time import time
from uuid import uuid4
import json

app = Flask(__name__)

class Todo(object):
    def __init__(self, content):
        self.id = str(uuid4())
        self.content = content #todo內容
        self.created_at = time() #建立時間
        self.is_finished = False #是否完成
        self.finished_at = None #完成時間

    def finish(self):
        self.is_finished = True
        self.finished_at = time()

    def json(self):
        return json.dumps({
            'id': self.id,
            'content': self.content,
            'created_at': self.created_at,
            'is_finished': self.is_finished,
            'finished_at': self.finished_at
        })

todos = {}
get_todo = lambda tid: todos.get(tid, False)

@app.route('/todo')
def index():
    return jsonify(data=[todo.json() for todo in todos.values()])

@app.route('/todo', methods=['POST'])
def add():
    content = request.form.get('content', None)
    if not content:
        abort(400)
    todo = Todo(content)
    todos[todo.id] = todo
    return Response() #200

@app.route('/todo/<tid>/finish', methods=['PUT'])
def finish(tid):
    todo = get_todo(tid)
    if todo:
        todo.finish()
        todos[todo.id] = todo
        return Response()
    abort(404)

@app.route('/todo/<tid>', methods=['DELETE'])
def delete(tid):
    todo = get_todo(tid)
    if todo:
        todos.pop(tid)
        return Response()
    abort(404)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

這個程序基本實現了須要的接口,如今測試一下功能。

  • 添加一個todo
http -f POST http://127.0.0.1:8000/todo content=好好學習
HTTP/1.0 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Thu, 26 Jan 2017 06:45:37 GMT
Server: Werkzeug/0.11.15 Python/3.5.2
  • 查看todo列表
http http://127.0.0.1:8000/todo
HTTP/1.0 200 OK
Content-Length: 203
Content-Type: application/json
Date: Thu, 26 Jan 2017 06:46:16 GMT
Server: Werkzeug/0.11.15 Python/3.5.2

{
    "data": [
        "{\"created_at\": 1485413137.305699, \"id\": \"6f2b28c4-1e83-45b2-8b86-20e28e21cd40\", \"is_finished\": false, \"finished_at\": null, \"content\": \"\\u597d\\u597d\\u5b66\\u4e60\"}"
    ]
}
  • 修改todo狀態
http -f PUT http://127.0.0.1:8000/todo/6f2b28c4-1e83-45b2-8b86-20e28e21cd40/finish
HTTP/1.0 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Thu, 26 Jan 2017 06:47:18 GMT
Server: Werkzeug/0.11.15 Python/3.5.2

http http://127.0.0.1:8000/todo
HTTP/1.0 200 OK
Content-Length: 215
Content-Type: application/json
Date: Thu, 26 Jan 2017 06:47:22 GMT
Server: Werkzeug/0.11.15 Python/3.5.2

{
    "data": [
        "{\"created_at\": 1485413137.305699, \"id\": \"6f2b28c4-1e83-45b2-8b86-20e28e21cd40\", \"is_finished\": true, \"finished_at\": 1485413238.650981, \"content\": \"\\u597d\\u597d\\u5b66\\u4e60\"}"
    ]
}
  • 刪除todo
http -f DELETE http://127.0.0.1:8000/todo/6f2b28c4-1e83-45b2-8b86-20e28e21cd40
HTTP/1.0 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Thu, 26 Jan 2017 06:48:20 GMT
Server: Werkzeug/0.11.15 Python/3.5.2

http http://127.0.0.1:8000/todo
HTTP/1.0 200 OK
Content-Length: 17
Content-Type: application/json
Date: Thu, 26 Jan 2017 06:48:22 GMT
Server: Werkzeug/0.11.15 Python/3.5.2

{
    "data": []
}

可是這個的程序的數據都保存在內存裏,只要服務一中止全部的數據就沒辦法保存下來了,所以,咱們還須要一個數據庫用於持久化數據。

那麼,應該選擇什麼數據庫呢?

  • 傳統的rdbms,例如mysql,postgresql等,他們具備很高的穩定性和不俗的性能,結構化查詢,支持事務,由ACID來保持數據的完整性。
  • nosql,例如mongodb,cassandra等,他們具備非結構化特性,易於橫向擴展,實現數據的自動分片,擁有靈活的存儲結構和強悍的讀寫性能。

這裏使用mongodb做例子,使用mongodb改造後的代碼是這樣的:

from flask import Flask, jsonify, request, abort, Response
from time import time
from bson.objectid import ObjectId
from bson.json_util import dumps
import pymongo

app = Flask(__name__)

mongo = pymongo.MongoClient('127.0.0.1', 27017)
db = mongo.todo

class Todo(object):
    @classmethod
    def create_doc(self, content):
        return {
            'content': content,
            'created_at': time(),
            'is_finished': False,
            'finished_at': None
        }

@app.route('/todo')
def index():
    todos = db.todos.find({})
    return dumps(todos)

@app.route('/todo', methods=['POST'])
def add():
    content = request.form.get('content', None)
    if not content:
        abort(400)
    db.todos.insert(Todo.create_doc(content))
    return Response() #200

@app.route('/todo/<tid>/finish', methods=['PUT'])
def finish(tid):
    result = db.todos.update_one(
        {'_id': ObjectId(tid)},
        {
            '$set': {
                'is_finished': True,
                'finished_at': time()
            }
        }    
    )
    if result.matched_count == 0:
        abort(404)
    return Response()

@app.route('/todo/<tid>', methods=['DELETE'])
def delete(tid):
    result = db.todos.delete_one(
        {'_id': ObjectId(tid)}  
    )
    if result.matched_count == 0:
        abort(404)
    return Response()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

這樣一來,應用的數據便能持久化到本地了。如今,整個應用看起來是下面這樣的:


如今往mongodb插入1萬條數據。

import requests

for i in range(10000):
    requests.post('http://127.0.0.1:8000/todo', {'content': str(i)})

獲取todo的接口目前是有問題的,由於它一次性把數據庫的全部記錄都返回了,當數據記錄增加到一萬條的時候,這個接口的請求就會變的很是慢,須要500ms後才能發出響應。如今對它進行以下的改造:

@app.route('/todo')
def index():
    start = request.args.get('start', '')
    start = int(start) if start.isdigit() else 0
    todos = db.todos.find().sort([('created_at', -1)]).limit(10).skip(start)
    return dumps(todos)

每次只取十條記錄,按建立日期排序,先取最新的,用分頁的方式獲取以往記錄。改造後的接口如今只需50ms便能返回響應。

如今對這個接口進行性能測試:

wrk -c 100 -t 12 -d 5s http://127.0.0.1:8000/todo
Running 5s test @ http://127.0.0.1:8000/todo
  12 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.22s   618.29ms   1.90s    48.12%
    Req/Sec    14.64     10.68    40.00     57.94%
  220 requests in 5.09s, 338.38KB read
  Socket errors: connect 0, read 0, write 0, timeout 87
Requests/sec:     43.20
Transfer/sec:     66.45KB

rps只有43。咱們繼續進行改進,經過觀察咱們發現咱們查詢todo時須要經過created_at這個字段進行排序再過濾,這樣以來每次查詢都要先對10000條記錄進行排序,效率天然變的很低,對於這個場景,能夠對created_at這個字段作索引:

db.todos.ensureIndex({'created_at': -1})

經過explain咱們輕易地看出mongo使用了索引作掃描

> db.todos.find().sort({'created_at': -1}).limit(10).explain()
/* 1 */
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "todo.todos",
        "indexFilterSet" : false,
        "parsedQuery" : {},
        "winningPlan" : {
            "stage" : "LIMIT",
            "limitAmount" : 10,
            "inputStage" : {
                "stage" : "FETCH",
                "inputStage" : {
                    "stage" : "IXSCAN",
                    "keyPattern" : {
                        "created_at" : -1.0
                    },
                    "indexName" : "created_at_-1",
                    "isMultiKey" : false,
                    "multiKeyPaths" : {
                        "created_at" : []
                    },
                    "isUnique" : false,
                    "isSparse" : false,
                    "isPartial" : false,
                    "indexVersion" : 2,
                    "direction" : "forward",
                    "indexBounds" : {
                        "created_at" : [ 
                            "[MaxKey, MinKey]"
                        ]
                    }
                }
            }
        },
        "rejectedPlans" : []
    },
    "serverInfo" : {
        "host" : "841bf506b6ec",
        "port" : 27017,
        "version" : "3.4.1",
        "gitVersion" : "5e103c4f5583e2566a45d740225dc250baacfbd7"
    },
    "ok" : 1.0
}

如今再作一輪性能測試,有了索引以後就大大下降了排序的成本,rps提升到了298。

wrk -c 100 -t 12 -d 5s http://127.0.0.1:8000/todo
Running 5s test @ http://127.0.0.1:8000/todo
  12 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   310.32ms   47.51ms 357.47ms   94.57%
    Req/Sec    26.88     14.11    80.00     76.64%
  1511 requests in 5.06s, 2.27MB read
Requests/sec:    298.34
Transfer/sec:    458.87KB

再把重心放到app server上,目前咱們使用flask內建的wsgi server,這個server因爲是單進程單線程模型的,因此性能不好,一個請求不處理完的話服務器就會阻塞住其餘請求,咱們須要對這個server作替換。關於python web的app server選擇,目前主流採用的有:

  • gunicorn
  • uWSGI

咱們看gunicorn文檔能夠得知,gunicorn是一個python編寫的高效的WSGI HTTP服務器,gunicorn使用pre-fork模型(一個master進程管理多個child子進程),使用gunicorn的方法十分簡單:

gunicorn --workers=9 server:app --bind 127.0.0.1:8000

根據文檔說明使用(2 * cpu核心數量)+1個worker,還要傳入一個兼容wsgi app的start up方法,經過Flask的源碼能夠看到,Flask這個類實現了下面這個接口:

def __call__(self, environ, start_response):
        """Shortcut for :attr:`wsgi_app`."""
        return self.wsgi_app(environ, start_response)

也就是說咱們只需把flask實例的名字傳給gunicorn就ok了:

gunicorn --workers=9 server:app --bind 127.0.0.1:8000
[2017-01-27 11:20:01 +0800] [5855] [INFO] Starting gunicorn 19.6.0
[2017-01-27 11:20:01 +0800] [5855] [INFO] Listening at: http://127.0.0.1:8000 (5855)
[2017-01-27 11:20:01 +0800] [5855] [INFO] Using worker: sync
[2017-01-27 11:20:01 +0800] [5889] [INFO] Booting worker with pid: 5889
[2017-01-27 11:20:01 +0800] [5890] [INFO] Booting worker with pid: 5890
[2017-01-27 11:20:01 +0800] [5891] [INFO] Booting worker with pid: 5891
[2017-01-27 11:20:01 +0800] [5892] [INFO] Booting worker with pid: 5892
[2017-01-27 11:20:02 +0800] [5893] [INFO] Booting worker with pid: 5893
[2017-01-27 11:20:02 +0800] [5894] [INFO] Booting worker with pid: 5894
[2017-01-27 11:20:02 +0800] [5895] [INFO] Booting worker with pid: 5895
[2017-01-27 11:20:02 +0800] [5896] [INFO] Booting worker with pid: 5896
[2017-01-27 11:20:02 +0800] [5897] [INFO] Booting worker with pid: 5897

能夠看到gunicorn啓動了9個進程(其中1個父進程)監聽請求。使用了多進程的模型看起來是下面這樣的:

繼續進行性能測試,能夠看到吞吐量又有了很大的提高:

wrk -c 100 -t 12 -d 5s http://127.0.0.1:8000/todo
Running 5s test @ http://127.0.0.1:8000/todo
  12 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   109.30ms   16.10ms 251.01ms   90.31%
    Req/Sec    72.47     10.48   100.00     78.89%
  4373 requests in 5.07s, 6.59MB read
Requests/sec:    863.35
Transfer/sec:      1.30MB

那麼gunicorn還能再優化嗎,答案是確定的。回到以前咱們發現了這一行:

[2017-01-27 11:20:01 +0800] [5855] [INFO] Using worker: sync

也就是說,gunicorn worker使用的是sync(同步)模式來處理請求,那麼它支持async(異步)模式嗎,再看gunicorn的文檔有下面一段說明:

Async Workers
The asynchronous workers available are based on Greenlets (via Eventlet and Gevent). Greenlets are an implementation of cooperative multi-threading for Python. In general, an application should be able to make use of these worker classes with no changes.

gunicorn支持基於greenlet的異步的worker,它使得worker可以協做式地工做。當worker阻塞在外部調用的IO操做時,gunicorn會聰明地把執行調度給其餘worker,掛起當前的worker,直至IO操做完成後,被掛起的worker又會從新加入到調度隊列中,這樣gunicorn便有能力處理大量的併發請求了。

gunicorn有兩個不錯的async worker:

  • meinheld
  • gevent

meinheld是一個基於picoev的異步WSGI Web服務器,它能夠很輕鬆地集成到gunicorn中,處理wsgi請求。

gunicorn --workers=9 --worker-class="meinheld.gmeinheld.MeinheldWorker" server:app --bind 127.0.0.1:8000
[2017-01-27 11:47:01 +0800] [7497] [INFO] Starting gunicorn 19.6.0
[2017-01-27 11:47:01 +0800] [7497] [INFO] Listening at: http://127.0.0.1:8000 (7497)
[2017-01-27 11:47:01 +0800] [7497] [INFO] Using worker: meinheld.gmeinheld.MeinheldWorker
[2017-01-27 11:47:01 +0800] [7531] [INFO] Booting worker with pid: 7531
[2017-01-27 11:47:01 +0800] [7532] [INFO] Booting worker with pid: 7532
[2017-01-27 11:47:01 +0800] [7533] [INFO] Booting worker with pid: 7533
[2017-01-27 11:47:01 +0800] [7534] [INFO] Booting worker with pid: 7534
[2017-01-27 11:47:01 +0800] [7535] [INFO] Booting worker with pid: 7535
[2017-01-27 11:47:01 +0800] [7536] [INFO] Booting worker with pid: 7536
[2017-01-27 11:47:01 +0800] [7537] [INFO] Booting worker with pid: 7537
[2017-01-27 11:47:01 +0800] [7538] [INFO] Booting worker with pid: 7538
[2017-01-27 11:47:01 +0800] [7539] [INFO] Booting worker with pid: 7539

能夠看到如今使用的是meinheld.gmeinheld.MeinheldWorker這個worker。再進行性能測試看看:

wrk -c 100 -t 12 -d 5s http://127.0.0.1:8000/todo
Running 5s test @ http://127.0.0.1:8000/todo
  12 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    84.53ms   39.90ms 354.42ms   72.11%
    Req/Sec    94.52     20.84   150.00     70.28%
  5684 requests in 5.04s, 8.59MB read
Requests/sec:   1128.72
Transfer/sec:      1.71MB

果真提高了很多。

如今有了app server,那須要nginx之類的web server嗎?看看nginx反向代理能帶給咱們什麼好處:

  • 負載均衡,把請求平均地分到上游的app server進程。
  • 靜態文件處理,靜態文件的訪問交給nginx來處理,下降了app server的壓力。
  • 接收完客戶端全部的TCP包,再一次交給上游的應用來處理,防止app server被慢請求干擾。
  • 訪問控制和路由重寫。
  • 強大的ngx_lua模塊。
  • Proxy cache。
  • Gzip,SSL...

爲了之後的擴展性,帶上一個nginx是有必要的,但若是你的應用沒大的需求,那麼可加可不加。

想讓nginx反向代理gunicorn,只需對nginx的配置文件加入幾行配置,讓nginx經過proxy_pass打到gunicorn監聽的端口上就能夠了:

server {
          listen 8888;

          location / {
               proxy_pass http://127.0.0.1:8000;
               proxy_redirect     off;
               proxy_set_header Host $host;
               proxy_set_header X-Real-IP $remote_addr;
               proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
      }

如今應用的結構是這樣的:

但僅僅是這樣仍是不足以應對高併發下的請求的,洪水般的請求勢必是對數據庫的一個重大考驗,把請求數提高到1000,出現了大量了timeout:

wrk -c 1000 -t 12 -d 5s http://127.0.0.1:8888/todo
Running 5s test @ http://127.0.0.1:8888/todo
  12 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   239.50ms  235.76ms   1.93s    91.13%
    Req/Sec    83.07     76.77   434.00     76.78%
  4548 requests in 5.10s, 6.52MB read
  Socket errors: connect 0, read 297, write 0, timeout 36
  Non-2xx or 3xx responses: 289
Requests/sec:    892.04
Transfer/sec:      1.28MB

阻止洪峯的方法有:

  • 限流(水桶算法)
  • 分流(負載均衡)
  • 緩存
  • 訪問控制

等等..這裏重點說緩存,緩存系統是每一個web應用程序重要的一個模塊,緩存的做用是把熱點數據放入內存中,下降對數據庫的壓力。

下面用redis來對第一頁的數據進行緩存:

rds = redis.StrictRedis('127.0.0.1', 6379)

@app.route('/todo')
def index():
    start = request.args.get('start', '')
    start = int(start) if start.isdigit() else 0
    data = rds.get('todos')
    if data and start == 0:
        return data
    todos = db.todos.find().sort([('created_at', -1)]).limit(10).skip(start)
    data = dumps(todos)
    rds.set('todos', data, 3600)
    return data

只有在第一次請求時接觸到數據庫,其他請求都會從緩存中讀取,瞬間就提升了應用的rps。

wrk -c 1000 -t 12 -d 5s http://127.0.0.1:8888/todo
Running 5s test @ http://127.0.0.1:8888/todo
  12 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    68.33ms   95.27ms   1.34s    93.69%
    Req/Sec   277.32    258.20     1.60k    77.33%
  15255 requests in 5.10s, 22.77MB read
  Socket errors: connect 0, read 382, write 0, timeout 0
  Non-2xx or 3xx responses: 207
Requests/sec:   2992.79
Transfer/sec:      4.47MB

上面的這個示例只展現了基礎的緩存方式,並無針對多用戶的狀況處理,在涉及到狀態條件的影響下,應該使用更加複雜的緩存策略。

如今再來考慮使用緩存不當會形成幾個問題,設置緩存的時間是3600秒,當3600秒事後緩存失效,而新緩存又沒完成的中間時間內,若是有大量請求到來,就會蜂擁去查詢數據庫,這種現象稱爲緩存雪崩,針對這個狀況,能夠對數據庫請求這個動做進行加鎖,只容許第一個請求訪問數據庫,更新緩存後其餘的請求都會訪問緩存,第二種方法是作二級緩存,拷貝緩存比一級緩存設置更長的過時時間。還有緩存穿透緩存一致性等問題雖然這裏沒有體現,但也是緩存設計中值得思考的幾個點。

下面是加入緩存後的系統結構:

目前爲止還不能說完善,若是中間某個進程掛掉了,那麼整個系統的穩定性就會土崩瓦解。爲此,要在中間加入一個進程管理工具:supervisor來監控和重啓應用進程。

首先要創建supervisor的配置文件:supervisord.conf

[program:gunicorn]
command=gunicorn --workers=9 --worker-class="meinheld.gmeinheld.MeinheldWorker" server:app --bind 127.0.0.1:8000
autostart=true
autorestart=true
stdout_logfile=access.log
stderr_logfile=error.log

而後啓動supervisord做爲後臺進程。

supervisord -c supervisord.conf

雖然緩存能夠有效地幫咱們減輕數據庫的壓力,但若是系統遇到大量併發的耗時任務時,進程也會阻塞在任務的處理上,影響了其餘普通請求的正常響應,嚴重時,系統極可能會出現假死現象,爲了針對對耗時任務的處理,咱們的應用還須要引入一個外部做業的處理系統,當程序接收到耗時任務的請求時,交給任務的工做進程池來處理,而後再經過異步回調或消息通知等方式來得到處理結果。

應用程序與任務進程的通訊一般藉助消息隊列的方式來進行通訊,簡單來講,應用程序會把任務信息序列化爲一條消息(message)放入(push)與特定任務進程之間的信道里(channel),消息的中間件(broker)負責把消息持久化到存儲系統,此時任務進程再經過輪詢的方式獲取消息,處理任務,再把結果存儲和返回。

顯然,此時咱們須要一個負責分發消息和與隊列打交道的調度器和一個存儲消息的中間件。

Celery是基於Python的一個分佈式的消息隊列調度系統,咱們把Celery做爲消息調度器,Redis做爲消息存儲器,那麼應用看起來應該是這樣的。

通常來講,這個結構已經知足大多數的小規模應用了,剩下作的就是代碼和組件配置的調優了。

而後還有一個很重要的點就是:測試

雖然不少人不喜歡寫測試(我也不喜歡),但良好的測試對調試和排錯是有很大幫助的。這裏指的測試不只僅是單元測試,關於測試能夠從幾個方面入手:

  • 壓力測試
    • wrk(請求)
    • htop(cpu和內存佔用)
    • dstat(硬盤讀寫)
    • tcpdump(網絡包)
    • iostat(io讀寫)
    • netstat(網絡鏈接)
  • 代碼測試
    • unittest(單元測試)
    • selenium(瀏覽器測試)
    • mock/stub
  • 黑盒測試
  • 功能測試
  • ...

還有另外一個沒提到的點就是:安全,主要注意幾個點,其餘奇形怪狀的坑根據實際狀況做相應的調整:

  • SQL注入
  • XSS攻擊
  • CSRF攻擊
  • 重要信息加密
  • HTTPS
  • 防火牆
  • 訪問控制

面對系統日益複雜和增加的依賴,有什麼好的方法來保持系統的高可用和穩定一致性呢?docker是應用隔離和自動化構建的最佳選擇。docker提供了一個抽象層,虛擬化了操做系統環境,用容器技術爲應用的部署提供了沙盒環境,使應用之間能靈活地組合部署。

將每一個組件獨立爲一個docker容器:

docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS                                                NAMES
cdca11112543        nginx               "nginx -g 'daemon off"   2 days ago          Exited (128) 2 days ago                                                         nginx
83119f92104a        cassandra           "/docker-entrypoint.s"   2 days ago          Exited (0) 2 days ago                                                           cassandra
841bf506b6ec        mongo               "/entrypoint.sh mongo"   2 days ago          Exited (1) 2 minutes ago   0.0.0.0:27017->27017/tcp, 0.0.0.0:28017->28017/tcp   mongo
b110a4530c4a        python:latest       "python3"                2 days ago          Exited (0) 46 hours ago                                                         python
b522b2a8313b        phpfpm              "docker-php-entrypoin"   4 days ago          Exited (0) 4 days ago                                                           php-fpm
f8630d4b48d7        spotify/kafka       "supervisord -n"         2 weeks ago         Exited (0) 6 days ago                                                           kafka

關於docker的用法,能夠了解官方文檔。

當業務逐漸快速增加時,原有的架構極可能已經不能支撐大流量帶來的訪問壓力了。

這時候就可使用進一步的方法來優化應用:

  • 優化查詢,用數據庫工具作explain,記錄慢查詢日誌。
  • 讀寫分離,主節點負責接收寫,複製的從節點負責讀,並保持與主節點同步數據。
  • 頁面緩存,若是你的應用是面向頁面的,能夠對頁面和數據片進行緩存。
  • 作冗餘表,把一些小表合併成大表以減小對數據庫的查詢次數。
  • 編寫C擴展,把性能痛點交給C來處理。
  • 提升機器配置,這也許是最簡單粗暴地提高性能的方式了...
  • PyPy

不過即便再怎麼優化,單機所能承載的壓力畢竟是有限的,這時候就要引入更多的服務器,作LVS負載均衡,提供更大的負載能力。但多機器的優化帶來了許多額外的問題,好比,機器之間怎麼共享狀態和數據,怎麼通訊,怎麼保持一致性,全部的這些都迫使着原有的結構要更進一步,進化爲一個分佈式系統,使各個組件之間鏈接爲一個互聯網絡。

這時候你須要解決的問題就更多了:

  • 集羣的部署搭建
  • 單點問題
  • 分佈式鎖
  • 數據一致性
  • 數據可用性
  • 分區容忍性
  • 數據備份
  • 數據分片
  • 數據熱啓動
  • 數據丟失
  • 事務

分佈式的應用的結構是下面這樣的:

 

展開來講還有不少,服務架構,自動化運維,自動化部署,版本控制、前端,接口設計等,不過我認爲到這裏,做爲後端的基本職責就算是完成了。除了基本功,幫你走得更遠的是內功:操做系統、數據結構、計算機網絡、設計模式、數據庫這些能力能幫助你設計出更加無缺的程序。

相關文章
相關標籤/搜索