こんにちは。ForSchool事業部エンジニアの石上です!息子が1歳半になり、お互いなんとなく言ってることがわかるようになってきました。
今回は Studyplus for School の一部のAPIに対して行った負荷試験について書きます。
3 行で
- リクエストが多くなりそうな新機能リリースに備えて負荷試験を行いました
- Locustから本番相当の仕様にした負荷試験用サーバーへリクエストを投げました
- 見つかった問題の解決や不安要素への対処をしました
なぜ負荷試験をしたのか
ざっくり書くと、リクエストが多くなりそうな新機能リリースに備えてです。
具体的には、入退館管理と出席管理という機能のリリースが予定されています。この機能によって、Studyplus for Schoolと契約している塾・スクールの生徒さんがStudyplusを使って校舎への入館・退館を記録したり、授業の出欠席を記録できるようになります。入退館や出欠席の時間は重なりますので、Studyplus for Schoolにもその時間にたくさんリクエストが飛んでくることが予想されました。ピーク時に予想されるリクエスト数を問題なくさばけるのかを知るために、今回の負荷試験を行いました。
負荷試験の方法
入退館と出欠席のAPIのエンドポイントに対して負荷試験ツールからリクエストを投げました。
ツール
弊社ではすでにLocustを動かすための設定が整っており、別サービスに対する負荷試験なども行われていました。今回の試験内容でも問題なく利用できそうだったため、Locustを使うことにしました。
Locustとは
Locustは、オープンソースのPython製負荷試験ツールです。
特別な記法や設定をほとんど覚えることなく、プレーンなPythonコードで負荷試験スクリプトを書けます。そのため、こういった試験に慣れていない私のようなエンジニアでも、ドキュメントを読みながらスクリプトを書くことができました。また、複数台での実行もサポートしているため、台数を増やして高負荷を与えることも可能のようです。
詳しくはLocustのドキュメントをご覧ください。
Web UI での設定
LocustのWeb UIからできる設定は以下の3つです。
- Number of users:並列ユーザー数
- Spawn rate:起動するユーザー数(秒間)
- Host:リクエスト対象のホスト名
たとえばこれを上から以下のように設定します。
100 10 http://www.example.com
すると、最大並列で100ユーザーからのリクエストをシミュレートできます。毎秒10ユーザーごと増えるので、10秒で100ユーザーに達します。以降はずっと100ユーザーがリクエストを投げてくる状態になります(リクエストの頻度など詳細はPythonで書くスクリプトによって制御します)。
RPS(リクエスト/秒)は実行後の画面に表示されます。
RPSを向上させる場合はワーカーを増やしたり、リクエストを投げるHTTPクライアントの部分に、より速いFastHttpUserを使うなどの手段があります。今回の試験では300人の生徒から一斉に入退館(あるいは出欠席)のリクエストが来る想定で行いたかったので、300 RPSを目標にしていました。HttpUserを使っているとそこに満たなかったので、今回はFastHttpUserを使いました。この辺の設定は負荷試験の要求によって変わってきそうです。
locustfile の記述
Locustではlocustfileと呼ばれるスクリプトに、どのエンドポイントへ、どういった頻度でリクエストを投げるかを記述できます。
私は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室は部署としては分かれているのですが、なにか相談するとすぐに一緒になって進めていただけるので助かっています。