Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

新機能リリース前のStudyplus for Schoolに対してLocustを使った負荷試験を行いました

こんにちは。ForSchool事業部エンジニアの石上です!息子が1歳半になり、お互いなんとなく言ってることがわかるようになってきました。

今回は Studyplus for School の一部のAPIに対して行った負荷試験について書きます。

3 行で

  • リクエストが多くなりそうな新機能リリースに備えて負荷試験を行いました
  • Locustから本番相当の仕様にした負荷試験用サーバーへリクエストを投げました
  • 見つかった問題の解決や不安要素への対処をしました

なぜ負荷試験をしたのか

ざっくり書くと、リクエストが多くなりそうな新機能リリースに備えてです。

具体的には、入退館管理と出席管理という機能のリリースが予定されています。この機能によって、Studyplus for Schoolと契約している塾・スクールの生徒さんがStudyplusを使って校舎への入館・退館を記録したり、授業の出欠席を記録できるようになります。入退館や出欠席の時間は重なりますので、Studyplus for Schoolにもその時間にたくさんリクエストが飛んでくることが予想されました。ピーク時に予想されるリクエスト数を問題なくさばけるのかを知るために、今回の負荷試験を行いました。

負荷試験の方法

入退館と出欠席のAPIのエンドポイントに対して負荷試験ツールからリクエストを投げました。

ツール

弊社ではすでにLocustを動かすための設定が整っており、別サービスに対する負荷試験なども行われていました。今回の試験内容でも問題なく利用できそうだったため、Locustを使うことにしました。

Locustとは

Locustは、オープンソースのPython製負荷試験ツールです。

特別な記法や設定をほとんど覚えることなく、プレーンなPythonコードで負荷試験スクリプトを書けます。そのため、こういった試験に慣れていない私のようなエンジニアでも、ドキュメントを読みながらスクリプトを書くことができました。また、複数台での実行もサポートしているため、台数を増やして高負荷を与えることも可能のようです。

詳しくはLocustのドキュメントをご覧ください。

locust.io

Web UI での設定

LocustのWeb UIからできる設定は以下の3つです。

  • Number of users:並列ユーザー数
  • Spawn rate:起動するユーザー数(秒間)
  • Host:リクエスト対象のホスト名

LocustのWeb UI

たとえばこれを上から以下のように設定します。

100
10
http://www.example.com

すると、最大並列で100ユーザーからのリクエストをシミュレートできます。毎秒10ユーザーごと増えるので、10秒で100ユーザーに達します。以降はずっと100ユーザーがリクエストを投げてくる状態になります(リクエストの頻度など詳細はPythonで書くスクリプトによって制御します)。

RPS(リクエスト/秒)は実行後の画面に表示されます。

負荷テスト実行後の画面例。右上にRPSが表示される

RPSを向上させる場合はワーカーを増やしたり、リクエストを投げるHTTPクライアントの部分に、より速いFastHttpUserを使うなどの手段があります。今回の試験では300人の生徒から一斉に入退館(あるいは出欠席)のリクエストが来る想定で行いたかったので、300 RPSを目標にしていました。HttpUserを使っているとそこに満たなかったので、今回はFastHttpUserを使いました。この辺の設定は負荷試験の要求によって変わってきそうです。

docs.locust.io

locustfile の記述

Locustではlocustfileと呼ばれるスクリプトに、どのエンドポイントへ、どういった頻度でリクエストを投げるかを記述できます。

docs.locust.io

私はPythonには不慣れでしたが*1、今回はあまり難しいことをする必要がなかったので、それほど苦労せず書くことができました。Locustのドキュメントが丁寧なので、そちらを読めば書き方がわかるようになっていました。

詳細は省きますが、たとえば入退室のテストであれば以下のようなコードになりました。

from locust import FastHttpUser, TaskSet, task, constant, events
import datetime
import logging

# ログ出力
@events.request_failure.add_listener
def request_failure_handler(request_type, name, response_time, exception, **kwargs):
    print("Request Fail! time:%s,  name:%s, exception:%s" %
          (datetime.datetime.now(), name, exception))

class PostStay(TaskSet):
    def on_start(self):
      # 必要な前処理があればここに書く

    @task
    def request(self):
        # ここではload_test_id関数の実装は省きますが、入退室するテスト生徒IDを取り出しています
        user_id = load_user_id() 
        path = "/api/stays?user_id=%s" % user_id
        data = {
            # 入退室時刻
            "datetime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        }

        # リクエスト送信
        self.client.post(path, data=data)

class WebsiteUser(FastHttpUser):
    tasks = [
        PostStay
    ]

nginx でエラー発生(11: Resource temporarily unavailable)

負荷試験を実施したところ、リクエストの半分以上が失敗していました。 ログを見たところnginxが以下の内容のエラーを出していました(IPアドレスやサービス固有の文字などは伏せ字に、serverのドメインはexample.comに変えています)。

connect() to unix:/path/to/xxx.sock failed (11: Resource temporarily unavailable) while connecting to upstream, client: x.x.x.x, server: www.example.com, request: "POST /xxx/yyy HTTP/1.1", upstream: "http://unix/:/path/to/xxx.sock:/xxx/yyy", host: "www.example.com"

どうやらLinuxカーネルパラメータのnet.core.somaxconnが足りなかったようです。*2

# 負荷試験実施時に確認した設定値
$ sysctl -n net.core.somaxconn
128

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt を見てみます。

somaxconn - INTEGER Limit of socket listen() backlog, known in userspace as SOMAXCONN. Defaults to 4096. (Was 128 before linux-5.4) See also tcp_max_syn_backlog for additional tuning for TCP sockets.

linux-5.4以前のデフォルトは128とのことなので、おそらくデフォルトのままでした。これまでは問題ありませんでしたが、今後予想されるリクエスト数を考えると小さかったので、余裕を持った数に増やしました。

まとめ

負荷試験を行うことで、問題を1つ潰すことができてよかったです。特にnginxのエラーの件は、試験をしなかったら気づかず新機能がリリースされていたでしょう。予めリクエストが増えると予想できる場合にはこういった負荷試験を事前にやっておけると良いですね。

また、今回の準備や試験後の調査、その後の対応までSREのサポートがありました。SRE室は部署としては分かれているのですが、なにか相談するとすぐに一緒になって進めていただけるので助かっています。

*1:一部ペアプロで行っていたので私だけではありませんでしたが、途中まで一緒にlocustfileを書いた島田さんも普段はRubyのコードを書いているので、二人とも不慣れではありました。

*2:負荷試験は私一人で行っていたわけではなく、ここについてはSREの菅原さんが特定してくれました。この辺のチューニングは私は慣れておらず、すぐ思いつきませんでした。SREのサポートがあるととても助かります。