mnagaaのメモ

技術的なことはこのブログで書きます

Amazon S3ストレージ最適化方法

aws.amazon.com

dev.classmethod.jp

要約

アプリケーションが生成するログファイルは時間とともに増加し、その多くが128KB未満の小さなファイルです。これらの小さなファイルをAmazon S3に大量に保存すると、特にS3 Standard-Infrequent AccessやS3 Glacier Instant Retrievalなどのストレージクラスでは、最小課金サイズが128KBであるため、実際のデータサイズよりも高いストレージコストが発生します。また、多数の小さなファイルをこれらのストレージクラスに移行すると、オブジェクトごとのライフサイクル移行料金がかかり、コストが増加します。

解決策としてのコンパクション

この問題を解決するために、AWS Step Functionsを使用して多数の小さなファイルをより大きなオブジェクトにまとめる「コンパクション」のパターンが紹介されています。コンパクションは、ファイルの形式や構造を変更せずに複数のファイルを結合する方法であり、以下の利点があります。

  • ストレージコストの削減: 小さなファイルを大きなオブジェクトにまとめることで、128KBの最小課金サイズの影響を軽減し、ストレージコストを最適化できます。
  • クエリパフォーマンスの向上: Amazon Athenaなどでクエリを実行する際、大きなファイルの方がI/Oオーバーヘッドが少なく、クエリの実行速度が向上します。

コンパクションとアーカイブの比較:

  • コンパクション:
    • データの分析価値を保持し、クエリパフォーマンスを向上させます。
    • データをそのままの形式で結合するため、既存のクエリや分析ツールに影響を与えません。
    • 主に頻繁にアクセスまたは分析が必要なデータに適しています。
  • アーカイブ:
    • 長期保存が目的で、クエリ頻度が低いデータに適しています。
    • データを圧縮して保存するため、さらなるコスト削減が可能ですが、クエリの前に解凍が必要です。

実装方法:

  • AWS LambdaとAWS Step Functionsの活用:
    • AWS Lambda関数を使用して、小さなファイルを読み込み、より大きなファイルに結合します。
    • AWS Step Functionsの分散マップ機能を使用して、複数のプレフィックスに対して並行してコンパクションを実行します。
    • この方法により、大量の小さなファイルを効率的かつ迅速に処理できます。

パフォーマンスとコストのメリット:

  • ストレージコストの削減:
    • 小さなファイルをコンパクト化することで、S3 Standard-IAã‚„S3 Glacierなどの低コストのストレージクラスに移行しても、不要なコスト増加を防げます。
    • コンパクション後の大きなオブジェクトに対してライフサイクルポリシーを適用し、古いデータを自動的に管理できます。
  • Amazon Athenaでのクエリパフォーマンスの向上:
    • コンパクションにより、クエリ実行時間が最大66%短縮されるケースが報告されています。
    • 大きなファイルはI/Oオーバーヘッドが少なく、Athenaのデータスキャン効率が向上します。
    • データのパーティショニングを適切に行うことで、さらなるパフォーマンス最適化が可能です。

結論:

小さなオブジェクトをコンパクト化することで、Amazon S3のストレージコストを最適化し、Amazon Athenaでのクエリパフォーマンスを向上させることができます。AWS LambdaとAWS Step Functionsを活用したサーバーレスソリューションにより、運用負荷を最小限に抑えつつ、大規模なデータ処理を効率的に実行できます。

通信方式 ~vol.3~ (HTTP1.1)

これまでに、WebSocket, Socketなどについて整理してきたので、今回はHTTP(/1.1)について整理する。バージョン2以降は次の記事にします。

mnagaa.hatenablog.com

HTTP

  • HTTP(HyperText Transfer Protocol)は、ウェブ上でデータをやり取りするためのプロトコルです。主にクライアント(例えばウェブブラウザ)とサーバー間で、リクエストとレスポンスのやり取りをする際に使用されます。

基本的な特徴

  • リクエスト-レスポンスモデル:
    • クライアントがサーバーにリクエストを送信し、サーバーがそのリクエストに対するレスポンスを返す。
    • 例えば、ウェブページを閲覧する際にブラウザがサーバーにページのリクエストを送り、サーバーがHTMLã‚„CSS、JavaScriptなどのデータを返す仕組み。
sequenceDiagram
    participant Client as クライアント
    participant Server as サーバー
    
    Client->>Server: HTTPリクエスト
    Server-->>Client: HTTPレスポンス
    Client->>Server: 次のHTTPリクエスト
    Server-->>Client: HTTPレスポンス
  • ステートレス(Stateless):

    • HTTPはステートレスなプロトコルである。各リクエストは独立しており、以前のリクエストやレスポンスの情報を個別のサーバー(自体)は保持しない。
    • セッション管理などは、クッキーやトークンなどの別の手段で行われる。←memcacheや永続化用のDBなどはサーバーとは分離される
  • HTTPメソッド: HTTPリクエストにはいくつかのメソッドがあり、目的に応じて選択される。

    • GET: サーバーからリソースを取得する。
    • POST: サーバーにデータを送信し、処理を依頼する。
    • PUT: サーバー上の既存のリソースを更新する。
    • DELETE: サーバー上のリソースを削除する。
    • PATCH: リソースの一部を更新する。
    • HEAD: GETと同様のリクエストを行いますが、ボディ部分は返されません。
  • HTTPステータスコード: サーバーからのレスポンスはステータスコードで示される。代表的なステータスコードとしては以下のようなコードがある。

    • 200 OK: リクエストが成功した。
    • 404 Not Found: リクエストされたリソースが見つからない。
    • 500 Internal Server Error: サーバーでエラーが発生した。
    • 301 Moved Permanently: リソースが恒久的に別の場所に移動した。
  • HTTPヘッダー: リクエストやレスポンスには、メタデータを含むヘッダーが付加される。ヘッダーを使って、データの形式やキャッシュの設定、認証情報などを指定する。

    • 例: Content-Type, Authorization, Cache-Control など。
  • HTTPS: HTTPにSSL/TLSを組み合わせたものがHTTPS(HTTP Secure)です。これにより、通信内容が暗号化され、セキュアなやり取りが可能になる。

ステートレスについて深ぼる

まずは、HTTPはステートレスなプロトコルであるが、サーバーにデータを保存せずに、ステートフルなアプリケーションを実現するときの方法を説明する。

1. クッキー

クッキーは、サーバーがクライアントにセッションIDなどを保存させ、次のリクエストの際に、そのデータを自動的に送信する仕組み。サーバーは特定のクライアントに対してセッションを維持し、ログイン状態などを追跡できる。

  • クライアント側が保持: クッキーは、サーバーからクライアント(ブラウザなど)に送信され、クライアントのデバイスに保存される。クライアントはそのクッキーを次回以降のリクエストに含めてサーバーに送信する。
  • 保存場所: クライアントのブラウザ内に保存される。
  • 使用目的: セッションID、ユーザーの認証情報、トラッキング情報、ユーザーの設定(例: 言語やテーマの選択)などを保存して、次回アクセス時に再利用される。
  • 有効期限: クッキーには有効期限が設定でき、一時的なもの(セッションクッキー)や長期間保存されるもの(永続的なクッキー)がある。

2. セッション

セッションは、サーバー側でクライアントごとの状態を保持する仕組み。セッションIDがクライアントに発行され、サーバー側にそのセッションIDに紐づく情報が保存される。 クライアントはクッキーやリクエストヘッダーでセッションIDを送信して、それに基づいてサーバーが状態を管理する。

一般的に、セッション管理は短期間でログイン情報やショッピングカートの内容などが保存される。

  • サーバー側が保持: セッションは、クライアントの状態(例えば、ログイン状態やショッピングカートの情報など)をサーバー側に保存する。クライアントがサーバーにリクエストを送るとき、セッションIDを使ってサーバーは対応するセッションデータにアクセスする。
  • 保存場所: サーバーのメモリやデータベース(時にはRedisã‚„Memcachedなど)に保存される。
  • 使用目的: ユーザーごとの状態管理(例: ログイン情報、セッションの一貫性維持、トランザクションデータの管理など)に利用される。
  • 有効期限: セッションは通常、短期間有効で、ユーザーがアクションを行わない場合は一定時間後に無効化される(タイムアウト)。

3. WebSocket

HTTPではなくて、WebSocketを使用することで双方向通信が可能になり、サーバーとクライアント間でリアルタイムに情報を保持することができる。 WebSocketはコネクションが確立されている間は継続的にデータをやり取りできるため、ステートフルな通信を実現できる。

ステートフルって?

サーバーが以前のリクエストの情報やセッションデータを保存することを広義で「ステートフル」と呼ぶ。

項目 ステートフルなサーバー ステートレスなサーバー
定義 サーバーがクライアントとの状態(セッション、認証情報、トランザクション)を保持し、クライアントごとの状態を管理する方式 各リクエストを独立したものとして扱い、クライアントの状態をサーバーに保持せずに処理する方式
セッションの保存場所 サーバーのメモリやローカルストレージにセッション情報が保存される セッション情報は外部システム(例: Redis, Memcached)やクライアント側に保存され、サーバー自体は状態を保持しない
負荷分散の柔軟性 クライアントは特定のサーバーに接続し続ける必要があるため、負荷分散が難しい サーバーがクライアントの状態を保持しないため、リクエストを任意のサーバーに振り分けやすく、スケーラビリティが向上する
スケーラビリティ セッションがサーバーに依存するため、スケールアウト(サーバーの増加)に制限がある ステートレスな設計のため、サーバーを簡単に追加・削除でき、大規模なスケールアウトが可能
障害時の影響 サーバーがダウンすると、そのサーバーに依存していたクライアントのセッションが失われる可能性がある サーバーに依存しないため、特定のサーバーがダウンしても影響は少なく、外部のセッションストレージによりフェイルオーバーが容易
実装の複雑さ 状態をサーバー側で管理するため、実装は比較的シンプルだが、負荷や障害管理が難しくなる 外部システムとの連携が必要であり、初期実装は複雑になることもあるが、長期的には管理が容易で、特に大規模システムに適している
用途の例 トラディショナルなWebアプリケーション、単一サーバーでのアプリケーション運用 マイクロサービスアーキテクチャ、クラウドベースのシステム、スケーラブルなAPIサービス
Sticky Session 特定のサーバーにクライアントを固定するSticky Sessionを必要とする場合が多い Sticky Sessionが不要。ロードバランサーを使用してリクエストを均等に振り分けられる
データの保持 状態をサーバーに保持し続けるため、メモリやストレージに依存することがある データの保持は外部ストレージに依存し、各リクエストが必要なデータを持ってくるか、外部データベースで参照する

ロードバランサーについて軽く触れる

ロードバランサーとサーバーのステートフル・ステートレスの設計は非常に密接であるため軽く説明する。

ロードバランサーの役割

ロードバランサーは、複数のサーバー(バックエンドサーバー群)に対して、トラフィックを分散させ、システム全体の負荷を平準化する役割を果たす。これにより、あるサーバーが故障しても他のサーバーでリクエストを処理できるため、システムのダウンタイムを最小限に抑えられる。つまり可用性を向上させることができる。

トラフィック量が増加した際には、追加のサーバーをバックエンドに追加して、ロードバランサーがそれらにリクエストを振り分けることで負荷を分散させることができる。

Sticky Sessionについて

ロードバランサーの機能として、Sticky Sessionというものがある。

  • Sticky Sessionは、クライアントが最初にアクセスしたサーバーに対して、以降のリクエストも一貫して送信されるように制御する。これにより、サーバー側でのセッション情報が維持され、ステートフルな動作が可能になります。
  • ロードバランサーは、クライアントのセッション情報を確認し、同じサーバーにリクエストをルーティングすることで、サーバー内の状態を利用して処理が続行されます。

Sticky Sessionの課題

  • スケーラビリティの制限:特定のサーバーにリクエストが集中するようになるため、負荷が均等に分散されず、スケーラビリティが制限される。サーバーが1台ダウンすると、そのサーバーに保存されていたクライアントのセッション情報が失われる可能性がある。
特徴 Sticky Session(ステートフル) ステートレス
セッション管理 各サーバーがセッションを保持 外部ストレージやトークンでセッション管理
ロードバランシング 同じサーバーにリクエストを送る必要がある リクエストは任意のサーバーに送信可能
スケーラビリティ スケールアウトが困難 容易にスケールアウト可能
サーバー障害時の影響 特定サーバーに依存 影響は少ない
実装の複雑さ シンプルだがサーバー間での負荷が偏る可能性あり 外部ストレージやJWTの実装が必要

HTTP以外の代表的な通信方式

通信方式 特徴 用途
WebSocket 双方向リアルタイム通信が可能。接続後、持続的なデータのやり取りが可能。 チャット、リアルタイム通知、オンラインゲーム
FTP ファイル転送プロトコル。TCPを使用してファイルをサーバー間で転送。 ファイルのアップロード/ダウンロード
SMTP メール送信の標準プロトコル。 メール送信(Gmail、Outlookなど)
IMAP/POP3 IMAPはサーバー上のメール管理、POP3はメールをダウンロードしてローカル管理。 メールの受信と管理
UDP 軽量で高速だが、信頼性が低い。順序やエラー制御なし。 ストリーミング、オンラインゲーム、VoIP
gRPC HTTP/2ベースの高速通信。プロトコルバッファを使った効率的なデータシリアライズ。 マイクロサービス間の通信、分散システム
MQTT 軽量なメッセージングプロトコル。低帯域で信頼性のある通信が可能。 IoTデバイス間のデータ送受信
SSH 暗号化されたセキュアな通信。リモートアクセスやファイル転送に使用。 サーバー管理、リモートアクセス、SFTPによる転送
QUIC UDPベースのプロトコル。高速で信頼性のある接続を提供。 HTTP/3、リアルタイム通信、ストリーミング
SNMP ネットワークデバイスの監視・管理プロトコル。 ネットワーク管理、デバイス監視

最後に

次のブログでは、HTTP/2とHTTP/3について説明しよう

通信方式 ~vol.2~ (Socket通信について)

前回の記事でWebSocketについての解説をしたが、先にSocket通信についての説明をしておくべきだと思ったので、今回はSocket通信について書く。

mnagaa.hatenablog.com

そもそもソケットとは?

  • ネットワーク通信におけるエンドポイントを定義する抽象的な概念
  • ソケットを使用することでアプリケーションがネットワークを介してデータを送受信できる
  • 特定のプロトコル(TCPã‚„UDP)に基づいて通信を行うためのインタフェースを提供

ソケット

  • IPアドレスとポート番号が関連づけられており、この組み合わせによって、特定のマシンと特定のアプリケーションが通信相手を特定できる
  • ストリームソケット(Stream Socket): TCPを使った信頼性のある通信に使用
    • プロトコル:TCP (Transmission Control Protocol)
    • 特徴:信頼性のある、順序を保証したデータ通信が行われる。データはストリーム(連続したバイト列)としてやり取りされる
    • 用途:HTTP、FTP、SSHなどの多くのアプリケーションがこのソケットタイプを利用
  • データグラムソケット(Datagram Socket):UDPを使った非信頼性のある通信に使用
    • プロトコル: UDP(User Datagram Protocol)
    • 特徴: 信頼性は保証されず、データが失われる可能性がある。コネクションレスのため、データはパケット単位(データグラム)で送信され、受信順序も保証されない。
    • 用途: DNSクエリや音声・映像ストリーミングなど、低遅延が求められるが信頼性がそれほど重要でない場合に使用される。
  • IPアドレス:通信相手を一意に識別するためのアドレス。IPv4では32ビット、IPv6では128ビット
  • ポート番号:同じマシン上で動作する複数のアプリケーション間で通信を区別するために使用される番号。ポート番号は0~65535の範囲で、HTTPサーバーなら80、HTTPSサーバーなら443など、特定のサービスに対して一般に使用されるポート番号がある。0~1023のポートはウェルノウンポートと呼ばれ、一般ユーザーはbindすることができない

TCP

特徴

  • 信頼性の高い通信: TCPはデータの送受信において信頼性を重視する。データの順序や完全性が保証され、パケットが失われた場合や順序が入れ替わった場合でも自動で再送や再構成が行われる。
  • 接続指向 (Connection-Oriented): TCPは通信を行う前に、送信元と受信先の間で「3-wayハンドシェイク」と呼ばれるプロセスを経て接続を確立する。この接続によって、データの送信・受信が安定的に行われる。
  • フロー制御 (Flow Control): TCPは送信速度を制御し、受信側がパケットを処理できる速度に合わせてデータを送信する。これにより、受信側のバッファがオーバーフローするのを防ぐ。
  • エラーチェックと再送: TCPは、受信側でデータに誤りがあった場合や、パケットが欠落した場合に自動的に再送を要求する仕組みを備えている。
  • 順序の保証: TCPでは、送信したパケットが送信順序通りに相手に届くことが保証される。到着順序が異なった場合でも、TCPはデータを適切に並べ替える。

典型的な用途

  • HTTP/HTTPS: WebブラウザとWebサーバー間の通信はTCPを使用する。信頼性の高い通信が必要なため。
  • FTP: ファイル転送プロトコルもTCPを使用し、ファイルが完全かつ正確に転送されるようにしている。
  • SMTP: メールの送受信もTCPを使用する。

UDP

特徴

  • 信頼性のない通信: UDPは、TCPとは異なり、データの信頼性や順序は保証しない。パケットが失われたり、順番が入れ替わっても、再送や順序の再構成は行われない。
  • コネクションレス (Connectionless): UDPでは、データを送信する前に接続を確立する手順は不要。パケットを送信したら即座に通信が完了する。したがって、オーバーヘッドが少なく、遅延が少ない通信を実現できる。
  • エラーチェックは限定的: UDPはパケットのエラーチェックを行うが、エラーが検出された場合にそのパケットを破棄するのみで、再送要求は行わない。
  • 軽量・高速: 信頼性を犠牲にする代わりに、UDPは非常に高速で、低遅延の通信が可能。

典型的な用途

  • DNSクエリ: DNSでは、短い要求と応答を行うためにUDPが使用される。応答の再送はアプリケーション側で実装されることが多い。
  • VoIP: 音声やビデオ通話のように、リアルタイム性が求められるが、多少のデータ損失が許容されるアプリケーションでUDPが使用される。
  • オンラインゲーム: ゲームのリアルタイムな操作やデータ転送では、多少のデータ損失や順序の違いよりも、低遅延が優先されるためUDPが使われる。
特徴 TCP UDP
プロトコルの種類 接続指向(コネクション型) コネクションレス(非接続型)
信頼性 高い(データの順序保証、再送) 低い(信頼性なし、再送なし)
接続確立 必要(3-wayハンドシェイク) 不要
データの順序保証 あり なし
フロー制御 あり なし
エラーチェックと再送 あり エラーチェックのみ(再送なし)
オーバーヘッド 高い 低い
遅延 比較的高い 低い
主な用途 ファイル転送、Web通信、Eメール DNSクエリ、ストリーミング、オンラインゲーム

通信手順

TCPソケットの通信手順

コネクション指向のため、3-way ハンドシェイク(SYN, SYN-ACK, ACK)で接続を確立し、データの送受信が終わると4-way ハンドシェイクで接続を終了する。

sequenceDiagram
    participant Client
    participant Server

    Server->>Server: socket() ソケット作成
    Server->>Server: bind() IPアドレスとポート番号をバインド
    Server->>Server: listen() 接続要求を待機

    Client->>Server: 接続要求 (SYN)
    Server->>Client: 接続受け入れ (SYN-ACK)
    Client->>Server: ACK (接続確立)
    
    Client->>Server: send() データ送信
    Server->>Client: recv() データ受信
    Server->>Client: send() データ送信
    Client->>Server: recv() データ受信

    Client->>Server: close() 接続終了要求
    Server->>Client: close() 接続終了

3-way ハンドシェイク

  1. クライアントはサーバーにSYN(同期)パケットを送信する。
  2. サーバーはSYN-ACK(同期・応答)パケットで応答する。
  3. クライアントがACK(応答)パケットを送信して、接続が確立される。

データ送受信

接続が確立されると、クライアントとサーバー間でデータが送受信される。 データの送信はパケットに分割され、TCPは受信側が正しい順序でデータを再構築することを保証する。

4-way ハンドシェイク(接続終了)

通信が終わると、クライアントまたはサーバーが接続を終了するために4-way ハンドシェイクが行われる。

  1. クライアントはFINパケットを送信して接続終了を要求する。
  2. サーバーがACKパケットで応答し、接続終了を承認する。
  3. サーバーは自分の終了準備ができた後、FINパケットを送信する。
  4. クライアントがACKパケットを返し、接続が終了する。

UDPソケットの通信手順

コネクションレスのため、接続の確立や終了の手順はなく、データを送信して終わり。

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: データ送信 (sendto)
    Server->>Client: データ受信 (recvfrom)

    Note over Client,Server: コネクションレスのためハンドシェイクなし
    Client->>Server: データ送信 (sendto)
    Server->>Client: データ受信 (recvfrom)

    Note over Client,Server: 通信終了

コード例

TCPサーバー

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    // TCPサーバーのIPアドレスとポートを指定してリッスン
    listener, err := net.Listen("tcp", "127.0.0.1:8080")
    if err != nil {
        fmt.Println("Error starting TCP server:", err)
        os.Exit(1)
    }
    defer listener.Close()
    fmt.Println("TCP server is listening on 127.0.0.1:8080")

    // クライアント接続を待機
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting connection:", err)
            continue
        }
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Println("Error reading from connection:", err)
        return
    }
    fmt.Printf("Received message: %s\n", string(buf[:n]))
}

TCPクライアント

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    // サーバーのIPアドレスとポート番号を指定して接続
    conn, err := net.Dial("tcp", "127.0.0.1:8080")
    if err != nil {
        fmt.Println("Error connecting to server:", err)
        os.Exit(1)
    }
    defer conn.Close()

    // メッセージを送信
    message := "Hello, Server!"
    _, err = conn.Write([]byte(message))
    if err != nil {
        fmt.Println("Error sending message:", err)
        return
    }
    fmt.Println("Message sent to server")
}

UDPサーバー

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
    if err != nil {
        fmt.Println("Error resolving address:", err)
        os.Exit(1)
    }

    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        fmt.Println("Error starting UDP server:", err)
        os.Exit(1)
    }
    defer conn.Close()
    fmt.Println("UDP server is listening on 127.0.0.1:8080")

    // データ受信
    buf := make([]byte, 1024)
    for {
        n, addr, err := conn.ReadFromUDP(buf)
        if err != nil {
            fmt.Println("Error reading from connection:", err)
            continue
        }
        fmt.Printf("Received message from %s: %s\n", addr.String(), string(buf[:n]))
    }
}

UDPクライアント

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
    if err != nil {
        fmt.Println("Error resolving address:", err)
        os.Exit(1)
    }

    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        fmt.Println("Error connecting to server:", err)
        os.Exit(1)
    }
    defer conn.Close()

    // メッセージを送信
    message := "Hello, UDP Server!"
    _, err = conn.Write([]byte(message))
    if err != nil {
        fmt.Println("Error sending message:", err)
        return
    }
    fmt.Println("Message sent to UDP server")
}

通信方式 ~vol.1~ (WebSocket)

最近自分が作りたいものがあり、それに必要な技術などを調査している。調査したことの整理のために、何回か分けてアウトプットする。今回はWebSocketについて

WebSocketの概要

  • クライアントとサーバー間で双方向通信をリアルタイムで行うためのプロトコル
  • HTTPのようなリクエスト・レスポンスの仕組みとは異なり、一度接続が確立されると、クライアントとサーバーの両方がいつでもデータを送受信することができる
  • リアルタイム性が求められるアプリケーションに非常に適している
    • チャットアプリ
    • オンラインゲーム
    • 金融取引のダッシュボード
  • 通信が軽量である

WebSocketの特徴

  1. 双方向通信: クライアントとサーバーの間でリアルタイムにデータの送受信可能。リクエストを待たずに、どちらからもデータを送信できる
  2. 持続的な接続: WebSocketは一度接続が確立されると、その接続がクローズするまでデータを送り続けることができる。HTTPのように1つのリクエストで1つのレスポンスを送る形とは異なる。
  3. 軽量: WebSocketのヘッダは非常に小さく、通信オーバーヘッドが少ないため、大量のメッセージを効率的にやり取りできる
  4. イベントドリブン: WebSocketでは、サーバーからクライアントに通知をプッシュすることができる

WebSocketの通信の流れ

sequenceDiagram participant Client participant Server Client->>Server: WebSocket Handshake (HTTP Upgrade) Server-->>Client: 101 Switching Protocols Client->>Server: WebSocket Connection Established Client->>Server: Send Data Frame Server-->>Client: Send Data Frame (Response) Client->>Server: Close Connection Frame Server-->>Client: Acknowledge Close Frame Client->>Server: WebSocket Connection Closed
  1. 接続の確立:クライアントはHTTPプロトコルを使って、サーバーに接続要求を送る。この時、Upgradeヘッダーを使って、WebSocketへのプロトコル変更を要求する。サーバーがその要求を承認すると、HTTP接続がWebSocketにアップグレードされて、継続的な接続が確立される。
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
  1. メッセージの送受信:接続が確立されると、クライアントとサーバーは双方向にメッセージを送受信できる。メッセージは通常、テキストまたはバイナリデータ。クライアントとサーバーは、例えば、チャットメッセージや通知などを相互にやり取りする。
  2. 接続の終了:接続はクライアントまたは、サーバーのどちらかが閉じるまで持続する。接続が不要になった場合、接続をクローズする手続きを行う。

WebSocketのヘッダが軽量とは?

従来のHTTPリクエスト/レスポンスとの通信オーバーヘッドの違い

簡単にまとめると

特徴 HTTP/1.1 WebSocket
ヘッダサイズ 500バイト〜1KB以上 2〜14バイト
ヘッダの送信頻度 各リクエスト/レスポンスごと 接続時(最初のハンドシェイクのみ)
持続的接続のサポート 基本的にリクエスト/レスポンス型 持続的
リアルタイム性 リクエストごとにラウンドトリップ 持続的でリアルタイム

HTTPリクエスト/レスポンスのヘッダの大きさ

  • HTTP/1.1では、リクエストやレスポンスごとに、以下のような多くのメタデータ情報を送るため、特に短いメッセージを送る際にも、リクエストごとに繰り返し送信されるため、オーバーヘッドが大きくなる。
    • URL
    • メソッド
    • プロトコルバージョン
    • ホスト名
    • ユーザーエージェント
    • クッキー
    • 認証情報
  • 一般的なHTTPリクエストのヘッダのサイズは、数百バイトから1KB程度になることが多い。
  • HTTPヘッダには、クッキー、リファラ、ホスト情報などの、通信内容には直接関係しない多くの情報が含まれることがある

HTTPリクエストでは、具体的には、以下のような形式をとる。HTTPヘッダ部分だけでも、約100〜200バイト程度になり、これに加えてデータ部分が送信される。

POST /chat HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Content-Type: application/json
Content-Length: 17

{"message":"Hello"}

WebSocketフレームのヘッダーの大きさ

  • WebSocketでは、最初のハンドシェイク(HTTP 101 Switching Protocols)でプロトコルをWebSocketにアップグレードした後、持続的な接続を維持する。そのため、通信のたびにHTTPのような重いヘッダを繰り返し送信する必要がなくなる
  • WebSocketフレームのヘッダは非常に小さく、基本的には2Byteから14Byteで構成される。
  • フレームのヘッダには、ペイロードの長さ、メッセージのタイプ(テキストかバイナリか)、マスクなどが含まれているが、HTTPに比べてはるかに効率が良い。

具体的なWebSocketフレームの基本ヘッダ構造

  • 1バイト目: FINビット(メッセージの終了を示す)とOpcode(データの種類: テキスト、バイナリ、Ping、Pongなど)。
  • 2バイト目: ペイロード長とマスクビット(クライアントからの送信は必ずマスクされる)。
  • ペイロードが126バイトを超える場合、追加のフィールドが使われるが、通常は非常に短いメッセージであれば2〜14バイトで済みます。

WebSocketフレームの具体例は以下のようになる。この場合には、ヘッダのサイズは約2バイトになる。HTTPに比べると効率的。

FIN=1, Opcode=1 (テキスト), Masked, Payload length=5
データ: Hello

フレームについて

フィールド名 ビット数/サイズ 説明
FIN 1ビット メッセージの最後のフレームかを示す(1 = 最後、0 = 続く)
RSV1, RSV2, RSV3 各1ビット 予約ビット。通常は0。拡張機能のために使用される場合がある
Opcode 4ビット フレームの種類を示す(例: 0x1 = テキストフレーム、0x2 = バイナリフレーム)
Mask 1ビット マスキングの有無を示す(クライアントから送信される場合は1、サーバーは通常0)
Payload Length 7ビット、または7+16ビット、7+64ビット ペイロードデータの長さを示す(0~125の場合はそのまま、126なら16ビット、127なら64ビット)
Masking Key 32ビット(クライアントからのみ) クライアント側から送信される場合に使用されるマスキングキー
Payload Data 可変長 実際のデータ(テキストまたはバイナリ)。UTF-8エンコード(テキストの場合)またはそのままバイナリ

Opcodeの主な値

Opcodeの値 意味
0x0 継続フレーム
0x1 テキストフレーム
0x2 バイナリフレーム
0x8 Closeフレーム
0x9 Pingフレーム
0xA Pongフレーム

WebSocket Opening ハンドシェイクについて

  • WebSocket通信が始まる際に、クライアントとサーバー間で行われる初期のプロトコル交渉。
  • ハンドシェイクプロセスは、クライアントが通常のHTTPリクエストを使ってサーバーにWebSocketプロトコルへの切り替えを要求して、サーバーがその要求を承認することで行われる。
  • このハンドシェイクを通じて、クライアントとサーバーはWebSocket接続を確立し、その後の双方向通信を可能にする。

関連技術メモ

今後、知りたいことのメモ

  • Operational Transformation (OT)
  • WebRTC
  • Server-Sent Events (SSE)
  • WebTransport
  • HTTP/2 Server Push
  • gRPC with HTTP/2
  • QUIC (Quick UDP Internet Connection)

チームトポロジー、SRE、Platform Engineering

チームトポロジーとは?

  • ソフトウェア開発およびIT組織におけるチーム構成と運用モデルのガイドライン
  • ソフトウェア開発と運用において、効果的なチーム構成が成功に不可欠なものであるということが前提となっている
  • チームの種類、責任、相互作用のパターンを定義して、組織が迅速かつ効果的にソフトウェアを提供するのをサポートする

4つの主要なチーム

https://martinfowler.com/bliki/TeamTopologies.html

ストリームアラインドチーム(Stream-aligned Team):

エンドユーザーに直接価値提供をする機能やサービスを開発する。具体的には次のようなことを実践するチームを指す。

  • 特定の製品ライン:例えば、ある一つの製品の開発に集中するチーム
  • 顧客セグメント:例えば、特定の顧客グループに向けたサービスを開発するチーム
  • 機能セット:例えば、特定の機能やサービスの開発に専念するチーム

エンジニアリングプラットフォームチーム(Enabling Team)

他のチームが新しい技術や手法を取り入れるのをサポートすることを目的とするチームである。 このチームは、他のチームが効果的に働けるようにコーチやガイドの役割を果たす。

具体的には、次のような活動を行う:

  • 新しい技術の導入支援:例えば、新しいプログラミング言語やツールの使い方を教える。
  • 効率化のためのサポート:例えば、開発プロセスの改善や自動化ツールの導入を手助けする。
  • 問題解決のアドバイス:他のチームが技術的な問題に直面したときに、解決方法を提案する。

コンプリケイテッドサブシステムチーム(Complicated Subsystem Team):

高度な専門知識が必要な特定のコンポーネントやサブシステムを扱うチームこのチームは他のチームが対処するのが難しい技術的な課題に取り組む。 例として、機械学習アルゴリズムや複雑なデータ処理エンジンを開発するチームが挙げられる。

具体的な活動目標:

  1. 高度な専門知識の提供: このチームは、非常に専門的な知識やスキルを持っているため、特定の技術的な領域における深い理解が求められます。 例えば、分散システムの設計や高度な暗号化技術の実装など、他のチームが扱うのが難しい技術的な課題に対処します。
  2. 特定のコンポーネントやサブシステムの開発: このチームは、特定のコンポーネントやサブシステムの設計と開発を担当します。これらのコンポーネントやサブシステムは、システム全体のパフォーマンスや機能にとって非常に重要です。 例えば、リアルタイムデータ処理エンジンや高性能データベースエンジンの開発を担当します。
  3. 技術的な課題の解決: 他のチームが直面する技術的な問題に対して、専門的な知識を提供し、解決方法を提案します。 例えば、スケーラビリティの問題やパフォーマンスのボトルネックに対する解決策を提供します。

プラットフォームチーム(Platform Team)

プラットフォームチームとは、他のチームが効率的に作業できるように、共通のインフラストラクチャやサービスを提供する専門チーム。 このチームは、ストリームアラインドチームや他の開発チームが迅速かつ効果的に仕事を進めるための基盤を整備し管理する。

具体的な活動

  1. 共通インフラストラクチャの提供と管理: プラットフォームチームは、開発環境、テスト環境、本番環境など、さまざまな環境を構築し、管理する。 これには、サーバーのセットアップ、ネットワークの設定、データベースの管理などが含まれる。

  2. CI/CDパイプラインの構築と運用: 継続的インテグレーション/継続的デリバリー(CI/CD)パイプラインを設計し、実装する。 これにより、コードのビルド、テスト、自動デプロイが効率的に行われる。 例えば、JenkinsやGitLab CI/CDなどのツールを使用して、開発チームがコードをリリースするプロセスを自動化する。

  3. クラウドインフラの管理: クラウドサービス(AWS、Azure、Google Cloudなど)の管理を行う。これには、リソースのプロビジョニング、監視、スケーリングが含まれる。 例えば、インフラストラクチャをコードとして管理し、TerraformやCloudFormationなどのツールを使用して、インフラの自動化を実現する。

  4. デベロッパーツールの提供: 開発者が効率的に作業できるように、各種ツールを提供します。これには、IDEの設定、デバッグツール、コードリポジトリの管理などが含まれる。 例えば、GitHubやGitLabを使用して、ソースコード管理を統一し、コラボレーションを促進する。

  5. セキュリティとコンプライアンスの維持: システム全体のセキュリティを確保し、コンプライアンスに準拠するためのポリシーとプロセスを設定する。 例えば、アクセス制御の設定、脆弱性スキャン、ログ管理などを行う。

3つの総合作用モード

Team Topologiesでは、チーム間の効果的な相互作用を促進するために、3つの主要な相互作用モードを定義している。

1. X-as-a-Serviceモード (XaaS)

このモードでは、一つのチームが他のチームに対してサービスを提供する。 サービスの提供者と利用者の関係が明確で、定義されたインターフェースを通じてサービスが提供される。 例として、プラットフォームチームがCI/CDサービスを他のチームに提供する場合が挙げられる。

2. コラボレーションモード (Collaboration)

コラボレーションモードでは、チーム間での短期間の強い協力関係を築く必要がある。 共通の特定の目標の達成のために、一時的に密接に協力していく。 例えば、新しい機能の共同開発や技術的課題の共同解決をするようなケースがこのモードに該当する。 このモードは、創造的な問題解決や新しいアイデアの実現に適していますが、長期的には依存関係が増える可能性があるため、慎重な管理が必要となる。

3. ファシリテーションモード (Facilitating)

このモードでは、エンジニアリングプラットフォームチームが他のチームのスキル向上や新技術の導入を支援する。 継続的な支援を通じて、他のチームの能力を向上させる。 例えば、新しい開発手法やツールの導入支援が挙げられる。

これらの相互作用モードを適切に利用することで、組織内のチームが効率的に連携し、迅速かつ効果的に価値を提供することができる。

何が大事なのか?

  • 強いリーダーシップ:新しい組織構成への変更を行うには、強いリーダーシップが必要である。そもそもある程度機能していた(効率的かどうかは別として)組織を分解して、新しい構成に移行するのは組織のメンバーには大きな負担になり得る。そのようなリスクを軽減し、管理するために段階的に導入したり、パイロットプロジェクトを選定し、影響範囲が限定的で、結果を測定しやすいプロジェクトを選定する必要もある。
  • フィードバックループの構築:効果の測定とフィードバックループの構築は、継続的な改善を可能にし、トポロジーの効果を最大化するために不可欠である。

Spotifyの例

Spotifyモデルは機能しなかった。

medium.com

agile.quora.com

References

web.devopstopologies.com

www.ryuzee.com

learn.microsoft.com

zodについて

Zod

  • TypeScriptとReactアプリケーションでデータバリデーションと型安全性を確保するためのライブラリ
  • Zodはスキーマベースのバリデーションライブラリで、定義されたスキーマに従ってデータを検証し、型推論もサポートする

zod.dev

スキーマの定義

Zodを使ってスキーマを定義することで、データ構造とバリデーションルールを指定する 例えば、ユーザー情報のスキーマを定義する場合:

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  age: z.number().min(0, "年齢は0以上である必要があります").max(120, "年齢は120以下である必要があります"),
  email: z.string().email("有効なメールアドレスを入力してください"),
});
  • name: 空でない文字列
  • age: 0以上120以下の数値
  • email: 有効なメールアドレス形式

データのバリデーション

定義したスキーマを使って、データのバリデーションを次のように行う。parseメソッドは、データがスキーマに適合しているかを検証し、適合していない場合はエラーをスローする。

const userData = {
  name: "John Doe",
  age: 25,
  email: "[email protected]",
};

try {
  userSchema.parse(userData);
  console.log("データは有効です");
} catch (e) {
  console.error("バリデーションエラー:", e.errors);
}

型の推論

ZodはTypeScriptの型推論と統合されていて、スキーマから型を自動生成することができる。

type User = z.infer<typeof userSchema>;
// User型は{ name: string; age: number; email: string; }となる

カスタムバリデーション(.refineメソッド)

const passwordSchema = z.string().refine((val) => val.length >= 8, {
  message: "パスワードは8文字以上でなければなりません",
});

passwordSchema.parse("short"); // ここでエラーが

複数のフィールドのバリデーションをする。

import { z } from 'zod';

const dateRangeSchema = z.object({
  startDate: z.date(),
  endDate: z.date(),
}).refine(data => data.startDate <= data.endDate, {
  message: "終了日は開始日より後でなければなりません",
  path: ["endDate"], // エラーメッセージをendDateフィールドに関連付ける
});

const dateRange = {
  startDate: new Date('2024-01-01'),
  endDate: new Date('2023-12-31'),
};

try {
  dateRangeSchema.parse(dateRange);
  console.log("データは有効です");
} catch (e) {
  if (e instanceof z.ZodError) {
    console.error("バリデーションエラー:", e.errors);
  }
}

golangci-lintとRenovate

たまにはGo関係のことも書くかということで書く。

golangci-lintについて

github.com

  • golangci-lintを使って、CIでコードスタイルや静的解析を行うことで、コード品質を高めている。これにより、プロジェクト全体のコードの一貫性と品質が向上する。
  • CIにはGitHub Actionsを使用し、lintのスタイルに合わない書き方があるとエラーとして検出される。これにより、コードレビュー前に自動的に品質を保証できる。
  • レビュイーには、lintが通るまで修正を続けてもらい、統一されたアウトプットを確保する。これにより、コードの一貫性が保たれる。
  • Goで推奨される書き方を自然に学ぶことができる。golangci-lintは、gofmtã‚„govetなどの基本的なツールから、errcheckã‚„staticcheckといった高度なツールまで、多くのリンターを統合している。
  • ある程度のレベルでのコードが上がってくるため、レビューの負担が減り、レビューで焦点を当てるポイントがハイレベルなものに絞られるので非常に楽になる。

運用方針

.golangci.ymlというファイルで設定をすることができるが、以下のようにenable-all: trueと設定し、無効にするlinterをリストアップする形式で運用している。

linters:
  enable-all: true
  disable:
    - wsl
    - wrapcheck
    - unparam
    - varnamelen

他の人の発表などを見ると、disable-all: trueにして、使いたいlinterだけをリストアップする運用方法を提案していることがある。その人たち曰く、「何も修正せずともバージョンアップできる」ということであるが、バージョンアップ時に追加されるlinterがあるならば、修正しつつ追加するべきだと思うので、disable-all: true派には自分は反対意見を持っている。

この後に紹介するが、Renovateという自動でバージョン更新のPRを作成してくれるツールがある。これを用いてバージョン更新をすることで、enable-all: trueと設定していれば、新しいlinterを漏れなく追加することが可能である。

Renovate

github.com

Renovateは、オープンソースの依存関係管理ツールで、プロジェクト内の依存関係の更新を自動化するために使用される。 Renovateを使用することで、依存関係のバージョンを定期的にチェックし、最新バージョンへの更新を提案するPR(プルリクエスト)を自動で生成する。

Goだけではなくて、Terraformなどの他の依存のPRも作成してくれる。

毎週月曜日など、PRを作成する時間指定が可能で、パッケージなどのバージョン更新があった場合にPRがくる。

GitHub ActionsのCIが通れば、マージするようにしている。

renovate

golangci-lintとRenovate

golangci-lintに新しいlinterが追加された場合、enable-all: trueに設定していれば、新しいlinterを含めてlint checkが行われるため、修正が必要かどうかをCIで検知することができる。

修正が必要で、かつ追加したいlinterであれば、修正してからマージする。これにより、抜け漏れなくlinterのバージョン更新が可能。

Renovateを使用してgolangci-lintのバージョンを自動更新する設定を行えば、新しいlinterの追加や既存のlinterのアップデートが自動的にPRとして提案される。PRが作成されると、自動化されたテストとlintチェックが行われ、問題がなければマージする。 これにより、手動での更新作業を減らし、常に最新の状態を維持することができる。

このように、Renovateとgolangci-lintを組み合わせて使用することで、コード品質を高く保ちながら、依存関係の管理を効率化し、プロジェクトの健全性を維持することができる。