「データ指向アプリケーションデザイン」から学ぶスケーラビリティ

# 概要

周りで名著だと言われているけど個人的には読んだことがなかった本の
[データ指向アプリケーションデザイン　―信頼性、拡張性、保守性の高い分散システム設計の原理](https://www.oreilly.co.jp/books/9784873118703/)
を長期休みの暇な時間に読んだので、忘れないうちにメモをしていた箇所を投稿しておく。

自分なりの理解を含めてメモを書いているので、誤っている部分がある場合にはご指摘いただけると喜びます。

前回書いた信頼性に関する内容は
[「データ指向アプリケーションデザイン」から学ぶ信頼性](https://qiita.com/KenUehara/items/1ccc8e89d54e005f1e17)
にまとめてあります。

:::note info
XがTwitterだった時代のエピソードが出てきますが、この記事では原著通りTwitterと表記しています。
:::

## スケーラビリティ

システムにおけるスケーラビリティとは、**負荷の増大に対してシステムが対応できる能力**を指す。 

スケーラビリティについて議論をする時は、特定のシステムがスケールする、スケールしないという話ではなく、「システムの特定の部分が成長した場合、どのような対応方法が存在するか？」という考慮を必要とする問いかけをすること。


### 負荷の表現

システムにおける負荷は負荷のパラメータと呼ばれる数値によって表現可能であり、以下の要素では

- Webサーバー→毎秒のリクエスト数
- データベース→読み書きの比率
- チャットルーム→同時アクティブユーザー数
- キャッシュ→ヒット率 

といった値が挙げられる。

**パラメータを選択するときは一律にこのパラメータを使用する、ということではなく、何が最適かはシステムのアーキテクチャに依存するということを理解しておくこと。**

例えば、Twitterは、システムにかかる負荷として

| 処理 | 平常時 | ピーク時 |
|:-----------|:------------|:------------|
| ツイートのポスト       | 4600リクエスト/秒        | 12000リクエスト/秒         |
| タイムラインリクエスト     | 300000リクエスト/秒      | 記載なし       |


という値を公開していた。
ツイートのポストのピーク時に記載のある、毎秒12000回の書き込みに対応することは極めて容易だが、**ファンアウト**への対応が難しい。 

:::note info
ここでのファンアウトはツイートをポストしたタイミングで、他のユーザーのタイムラインに反映を行うことを指す。
:::

これはTwitterなどのSNSに特有の問題であり、ユーザーが他のユーザーをフォローしたり、フォローされたりする仕様に起因する。

当初、Twitterはタイムラインの取得のために、以下のようなテーブル構成（あくまで簡易的な構成）を用いたアプローチを取っていた。

![Group 10575.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/743752d3-3e5a-c52e-6f99-0f4743858373.png)

このような構成であれば、

```sql
SELECT tweets.*,users.* FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_users
```

というようなSQLクエリを発行すれば特定のユーザーのタイムラインを取得できる。
このSQLクエリは、実装してからしばらくの間は適切に動作していたが、システムが成長するにつれてタイムラインを取得するクエリの負荷が大きくなりすぎて、上手く動かなくなっていった。

そこで、新たにTwitterは以下の図のようなアプローチを考案した。

![Group 10607.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/e50d5054-9765-71d6-fa9b-c37f737cd40d.png)

このアプローチは

1.あらかじめ各ユーザーのタイムラインをキャッシュする
2.ユーザーがツイートをポストしたタイミングでそのユーザーをフォローしているユーザーを検索し、該当するユーザーのタイムラインにツイートを差し込む

という順序で動作している。
このアプローチには、最初のアプローチに比べてタイムラインの読み込み回数が**2桁分**少なく済むというメリットや、**ツイートのポスト時に発生するタイムラインの読み取りリクエストの結果は前もって生成されているため、必然的にポスト時の負荷が軽くなる**、というメリットがある一方で、フォロワー数の違いによって書き込み時間に大きな差が生まれるというデメリットが存在する。

例えば、1億人のフォロワーがいるような有名人がツイートをポストした場合、1億回の書き込みがそのまま行われる、ということになる。

そこで、Twitterは1つ目の方法と改善した2つ目の方法の二つを組み合わせた形式を採用した。
具体的には、
**フォロワーが一定数以上存在するユーザーに対しては1つ目の個別にクエリを発行する方法を、
それ以外のユーザーは2つ目の方法を採用する**、という内容だった。

### パフォーマンスの表現

先述の「負荷の表現」段落内に記載されているシステムの負荷について理解すれば、以下のような観点からシステムのスケーラビリティの調査が出来る。

- 負荷のパラメータを増やしながらシステムのリソースを一定のままに保つとシステムのパフォーマンスにどういった影響をあたえるか？ 
- 負荷のパラメータを増やした時にパフォーマンスを一定に保つにはリソースをどれだけ増やさなければならないか？ 


例えば、[Hadoop](https://ja.wikipedia.org/wiki/Apache_Hadoop)のようなバッチ処理システムの場合、一般的に注目するのは**スループット**。 
ここでのスループットとは、1秒あたりに処理できるレコード数や、あるサイズのデータセットに対して1つのジョブを実行するのにかかる時間のこと。

オンラインシステムの場合には、クライアントがリクエストを送信してから返ってくるまでの**レスポンスタイム**に注目する。
ここでのレスポンスタイムはネットワーク、キューイングなどの遅延も含まれるため、**リクエストが処理を待っている期間であるレイテンシーとは異なる**。

この定義に基づくと、レスポンスタイムは基本的には毎回異なる値となる。
その為、レスポンスタイムは計測可能な値の分布として考えるべき。

サービスのレスポンスタイムについては平均値を見ることが一般的だが、レスポンスタイムの平均値でサービスを分析しようとした場合、平均値から離れたユーザーがどれくらい存在しているかを把握できないため、そういった観点で分析する際には不向き。
なので、平均値ではなく中央値（50パーセンタイル値）などの**パーセンタイル**を使用することが推奨されている。 

:::note info
パーセンタイル：計測値の分布（ばらつき）を小さい数字から大きい数字に並べ変え、パーセント表示することによって、小さい数字から大きな数字に並べ変えた計測値においてどこに位置するのかを測定する単位のこと。
:::

中央値であれば、仮にレスポンスタイムの中央値が200msだった場合、
クライアントからのリクエストの半分に対しては200ms以下でレスポンスが返されたことになり、
もう半分には200ms以上の時間がかかったことになるため、より正確にシステムのレスポンスタイムを把握できる。

より詳細にレスポンスタイムを分析する時には、95パーセンタイル値、99パーセンタイル値、99.9パーセンタイル値（それぞれp95,p99,p99.9と略されることがある）を用いる。
例えば、p99.9の場合には全レスポンスタイムのうち99.9%がこの値以下であることを意味するため、極端なケースでの挙動を理解するのに役立つ。

Amazonでは内部的なサービスのレスポンスタイムに対する要件を99.9パーセンタイルで表している。
この値は、仮に1000件のリクエストが存在する場合、最もレスポンスタイムがかかっているリクエストを指す。 
Amazonがこのような厳格な基準を設けている理由は、**特に処理時間が長いリクエストを発行しているユーザーが大量の購入を行い、アカウントに多くのデータを保持している可能性が高いためであり、これらは重要な顧客である可能性が高い**からだ。

なお、**大きなパーセンタイルのレスポンス値はキューイングの遅延によるものが多い**。 サーバーが同時に並列処理できる数は少ない（CPUコア数で制限されたりする）ため、低速なリクエストが少し存在するだけで、その後のリクエストが待たされることになる。
この現象はヘッドオブラインブロッキング（head-of-line blocking,HOLブロッキング）と呼ばれる。

これらの後続リクエストが高速に処理されたとしても、遅延した分、クライアントから見たリクエストタイムは大きくなる。
その為、**レスポンスタイムはクライアント側で計測することが重要**。

### 負荷の対処へのアプローチ

仮に負荷が1桁増えた場合、アーキテクチャを考え直さなければならなくなることが多い。 
その際、

- **スケールアップ（垂直スケーリング、マシンのスペックを強化すること）**
- **スケールアウト（水平スケーリング、複数のマシンに負荷を分散すること。シェアードナッシングアーキテクチャともいう。）**

のどちらかを検討することになる。

それぞれの方法のメリット、デメリットは以下の通り。(本の内容だけではメリットデメリットを網羅しきれていないと感じたため、調査し、何点か補足しています。出典は参考に記述。)

| 方法 | メリット | デメリット |
|:-----------|:-----------|:------------|
| スケールアップ     | ・管理の方法がシンプル<br>・アプリケーション設計が比較的簡単<br>        | ・高コスト（ハイエンドのマシンが高価）<br>・物理的な限界（ハードウェアの上限に制約）<br>・耐障害性が低い（単一障害点のリスク）         |
| スケールアウト     | ・コスト効率が良い<br>・拡張性が高い<br>・耐障害性が高い<br>      | ・設定と管理が複雑（複数のノードを管理する必要がある）<br>・ネットワーク依存度が高い（ノード間の通信が重要）<br>・アプリケーションの設計が難しい（分散処理を前提とした設計が必要）<br>・データ整合性の保持が挑戦的（特にステートフルアプリケーションで）|

どのようなシステムを設計するにしろ、**汎用的で、1つのサイズで全てのケースに対応できる（one-size-fits-all）スケーラブルなアーキテクチャは存在しないことを意識する**こと。

### 参考

[スケールアップとスケールアウトの違い](https://azure.microsoft.com/ja-jp/resources/cloud-computing-dictionary/scaling-out-vs-scaling-up)
[“スケールアウト”と“スケールアップ”の違いは？](https://xtech.nikkei.com/it/article/COLUMN/20120207/380334/)