アプリサーバーを構築する時、少しニッチな問題があります。それがThundering Herd問題。ここでは、docker環境で、uwsgiのthunder-lock設定を有効にしたらこの問題をどのくらい改善できるかを検証して見たいです。
Thundering Herd問題とは
Thundering Herd問題、またの名はZeeg問題はマルチプロセス&マルチスレッドでリクエストを処理する時起こる問題です。もし一つのリクエストが届いて、用意した全部のスレッドが起こされたらリソースが無駄に消費されます。なぜなら結局一リクエストをサーブするのは普通1スレッドです、他のスレッドはただ起きて、タスクがないことを確認して、また寝てしまいます。現在のカーネルでは同じプロセス内のスレッドにロックを待たせて、同プロセス内の全スレッドを起こされる事態を防げました。しかし、プロセス数が多い時プロセス間ロックをかけなかったら、また競合と無駄が発生します。
検証してみます
自社のアプリサーバーがnginx+uwsgi+Djangoで構築されていますので、nginx+uwsgiの構成で問題検証を行いたいです。また、ローカル環境普段色々いじってますので、今回はdockerを使って環境構築します。
検証プラン:
A:環境構築(uwsgiのthunder-lock設定入れない)+k6で負荷をかける+uwsgitopで性能測定
B:環境構築(uwsgiのthunder-lock設定入れる)+k6で負荷をかける+uwsgitopで性能測定
環境構築
まずはdocker-composeを使ってdocker環境を構築する、ワーキングディレクトリに以下のようなymlファイルを用意します
version: '2'
services:
web:
image: tiangolo/uwsgi-nginx:python2.7
ports:
- "30301:80"
networks:
- "mynet"
volumes:
- ./workingD:/var/workingD
networks:
mynet:
次にdocker-compose upコマンドdockerのイメージなどをプルして設定したnginx+uwsgiが入ってたwebという名のサービスをスタートします。
サービスが起動されたあと(uwsgiのログがプリントされるのが確認できます)、下のコマンドでdockerに入ります
docker exec -it dockusginginx_web_1 /bin/bash
これで当該ディレクトリに行ってnginxとuwsgiの設定を変更できます。
nginxの環境設定(/etc/nginx/conf.d/nginx.conf)
ここはデフォルトの設定でおっけーです。
root@1c30155afc5e:/etc/nginx/conf.d# cat nginx.conf
server {
location / {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi.sock;
}
uwsgiの環境設定(/etc/uwsgi/uwsgi.ini)
マルチスレッドを4と設定した上、プロセス数を16にしました。この方(プロセス数多め)がthundering herd問題を確認しやすいと思いました。
[uwsgi]
socket = /tmp/uwsgi.sock
chown-socket = nginx:nginx
chmod-socket = 664
processes = 16
threads = 4
uwsgiのappの設定
ここではuwsgitopが性能のログを読めるようにするためstatsの吐き出し先を設定しました。
/var/workingD/stats.sockはローカル環境からマウントされたディレクトリです(docker内のファイルを指定しても書き込めないためです)。検証プランBへ変更する時はここにthunder-lock = trueを入れれば変更完了です。
[uwsgi]
wsgi-file=/app/main.py
stats = /var/workingD/stats.sock
memory-report = true
アプリのロジック
uwsgiで走るアプリの内容はほぼハローワールドを返すだけですが、それだと軽すぎなのでCPUバウンドなジョブを任せました。
def application(env, start_response):
workload()
start_response('200 OK', [('Content-Type', 'text/html')])
return ["NEWWWWWWW Hello World from a default Nginx uWSGI Python 2.7 app in a\
Docker container (default)"]
def workload():
"""
provide CPU pound workload
"""
counter = 1000000
while counter > 0:
counter -= 1
uwsgitopについて
uwsgitopはuwsgiのパーフォマンスを観測するためのツールです。pythonのパッケージなので
pip install uwsgitop
をdocker内で入力すればインストールできます。
k6について
今回負荷をかけるのにk6を使いました、python発のlocustでもいいですが、新しいものには試したくなったのでk6に〜
k6を使うにあたりまずスクリプトを用意します
import http from "k6/http";
export default function(){
http.get("http://localhost:30301/");
};
ここではk6のエントリーとしてdefault関数を定義します。やることはdockerが開放&リッスンしているポートにgetリクエストを送るだけです。
ロードテストを開始したい時、loadtest.jsがあるディレクトリで下記コマンドを入力すればおっけーです
k6 run --vus 64 --duration 0 loadtest.js
ここでは64virtual user(疑似ユーザー)に同時テストスクリプトを実行させます。durationを0に指定すれば中止させるまでテストが続行されます。
検証結果
検証プランA:
約90秒間k6を走らせて計測結果を取ってみると:
uwsgitop
k6
検証プランB:
同じく約90秒間k6を走れせた計測結果:
uwsgitop
k6
結果分析
attention:環境ローカルが4コアなので、dockerで走ってるuwsgiとk6とも実運用環境とかなり違いがあります。
uwsgitop:プランAのRPS(Request Per Second)がプロセス間にばらつきが大きいです、RPSが0なプロセスが二つあります。(とはいえ一瞬の結果をスクショした結果だけです)
他に大きな違いは無いようです。
k6のメトリックス:
http_req_blocked:tcpコネクションが初期化されるまでの時間
http_req_connecting:リモートホストとコネクションを立てる時間
以上二項目はプランBの方が平均とマックスも大きいです。
http_req_duration:ブロックなどを除いたリクエストの処理時間
http_req_waiting:リクエストの初めてレスポンスを返すまでの時間("time to first byte")
の二項目では平均こそ大きな差は無いが、プランBの方がマックスが小さいです。
複数のメトッリクスを比較してみるとthunder-lockを入れたプランBが入れないプランAに全面圧勝できたわけではありません。しかし、レスポンスタイムの外れ値が小さい、uwsgiのプロセス間処理するリクエスト数のばらつきが小さいなどメリットが見れます。