一覧取得系APIのlimit, offsetの罠
はじめに
一覧取得系のAPIを実装する際、パフォーマンスの観点から取得件数を制限するため取得件数とオフセットをパラメータで受け取るということはよくあると思います.
今回はこの取得件数の制限の仕方によっては意図しない挙動をしてしまう可能性についての記事になります.
罠
例えば、以下のように取得件数を limit
というパラメータで受け取っているとします.また、今回は20で指定していますが、なにも指定がない場合はデフォルトで20とするとします.
GET /api/hoge?limit=20
このようなAPIで、取得件数のセットを、limit
というパラメータがセットされていればそれを使う、セットされていなければデフォルト値を入れる、というような実装がされていたとします.
このような場合、以下のように limit
に大きな値が入っていたとしても指定された 10000件のデータを取得してしまいます.
GET /api/hoge?limit=10000
そのため、パラメータにセットされていたとしても最大値と比較するというような処理が必要になってくると思います.
パフォーマンス
簡単なサンプルアプリを作ってパフォーマンス計測をしてみました.
リポジトリは以下です.
以下のようなテーブルから全カラム一覧取得するAPIを考えます.
CREATE TABLE IF NOT EXISTS articles (
id INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(128) NOT NULL DEFAULT "",
content TEXT,
created_at INTEGER NOT NULL DEFAULT 1674090479,
updated_at INTEGER NOT NULL DEFAULT 1674090479
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ダミーデータとして1,048,576件登録されています.
シナリオ1
まず、通常の状態を想定して、以下のようなシナリオで負荷テストを実施しました.
import http from 'k6/http';
import { sleep } from 'k6';
const VUS = 1000;
export const options = {
stages: [
{ duration: '30s', target: VUS },
{ duration: '30s', target: 0 },
]
}
export default function scenarioFunc() {
http.get(`http://localhost/article?limit=20`);
sleep(1);
}
結果は以下のようになりました.
data_received..................: 79 MB 1.3 MB/s
data_sent......................: 2.8 MB 46 kB/s
http_req_blocked...............: avg=15.58µs min=0s med=3µs max=3.84ms p(90)=7µs p(95)=11µs
http_req_connecting............: avg=8.12µs min=0s med=0s max=2.79ms p(90)=0s p(95)=0s
http_req_duration..............: avg=6.87ms min=1.7ms med=3.53ms max=229.21ms p(90)=10.66ms p(95)=18.58ms
{ expected_response:true }...: avg=6.87ms min=1.7ms med=3.53ms max=229.21ms p(90)=10.66ms p(95)=18.58ms
http_req_failed................: 0.00% ✓ 0 ✗ 30307
http_req_receiving.............: avg=41.91µs min=9µs med=34µs max=1.65ms p(90)=68µs p(95)=88µs
http_req_sending...............: avg=17.09µs min=3µs med=12µs max=1.61ms p(90)=28µs p(95)=38µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=6.81ms min=1.67ms med=3.47ms max=229.13ms p(90)=10.59ms p(95)=18.53ms
http_reqs......................: 30307 499.673548/s
iteration_duration.............: avg=1s min=1s med=1s max=1.22s p(90)=1.01s p(95)=1.01s
iterations.....................: 30307 499.673548/s
vus............................: 24 min=24 max=995
vus_max........................: 1000 min=1000 max=1000
シナリオ2
次にlimitを大きな値に設定した以下のようなシナリオで負荷テストを実施しました.
import http from 'k6/http';
import { sleep } from 'k6';
const VUS = 1000;
export const options = {
stages: [
{ duration: '30s', target: VUS },
{ duration: '30s', target: 0 },
]
}
export default function scenarioFunc() {
http.get(`http://localhost/article?limit=10000`);
sleep(1);
}
結果は以下のようになりました.
data_received..................: 1.3 GB 16 MB/s
data_sent......................: 238 kB 3.0 kB/s
http_req_blocked...............: avg=127.43µs min=1µs med=8µs max=2.67ms p(90)=365µs p(95)=417µs
http_req_connecting............: avg=83.14µs min=0s med=0s max=1.12ms p(90)=247µs p(95)=284.44µs
http_req_duration..............: avg=17.03s min=307.76ms med=5.88s max=58.74s p(90)=45.71s p(95)=48.96s
{ expected_response:true }...: avg=32.49s min=307.76ms med=36.01s max=58.74s p(90)=49.34s p(95)=51.61s
http_req_failed................: 57.95% ✓ 1224 ✗ 888
http_req_receiving.............: avg=9.68s min=12µs med=100µs max=40.66s p(90)=35.68s p(95)=37.4s
http_req_sending...............: avg=48.47µs min=4µs med=32µs max=899µs p(90)=94µs p(95)=123.44µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=7.35s min=170.16ms med=5.77s max=39.87s p(90)=13.02s p(95)=15.31s
http_reqs......................: 2112 26.230087/s
iteration_duration.............: avg=17.6s min=1.31s med=6.86s max=59.74s p(90)=46.35s p(95)=49.55s
iterations.....................: 2072 25.733305/s
vus............................: 152 min=30 max=1000
vus_max........................: 1000 min=1000 max=1000
結果
シナリオ1と2を比較すると、http_req_duration
の90パーセンタイルが 10.66ms
から 45.71s
と大幅に増加していました.また、http_req_failed
に関しても 0.00%
から 57.95%
と大幅に増加していました.
今回のシナリオではシナリオ2で全てのリクエストで大きいlimitを設定している想定で、現実的に起こりうる状況ではないと思います.
しかし、上記の結果からもパフォーマンスに大きな影響があることは確認できたと思います.
最後に
今回は一覧取得系APIの取得件数の制限の仕方によってパフォーマンスに影響が出る可能性のある実装について紹介しました.
当たり前と思っている方もいるかもしれませんが、自分ではこれまで気づいていなかったので、このような記事を書きました.最後まで見ていただきありがとうございました.
Discussion