💨

分散システムを支えるネットワークの基礎 (分散システム入門 1)

に公開

分散システムとネットワーク

分散システムは、従来の単一システム(一つの計算機で完結するシステム)とは異なり、複数の計算機がネットワークを介して協働して動作するシステムです。

一方で、単一システムにおいても、関数間での通信や内部でのデータ転送が行われるため、なぜ分散システムにおいてとりわけネットワークの知識が重要なのでしょうか。

大きく分けて2つの理由があげられます。

  • ネットワークが不確実な環境である
  • レイテンシのスケールが大きく異なる

これらの理由により、分散システムではネットワークの知識も必要となります。
システムの性能はScalability, Reliability, Security, MobiltyやQoS[1]など様々な観点で評価されますが、ネットワークの特性を理解していないと、これらの要件を満たすシステムを設計・実装することが困難になります。また、ネットワークの概要を理解しておくことで今後の分散システムの特性も理解しやすくなると思います。

レイテンシと転送速度

レイテンシと転送速度はネットワークの基本的な性能指標です。よく混同されがちですが、両者は異なる概念です。

  • レイテンシ (Latency): 空のデータが送信元から受信先に到達するまでの時間
  • 転送速度 (Transfer Rate): 単位時間あたりに送信できるデータ量

さらにLatencyはPacket delivery timeとも呼ばれ、以下のように分解されます。

  • Packet delivery time: パケットが送信元から受信先に届くまでの時間でTransmission timeとPropagation timeの和
  • Transmission time: データの送信にかかる時間
  • Propagation time: 信号が伝搬するのにかかる時間

Transmission timeはPacket Size / Bit rateで計算され、Propagation timeはDistance / Propagation speedで計算されます。
感覚的な説明として、Transmission timeは「データを送り出すのにかかる時間で、データが大きければ大きいほど時間がかかる」、Propagation timeは「信号が伝わるのにかかる時間で、距離が遠ければ遠いほど時間がかかる」と言えます。

Bit rateはbps (bits per second)で表されます。bpsは物理的な制約に加え、これから説明するネットワークプロトコルのオーバーヘッドなどの影響を受けており、理論上の最大値よりも低くなることが一般的です。


ここからはネットワーク性能を理解した上で、それを支える構造(階層モデル)とプロトコルを紹介することで、分散システムにおけるネットワークの基礎知識を深めていきたいと思います。

Multi-Layered Network Architecture

ネットワークの構造を説明する際に、OSI参照モデルやTCP/IPモデルといった多層アーキテクチャがよく用いられます。これらのモデルは、ネットワーク通信をPhysical層からApplication層までの複数の層に分割し、各層が特定の機能を担当することで、ネットワーク通信の設計と理解を容易にします。

これらに関してはすでに多くの資料があるため、ここでは詳細な説明は省略します。

Network protocols

少しパソコンをいじった事がある人ならIPやTCP, UDPといった言葉を聞いたことがあるかもしれません。プロトコルとは、異なる機器やソフトウェアが通信を行う際のルールや手順を定めたもの全般を指します。
多くの場合はTCP/IPモデルでのNetwork層とTransport層に該当するプロトコルを指すことが多いです。[2]

UDP

UDPはUser Datagram Protocolの略で、筆者はマイコン通信で使ってことがあります。特徴としては

  • 送信者と受信者の間でコネクションを確立しない
  • プロトコルとしてのオーバーヘッドが小さいため高速に通信できる
  • 信頼性が低い (パケットの損失や順序の入れ替わりが発生する可能性がある)
  • リアルタイム性が求められる通信に適している (例: 音声通話、動画ストリーミング、オンラインゲーム)

一つ目の特徴である「送信者と受信者の間でコネクションを確立しない」ことから、UDPはコネクションレス型プロトコルとも呼ばれます。これは通信の信頼性と引き換えに、他の特徴である高速性やリアルタイム性を実現しています。小規模なデータ転送や、多少のデータ損失が許容される場合に適しています。
用途としてはDNS, IP電話(スマホアプリでの通話), QUICプロトコル(HTTP/3)などがあります。

TCP

普段の"インターネット"での通信に使われることが多いプロトコルです。例えば、Webページの閲覧やメールの送受信などがTCPを利用しています。
UDPと比較して、TCPは以下の特徴を持っています。

  • 送信者と受信者の間でコネクション: Full Duplex Connectionを確立する
  • 信頼性が高い (パケットの損失や順序の入れ替わりを検出し、再送要求を行う)
  • 転送速度が遅い
  • 大量のデータ転送に適している (例: ファイル転送、Webページの読み込み)

Full Duplex Connectionは、2つの独立した単方向ストリームで構成され、双方向のデータ転送を効率的に行うことができます。またUDPでは送信者と受信者という関係でしたが、TCPでは一度コネクションを確立すると双方が送信者・受信者の役割を持つことができます。
ただしTCPはエラーを含めると、データを正しい順序で届けます。確実なPacket deliveryを保証するわけではなく、例えば通信が途中で途切れた場合などはデータが失われる可能性があります。

IP

IPはInternet Protocolの略で、TCP/UDPとは異なりTransport層ではなくNetwork層のプロトコルです。IPはよくインターネットでの住所に例えられ、データが送信元から受信先に正しく届けられるようにする役割を担っています。
でも、自分の端末になんのIPが割り当てられているかってあんまり気にしないですよね?しかもIPは動的に変わることもあるし,,,
この問題を解決するために、NAT (Network Address Translation)やDHCP (Dynamic Host Configuration Protocol)といった技術が使われています。これらは動的にIPを管理する仕組みでNATはプライベートIPアドレスとグローバルIPアドレスの変換を行い、DHCPは動的にIPアドレスを割り当てる役割を果たします。筆者も詳しくはないので、また別の機会にまとめたいと思います。

Sockets

SocketはTransport層とNetwork層を抽象化したインターフェースで、OSの提供するAPIを通じて利用されます。Socketを使用することで、アプリケーションはTCPやUDPなどのプロトコルを意識せずにネットワーク通信を行うことができます。
TCPのSocketはストリーム指向で、データの送受信が連続的に行われます。一方、UDPのSocketはデータグラム指向で、独立したパケット単位でデータが送受信されます。

ここからは少し実装例を交えながら説明します。Golangのnetパッケージを使った例を紹介しますが、他の言語でも概念は同じです。

Stream Socket

TCPのSocketはストリーム指向で、データの送受信が連続的に行われます。

簡単のためにServer-Clientモデルを考えます。
Serverは特定のポートにバインドしたSocketを作成し、Clientからの接続要求を待ち受けます。Clientごとに新しいSocketを作成し、Byte Streamとしてデータの送受信を行います。
ClientはServerのIPアドレスとポート番号を指定して接続要求を送信し、接続が確立されるとByte Streamとしてデータの送受信を行います。
例えばGolangでの実装例は以下のようになります。

// Server
import (
    "net"
    "log"
)
func main() {
    listener, _ := net.Listen("tcp", ":8080") // ポート8080で待ち受け
    defer listener.Close()

    for {
        conn, _ := listener.Accept()
        // Connectionごとに新しいGoroutineを起動して処理
        go func(c net.Conn) {
            defer c.Close() // 最後に接続を閉じる
            // データの送受信処理
            // 例: c.Read(buffer)
            // 例: c.Write([]byte("Hello, Client!"))
        }(conn)
    }
}
// Client
import (
    "net"
)
func main() {
    conn, _ := net.Dial("tcp", "server_ip:8080")
    defer conn.Close() // 最後に接続を閉じる
    // データの送受信処理
    // 例: conn.Write([]byte("Hello, Server!"))
    // 例: conn.Read(buffer)
}

Datagram Socket

UDPのSocketはデータグラム指向で、独立したパケット単位でデータが送受信されます。

Serverは特定のポートにバインドしたSocketを作成し、Clientからのデータグラムを待ち受けます。ClientはServerのIPアドレスとポート番号を指定してデータグラムを送信します。
Clientからのmessageには送信元のアドレス情報が含まれています。

例えばGolangでの実装例は以下のようになります。

// Server
import (
    "net"
)
func main() {
    addr, _ := net.ResolveUDPAddr("udp", ":8080") // ポート8080で待ち受け
    conn, _ := net.ListenUDP("udp", addr)
    defer conn.Close()

    buffer := make([]byte, 1024)
    for {
        n, clientAddr, _ := conn.ReadFromUDP(buffer)
        // 受信したデータを処理
    }
}
// Client
import (
    "net"
)
func main() {
    serverAddr, _ := net.ResolveUDPAddr("udp", "server_ip:8080")
    conn, _ := net.DialUDP("udp", nil, serverAddr)
    defer conn.Close() // 最後に接続を閉じる
    // データの送信処理
    conn.Write([]byte("Hello, Server!"))

}

ところで、Golangのnetパッケージでは(conn *UDPConn) WriteTo(conn *UDPConn) ReadFromといった関数も提供されています。UDPは受信側が送信元のアドレス情報を知ることができるため、これらの関数を使うことで双方向の通信も可能です。
詳しくは公式ドキュメントを参照してください。

まとめ

分散システムにおけるネットワークの基礎知識について説明しました。ネットワークの特性を理解することで、分散システムの設計や実装に役立てることができます。今後の記事では、さらに分散システムの他の重要な要素についても掘り下げていきたいと思います。

脚注
  1. Quality of Service ↩︎

  2. アプリケーション層のプロトコルとしてはHTTP, FTP, SMTPなどがあります。 ↩︎

GitHubで編集を提案

Discussion