🏢

システム設計の基礎をまとめる

に公開

システムを設計するためには幅広い知識が必要になると思います。技術的な知識に限っても、考慮しなければいけない非機能要件の特徴や、システムを構成する技術がサポートする非機能要件などの様々な知識が必要になります。

この投稿は、自分の理解のために、「そもそもシステム設計とはなにか」という話から、システム設計をするうえで必要になる技術的な知識を幅広くまとめたものです。

システム設計とは

システム設計とは、システムのアーキテクチャ特性を分析し、それに基づいてシステムの最適な構造を定義するプロセスのことをいいます。さらに詳しく言えば、アーキテクチャ特性や構造の定義だけではなく、アーキテクチャ決定というルールや設計指針というガイドの策定も含みます。アーキテクチャ特性は非機能要件のようなもので、詳細については後述します。

システム設計の成果物は、アーキテクチャ設計と呼ばれます。どちらも同じ意味で使われることも多いですが、システム全体の構造をアーキテクチャ、より具体的で詳細なコードレベルの構造を設計と区別することもあります。このような区別をする場合、システム設計のプロセスで定義するのは、設計ではなくアーキテクチャと言えます。この投稿ではアーキテクチャと設計は区別はせず、システム全体の構造のことも設計と呼びます。

システム設計はトレードオフが全てだと言われています。システム設計のすべての決定は、多くの相反する要素を考慮しなければならず、何も犠牲にしない決定は存在しません。あらゆるシステムに最適な一つの設計は存在せず、システムの特性や要件によって最適な設計は変化していきます。良い設計のためには、複数の選択肢を比較検討し、それぞれのメリットとデメリットを理解したうえで適切なバランスを見つけ出す必要があります。

一度のシステム設計で最適な設計を見つけ出すことは容易ではないため、システム設計はできる限りイテレーティブに行う必要があります。そのためには、システム設計とシステム開発を分断させずに協調して行い、システム開発からのフィードバックを活かしてシステム設計を反復的に行うことが重要になってきます。

設計におけるコンポーネント

システム設計で定義する構造の最小の構成要素はコードの集まりであり、コンポーネントと呼ばれます。コンポーネントの種類としてはサブシステム・レイヤー・サービスなどがあり、入れ子構造になることもあります。サブシステムやレイヤーは関連するコードの集まりのことで、サービスは独自のアドレスを持つ通信が可能なコンポーネントです。この通信にはTCP/IPのような低レベルのプロトコルやHTTPなどのプロトコルが使用されます。

設計者はシステム設計のプロセスの中で、どのようなコードの集まりをコンポーネントとするかを識別して構成し、これが設計における主要な作業になります。この作業は、あらかじめ決められたコンポーネントを組み合わせるというものではなく、システム全体の特性や要件に合わせて最適なコンポーネントを見つけ出すという作業に近いと言えます。

一番初めにコンポーネントを識別するためには、コンポーネントの最上位分割を行うとよいです。最上位分割とは、システムの最上位のコンポーネントをどのように分割して子コンポーネントを構成していくかの方法のことで、以下のような方法があります。

  • 技術による分割: システムをプレゼンテーション、ビジネスロジック、データアクセスといった技術能力ごとに分割する
  • ドメインによる分割: システムを顧客管理、商品管理、注文管理といったビジネス上の意味合いに基づいて分割する

最上位分割を行ってコンポーネントを構成するための指針を決めたあと、より具体的なコンポーネントを見つけていきます。このアプローチには、ロールとアクションを明らかにするアクター/アクションアプローチ、ロールとイベントを明らかにするイベントストーミング、ロールとワークフローを明らかにするワークフローアプローチなどがあります。

コンポーネントを識別する際には、そのコンポーネントがシステムの特性や要件を満たせるかについても考慮する必要があります。コンポーネントの設計をシステムの機能面のみから捉えてしまうと、システムの特性や要件を満たせない可能性があるため、それらを意識しながら、さらににコンポーネントを分割・統合する必要があります。

初期に行うコンポーネントの識別が良い設計になっている可能性はあまり高くないため、コンポーネントをイテレーティブに改善していく必要があります。そのため、システム設計はシステムの開発からのフィードバックも重要になってきます。

アーキテクチャスタイル

コンポーネント同士の一般的な構成パターンはアーキテクチャスタイルと呼ばれています。アーキテクチャスタイルは名前を持っており、そのスタイルの構造的な側面やサポートされるアーキテクチャ特性、典型的なデプロイメントパターンなどを示すことがあります。

アーキテクチャスタイルはモノリシックアーキテクチャと分散アーキテクチャに分類することができます。モノリシックアーキテクチャは、すべてのコードが単一のデプロイメントユニットで構成されているアーキテクチャです。分散アーキテクチャは、なんらかのリモートアクセスプロトコルで接続された複数のデプロイメントユニットで構成されているアーキテクチャのことです。

それぞれのアーキテクチャには以下のような分類が存在します。この投稿ではそれぞれのアーキテクチャの名前だけを紹介し、どういったアーキテクチャなのかには触れません。

  • モノリシック
    • レイヤードアーキテクチャ
    • パイプラインアーキテクチャ
    • マイクロカーネルアーキテクチャ
  • 分散
    • サービスベースアーキテクチャ
    • イベント駆動アーキテクチャ
    • スペースベースアーキテクチャ
    • サービス指向アーキテクチャ
    • マイクロサービスアーキテクチャ

分散アーキテクチャは、スケーラビリティや可用性などのアーキテクチャ特性の点でモノリシックアーキテクチャよりも強力ですが、様々な課題が存在し、大きな複雑性をもたらします。主な課題は「分散コンピューティングの落とし穴」などと呼ばれています。この他にも様々な考慮事項が存在し、モノリシックアーキテクチャよりも複雑になります。

近年ではマイクロサービスというワードが流行っていますが、分散アーキテクチャは複雑であるため、導入には慎重になる必要があると考えています。実際にマイクロサービスが必要な企業は多くないのではないかという記事も存在します。

システムの全体像を決める際にアーキテクチャスタイルを用いることで、設計者はシステムの詳細な設計に集中することができるようになります。

アーキテクチャスタイルを選択するためには、構造に影響する様々な要因を考慮して、「モノリスか分散か?」「データをどこに置くべきか?」「サービス間の通信スタイルは同期か非同期か?」などを決定する必要があります。

アーキテクチャ特性と関連技術

システム設計で定義する必要のあるアーキテクチャ特性とは、システムが満たすべき要件のうち、機能に関係せず、構造上の特別な配慮が必要なものをいいます。これは一般的には非機能要件や品質要件などと呼ばれています。構造上の特別な配慮が必要というのは要件を満たすためにシステム構造で対応する必要があるということで、例えばコーディングによってサポートすることを決めた場合はアーキテクチャ特性にはなりません。

一般的にはシステムのデプロイメントユニットごとにアーキテクチャ特性のセットをサポートすることができます。一つシステムで一つのアーキテクチャ特性のセットをサポートするといった制限はありません。例えば分散アーキテクチャの各デプロイメントユニットは独自のアーキテクチャ特性のセットをサポートすることができます。一方でモノリシックアーキテクチャはデプロイメントユニットが一つなので一つのアーキテクチャ特性のセットしかサポートできません。

システム設計のトレードオフの多くは、アーキテクチャ特性間のトレードオフとして捉えることができます。アーキテクチャ特性は相互に影響し合うため、システムは一部のアーキテクチャ特性しかサポートすることができません。例えばセキュリティを向上させようとする場合、ほぼ確実にパフォーマンスにマイナスの影響を及ぼします。あらゆるアーキテクチャ特性をサポートすることは不可能なため、システムに必要なアーキテクチャ特性を分析する必要があります。

CAP定理と呼ばれる、一貫性・可用性・分断耐性をすべてサポートすることは不可能だという定理がトレードオフの分析に使われることがありますが、現実のシステムに当てはめられるものではないという批判もあります。CAP定理がいう可用性や一貫性は厳密すぎるため、ほとんどのシステムはどちらもサポートできていないという主張だと思います。

システム設計では、そういった定理に頼ることなく、自分たちでアーキテクチャ特性のトレードオフについて考え抜く必要があります。

ここからは、以下のアーキテクチャ特性と、関連する技術を簡単に紹介していきます。

  • 一貫性
  • 可用性
  • スケーラビリティ
  • パフォーマンス

関連技術は複数のアーキテクチャ特性をサポートすることも多いのですが、サポートする主要なアーキテクチャ特性を独断で決めて、その特性に分類しています。また、関連技術の抽象度は揃えておらず、関連しているものを横並びで雑多に紹介しています。

一貫性

一貫性(整合性、Consistency)とは、データが期待される状態にどの程度合致しており、その状態が予測可能であるかをいいます。期待される状態というのは例えば、「書き込んだデータは読み込める」「変更したデータは反映される」「削除したデータは読み込めない」などです。クライアントから書き込んだデータを別のクライアントから読み取れるとき、期待される状態といえます。

一貫性には数多くの分類があり、代表的なものは線形化可能性最終的な一貫性です。

線形化可能性(Linearizability)は、現実的で最も強いとされる一貫性のことです。データが瞬時に期待された状態になっていることを観測でき、変更が遅延なく伝わっているように見えます。

最終的な一貫性(結果整合性、Eventual Consistency)は、最終的に期待された状態になる一貫性のことです。データを更新したあと、いつかは更新後のデータを読み取れるようになります。

一貫性という用語は文脈によって微妙に意味が異なるため、それらをまとめて大雑把に把握するために強い一貫性弱い一貫性結果整合性などと分類することがよくあると思います。強い一貫性はすべてのデータが瞬時に反映されること、弱い一貫性は反映されない可能性があること、結果整合性はいつかは反映されることを示します。

次に一貫性に関する技術について見ていきます。

分散トランザクション

分散トランザクションとは、複数のサービスにまたがる一連の処理を、あたかも一つのトランザクションのように扱う技術のことで、一貫性にも関わってきます。例えば複数のデータベースのデータを更新する場合、一部の更新が失敗していると全体の一貫性は弱くなっていると言えます。分散トランザクションを使用することで、すべて成功するか・すべて失敗するかという原子性が保証され、一貫性も向上すると思います。

分散トランザクションを実現する方法としては、2相コミットSagaパターンなどがあります。

2相コミットは、複数の参加ノード間でトランザクションをコミットするかロールバックするかを合意するプロトコルのことです。具体的には、コーディネーターと呼ばれるノードがすべての参加ノードにコミットの準備ができているかを確認し(準備フェーズ)、すべてのノードの準備が完了していれば、コーディネーターが全てのノードにコミットの指示を出します(コミットフェーズ)。

2相コミットには、すべてのノードが応答を完了するまでブロッキングするためレイテンシが大きい、ノードの故障や復活で壊れやすいというデメリットがあります。

Sagaパターンは、ローカルトランザクション(擬似的なトランザクション)と補償トランザクションによって分散トランザクションを実現するパターンです。Sagaパターンでは、まず一連の処理を複数の小さな独立したローカルトランザクションに分割します。ローカルトランザクションは一つのサービスで実行され、成功すれば次のローカルトランザクションに進み、失敗した場合には変更を打ち消すような補償トランザクションを実行します。

Sagaパターンは、各サービスがイベントを介してお互いに連携してトランザクションを調整するコレオグラフィー方式と、専用のオーケストレーターが各ローカルトランザクションの実行を管理するオーケストレーション方式という2つの手法があります。

Sagaパターンには、失敗した場合の補償処理を適切に実装する必要があり、開発が複雑になりやすいというデメリットがあります。

可用性

可用性(Availability)とはシステムが継続して稼働できる度合いのことで、数値として表現したものは稼働率と呼びます。この性質は平均故障間隔(MTBF、Mean Time Between Failure)と平均修復時間(MTTR、Mean Time To Recovery)によって数値で以下のように計算することができます。

Availability = MTBF / (MTBF + MTTR)

MTBFは連続稼働している時間の平均で、MTTRは障害が発生している時間の平均です。MTBFは故障から次の故障までの平均的な間隔を表しているので、システムが連続稼働している時間の平均と言い換えることができます。MTTRは障害が発生してから修復が完了するまでの時間なので、障害が発生している時間の平均と言い換えることができます。

システムが複数のコンポーネントで構成されている場合、直列で配置されているか並列で配置されているかによって以下のように計算できます。以下はシステム全体がシステムAとシステムBで構成されているケースです。

# 直列
Availability = Availability(A) * Availability(B)

# 並列
Availability = 1 - ((1 - Availability(A)) * (1 - Availability(B)))

直列は一部のシステムが故障すると全体の故障になり、並列はすべてのシステムが故障しないと全体の故障にならないため、このような式になります。故障がシステム全体の障害になるような箇所は単一障害点(SPOF)と呼ばれ、後述する冗長化を行うことで可用性を高めることができます。

次に可用性に関連する技術について見ていきます。

冗長化

冗長化とは、システムを複数用意して可用性を高めるための仕組みです。例えば直列で配置されたシステムの一部が故障すると全体の障害になるため、そのシステムを複数用意して並列に配置することで可用性を高めることができます。

冗長化にはアクティブ/スタンバイ構成とアクティブ/アクティブ構成があります。アクティブ/スタンバイ構成は、稼働中のシステムとスタンバイ状態のシステムで構成されるもので、アクティブ/アクティブ構成は複数のシステムが稼働中な構成です。アクティブ/アクティブ構成は、それぞれのシステムに負荷を分担させることができるため、負荷分散の文脈でも使用されます。

アクティブ/スタンバイ構成では、スタンバイ側の稼働状況によって以下のように分類できます。

  • ホットスタンバイ: スタンバイ側が常に稼働しており、即座に切り替えが可能な状態
  • ウォームスタンバイ: スタンバイ側が一部稼働しており、短時間で切り替えが可能な状態
  • コールドスタンバイ: スタンバイ側が停止しており、切り替えに時間が掛かる状態

フェイルオーバー

フェイルオーバーとは、稼働中のシステムの障害時に、冗長化されている別システムに自動的に切り替える技術のことです。これによりシステム全体の停止時間を最小限に抑えて、システムの可用性を高めることができます。これを行うためには、外部からシステムが正常に稼働しているかを判定するための正常性エンドポイントを実装する必要があります。また、障害から復旧したあとに元のシステムに戻すことはフェイルバックと呼びます。

障害の復旧後に元のシステムに戻すフェイルバックは、場合によっては手動で行ったほうが良いです。自動でフェイルバックを行う際には正常性エンドポイントの判断を信用することになりますが、例えばシステムが外部のシステムの状態を検証していなかったり、システムのデータの一貫性のチェックが必要になるケースがあります。そういった場合、正常性エンドポイントだけでシステム全体が正常かを判断できないので、手動で行うほうが良いと思います。

障害から復旧するフェイルバックと似た用語で、縮退運転であるフォールバックというものもあります。こちらは同等のシステムに切り替えるのではなく、別の機能や手段に切り替えることで、機能を絞りながら復旧を待ちます。機能は限定されるものの、可用性を高くすることは可能ですが、実装は複雑になりがちです。

レプリケーション

(データの)レプリケーションとは、複製(レプリカ)を別のサーバーに持つことのできる機能です。これにより、障害時にデータベースを切り替えて可用性を向上させたり、複数のサーバーに読み取り処理を分散させることで後述するスケーラビリティを向上させることもできます。

レプリケーションは通常、プライマリサーバーとセカンダリサーバー(レプリカ)で構成されます。プライマリサーバーはデータの更新と参照の両方を実行できるサーバーで、データの更新が発生すると、その内容をセカンダリサーバーに伝達し反映させます。一方で、セカンダリサーバーはデータの参照のみを実行でき、リードレプリカと呼ばれることもあります。プライマリサーバーは複数のセカンダリサーバーを持つことができますが、セカンダリサーバーは一般的に一つのプライマリサーバーのみを持つことができます。

レプリケーションには他にもプライマリ/プライマリの構成や、プライマリサーバーが存在しない構成もありますが、一般的にはプライマリ/セカンダリが使用されることが多いと思います。

プライマリ/セカンダリ構成は可用性とスケーラビリティを向上させるために有効です。プライマリサーバーに障害が発生した際には、セカンダリサーバーがプライマリサーバーの役割を引き継ぎ可用性を高めることができます。また、複数のセカンダリサーバーを配置することで、読み取り処理を複数のセカンダリサーバーに分散させ、スケーラビリティを向上させることも可能です。

レプリケーションは、同期レプリケーション非同期レプリケーションなどに分類できます。同期レプリケーションはプライマリの更新とセカンダリの更新が同時に実行される方式で、一貫性は高いですが更新の時間がかかります。非同期レプリケーションはそれぞれの更新が時間差で行われる方式で、更新が速いですが一貫性が低いです。

スケーラビリティ

スケーラビリティとは、システムが負荷の増大に対してどの程度うまく対応できるかを表す能力です。スケーラビリティが高い状態では、アクセス数やデータ量が増加してもシステムが安定して動作し、パフォーマンスを維持できます。一方でスケーラビリティが低い状態では、負荷が増加すると応答時間が遅延したり、システムがダウンしてしまいます。

次にスケーラビリティに関連する技術について見ていきます。

水平/垂直スケーリング

水平スケーリング(スケールアウト)と垂直スケーリング(スケールアップ)は、システムのスケーラビリティを向上させるための手法です。

水平スケーリングは、処理を行うマシンを追加することでスケーラビリティを向上させる手法です。この手法では、後述するロードバランサのようなコンポーネントを使用して、複数のマシンに処理を分散させる必要があります。水平スケーリングはスケーラビリティを向上させるだけでなく、一部のマシンで障害が発生した場合でも他のマシンが処理を実行できるため、可用性を向上させることもできます。

水平スケーリングは、システム構成が複雑になりやすいというデメリットはありますが、ほぼ無限にスケールアウトすることができます。この複雑性も、クラウドサービスではシステムのメトリクスに基づいて自動的にマシンの数を増減できる機能が提供されているため、ある程度軽減することはできます。

垂直スケーリングは、既存のマシンの性能を上げることでスケーラビリティを向上させる手法です。具体的にはCPUのコア数を増やしたり、メモリやストレージの容量を増やしたりすることでマシンの処理能力を向上させます。

垂直スケーリングは、導入は比較的容易ですが、ハードウェアの限界やコストによってスケールアップできる範囲に限りがあります。一つのマシンが追加できるCPUやメモリは有限ですし、高性能なものはコストもかかってしまいます。

水平/垂直スケーリングを組み合わせることで、より柔軟で効率的なスケーリングを実現できます。例えば、初期段階では垂直スケーリングを行いつつ、負荷が高まってきたら水平スケーリングを行う方法があります。

シャーディング

シャーディングは、RDBにおいて、一つのテーブルの行を複数のシャード(データベースインスタンス)に分割する方法です。この分割は、シャードキー(パーティションキー)として設定されたテーブル内の特定の列の値に基づいて行われます。これは水平スケーリングの一種であり、適切に分割されたシャードを異なるデータベースサーバーに配置することでスケーラビリティを向上させることができます。

シャーディングを行うためにはクエリを適切なシャードにルーティングする必要があり、これをアプリ側で実装すると複雑になるため、プロキシがその役割を担う場合があります。

シャーディングはレプリケーションと異なり、更新と参照両方のスケールアウトに有効です。レプリケーションではレプリカは参照専用なので、更新はプライマリサーバーだけで行う必要がありますが、シャーディングでは各シャードで独立して更新処理を行うことができます。

ただ、シャーディングはレプリケーションとは異なり、分散されたデータベースに分割されたデータを保存する必要があるため、複雑になりやすいです。大規模ではないシステムでは、レプリケーションなどの比較的シンプルなスケーリング手法で十分な場合が多いと思います。

シャードの分割方法としては、主に範囲シャーディングとハッシュシャーディングがあります。

範囲シャーディングは、シャードキーの値の範囲に基づいてデータを分割する方法です。例えば、日付データが含まれている場合には年ごとや月ごとに分割することができます。この方法は範囲検索に適しているという利点がありますが、特定の値範囲にアクセスが集中した場合に一部のシャードだけに負荷がかかりやすいです。

ハッシュシャーディングは、シャードキーの値のハッシュに基づいてデータを分割する方法です。ハッシュ関数によってデータが均等に分散されるため、一部のシャードだけに負荷がかかることを防ぐことができます。しかし、データベースサーバーを動的に追加・削除すると、データの再配置が必要になりやすいため、コストがかかるというデメリットがあります。

ロードバランサ

ロードバランサは、複数のサーバーにクライアントからのリクエストを分散させるためのコンポーネントです。水平スケーリングが行われているシステムで、追加されたマシンに処理を分散させるために使われます。ロードバランサにはハードウェア型とソフトウェア型がありますが、ハードウェア型はコストが高いためあまり使用されていません。また、ロードバランサは単一障害点になってしまうため、一般的には冗長化されていることが多いです。

ロードバランサは分散処理をどのレイヤーで行うかによって、主にL4LB(レイヤー4ロードバランサ)とL7LB(レイヤー7ロードバランサ)に分類できます。ここでいうレイヤーとは、ネットワークプロトコルの階層モデルであるOSI参照モデルのなかのレイヤーを指しています。

L4LBは、トランスポート層で動作するロードバランサのことで、主にTCPやUDPといったプロトコルに基づいてリクエストを分散します。IPアドレスとポート番号などを基に分散先を決定するため処理がシンプルで、より高速に動作することが特徴です。

L7LBは、アプリケーション層で動作するロードバランサのことで、主にHTTPやHTTPSなどのプロトコルに基づいてリクエストを分散します。URLやヘッダー、クッキーなどを基に分散先を決定するため処理が複雑で、より高度なトラフィック制御が可能であることが特徴です。

パフォーマンス

パフォーマンスとは、システムが特定の処理をどれだけ効率的に実行できるかの度合いです。パフォーマンスが高いとは、システムが短時間で多くの処理をこなせる状態を指します。

パフォーマンスには数多くの指標がありますが、代表的なものはレイテンシスループットです。レイテンシはリクエストを送ってからレスポンスが返ってくるまでの遅延時間のことで、スループットは、単位時間あたりにシステムが処理できるデータの量やリクエスト数のことです。

次にパフォーマンスに関連する技術について見ていきます。

キャッシュ

キャッシュは頻繁にアクセスされるデータを一時的に保存する仕組みで、さまざまな場所やレイヤーで使用されています。これによりデータへのアクセスを高速化し、システムのレイテンシを減らすことができます。

キャッシュを適切に使用するためには、以下のようなことを決める必要があります。

  • キャッシュする対象
  • キャッシュのアルゴリズム
  • キャッシュの生存期間

これらを行わずにキャッシュを使用してしまうと、パフォーマンスの低下・データの不整合・リソースの無駄遣い・運用コストの増大など、様々な問題を引き起こす可能性があります。

キャッシュする対象を選択する際に重要になるのは、データの更新頻度、頻繁にアクセスされるデータ、計算コストの高いデータを特定することです。データの更新頻度が低く、頻繁にアクセスされたり計算コストの高いデータはキャッシュとの相性が良く、効果的にパフォーマンスを向上させることができます。

キャッシュのアルゴリズムとは、キャッシュをどのように参照・更新するかを決めるアルゴリズムのことです。主なアルゴリズムとしては以下のようなものがあります。

  • Cache-aside
    • データを読み込む際はキャッシュを参照し、キャッシュにデータが存在しない場合はアプリがストレージからデータを読み込んでキャッシュに保存する
    • データの書き込みはストレージに行い、関連するキャッシュを無効化・更新する
    • 書き込み後に読み取られなくてもキャッシュに乗っている可能性が高い
    • 書き込み後のキャッシュ更新の方法によってはキャッシュとストレージの一貫性が低くなる
  • Read-through
    • データを読み込む際には常にキャッシュを参照し、キャッシュにデータが存在しない場合はアプリを介さずにキャッシュ自身がストレージからデータを読み込んで保存するパターン
    • データの書き込みはストレージに行う
    • キャッシュミス時の処理をキャッシュ自身が行うため、アプリがシンプルになる
    • 読み込みが必ずキャッシュを中継するため、レイテンシが大きくなる可能性がある
  • Write-through
    • データの書き込みはキャッシュに行い、その後キャッシュがストレージを更新するパターン
    • キャッシュとストレージのデータが常に同期しているため、一貫性が高い
    • キャッシュとストレージの両方に書き込むため、書き込みのレイテンシは大きくなる
  • Write-back (Write-behind)
    • データの書き込みはキャッシュに行い、ストレージの更新は後回しにするパターン
    • キャッシュの書き込みが終了した時点で書き込み処理が完了するため、ライトスルーよりはレイテンシが小さくなる
    • キャッシュのみにデータが存在する場合に障害が発生すると、データを失う可能性がある
  • Write-around
    • Cache-asideとほぼ同じだが、書き込み後にキャッシュを触らない
    • キャッシュの不要な汚染を防ぐことができ、頻繁な書き込みと少ない読み取りに有効
    • 書き込み後に読み取られないとキャッシュにのらない

キャッシュの生存期間とは、キャッシュされたデータが有効であるとみなされる時間の長さのことです。主な生存期間としては以下のようなものがあります。

  • Least-recently-used (LRU)
    • 最後に使用してから最も時間が経過しているデータをキャッシュから削除する
  • Time-to-live (TTL)
    • データに有効期限を設定して、期限が切れたアイテムを削除する
  • Refresh-ahead
    • 特定のイベント発生時や一定時間の経過などで自動的に更新されるようにする

キャッシュで問題になりやすいのは、ストレージとキャッシュの一貫性の問題です。一貫性はパフォーマンスとトレードオフになっていることも多く、完璧な一貫性を追求するとキャッシュの利点である高速なデータアクセスが損なわれる可能性があります。アプリケーションの要件に合わせて、一貫性を妥協できる点を探るのが重要だと思います。

CDN

CDN (Content Delivery Network) とは、主にHTML/CSS/JSや画像などの静的コンテンツををユーザーに近い位置に配置し、配信を高速化するためのネットワークです。CDNは地理的に分散した多数のサーバー(エッジサーバー)がコンテンツを持っており、ユーザーは地理的に最も近いエッジサーバーからコンテンツを受け取ることがで、読み取りのレイテンシを減らすことができます。

CDNのコンテンツ配信方法は、主にPush型とPull型に分類できます。

Push型はオリジンサーバーからエッジサーバーへコンテンツをプッシュする方式です。オリジンサーバーでコンテンツが更新された際に、オリジンサーバーがCDNに通知を行い、各エッジサーバーへコンテンツを配信します。

Pull型はユーザーからのリクエストに応じてエッジサーバーがオリジンサーバーからコンテンツをプルする方法です。ユーザーが最初にコンテンツをリクエストした際に、エッジサーバーにキャッシュが存在しなければ、オリジンサーバーからコンテンツを取得してキャッシュします。

ジョブキュー/メッセージキュー

ジョブキュー (メッセージキュー) とは、時間のかかる処理をバックグラウンドで非同期に実行するための仕組みです。この仕組みにより、リクエストを受け付ける側は、処理完了を待つことなく次のリクエストを受け付けることができ、結果としてスループットが向上します。

ジョブキューは以下のようなコンポーネントで構成されています。

  • プロデューサー
    • 処理内容をジョブとしてキューに登録するコンポーネント
  • キュー
    • ジョブを一時的に保存するコンポーネント
  • コンシューマー
    • キューに登録されたジョブを取り出して実行するコンポーネント

ジョブキューはスケーラビリティの向上のためにも有効です。コンシューマーを複数用意して水平スケーリングを行うこともできますし、リクエスト数が急増して処理が間に合わなくても、キューに処理を一時的に溜め込んで順次処理することで処理の負荷を抑えることができます。

ジョブキューでは同じジョブが複数回実行される可能性があるため、処理の冪等性を確保することが大切です。冪等性とはある操作を何度実行しても結果が同じになる性質のことです。ジョブキューは異なるコンシューマーへの重複配信や、ジョブの失敗によるリトライ、コンシューマーの障害と復旧などによって複数回実行される可能性があるため、冪等性が大切になってきます。

ジョブキューを提供するクラウドサービスの中には、冪等性を確保できないジョブのために、重複を排除する機能を提供しているものもありますが、パフォーマンスは低下します。例えば、外部システムとの連携を伴うような処理では冪等性を確保するのが難しいことがあります。このようなケースでは重複を排除する機能を活用することができますが、パフォーマンスへの影響も考慮する必要があります。

さいごに

システム設計の基礎的な知識をまとめました。

システム設計において重要なのは、すべての選択がトレードオフを伴うという認識だと思います。どのアーキテクチャ特性を優先するか、どのような技術を採用するか、その決定には必ずメリットとデメリットが存在します。このトレードオフの対象は、技術的な側面にとどまらず、ビジネス上の意思決定にも及びます。システム設計においては、システムに関わる技術とビジネスすべての要素をトレードオフの天秤にかける必要があると考えています。

この投稿が、システム設計の基礎的な知識を理解する役に立つことを願っています。

参考資料

Discussion