t0mmy

2020年12月30日に参加

学習履歴詳細

API デザインパターン 21章 ページ分割 読了

やったこと

  • API デザインパターン 21章 ページ分割 読了

学んだこと

ポイント

  • ページ分割で使用する、以下3つのフィールドの意味
    • pageToken
    • maxPageSize
    • nextPageToken
  • maxPageSize で指定した数をキッチリ返すのではなく、ベストエフォートとする理由
  • ページトークンの情報は簡単に解析されてはならない。その理由は?
  • オフセットとリミットを使用するべきではない理由

学び

  • 「ページ分割パターン」によって、レスポンスを分割できる
  • ページ分割パターンでは、以下のフィールドを活用する
    1. pageToken : 分割したページの位置を表す「トークン」情報
    2. maxPageSize : レスポンスとして要求する、1ページ当たりのデータ数
    3. nextPageToken : 次の「ページ」を要求するために必要な「トークン」
  • maxPageSize で指定した数だけ、ベストエフォートで検索して返す
    • これにより、検索にタイムアウト時間を設定できるようになる
  • ページトークンから実装の詳細が漏れると、APIの変更が難しくなる
    • ページトークンは、簡単に解析できないような値にする
  • オフセットとリミットは、「RDBを使用している」という実装の詳細
    • 将来RDBをやめた後でも、オフセットとリミットをサポートし続けなければならない

メモ

対象とする問題

APIで扱うリソースサイズや数が大きくなると、一度のAPIリクエストで扱うのが困難になる。

  • 総計100Gや10億個のデータを扱うような場合

酷く時間がかかったり、そもそも扱えなかったりする。
こういった場合は、データを分割して提供することになるが、どういったインターフェースとなるか。

広く普及している「ページ分割」パターンについて述べる。

概要

データを「ページ」という単位に分割する。
利用者は、「ページ」単位でAPIリクエストを送信する。
API側は、対応する「ページ」と、利用者が次の「ページ」を要求するために必要な「トークン」と呼ばれるデータを返す。

実装

以下の3つのフィールドを使用する。

  1. pageToken : 分割したページの位置を表す「トークン」情報
  2. maxPageSize : レスポンスとして要求する、1ページ当たりのデータ数
  3. nextPageToken : 次の「ページ」を要求するために必要な「トークン」

nextPageToken に何らかの値が存在する場合、次のページが存在することを示す。
利用者は、レスポンスで受け取った nextPageToken の値を、リクエストの pageToken フィールドに設定してリクエストを送信することで、次の「ページ」情報を取得できる。

◆初回リクエスト

{
maxPageSize: xxx
}

◆レスポンス

{
result: [...],
nextPageToken : "hogefuga"
}

◆次ページを取得するリクエスト

{
maxPageSize: xxx,
pageToken : "hogefuga"
}

【ポイント1】 ページサイズ

◆maxPageSize は ベストエフォート

maxPageSize で設定した値は、ベストエフォート(≒努力目標)。
10 を指定したからと言って、必ず10件返却するわけではない。
努力目標とすることで、タイムアウト時間を設定できるようになる。
これにより、「検索が終わるまで長時間待つ」といった問題を解消できる。

  • そして、追加でデータが欲しければ、pageTokenを使って追加でデータを取得できる

maxPageSize を 100 , タイムアウト を 0.2 秒 に設定。
最大0.2秒間の間でデータを検索
- 0.2秒 以内 100件以上見つかれば、100件返す
- 0.2秒 経っても、82件しか見つからなければ、 見つけた82件だけ返す
- 本当は100件以上あるが、検索に時間がかかりすぎて、0.2 秒という制限時間内では82件しか見つからなかった
- データが82件しかなかった

◆デフォルト値

maxPageSize には、利用者が驚かない程度に適切なデフォルト値を設定する。
デフォルト値の存在はドキュメントに明記する。
可能な限り他のAPIのデフォルト値でも、同じデフォルト値を設定する(APIの一貫性を担保)。

【ポイント2】ページトークン

カーソルのような動作をする。

◆終了基準

サーバー側は、全データの検索終了を、pageTokenに空の文字列を設定することで表現する。

そして、レスポンスの結果が空かで判断してはならない。

  • 最初の0.2秒では0件、次の0.2秒で1件以上ヒットする ... という可能性は十分考えられる

以下のレスポンスを考える。

{
result: [],
nextPageToken: "xxxxxx"
}

これは、「0.2秒検索した結果、何も見つかりませんでした。このトークンを使う事で、続きから検索できます。」という意味になる。

◆不透明性

ページトークンは、サーバー側でリスト順次処理する際に使用する。
そして、ページトークンの実装は利用者から隠匿すること。

-「 { offset : 10}のBase64値」のような、簡単に解読されるような値ではいけない

簡単に解読されることは、利用者に実装の詳細を漏らすことを意味する。
これにより、利用者が、自身のアプリケーションに実装の詳細を盛り込む可能性が生まれ、API定義を変更しにくくなってしまう。

◆書式

WebAPIで使用する以上、トークンは文字列で扱うことになる。

◆一貫性

途中でデータが変化した場合でも、最新のページを返却できる方法が必要。
この問題に対する簡潔な答えは存在しない。

  • ポイントインタイムスナップショットをサポートするDBであれば、この情報をエンコードしてページトークンとして用いることで、強い一貫性を提供できる

他には、以下の手段が考えられる。
- ドキュメントにて、ページ情報が最新ではない可能性を説明しておく
- 最後の検索結果をカーソルとして使用し、次のページトークンで中断した場所から拾い上げるよう実装する

◆有効期限

一般的に、ページトークンの有効期限は設定しない

  • 期限切れトークンはリトライしてもらうしかなく、単に利用者に不便を強いるだけ

有効期限を明確にすると、「利用者は、ページング操作をいつまでに行えばよいか予想できる」という利点が存在する。

この場合、一般的なユースケースで十分な、60分程度を有効期限とするのが良い。

【ポイント3】総数

[xxx件中10~20件]といったUI向けに、全データの総数を返すか。

  • データがそこまで多くなく、総数の計算に時間がかからない => 正確な総数を返す
  • データが非常に多く、総数の計算に時間がかかる => 最良の推定値を総数として返す

どちらの場合も、総数は、レスポンスのフィールドに設定する。

{
  result: [...],
  pageToken: "xxx",
  totalResults: n // 総数
}

【ポイント4】リソース内でのページ分割

一つのリソースが巨大化した場合、一つのリソースを分割し、複数回のリクエストに分けて取得する方法が考えられる。

この場合、本項で取り上げているページ分割をそのまま応用できる。

この時、リソースの読み込み処理は強い一貫性を持つこと。
言い換えると、読み込み中にリソースの変更があった場合、読み込みを中断する事。

トレードオフ

◆双方向ページング

ページ分割では、双方向ページングが提供できない。

  • nextPageTokenを使う事で「次のリソース」を要求することはできるは、前のリソースは要求できない

一部のユースケースを除き、基本的に双方向ページングが必要になる可能性は非常に低い。
双方向ページングを実装する場合、ページ分割の結果をキャッシュすることで対応できる。

任意のウィンドウ

任意の位置のページをリクエストすることはできない。
この機能も、一部のユースケースを除き、基本的には必要になる可能性が非常に低い。

避けるべきパターン : オフセットとリミット

RDBの OFFSETLIMIT に合わせる形で、APIで OFFSETLIMIT フィールドを扱う。
APIの OFFSETLIMIT フィールドの値を、そのままSQLに渡すだけ。

OFFSETLIMIT はRDBの機能。
OFFSETLIMIT を提供すると、RDBから別のデータストアに移行した後も、これらの機能のサポートを続けなくてはならなくなる。

言い換えると、OFFSETLIMIT フィールドは実装の詳細であり、利用者に漏らすべきではない。


扱うこと

  • ページ分割により、大きなデータセットを少しずつ利用する方法
  • ページ分割により、APIの利用者はどのようにして大きなデータセットを少しずつ利用できるようになるか
  • データを構成するページの具体的なサイズ
  • APIサービスは、ページングの完了をどのようにして伝えるべきか
  • ページトークンの形式はどのように定義するか
  • ひとつのリソース内の大きなデータチャンクをページングする方法

まとめ

  • ページ分割は、3つの特別なフィールドを使用することで、APIレスポンスを分割し、一口サイズのチャンクで送信できるようになる
    • 3つの特別なフィールド : maxPageSize , pageToken , nextPageToken
  • ページがまだ存在する場合は、 nextPageTokenが何らかの値を持つ
    • この値の使用方法 : 後続リクエストの pageToken フィールドに設定する
  • 以下の理由により、正確なページサイズではなく、最大ページサイズを用いる
    • 正確なデータを取得するために必要な時間がわからない
    • 完全なデータが手に入る前でも、レスポンスを返す必要がある
  • ページングが終了するタイミングは、レスポンスの nextPageTokenに値が存在しない場合
    • 結果フィールドが空の時ではない
WebAPI

2023年11月19日(日)

1.5時間