dockerでuwsgiのthunder lock設定を検証

アプリサーバーを構築する時、少しニッチな問題があります。それがThundering Herd問題。ここでは、docker環境で、uwsgiのthunder-lock設定を有効にしたらこの問題をどのくらい改善できるかを検証して見たいです。
# Thundering Herd問題とは
[Thundering Herd問題、またの名はZeeg問題](http://uwsgi-docs.readthedocs.io/en/latest/articles/SerializingAccept.html)はマルチプロセス＆マルチスレッドでリクエストを処理する時起こる問題です。もし一つのリクエストが届いて、用意した全部のスレッドが起こされたらリソースが無駄に消費されます。なぜなら結局一リクエストをサーブするのは普通１スレッドです、他のスレッドはただ起きて、タスクがないことを確認して、また寝てしまいます。現在のカーネルでは同じプロセス内のスレッドにロックを待たせて、同プロセス内の全スレッドを起こされる事態を防げました。しかし、プロセス数が多い時プロセス間ロックをかけなかったら、また競合と無駄が発生します。

# 検証してみます
自社のアプリサーバーがnginx+uwsgi+Djangoで構築されていますので、nginx+uwsgiの構成で問題検証を行いたいです。また、ローカル環境普段色々いじってますので、今回はdockerを使って環境構築します。
検証プラン：
A:環境構築（uwsgiのthunder-lock設定入れない）+k6で負荷をかける+uwsgitopで性能測定
B:環境構築（uwsgiのthunder-lock設定入れる）+k6で負荷をかける+uwsgitopで性能測定

## 環境構築
まずはdocker-composeを使ってdocker環境を構築する、ワーキングディレクトリに以下のようなymlファイルを用意します

```docker-compose.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）
マルチスレッドを４と設定した上、プロセス数を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](https://pypi.python.org/pypi/uwsgitop/0.10)はuwsgiのパーフォマンスを観測するためのツールです。pythonのパッケージなので

```
pip install uwsgitop
```
をdocker内で入力すればインストールできます。

#### k6について
今回負荷をかけるのにk6を使いました、python発のlocustでもいいですが、新しいものには試したくなったのでk6に〜
k6を使うにあたりまずスクリプトを用意します

```js:loadtest.js
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：
約９０秒間k6を走らせて計測結果を取ってみると：
#### uwsgitop
<img width="1029" alt="planA.png" src="https://qiita-image-store.s3.amazonaws.com/0/97508/a9861756-5f62-2a2b-38e1-58f73dc73f5c.png">

#### k6
<img width="773" alt="planAk6.png" src="https://qiita-image-store.s3.amazonaws.com/0/97508/01faa42c-b4f3-cfd4-599d-46454b587eb7.png">


検証プランB:
同じく約９０秒間k6を走れせた計測結果：
#### uwsgitop
<img width="1030" alt="planB.png" src="https://qiita-image-store.s3.amazonaws.com/0/97508/17bc8994-c85f-29f1-f9cf-486ff2a681e5.png">

#### k6
<img width="737" alt="planBk6.png" src="https://qiita-image-store.s3.amazonaws.com/0/97508/85c5f715-28ba-4dfd-1f3f-ccf51e7ba23e.png">

### 結果分析
attention:環境ローカルが４コアなので、dockerで走ってるuwsgiとk6とも実運用環境とかなり違いがあります。
uwsgitop:プランAのRPS（Request Per Second）がプロセス間にばらつきが大きいです、RPSが０なプロセスが二つあります。（とはいえ一瞬の結果をスクショした結果だけです）
他に大きな違いは無いようです。
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のプロセス間処理するリクエスト数のばらつきが小さいなどメリットが見れます。