L7ロードバランサとL4ロードバランサ

ロードバランサとは

ロードバランサは、複数のバックエンドサーバにリクエストを分散させるための仕組み。

f:id:kimulla:20191201134523j:plain

DNSラウンドロビン

似たような負荷分散の仕組みとしてDNSラウンドロビンがある。

これはバックエンドサーバのIPアドレスをDNSレコードとして登録し、DNSの問い合わせ時にバックエンドサーバのIPアドレスを順番に返事することで負荷分散する。

DNSラウンドロビンでは対応できないこと

  • DNSの問い合わせ結果はクライアント側でキャッシュされるため
    • 負荷が偏る可能性がある
    • サーバダウンした時に、即座に切り替えられないクライアントがいる
  • パーシステンス機能がない(クライアントごとに同一のサーバに割り振れない)

上記に対応したいときに、ロードバランサが使われる。

L7ロードバランサ

  • L7レベルでロードバランシングする
    • クライアントから見たときの接続先IPアドレスはLBのIPアドレスになる
    • バックエンドサーバから見たときの接続元IPアドレスはLBのIPアドレスになる
  • HTTPの情報を用いた、高度な制御ができる
  • Cookieを利用したスティッキーセッション
  • X-Forwarded-Forヘッダー を利用したクライアントのIPアドレス保持
  • HTTP情報を取得するために、LBでTCPコネクションが終端する
    • クライアントとLB
    • LBとバックエンドサーバ

パケットの流れ

TCP コネクションが LB で終端するため、合計で 2 本のコネクションが張られる。 f:id:kimulla:20191201134622j:plain

L4ロードバランサ

  • L4以下のレベルでロードバランシングする
  • IPレベルでのセッションパーシステンスができる
    • 接続元IPが同じなら同じバックエンドサーバに送れる
  • IPアドレス変換方式(NAT方式) と MACアドレス変換方式(DSR方式) がある
  • ロードバランサでTCPコネクションは終端しない

IPアドレス変換方式(NAT方式)

  • NATを利用してL4レベルでロードバランシングする
    • クライアントから見たときの接続先IPアドレスはLBのIPアドレスになる
    • バックエンドサーバから見たときの接続元IPアドレスは
      • LBのIPアドレスになる(SNAT)
      • 接続元のIPアドレスになる(NAT。この場合はリアルサーバのデフォルトゲートウェイを LB に設定するなどして LB にパケットを戻す必要がある。)
  • NAT変換のために全ての通信がLBを経由する

パケットの流れ

LB で TCP コネクションが終端しないため、コネクションは 1 本しか張られない。

f:id:kimulla:20191201134820j:plain

MACアドレス変換方式(DSR方式)

  • 宛先MACアドレスを差し替えてロードバランシングする
    • ロードバランサとバックエンドサーバは同一セグメントに置く必要がある
    • クライアントから見たときの接続先IPアドレスはLBのIPアドレスになる
    • バックエンドサーバから見たときの接続元IPアドレスはクライアントのIPアドレスになる
  • バックエンドサーバの戻りパケットが、ロードバランサを経由せずにクライアントと直接通信する
  • レスポンスの接続元IPアドレスをロードバランサに見せかける必要がある
    • ロードバランサのIPアドレスをバックエンドサーバのループバックIPアドレスに設定する
  • ループバックIPアドレスはARPに答えないように設定する

パケットの流れ

戻りパケットは LB を経由せずに処理される。

f:id:kimulla:20191201134924j:plain

構築してみる

L7ロードバランサ

構成図は以下のとおり。

f:id:kimulla:20191201135038j:plain

今回はLBとして、nginx を利用する。

設定

リファレンスを参考に、nginxの設定をする。

http {
 ...
  upstream myapp1 {
    server 10.133.1.2;
    server 10.133.1.3;
  }
 ...
  server {
    ...
    location / {
      proxy_pass http://myapp1;
    }
  }
  ...
}

パケットキャプチャ

次に、バックエンドサーバでhttpdを起動し、クライアントからHTTPリクエストを投げる。

LBのeth0(クライアント側NIC)のパケットをキャプチャする。

# tcpdump port 80 -i eth0 -w eth0.pcap

f:id:kimulla:20191201135204j:plain

LBのeth1(バックエンド側NIC)のパケットをキャプチャする。

# tcpdump port 80 -i eth1 -w eth1.pcap

f:id:kimulla:20191201135215j:plain

上記から各ethごとにTCPハンドシェイクを行っており、TCPコネクションが2本張られていることがわかる。また、クライアントからLBにHTTPリクエストが送られた後に、LBからバックエンドサーバにTCPコネクションを張っているのがわかる。

接続元IPアドレスをバックエンドサーバに伝える

クライアントの接続元IPアドレスをバックエンドサーバが知りたい場合は、X-Forwarded-Forヘッダーをnginx側で付与すればよい。リファレンス

すると以下のように、HTTPヘッダーに接続元IPアドレスが付与される。

# tcpdump -A port 80 -i eth1
...
.*.{.).
GET / HTTP/1.0
X-Forwarded-for: 192.168.11.104
Host: myapp1
Connection: close
Cache-Control: max-age=0
...

IPアドレス変換方式(NAT方式)

構成図は以下のとおり。今回は SNAT を利用する。

f:id:kimulla:20191201135146j:plain

以下を参考に、LBとしてipvsadm を利用する。
http://dsas.blog.klab.org/archives/50664843.html

設定

LBでipvsadmの設定をする。

# ipvsadm -C
# ipvsadm -A -t 192.168.11.107:80 -s lc
# ipvsadm -a -t 192.168.11.107:80 -r 10.133.1.2 -m
# ipvsadm -a -t 192.168.11.107:80 -r 10.133.1.3 -m
# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.11.107:80 lc
  -> 10.133.1.2:80                Masq    1      0          0
  -> 10.133.1.3:80                Masq    1      0          0

次に、バックエンドサーバでhttpdを起動し、クライアントからHTTPリクエストを投げる。

パケットキャプチャ

LBのeth0(クライアント側NIC)のパケットをキャプチャする。

# tcpdump port 80 -i eth0 -w eth0.pcap

f:id:kimulla:20191201135246j:plain

LBのeth1(バックエンド側NIC)のパケットをキャプチャする。

# tcpdump port 80 -i eth1 -w eth1.pcap

f:id:kimulla:20191201135301j:plain

上記から、LBで接続元IPや接続先IPの変換はするものの、TCPコネクションが1本しか張られていないことがわかる。(接続元のエフェメラルポート番号がそのまま接続先に渡されており、また、時刻から、LBはパケットを横流ししているようになっている)

MACアドレス変換方式(DSR方式)

構成図は以下のとおり。

f:id:kimulla:20191201135314j:plain

以下を参考に、LBとしてipvsadm を利用する。
http://momijiame.tumblr.com/post/71390424576/centos-65-%E3%81%A7-lvs-ipvs-%E3%81%AE-direct-server-return

設定

LBでipvsadmの設定をする。

]# iptables -L
]# ipvsadm -A -t 192.168.11.107:80 -s lc
]# ipvsadm -a -t 192.168.11.107:80 -r 192.168.11.108:80 -g
]# ipvsadm -L
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.11.107:http lc
  -> 192.168.11.108:http          Route   1      0          0

次に、バックエンドサーバでhttpdを起動し、クライアントからHTTPリクエストを投げる。

パケットキャプチャ

LBのパケットをキャプチャする。

# tcpdump port not 22 -w lb.pcap

戻りパケットがLBを経由しないため、Wiresharkがパケットが欠けてると警告を出している。

f:id:kimulla:20191201135330j:plain

バックエンドサーバのパケットをキャプチャする。

# tcpdump port 80 -w backend.pcap

接続先IPアドレスがLBのIPアドレスになっている。また、接続元IPアドレスがクライアントのアドレスになっている。

f:id:kimulla:20191201135344j:plain

上記から、戻りパケットだけはLBを経由せずにクライアントにダイレクトに返っていることがわかる。まさにDirect Server Return…

参考