Ubuntu 20.04 で Keepalived を同一筐体で用いて冗長構成/負荷分散する

はじめに

こんにちは、WESEEK にてエンジニアをしている藤澤です。
この記事では keepalived と real server を同一筐体にのせ、 VRRP(Virtual Router Redundancy Protocol) による冗長構成, LVS による負荷分散を行う方法について解説します。

冗長構成/負荷分散を行う方法はいくつかありますが、今回は keepalived を使って実現します。

keepalived のメリット/デメリット

keepalived を使うメリットとして

  • 無料
  • LVS がカーネル空間で処理するので比較的早い。

また、デメリットとしては

  • nginx 等で行われるような L7 header による処理等ができない

などがあります

前提知識

構築の話をメインとするので概要のみ説明します。

VRRP(Virtual Router Redundancy Protocol)

VRRP はサーバやデフォルトゲートウェイ等を冗長化するためのプロトコルです。
複数台ある VRRP を適用したサーバに対して同一の仮想 IP (VIP)を割り当てることで一台のサーバのように見せかけることが出来ます。
ある 1 台のサーバが master となり VIP へのリクエストを受け付けます。他のサーバは backup となり、master、backup が相互に死活監視しあうことで master が死んだ際には backup が master に昇格することで冗長性が保たれています。

今回の構成では keepalived が持っている VRRP の機能を利用します

LVS(Linux Virtual Server)

LVS とは L4 負荷分散機能を提供するソフトウェアです。
IPVS(IP Virtual Server) という Linux カーネルモジュールで提供された機能をもとに動作するので比較的速いです。
LVS の構成として NAT と DR があります。以下ではその概要とメリット/デメリットについて述べます。
また、実際にアプリケーションがいるサーバを real server と呼びます。

NAT

VIP 宛のパケットを受け取った LVS は real server へパケットを適切に(ラウンドロビン等)送信し、real server はパケットを処理し、再び LVS へ送信します。
応答のパケットを受け取った LVS は送信元アドレスを LVS のものに変換しクライアントへ送信します。

つまり LVS が NAT パケットを転送する構成になります。

メリット

  • DR に比べて設定が楽
  • real server は外部と疎通がなくても LVS のあるサーバが疎通を持てば良い

デメリット

  • LVS へ負荷が集中する

DR

VIP 宛のパケットを受け取った LVS は real server へパケットを適切に(ラウンドロビン等)送信し、real server は直接パケットを処理してクライアントへ返します。

メリット

  • NAT と違い LVS へ返さないので LVS の負荷が低い

デメリット

  • 特に同一筐体では特殊な設定が必要

今回はクライアント、実サーバ、LVS が同一セグメントにある場合を想定し、こちらの DR の方式を採用します。

Amazon EC2 で検証

  • EC2 でインスタンスを 3 台建てる
    • スペック
      • Canonical, Ubuntu, 22.04 LTS, amd64 jammy image build on 2022-06-09
      • 64bit (x86)
      • t2.micro
      • storage: 8GiB gp2
  • VPC で同一サブネットに作成後、セキュリティグループ等で相互に通信できるようにしましょう

今回の構成

今回のシステム構成です、クライアントは VIP へリクエストすることで keepalived によって VRRP master となっているノードへリクエストが届きます。
その後 LVS によって各ノードの application へ適切に振り分けられます。

今回の記事では real server を vrrp_instance に属するノード, application を real server 上で 8000番ポートで動く http server として説明します。

注: 今回の構成では VRRP グループに属するノード内部からのリクエストは正常に処理できません。
注: application を docker で起動すると正常に振り分けが出来ない場合があります。今後時間があれば対処法を調べて追記します。

keepalived 導入

以下からは実際の設定方法と設定項目について解説します。

各ノード共通

各ノードで共通に実行してください。
ただし、keepalived.conf の priority のみ変更してください。

keepalived 関連の設定

$ sudo apt-get update
$ sudo apt-get install -y keepalived
$ keepalived --version
# インストール出来ていることを確認

ここまでで keepalived のインストールが完了

$ sudo vi /etc/keepalived/keepalived.conf
# 以下のコンフィグを入れる
# priority はノードごとに変えてください
global_defs {
    enable_script_security
}

vrrp_instance LVS_SETTINGS {
    state BACKUP # state は priority を見て自動で MASTER BACKUP 切り替えをしてくれるので全て BACKUP で指定
    interface eth0 # 受け付けるインターフェースを指定
    virtual_router_id 50 # VRRP に属するグループをユニークに指定する id
    priority 100 # Master state にするノードの優先度(ノードごとに変更してください)
    advert_int 1 # VRRP による生存確認パケットの間隔
    notify_master "/etc/keepalived/notify_master.sh" # 後述
    notify_backup "/etc/keepalived/notify_backup.sh" # 後述
    virtual_ipaddress {
        10.0.0.5 # VIP を指定してください
    }
}

include virtualserver-backup.conf # 後述
$ sudo vi /etc/keepalived/virtualserver-backup.conf
# 空で ok
  • global_defs
    • 各ノード共有の設定を入れるところ
    • enable_script_security: 今回は master/backup で設定を切り替えるスクリプトを走らせるのでセキュリティ上 root で実行出来ないようにしている
$ sudo vi /etc/keepalived/virtualserver-master.conf
# 以下のコンフィグを入れる
virtual_server 10.0.0.5 8000 { # 対象の VIP を指定
    delay_loop 6 # アプリの生存確認間隔
    lb_algo rr # 負荷分散手法
    lb_kind DR # パケット転送方式
    protocol TCP # 対象のプロトコル

    real_server 10.0.0.12 8000 {
        weight 1 # 負荷分散で使う重み
        HTTP_GET { # 生存確認のプロトコル
            url {
                path /health # 生存確認のパス
            }
            connect_timeout 10
        }
    }
    real_server 10.0.0.14 8000 {
        weight 1
        HTTP_GET {
            url {
                path /health
            }
            connect_timeout 10
        }
    }
}
$ sudo vi /etc/keepalived/notify_master.sh
# 以下のコンフィグを入れる
#!/bin/bash -u

sudo sed -i -e "s/include virtualserver-backup.conf/include virtualserver-master.conf/g" /etc/keepalived/keepalived.conf
sudo service keepalived reload

state が MASTER になったときに virtualserver-master.conf を使うようにすることで、virtualserver を適用する。

$ sudo vi /etc/keepalived/notify_backup.sh
# 以下のコンフィグを入れる
#!/bin/bash -u

RELOAD_FLG=0
grep "include virtualserver-backup.conf" /etc/keepalived/keepalived.conf > /dev/null # Normally the exit status is 0 if a line is selected, 1 if no lines were selected, and 2 if an error occurred. see: man grep
if [ $? -ne 0 ] ; then
  RELOAD_FLG=1
fi
sudo sed -i -e "s/include virtualserver-master.conf/include virtualserver-backup.conf/g" /etc/keepalived/keepalived.conf

# reload のタイミングで BACKUP state に移行して再度このスクリプトが実行されるため、無限実行を防ぐためにチェックを行う
if [ ${RELOAD_FLG} -eq 1 ] ; then
  sudo service keepalived reload
fi

state が BACKUP になったときに virtualserver-backup.conf を使うようにすることで、virtualserver の設定を削除する。

今回は同一筐体にアプリケーションと keepalived(LVS) を載せる構成になっています。その場合 backup で LVS を起動していると master の LVS によって backup へ振り分けられたパケットが再び LVS に捕まり別のノードへ振り分けられループします。
そのため、notify_master/backup (VRRP state が切り替わった際に実行されるスクリプト)によって keepalived の設定を書き換えています。

keepalived が script を実行する user を作成

master/backup 切り替え時に keepalived が script を実行するためのユーザを作成します。

$ sudo useradd keepalived_script
$ sudo visudo
# 以下を追記
# keepalived_script ALL=(ALL:ALL) NOPASSWD: ALL

スクリプトへ実行する権限をつけます。

$ cd /etc/keepalived
$ sudo chmod u+x notify_backup.sh notify_master.sh
$ sudo chown keepalived_script:keepalived_script notify_backup.sh notify_master.sh

DR 用の設定

同一筐体で DR を使う場合いくつかネットワークの設定が必要になります。

sysctl.conf

sudo vi /etc/sysctl.d/98-keepalived.conf
# ファイル名はアルファベット順に読み込まれるのでそこまで気にしなくても大丈夫です
# 以下の設定を入れる
# Do not rename this file so it is run at the end of /etc/sysctl.d/*.conf files load.
# Run `sysctl -p` to apply

# パケットを転送するために IP フォーワードを許可
net.ipv4.ip_forward = 1

# ARP リクエストが eth0 に来たときに eth0 に vip が設定されていなかったとしても、
# lo に vip が設定されていると ARP レスポンスを返してしまうため、
# それを防ぐために eth0 に設定されていないアドレスに対する ARP リクエストには応答しないようにしている
# see: https://www.valinux.co.jp/technologylibrary/document/linux/arp0001/
net.ipv4.conf.eth0.arp_ignore = 1
net.ipv4.conf.eth0.arp_announce = 2

# LVS 間での通信とクライアントとの通信で異なる NIC を使用する場合は rp_filter を無効化(0)に
# net.ipv4.conf.eth0.rp_filter = 0
$ sudo sysctl -p
# 適用する

lo に VIP をつける

DR を同一筐体で用いる場合、real server が VIP 宛の通信を処理する必要があります。
STATE が MASTER である real server は keepalived によって NIC に VIP が割り当てられるのですが、STATE が BACKUP である real server では NIC に VIP が割り当てられないので、今回は lo に VIP を割り当てることで VIP 宛のパケットを処理できるようにします。

$ sudo vi /etc/netplan/99_lo_config.yaml
# 以下のコンフィグを入れる
# This config is used for applying vip to lo interface.
# By this, each real server is able to use vip as its own ip address.
# Run `netplan apply` to apply
network:
  version: 2
  renderer: networkd
  ethernets:
    lo:
      addresses:
      - 10.0.0.5/32
$ sudo netplan apply
# 適用する

以下のように lo に VIP (10.0.0.5) がついていることが確認できれば ok です。

ubuntu@ip-10-0-0-12:~$ ip a show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet 10.0.0.5/32 scope global lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever

検証

最初に keepalived を再起動してください。

$ sudo systemctl restart keepalived
# keepalived の再起動

適当なアプリケーションを 8000 番で起動

本題とは外れるので詳しくは説明しませんが、参考までに今回使用したサンプルを載せておきます。

test-webserver.rb

require 'webrick'

srv = WEBrick::HTTPServer.new(
  DocumentRoot: './',
  BindAddress: "0.0.0.0",
  Port: 8000,
)

srv.mount('/', WEBrick::HTTPServlet::FileHandler, 'index.html')
srv.start

index.html

hello world (real server (区別するためにノードごとに別の文字を入れてください))

上記を ruby で実行

まず設定がうまくいっているかどうか確認

$ sudo journalctl -u keepalived

Entering MASTER/BACKUP STATE
のような記述があれば ok です。

VIP 宛の通信が冗長/負荷分散されていることを確認する

検証用のクライアントから VIP へ向けて curl し、負荷分散されていることを確認する

$ curl http://10.0.0.5:8000/
hello world(real server 1)
$ curl http://10.0.0.5:8000/
hello world(real server 2)
$ curl http://10.0.0.5:8000/
hello world(real server 1)
$ curl http://10.0.0.5:8000/
hello world(real server 2)

以上のように交互に real server へ送られることが確認できれば ok です。

real server のアプリケーションを落とし、冗長化されていることを確認する

real server でアプリケーションを落として、前項目同様 curl を実行します。

生きている方の real server の通信のみが返ってくれば ok です。