skydum

個人的な作業記録とか備忘録代わりのメモ

CentOS7 + apache + uwsgi + python3 + flaskでAPIサーバを構築する その1

APIサーバを作るための環境を構築する その1

  • dockerでやったほうが楽だと思うけれども、dockerを使えないので…。
  • apche + コンソールから起動したuwsgiと連携して動作するまでの確認を行う

構築順序

  1. OSのインストールと関連するソフトのインストール
  2. uwsgi + flaskでAPIを起動して動作テスト
  3. uwsgi + uwsgi.ini + flaskでAPIを起動して動作テスト
  4. apache + uwsgi(http) + uwsgi.ini + flaskでAPIを起動して動作テスト
  5. apache + uwsgi(socket) + uwsgi.ini + flaskでAPIを起動して動作テスト
    • httpで連携するより多少パフォーマンスが良いらしい?

構築する環境

  • CentOS Linux release 7.9.2009 (Core)
  • apach(2.4.6-97)
  • python(3.6.8)
  • uwsgi(2.0.20)
  • Flask(2.0.3)
  • mod_proxy_uwsgi(2.0.18)

CentOS7をインストール

  • テストで利用するユーザーはwww_user
  • IPアドレス 192.168.0.150/24
  • GW 192.168.0.1
  • DNS 192.168.0.1
  • SELinuxはdisabled
  • yum install httpdapacheをインストールと自動起動の設定
  • 検証なのでfirewalldもストップ
    • firewalldを使いたいときは以下のコマンドで(永続化するときは--permanentをつける)
      • firewall-cmd --add-port=使うポート番号 --zone=public --permanent
      • firewall-cmd --reload ← 永続化したときはリロード必須

uwsgi + flaskの連携

python3のインストール

  • python3.6をepelからインストール
  • ついでにpipのバージョンアップ
# yum install -y epel-release
# yum install -y python36
# python3 -m pip install --upgrade pip

uWSGIのインストール

  • uWSGIのインストールには以下のパッケージが必要
    • gcc, python36-devel
# yum install -y gcc python36-devel
# python3 -m pip install uWSGI flask
# python3 -m pip list

flaskでサンプルのAPIを作成

  • /, /ok, /ng, /raiseの4個のエンドポイントがある

  • api.py

from flask import Flask, jsonify

app = Flask(__name__)


@app.errorhandler(Exception)
def error_handler(e):
    return jsonify(e.args), 500


@app.route("/")
def root():
    return "root", 200


@app.route("/ok")
def ok():
    return "OK", 200


@app.route("/ng")
def ng():
    return "ng", 400


@app.route("/raise")
def raise_error():
    raise Exception("RAISE ERROR")


if __name__ == "__main__":
    app.run()

uwsgi + flaskでapiのテスト

uwsgiでコマンドラインからapiを起動

  • 以下のコマンドでapiを起動した後ブラウザからアクセスして問題がないことを確認する
  • http;//192.168.0.150:8080/
$ uwsgi --http=192.168.0.150:8080 --wsgi-file=api.py --callable=app

コマンド省略用のuwsgi.iniファイルを作成してuwsgi + flask apiの動作確認を行う

uwsgi.iniファイルの作成

[uwsgi]
########################
# 後で参照するように設定する
########################
base_dir = /home/www_user/api_test
app_name = api

########################
# uwsgiの設定
########################
# APIを起動する前にカレントディレクトリを変更
chdir = %(base_dir)

# 起動するIPアドレスとポート番号
http = 192.168.0.150:8080

# 起動するファイル
wsgi-file = api.py

# 起動するオブジェクト app = Flask(__name__)のappの事
callable = app

# 起動するグループID
gid = www_user

# 起動するユーザーID
uid = www_user

# 起動するプロセス数
# processes = 1 は workers = 1と同じ意味
processes = 1

# マルチスレットで動作させ、スレッドとして15スレッド起動する
threads = 15

# 以下に指定したファイルを更新するとAPIがリロードされる
touch-reload = %(base_dir)/api_touch_reload

# タイムアウトまでの時間(秒)
harakiri = 60

# 基本的に有効とする
# 有効にするとうまいことアプリケーションのリロードを行ってくれる
master = true

########################
# ログ関係
########################
# access_logの設定
req-logger = file:/var/log/uwsgi/%(app_name)/access_log

# error_logの設定
logger = file:/var/log/uwsgi/%(app_name)/error_log

uwsgi + uwsgi.ini + flaskでapiのテスト

uwsgiでコマンドラインからapiを起動

  • 以下のコマンドでapiを起動した後ブラウザからアクセスして問題がないことを確認する
  • ログはuwsgi.iniで指定した/var/log/uwsgi/api/access_logと/var/log/uwsgi/api/error_logに出力される
  • http;//192.168.0.150:8080/
$ uwsgi uwsgi.ini

apacheとuwsgiの連携

apacheのインストールと必要なモジュールの確認

  • apacheとuwsgiの連携にはいくつかLoadModuleで必要なモジュールがapacheに組み込まれている必要がある
    • apacheとuwsgiをhttpで連携する場合
      • proxy_http_moduleがapacheに組み込まれている必要がある
    • apacheとuwsgiをunix socketで連携する場合
      • mod_proxy_uwsgiがapacheに組み込まれている必要がある
# yum install httpd
# systemctl start httpd
# systemctl enable httpd
# http -M | grep proxy_http_module  ← proxy_http_moduleが組み込まれているか確認
 proxy_http_module (shared)    ← この表示があれば問題なし(なければ次の項目で組み込みを行う)

apacheとuwsgiをhttpで連携

  • /etc/httpd/conf.d/uwsgi.confファイルを新規で作成する
# apacheにproxy_http_moduleを組み込むときは行頭の#を外す
# LoadModule proxy_http_module modules/mod_proxy_http.so

# mod_proxy_uwsgiをapacheに組み込む
LoadModule proxy_uwsgi_module /usr/lib64/httpd/modules/mod_proxy_uwsgi.so

# フォワードプロキシをOffにする
ProxyRequests Off

# /apiに来たリクエストをhttp://192.168.0.150:8080へ転送する
ProxyPass /api http://192.168.0.150:8080
ProxyPassReverse /api http://192.168.0.150:8080
# systemctl reload httpd
# systemctl status httpd

uwsgi + uwsgi.ini + flaskでapiのテスト

  • 以下のコマンドでapiを起動した後ブラウザからアクセスして問題がないことを確認する
  • ログはuwsgi.iniで指定した/var/log/uwsgi/api/access_logと/var/log/uwsgi/api/error_logに出力される
  • http;//192.168.0.150/ ← 先程までと違いport番号が不要
$ uwsgi uwsgi.ini

apacheとuwsgiをsocketで連携

  • uwsgi.iniの以下のところを書き換える
# 起動するIPアドレスとポート番号
# http = 192.168.0.150:8080  ← コメントアウトする

# 以下を追記
socket = 127.0.0.1:8080
  • uwsgi.confの以下のところを以下のように書き換える
# apacheにproxy_http_moduleを組み込むときは行頭の#を外す
# LoadModule proxy_http_module modules/mod_proxy_http.so

# mod_proxy_uwsgiをapacheに組み込む
LoadModule proxy_uwsgi_module /usr/lib64/httpd/modules/mod_proxy_uwsgi.so

# フォワードプロキシをOffにする
ProxyRequests Off

# /apiに来たリクエストをhttp://192.168.0.150:8080へ転送する
ProxyPass /api uwsgi://192.168.0.150:8080
ProxyPassReverse /api uwsgi://192.168.0.150:8080
  • apacheをリロードして設定を再読み込み
# systemctl reload httpd

uwsgi + uwsgi.ini + flaskでapiのテスト

  • 以下のコマンドでapiを起動した後ブラウザからアクセスして問題がないことを確認する
  • ログはuwsgi.iniで指定した/var/log/uwsgi/api/access_logと/var/log/uwsgi/api/error_logに出力される
  • http;//192.168.0.150/ ← httpでの連携と同じくport番号が不要
$ uwsgi uwsgi.ini

httpとsocketのどっちを使った方がいい?(番外編)

  • どっちのほうがレスポンスが良いのか負荷をかけて検証してみる
    • 環境はHyper-V上に構築したCentOS7 + プロセッサ4 + メモリ 1G
  • 利用するuwsgi.iniは以下の内容(上の方に書いたものと殆ど同じ)
    • uwsgi_http.iniとuwsgi_socket.iniとして作成する

uwsgi_http.ini

[uwsgi]
########################
# 後で参照するように設定する
########################
base_dir = /home/www_user/api_test
app_name = api


########################
# uwsgiの設定
########################
# APIを起動する前にカレントディレクトリを変更
chdir = %(base_dir)

# httpで起動するIPアドレスとポート番号
http = 127.0.0.1:8080

# 起動するファイル
wsgi-file = api.py

# 起動するオブジェクト app = Flask(__name__)のappの事
callable = app

# 起動するグループID
gid = www_user

# 起動するユーザーID
uid = www_user

# 起動するプロセス数
# processes = 1 は workers = 1と同じ意味
processes = 1

# マルチスレッドで動作させる
threads = 15

# 以下に指定したファイルを更新するとAPIがリロードされる
touch-reload = %(base_dir)/api_touch_reload

# タイムアウトまでの時間(秒)
harakiri = 60

# 基本的に有効とする
# 有効にするとうまいことアプリケーションのリロードを行ってくれる
master = true

########################
# ログ関係
########################
# access_logの設定
req-logger = file:/var/log/uwsgi/%(app_name)/http_access_log

# error_logの設定
logger = file:/var/log/uwsgi/%(app_name)/http_error_log

uwsgi_socket.ini

[uwsgi]
########################
# 後で参照するように設定する
########################
base_dir = /home/www_user/api_test
app_name = api


########################
# uwsgiの設定
########################
# APIを起動する前にカレントディレクトリを変更
chdir = %(base_dir)

# socketで起動するIPアドレスとポート番号 
socket = 127.0.0.1:9090

# 起動するファイル
wsgi-file = api.py

# 起動するオブジェクト app = Flask(__name__)のappの事
callable = app

# 起動するグループID
gid = www_user

# 起動するユーザーID
uid = www_user

# 起動するプロセス数
# processes = 1 は workers = 1と同じ意味
processes = 1

# マルチスレッドで動作させる
threads = 15

# 以下に指定したファイルを更新するとAPIがリロードされる
touch-reload = %(base_dir)/api_touch_reload

# タイムアウトまでの時間(秒)
harakiri = 60

# 基本的に有効とする
# 有効にするとうまいことアプリケーションのリロードを行ってくれる
master = true

########################
# ログ関係
########################
# access_logの設定
req-logger = file:/var/log/uwsgi/%(app_name)/socket_access_log

# error_logの設定
logger = file:/var/log/uwsgi/%(app_name)/socket_error_log
# mod_proxy_uwsgiをapacheに組み込む
LoadModule proxy_uwsgi_module /usr/lib64/httpd/modules/mod_proxy_uwsgi.so

# フォワードプロキシをOffにする
ProxyRequests Off

ProxyPass /http http://127.0.0.1:8080
ProxyPassReverse /http http://127.0.0.1:8080

ProxyPass /unix uwsgi://127.0.0.1:9090
ProxyPassReverse /unix uwsgi://127.0.0.1:9090
  • uwsgiコマンドでapiを2個起動する
$ uwsgi uwsgi_http.ini & uwsgi uwsgi_socket.ini
  • APIが正常に起動しているか確認する
$ curl http://192.168.0.150/http
$ curl http://192.168.0.150/unix

負荷をかけてみる

  • httpでの連携からテスト
# ab -c 100 -n 10000 http://192.168.0.150/http
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.0.150 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        Apache/2.4.6
Server Hostname:        192.168.0.150
Server Port:            80

Document Path:          /http
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   4.112 seconds
Complete requests:      10000
Failed requests:        3555
   (Connect: 0, Receive: 0, Length: 3555, Exceptions: 0)
Non-2xx responses:      3555
Total transferred:      3314685 bytes
HTML transferred:       1401565 bytes
Requests per second:    2431.91 [#/sec] (mean)
Time per request:       41.120 [ms] (mean)
Time per request:       0.411 [ms] (mean, across all concurrent requests)
Transfer rate:          787.21 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       5
Processing:     2   40  11.3     37      94
Waiting:        2   40  11.3     37      94
Total:          5   41  11.3     37      94

Percentage of the requests served within a certain time (ms)
  50%     37
  66%     42
  75%     47
  80%     50
  90%     59
  95%     65
  98%     70
  99%     73
 100%     94 (longest request)
  • socketでの連携をテスト
# ab -c 100 -n 10000 http://192.168.0.150/unix
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.0.150 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        Apache/2.4.6
Server Hostname:        192.168.0.150
Server Port:            80

Document Path:          /unix
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   5.569 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2010000 bytes
HTML transferred:       40000 bytes
Requests per second:    1795.74 [#/sec] (mean)
Time per request:       55.687 [ms] (mean)
Time per request:       0.557 [ms] (mean, across all concurrent requests)
Transfer rate:          352.48 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       5
Processing:     4   55  14.8     51     126
Waiting:        2   53  14.5     48     118
Total:          5   55  14.9     51     126

Percentage of the requests served within a certain time (ms)
  50%     51
  66%     57
  75%     62
  80%     65
  90%     75
  95%     88
  98%    100
  99%    106
 100%    126 (longest request)

負荷テストの結果

  • httpでapache + uwsgiで連携した場合はNon-2xx responsesのエラーが出る

  • /http

Concurrency Level:      100
Time taken for tests:   4.112 seconds
Complete requests:      10000
Failed requests:        3555
   (Connect: 0, Receive: 0, Length: 3555, Exceptions: 0)
Non-2xx responses:      3555
Total transferred:      3314685 bytes
HTML transferred:       1401565 bytes
Requests per second:    2431.91 [#/sec] (mean)
Time per request:       41.120 [ms] (mean)
Time per request:       0.411 [ms] (mean, across all concurrent requests)
Transfer rate:          787.21 [Kbytes/sec] received
Concurrency Level:      100
Time taken for tests:   5.569 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2010000 bytes
HTML transferred:       40000 bytes
Requests per second:    1795.74 [#/sec] (mean)
Time per request:       55.687 [ms] (mean)
Time per request:       0.557 [ms] (mean, across all concurrent requests)
Transfer rate:          352.48 [Kbytes/sec] received

負荷テストのエラーの理由

[Thu Jun 30 20:48:53.238914 2022] [proxy_http:error] [pid 3434] (20014)Internal error: [client 192.168.0.252:53796] AH01102: error reading status line from remote server 127.0.0.1:8080
[Thu Jun 30 20:48:53.238937 2022] [proxy:error] [pid 3434] [client 192.168.0.252:53796] AH00898: Error reading from remote server returned by /http
[Thu Jun 30 20:48:53.240173 2022] [proxy_http:error] [pid 3421] (20014)Internal error: [client 192.168.0.252:53828] AH01102: error reading status line from remote server 127.0.0.1:8080
[Thu Jun 30 20:48:53.240193 2022] [proxy:error] [pid 3421] [client 192.168.0.252:53828] AH00898: Error reading from remote server returned by /http
[Thu Jun 30 20:48:53.242943 2022] [proxy_http:error] [pid 3416] (20014)Internal error: [client 192.168.0.252:53668] AH01102: error reading status line from remote server 127.0.0.1:8080
[Thu Jun 30 20:48:53.242958 2022] [proxy:error] [pid 3416] [client 192.168.0.252:53668] AH00898: Error reading from remote server returned by /http
[Thu Jun 30 20:48:53.243063 2022] [proxy_http:error] [pid 3429] (20014)Internal error: [client 192.168.0.252:53840] AH01102: error reading status line from remote server 127.0.0.1:8080
[Thu Jun 30 20:48:53.243084 2022] [proxy:error] [pid 3429] [client 192.168.0.252:53840] AH00898: Error reading from remote server returned by /http
[Thu Jun 30 20:48:53.243189 2022] [proxy_http:error] [pid 3405] (20014)Internal error: [client 192.168.0.252:53838] AH01102: error reading status line from remote server 127.0.0.1:8080
[Thu Jun 30 20:48:53.243204 2022] [proxy:error] [pid 3405] [client 192.168.0.252:53838] AH00898: Error reading from remote server returned by /http

対応方法

  • 長くなったので次へ続く