[Sưu tầm] Chạy ứng dụng Flask với Gunicorn trên ubuntu

Table of Contents

[Sưu tầm] Chạy ứng dụng Flask với Gunicorn trên ubuntu

https://vimentor.com/vi/lesson/6-chay-ung-dung-flask-voi-gunicorn-tren-ubuntu

Giới thiệu

Flask và cách xây dựng web API với Flask đã được giới thiệu trong bài viết xây dựng web API với Flask .

Flask không phải là một web server, tuy nhiên để tiện cho quá trình phát triển sản phẩm, tránh việc phải triển khai nhiều thành phần phức tạp, tiết kiệm thời gian cho lập trình viên mà  Flask có thể được sử dụng như một web server trong môi trường phát triển. Không nên sử dụng nó trong môi trường sản phẩm bởi vì Flask chỉ bao gồm những tính năng cơ bản nhất của web server, nó là một web framework ổn định đã được triển khai rất nhiều trong môi trường sản phẩm (môi trường production), nhưng ở khía cạnh web server thì nó không phải là một web server mạnh mẽ, không phù hợp cho môi trường production cần phục vụ số lượng yêu cầu lớn trong một thời gian dài.

Trong phần tiếp theo tôi sẽ giới thiệu cách dùng các công cụ để triển khai hệ thống web server và web API trong thực tế, nội dung bao gồm.

1. Python web server Gunicorn  

2. Chạy microservice bên trong docker container với Flask và Gunicorn

3. Dùng postman để kiểm tra hoạt động của microservice

Python web server Gunicorn

Gunicorn là một trong những python web server theo chuẩn WSGI (Web Server Gateway Interface). Nó ổn định và được triển khai nhiều trong các môi trường sản phẩm thực tế. Instagram là một trong những website mạng xã hội lớn nhất thế giới, được xây dựng bằng python và triển khai dùng gunicorn.

Gunicorn rất dễ sử dụng và hỗ trợ tốt cho nhiều web framework khác nhau như Django, Flask ... Các đặc điểm của Gunicorn:

  • Có thể chạy bất kỳ ứng dụng và framework Python web nào theo chuẩn WSGI

  • Dễ dàng thay thế các web server trong môi trường phát triển sản phẩm (môi trường dành cho lập trình viên để kiểm tra hoạt động của các ứng dụng) mà không cần thay đổi mã nguồn ứng dụng

  • Hỗ trợ rất nhiều loại worker và các cấu hình cho từng loại worker

  • Hỗ trợ các loại worker đồng bộ và bất đồng bộ

  • Hỗ trợ SSL

  • Hỗ trợ python 2 và python 3

Từ khóa worker được nhắc đến nhiều ở phần trên, để hiểu sâu hơn về Gunicorn và khái niệm worker, việc tìm hiểu về hoạt động của nó là điều cần thiết.

Hoạt động của Gunicorn

Gunicorn được thực hiện theo mô hình UNIX pre-fork server

  • Khi khởi chạy, Gunicorn mở một tiến trình gốc (master process), tiến trình gốc này có thể được nhân bản (fork)  thành các tiến trình con, các tiến trình con này được gọi là các worker.

  • Vai trò của tiến trình gốc là đảm bảo số lượng các worker luôn luôn giống với con số đã được định nghĩa trong các file cấu hình hay trong tham số dòng lệnh. Nếu một worker vì vấn đề gì đó mà bị chết, tiến trình gốc sẽ tạo ra một worker mới bằng cách fork chính nó một lần nữa.

  • Vai trò của các worker là tiếp nhận và xử lý các yêu cầu HTTP

  • Từ pre trong mô hình pre-fork nghĩa là tiến trình gốc tạo ra các worker trước khi xử lý bất kỳ yêu cầu HTTP nào.

  • Kernel của hệ điều hành đảm nhận vai trò cân bằng tải giữa các worker

Khi triển khai hệ thống với bất kỳ web server hay web framework nào, hiệu năng là vấn đề cực kỳ quan trong. Với Gunicorn, để tối ưu về mặt hiệu năng chúng ta cần chú ý tới các thành phần sau.

1. Worker - Tiến trình UNIX (workers)

Mỗi worker là một tiến trình UNIX, nó là một thực thể của ứng dụng, các worker là các thực thể riêng biệt của ứng dụng và chúng không chia sẻ tài nguyên bộ nhớ với nhau.

Số lượng worker hợp lý khi chạy Gunicorn trên một máy vật lý (máy ảo) thường là (2*number_of_cpu)+1.

Cho một máy vật lý có 4 CPU, 9 là số lượng worker hợp lý để chạy trên máy đó.

gunicorn --workers=9 main:app

2. Các luồng (threads)

Ngoài việc cho phép tạo ra nhiều worker, gunicorn còn cho phép một worker có thể tạo ra nhiều thread, các thread trong một worker chia sẻ cùng tài nguyên bộ nhớ với nhau.

Để sử dụng thread với gunicorn, chúng ta sử dụng tham số threads.

gunicorn --workers=9 --threads=4 main:app

Số lượng tác vụ cùng lúc tối đa là workers * threads = 36 trong trường hợp ví dụ của chúng ta.

Tuy nhiên số lượng tác vụ được gợi ý nên dùng cho trường hợp dùng lẫn cả workers và threads vẫn là (2*number_of_cpu)+1 = 9 (trong trường hợp máy có 4 CPU). Do vậy để có được tối đa 9 tác vụ chạy đồng thời, ta có thể sử dụng 3 worker và mỗi worker có 3 thread.

gunicorn --workers=3 --threads=3 main:app

3. Loại worker

Gunicorn hỗ trợ nhiều loại worker

  • Sync worker

  • Async worker

  • Tornado worker

  • AsyncIO worker

Mỗi loại worker cung cấp các tính năng khác nhau để xử lý các yêu cầu. Cấu hình với lựa chọn (option) worker-class được sử dụng để chọn loại worker mà gunicorn. Giá trị của worker-class có thể là một trong số sau: sync, gevent, eventlet, tornado, gaiohttp, và gthread. Nếu không sử dụng lựa chọn worker-class thì giá trị mặc định của nó là sync.

gunicorn --worker-class=gevent --worker-connections=1000 --workers=9 main:app

worker-connections là cài đặt cụ thể cho worker loại gevent

Trong ví dụ trên, số lượng yêu cầu đồng thời tối đa là 9000 ( 9 worker * 1000 kết nối trên một worker)

a. Sync worker

Synchronous worker là loại worker mặc định của Gunicorn, nó thể hiện cho mô hình worker cơ bản. Với mô hình này, mỗi worker sẽ xử lý một và chỉ một yêu cầu tại một thời điểm.

Loại worker này thường được sử dụng với ứng dụng không có thời gian đọc ghi vào ổ đĩa dài, hoặc không có những yêu cầu với thời gian lớn (long request - loại yêu cầu như này sẽ làm cho các yêu cầu khác phải đợi cho tới khi yêu cầu hiện tại được hoàn thành, điều này có thể làm cho các yêu cầu thất bại do kết nối bị lỗi timeout),

b. Async worker

Asynchronous worker bao gồm hai loại: gevent và eventlet, hai loại này được phát triển dựa trên thư viện Greenlet. Thư viện này cung cấp các phương thức giúp các tác vụ được xử lý đồng thời, nó giải quyết vấn đề của sync worker, cái mà chỉ xử lý được một yêu cầu tại một thời điểm.

c. Tornado worker

Loại worker này được thiết kế để làm việc với python framework Tornado. Tornado framework là một thư viện mạng cung cấp giải pháp đọc ghi bất đồng bộ non-blocking. Nó xử lý tốt các yêu cầu với thời gian lớn (long request).

d. AsyncIO Worker

AsyncIO bao gồm hai loại gthread và gaiohttp.

gaiohttp sử dụng thư viện aiohttp. Thư viện này thực hiện việc nhập xuất bất đồng bộ trong môi trường mạng ở cả phía client lẫn server và đặc biệt nó hỗ trợ web socket rất tốt.

gthread giữ kết nối trong một nhóm thread và đợi cho đến khi có sự kiện, sự kiện đó sẽ được xử lý bởi một trong các thread ở trong nhóm.

Khi nào nên dùng worker, khi nào nên dùng thread

Trong trường hợp ở môi trường development, chúng ta chỉ khởi chạy một worker, điều này cho phép chúng ta có thể xử lý một yêu cầu duy nhất tại một thời điểm, để tăng được năng lực xử lý của ứng dụng, ta có thể nâng số lượng worker hoặc số thread lên.

So với các worker,các thread nhẹ hơn, chúng tiêu thụ ít bộ nhớ. Gunicorn sẽ đảm bảo việc tiến trình gốc có thể gửi nhiều yêu cầu tới worker hơn bằng cách sử dụng 1 worker và nhiều thread (giả sử là 4). Như vậy có phải lúc nào sử dụng thread cũng có lợi ích nhiều hơn khi sử dụng worker không?

Để trả lời câu hỏi trên, hãy cùng xem xét ví dụ sau;

Giả sử tôi cần làm vài việc với kết quả tìm kiếm được từ một search engine (elasticsearch hay google chẳng hạn). Cùng lúc đó tôi cũng muốn tính toán xem trong kết quả tìm kiếm có số nào là số nguyên tố hay không trong mỗi truy vấn, lúc này tôi gặp phải một vấn đề với Python GIL (Global Interpreter Lock), nói nôm na GIL là một kỹ thuật khóa (lock) của trình thông dịch (trình thông dịch sẽ tự làm việc này chứ không phải lập trình viên), nó chỉ cho phép một thread được thao tác với một tài nguyên tại một thời điểm, điều này giúp trình thông dịch Python kiểm soát được sự thay đổi của tài nguyên (tài nguyên ở đây có thể là một biến, một đối tượng ...), tránh trường hợp chương trình trả về một kết quả không mong muốn mà không biết tại sao. Nhưng nó cũng dẫn đến việc, tuy rằng tôi có 4 thread, nhưng thực chất chỉ có một thread thực sự xử lý kết quả tại một thời điểm, nếu tác vụ xử lý kết quả này tốn nhiều thời gian thì lúc này tôi cần một phương án khác để giúp các tác vụ thực sự được xử lý song song, đó là dùng nhiều các worker hơn.

Chạy microservice bên trong docker container với Flask và Gunicorn

Cài đặt gunicorn

pip3 install gunicorn

Chạy ứng dụng Flask với gunicorn

Trong bài viết trước xây dựng web API với Flask tôi đã nêu ra cách chạy ứng dụng pythonbackenddemo (github: https://github.com/vimentor-com/pythonbackenddemo/tree/5-build-webapi-with-flask) bằng cách chạy lệnh :

python3 api.py

Sau khi cài gunicorn, chúng ta có thể chạy ứng dụng bằng lệnh sau:

gunicorn --workers=3 --threads=3 api:app

Hình 1. Chạy ứng dụng Flask với Gunicorn

Chú ý thay đổi số lượng worker và số lượng thread cho phù hợp với máy của bạn như tôi đã nêu ra trong những phần trước.

Trong dòng lệnh ở trên thì api:app tuân theo cú pháp

gunicorn [OPTIONS] APP_MODULE

Với APP_MODULE có dạng $(MODULE_NAME):$(VARIABLE_NAME).

Tên module là tên python module mà chúng ta muốn chạy. Ở đây tên của module là api vì ta đang chạy module api (file api.py).

Tên biến là tên của ứng dụng WSGI, có thể tìm thấy tên biến này trong một module cụ thể, ở đây ta có thể tìm thấy nó trong module api.

app = Flask(__name__)

Nội dung file api.py:

import cv2
import numpy as np

from flask import Flask, request

from house3d_worker_demo import process_img

app = Flask(__name__)

@app.route("/change_img_color", methods=["POST"])
def convert_img_color():
    # Doc anh gui len tu phia client
    filestr = request.files['image'].read()
    # Do anh duoc gui len co dang du lieu chuoi (string),
    # can duoc chuyen doi sang dang ma tran numpy
    # de tien thao tac ve sau
    npimg = np.fromstring(filestr, np.uint8)
    # Chuyen doi du lieu numpy array ve du lieu ma tran anh chuan
    img = cv2.imdecode(npimg, cv2.IMREAD_COLOR)
    process_img(img)

    return "Done"

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

Khi chạy ứng dụng Flask với gunicorn, những cấu hình như host, port ... trong dòng lệnh app.run(host='0.0.0.0', port=5001) sẽ không còn tác dụng. Vì lúc này gunicorn sẽ đảm nhận việc xử lý các cấu hình đó. Mặc định gunicorn sẽ mở cổng 8000 trên localhost khi chạy ứng dụng. Tuy nhiên chúng ta hoàn toàn có thể thay đổi cấu hình này theo ý muốn.

Việc chạy ứng dụng bên trong docker không có gì khác so với việc chạy ứng dụng bên ngoài docker. Tuy nhiên khi được triển khai trên hệ thống thật, các ứng dụng Flask thường được chạy luôn khi ta khởi chạy docker container. Để làm việc này ta có thể sửa đổi lại Dockerfile và build lại docker image với nội dung của Dockerfile như sau:

FROM ubuntu:18.04

RUN ln -s /usr/share/zoneinfo/Etc/GMT+7 /etc/localtime
RUN apt update\
    && apt install -y git python3\
    python3-pip\
    libsm6\
    libxext6\
    libfontconfig1\
    libxrender1\
    python3-tk

RUN git clone https://github.com/vimentor-com/pythonbackenddemo.git
RUN cd pythonbackenddemo &&\
    git checkout 6-gunicorn-flask &&\
    pip3 install -r requirements.txt
RUN mkdir -p ~/.config/matplotlib/
RUN touch ~/.config/matplotlib/matplotlibrc
RUN echo "backend: Agg" > ~/.config/matplotlib/matplotlibrc

ENTRYPOINT ["/bin/bash", "/pythonbackenddemo/entrypoint.sh"]

So với ví dụ trong bài viết Docker container và các ứng dụng trong kiến trúc microservice. Dockerfile có 2 điểm thay đổi gồm việc chuyển qua nhánh 6-gunicorn-flask khi clone ứng dụng pythonbackenddemo từ github về, và thêm ENTRYPOINT để chạy ứng dụng ngay khi chạy docker container. Ở ENTRYPOINT ta gọi đến script entrypoint.sh, script này bao gồm các câu lệnh để chạy ứng dụng.

Nội dung của entrypoint.sh như sau:

#!/usr/bin/env bash

cd /pythonbackenddemo
gunicorn --workers=3 --threads=3 api:app

Các bước để chạy ứng dụng Flask ngay khi khởi chạy docker container

B1. Clone mã nguồn và chuyển về nhánh 6-gunicorn-flask

git clone https://github.com/vimentor-com/pythonbackenddemo.git
cd pythonbackenddemo
git checkout 6-gunicorn-flask

B2. Build docker image với lệnh

sudo docker build . -t demoimg:v0.2

B2. Khởi chạy docker container

sudo docker run -it --name='pythonbackenddemo' demoimg:v0.2 bash

Log xuất hiện trên màn hình sau khi chạy docker container

$ sudo docker run -it --name='pythonbackenddemo' -p 8000:8000 demoimg:v0.2 bash
[2019-02-28 01:44:38 -0700] [6] [INFO] Starting gunicorn 19.9.0
[2019-02-28 01:44:38 -0700] [6] [INFO] Listening at: http://127.0.0.1:8000 (6)
[2019-02-28 01:44:38 -0700] [6] [INFO] Using worker: threads
[2019-02-28 01:44:38 -0700] [9] [INFO] Booting worker with pid: 9
[2019-02-28 01:44:38 -0700] [17] [INFO] Booting worker with pid: 17
[2019-02-28 01:44:38 -0700] [26] [INFO] Booting worker with pid: 26

Chi tiết cách chạy và mã nguồn được đặt tại đường dẫn sau:

https://github.com/vimentor-com/pythonbackenddemo/tree/6-gunicorn-flask

Dùng postman để kiểm tra hoạt động của microservice

Giới thiệu postman

Postman là một công cụ làm việc với API, nó thường được sử dụng để kiểm thử hoạt động của các API trong quá trình phát triển sản phẩm. Với postman chúng ta hoàn toàn có thể gọi một REST API bất kỳ mà không cần viết một dòng code nào. Nó hỗ trợ các phương thức HTTP như: GET, POST, PUT, DELETE, ...

Cài đặt postman

Các bạn vào trang sau tải phần mềm tương ứng với hệ điều hành của bạn: https://www.getpostman.com/downloads/ và thực hiện cài đặt.

Testing API với postman

Trong phần trên chúng ta đã chạy một docker container và đồng thời chạy ứng dụng Flask với gunicorn ngay khi chạy container.  Cổng 8000 là cổng mặc định mà gunicorn sẽ mở khi chạy ứng dụng.

Chúng ta hoàn toàn có thể dùng postman để test các microservice trên một máy bất kỳ miễn là postman của thể kết nối tới máy đó. Trong ví dụ này tôi sử dụng postman để kiểm thử API trên cùng một máy.

Thiết lập postman như sau để gửi yêu cầu tới web server.

1.Phương thức gửi chọn là POST với đường dẫn http://127.0.0.1:8000/change_img_color, web server sẽ chỉ chấp nhận phương thức POST do chúng ta đã định nghĩa trong mã nguồn của file api.py

@app.route("/change_img_color", methods=["POST"])

2. Chọn loại body của yêu cầu gửi lên là form-data để có thể gửi yêu cầu kèm theo file, với nội dung của body bao gồm key là "image", chọn loại dữ liệu của key là file , value là file cần tải lên, có thể chọn từ máy của bạn.

Sau đó nhấn nút "Send" để gửi yêu cầu tới server. Ở phần hiển thị nội dung phản hồi từ server xuất hiện text có nội dung là "Done" chứng tỏ yêu cầu đã được xử lý thành công.

Tóm tắt

Qua bài viết, chúng ta đã hiểu được cách làm việc cơ bản nhất với Gunicorn và kiểm tra được hoạt động của API, Tuy nhiên kết quả trả về chưa đạt được sự tin tưởng bởi vì không kiểm tra được sự thay đổi thực sự của ảnh ra sao. Trong những bài viết sau chúng ta sẽ viết một phần website đơn giản có phần frontend để hiển thị kết quả trả về sau khi đã xử lý xong yêu cầu.

Mọi khó khăn trong việc chạy ứng dụng hay các đóng góp ý kiến về nội dung các bạn có thể bình luận dưới bài viết hoặc post vào group facebook Python Backend Learning Path

Cảm ơn các bạn đã đọc bài!

Tham khảo

[1] Phát triển phần mềm theo kiến trúc microservice

[2] Docker container và các ứng dụng trong kiến trúc microservice (phần 1)

[3] Docker container và các ứng dụng trong kiến trúc microservice (phần 2)

[4] Better performance by optimizing Gunicorn config

[5] http://docs.gunicorn.org/en/stable/settings.html

[6] https://www.fullstackpython.com/wsgi-servers.html

[7] https://realpython.com/python-gil/

[8] https://gunicorn.org/

Leave a Reply

Your email address will not be published. Required fields are marked *