mnagaaのメモ

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

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

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

mnagaa.hatenablog.com

HTTP

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

基本的な特徴

  • リクエスト-レスポンスモデル:
    • クライアントがサーバーにリクエストを送信し、サーバーがそのリクエストに対するレスポンスを返す。
    • 例えば、ウェブページを閲覧する際にブラウザがサーバーにページのリクエストを送り、サーバーがHTMLやCSSJavaScriptなどのデータを返す仕組み。
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 メール送信の標準プロトコル メール送信(GmailOutlookなど)
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

そもそもソケットとは?

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

ソケット

  • IPアドレスとポート番号が関連づけられており、この組み合わせによって、特定のマシンと特定のアプリケーションが通信相手を特定できる
  • ストリームソケット(Stream Socket): TCPを使った信頼性のある通信に使用
    • プロトコルTCP (Transmission Control Protocol)
    • 特徴:信頼性のある、順序を保証したデータ通信が行われる。データはストリーム(連続したバイト列)としてやり取りされる
    • 用途:HTTP、FTPSSHなどの多くのアプリケーションがこのソケットタイプを利用
  • データグラムソケット(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: "john.doe@example.com",
};

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

データストア #2 ~Redis~

【導入】Redis (Remote Dictionary Server)

  • 高速なインメモリデータストア
  • 用途・ユースケース
    • キャッシュ
    • メッセージブローカー
    • セッション管理
    • リアルタイム分析
    • ランキングシステム
  • 特徴
    • 文字列、リスト、セット、ソートセット、ハッシュ、ビットマップ、HyperLogLog、地理空間インデックス、ストリームなどのデータ構造をサポートしている
    • データの読み書きがミリ秒単位で行える
    • 永続化機能を持ち、データをディスクに保存することで再起動後もデータを保持できる
    • クラスタリングレプリケーションをサポートしていて、高可用性とフォールトトレランスを実現
      • フォールトトレランス: システムが部分的な故障やエラーが発生しても、サービスを中断することなく正常に動作し続ける能力

パフォーマンスについて

  • すべてのデータがメモリ内に保存されるため、読み書き操作が非常に高速で、ミリ秒単位のレスポンスが可能
    • メモリアクセスがディスクアクセスよりも早い
  • リアルタイムアプリケーションや高頻度の読み書き操作が必要なシステムで有用

永続化

  • インメモリデータベースでありながら、データの永続化機能も有する
  • スナップショット方式(RDB)や、アペンドオンリーログ(AOF)といったメカニズムを使用して、メモリ内のデータをディスクに保存
  • Redisサーバーが再起動してもデータの整合性が保たれる

クラスタリングレプリケーション

  • スケーラビリティ
  • クラスタリング機能でデータを複数のノードに分散して保存することで、負荷分散が可能→大規模なデータセットを扱うことができる
  • レプリケーション機能により、データの冗長性を確保して、フェイルオーバーを容易に実現できる
    • フェイルオーバー:システムやコンポーネントが故障した時に自動的に予備のシステムに切り替わるプロセス

多様なデータ構造のサポート

  • 多様なデータ構造をネイティブにサポートしていて、複雑なデータ処理を簡単に行うことができる
    • リストを使用して、メッセージキュー実装する
    • ソートセットを利用して、ランキングシステムを構築する
    • ハッシュを使用してユーザーセッション情報を管理する
  • 対応されているデータ構造(一部)

    Data Type データ構造 説明
    Strings 文字列 バイナリセーフな文字列データ
    Lists リスト 双方向リンクリスト。メッセージキューやタスクキューとして利用される
    Sets セット 重複を許さない無序集合。タグやカテゴリーの管理に有効
    Sorted Sets ソートセット スコア付きの集合。ランキングシステムやリーダーボードに最適
    Hashes ハッシュ フィールドと値のペア。ユーザーセッションの情報や設定データの管理に利用。
    Bitmaps ビットマップ ビット単位の操作。フラグやビットフィールドの管理に適している。
    HyperLogLogs ハイパーログログ 大規模データセットのユニーク要素数を近似的に数える。カウントデータの効率的な管理に役立ちます。
    Geospatial Indexes 地理空間インデックス 地理空間データの管理。位置情報に基づくクエリや検索が可能です。
    Streams ストリーム 時間順に追加されるデータのログ。リアルタイムデータ処理やイベントログの管理に使用されます。
  • 開発者のためのRedisチュートリアル(1): https://meetup-jp.toast.com/3125

  • ざっくり分かるRedis入門 ~データ構造と主な機能について~: https://qiita.com/t-kuni/items/c00d0994c2eb20681cb1

パフォーマンス

Redisのレスポンスが速い理由について、簡単にまとめます。

インメモリデータストア

データをメモリ内に保持するインメモリデータストアであるため、メモリはディスクと比較してアクセス速度が非常に高速で、ナノ秒単位での読み書きが可能

ディスクベースのデータベースシステムでは、ディスクI/Oがボトルネックになることが多く、遅延が発生することがある

RAMを使用するため、ディスクアクセスよりも高速

www.youtube.com

単一スレッドアーキテクチャ

シンプルな並行性モデル

単一スレッドアーキテクチャを採用しているため、複雑なマルチスレッド処理やロック機構が不要となり、データの一貫性を維持しながら高速な処理を実現

単一スレッドであるため、コンテキストスイッチやスレッド間の競合、デッドロックなどの複雑な問題が発生せず、リクエストの処理が効率的

イベントループ

Redisはlibeventやlibevなどのイベントライブラリを使用して、非同期I/O操作を効率的に処理する

イベントループを使用することで非同期I/O操作を行いながらCPUの使用効率を最大化し、リクエストに対するレスポンス時間を短縮

イベント駆動型のアーキテクチャは、高いスループットと低いレイテンシを実現するために重要

永続化について

  • スナップショット(RDB: Redis Database File)
    • 特定の時点でのデータ全体のスナップショットをディスクに保存
    • メモリ内のデータをバイナリ形式でディスクに書き出し、一定の間隔で自動的に保存されるように設定できる
  • AOF (Append-Only File)
    • すべての書き込み操作をログ形式で順次ディスクに追加する方法

Redis Clusterの基本概念

  • Redis クラスタ
  • ノード
    • Redis クラスタは複数のノードで構成される
    • 各ノードは独立したRedisインスタンス
    • それぞれが独自のデータセットを持っている
    • マスターノードまたはリードレプリカとして機能する
      • マスターノード:データの読み書きを直接処理するノードで、クラスタ内のデータをシャーディングして保持する
      • レプリカノード:マスターノードのデータを複製するノードで、マスターノードがダウンした時には、自動でマスターに昇格する
  • シャーディング
    • データをクラスタ内の複数のノードに分割して保存するプロセス
    • データの分散を効率的に行うためにシャーディングを利用する
    • スケーラビリティが向上
    • ハッシュスロット
      • redisクラスタ16384個のハッシュスロットを持つ
      • 各データキーはハッシュ関数を通じてこれらのスロットのいずれかにマッピングされる
      • ノードはこれらの割り当てられたスロットを担当してデータを保持する
    • シャード内の1つのノードがマスターノードで、他のノードはレプリカノードとして機能する。レプリカノードはマスターノードのデータを複製して、フェイルオーバー時にはマスターに昇格する
  • スロット
    • スロットはRedisクラスタにおけるデータ分散の単位
    • 各ノードは特定の範囲のスロットを担当する
    • 例えば、3つのノードでクラスタを作成した場合のスロットの分割は、以下のように分割が行われる →6台でレプリカも追加した場合もやったのでチェック

  • クラスタ通信

    ノード間の通信は以下のプロトコルを使用して行われる

    • ゴシッププロトコル
      • 分散システムにおいて、ノード間で状態情報を交換するためのプロトコル
      • ノードの追加・削除やスロットの移動など、クラスタの情報を共有する
      • 情報の伝搬
        • 各ノードは定期的にランダムに選ばれた他のノードと通信をして、自身の知っている情報を交換する
        • この情報交換を繰り返すことで、クラスタ全体に情報が広まる。
      • 信頼性
        • ゴシッププロトコルは、部分的なネットワーク障害やノード障害に対しても耐性がある
        • 他のノードは他の複数のノードと情報を交換するため、情報が失われる可能性が低くなる
      • スケーラビリティ
        • ゴシッププロトコルは、大規模なクラスタ環境においても効果的に動作する
        • ノード数が増えても、プロトコルのオーバーヘッドは比較的小さく抑えられる
    • クラスタバス: https://mogile.web.fc2.com/redis/docs/reference/cluster-spec/index.html
      • クラスタ内のノード間通信専用のチャネル
      • ゴシッププロトコルやフェイルオーバー情報の伝達に使用
      • 専用チャネル
        • クラスタバスは、ノード間通信専用のチャネルであるため、データ操作のトラフィックとは分離されていて、クラスタ管理に必要な通信がデータ操作の負荷に影響されない
      • 低レイテンシ通信
        • クラスタバスは、低レイテンシの通信を提供し、ノード間の迅速な情報交換を可能にする
        • フェイルオーバーやスロット再配分など、クラスタの重要な操作が迅速に行われる
      • ノードの参加と離脱
        • 新しいノードがクラスタに参加する際・既存ノードがクラスタから離脱する際に、クラスタバスを通じてその情報が全ノードに伝達される
      • フェイルオーバーのトリガー
        • マスターノードの障害が検出されるとクラスタバスを通じてフェイルオーバーがトリガーされる
        • 障害が発生したマスターのレプリカが自動的に昇格して、クラスタの一貫性が維持される
      • スロットの再分配
        • ノードの追加・削除に伴うスロットの再分配もクラスタバスを通じて調整される
        • スロットの再分配プロセスがクラスタ全体に伝播し、一貫性が確保
  • フェイルオーバー
  • スレーブ
    • Slave = Replica
    • マスターノードのデータを複製する役割を持つノード

レプリカノードを追加した時