安全な振り分けの備忘録 レイヤ4スイッチからサービス機を外す

はじめに

レイヤ4(L4)スイッチで負荷分散設定をしている環境では、サービス機をなんらかの理由で振り分け対象から外したいことがあるでしょう。ヘルスチェックの仕組みとサービス機を振り分けから外すための留意点をまとめました。

L4スイッチからサービスのヘルスチェック(health check)

Port ヘルスチェック(L4)方式

  • OSI参照モデルでレイヤ4に属するTCPの接続方式(SYN / ACK / RST)を使用してヘルスチェックする方式です。

Port ヘルスチェックの種類

  • Half-Open方式
    (左がL4スイッチ、右側がサーバー)
  • TCP Open方式
    (左がL4スイッチ、右がサーバー)

L7 ヘルスチェック方式

  • HTTPでL7 URLを読み込み、レスポンスのステータスおよび本文データ(ボディ)を検証し、ヘルスチェックする方式です。(レスポンスボディ側の検証は必須ではありません。)
  • L4スイッチといえども、多くのスイッチがレイヤ7(L7)のアプリケーションレベルのヘルスチェックに対応しています。
  • ヘルスチェックOK状態フロー(下記フローのうち、1つでもエラーがあれば、サービスDOWN状態と判断し、チェックはNGとなります。)

L4スイッチからサービスを除外させる

  • 当然ですが、L4スイッチがヘルスチェック時にサービスがDOWNしているとして認識させましょう。
  • しかし、サービス機のヘルスチェックの結果がNGとなっても、すぐにはL4からユーザーの流入がなくなるわけではないため、実際にユーザー流入が発生していないか確認が必要です。
    (L4スイッチのヘルスチェック方式はポーリング監視のため、ヘルスチェック周期があり、数秒間は新規ユーザーの流入が発生します。)

L4スイッチからサービスを除外させる

Port ヘルスチェック方式

  • サービスサーバーからL4 ヘルスチェック IPへRST(リセット)を送信するように拒否(REJECT)設定する、もしくはDROPします。
  • 例:iptablesでのDROP設定
#!/bin/bash
L4_HEALTHCHECK_IPS=("10.0.0.252" "10.0.0.253")
PORT=80

for i in ${L4_HEALTHCHECK_IPS[@]}
do
sudo iptables -A INPUT -s $i -p tcp --dport $PORT -j DROP #DISABLE HEALTH CHECK, $L4_HEALTHCHECK_IPSから$PORTで入ってくる要請をDROPするファイアウォールのルール追加
# sudo iptables -D INPUT -s $i -p tcp --dport $PORT -j DROP #ENABLE HEALTH CHECK, 上記ファイアウォールのルールを削除
done
  • L4スイッチのヘルスチェックIP(= L4スイッチがヘルスチェックを試すときに使用するIP)は、サーバーでtcpdumpから定期的に接続を要求しているIPアドレスを探します。(または、ネットワーク担当者にお問い合わせいただく方法もあります。)

L7 ヘルスチェック方式

  • レスポンス ステータスコード 200(OK)以外で(たとえば 404で)送信するように設定することで、チェックNGとなりバランス対象から除外できます。
  • つまり監視用に設定しているサービスURLにアクセスしたら「File Not Found」などのエラーとなるように、コンテンツ側で準備しておくなどです。

ユーザーの流入がないことを検証する

その前に、TCP コネクションステータスについて簡単に説明します。

TCP コネクションステータス

主要な部分だけを説明します。

  • LISTEN:サーバーのポートがオープンされ、ユーザーの流入を待っている状態
  • ESTABLISHED:サーバーとユーザー間でコネクションが結ばれ、実際の通信が行われる状態
  • CLOSE_WAIT:サーバー側の状態で、(データをすべて受信した)ユーザーが送信した接続解除要求(FIN)を受信した状態
  • TIME_WAIT:ユーザー側の状態でサーバーからFIN(接続終了)を受け取った状態
  • 参考 – TCP ステータス変遷図 ( TCP state transition diagram )(出典:IBM Knowledge Center

ユーザー流入の検証

  • ユーザーが流入して、実際の通信が発生すると、ESTABLISHEDになります。
  • したがって、ユーザーの流入がないということは、TCP ESTABLISHED ステータスが新たに発生していない状態です。
#!/bin/bash

SERVICE_PORT=443
L4_HEALTH_CHECK_IPS=("10.0.0.253" "10.0.0.254")

grep_cond_exclude_ips=$(IFS='|'; echo "${L4_HEALTH_CHECK_IPS[*]}") #(L7 health check方式で) L4 health check IPを除く
if [ $(sudo netstat --tcp -np | grep ":$SERVICE_PORT " | grep -vE "$grep_cond_exclude_ips" | grep -i established | wc -l) -gt 0 ];
then
    echo "User connection still exist" >&2
    exit 1 #exit with error
else
    echo "NO connection"
    exit 0
fi
  • (L7 ヘルスチェックの場合に)L4のヘルスチェックIP(= L4スイッチがヘルスチェックを試すときに使用するIP)とのESTABLISHED は除外しました。
  • サービスのヘルスチェックがNGと判断されてもL4は継続的にヘルスチェックをしようとするので、L4のヘルスチェック IPに引き続きESTABLISHED コネクションが生じるからです。
  • L4 ヘルスチェックIPはapache のaccess logから見つけました。(繰り返しヘルスチェック用ページへリクエストしてくるIP)

仕上げ – 安全にサービスを終わらせる

  • 上記の内容をまとめると、安全にサービスを終わらせるには、 たとえばサービスしているプロセスがTomcatの場合、次の手順が必要です。
    • サービスのL4 / L7 ヘルスチェックをNGとなるよう設定調整(=ロードバランス対象から外させる)
    • ユーザー流入がないことを検証(新しいユーザーが流入する恐れがあるので、時間をおいて繰り返し検証が必要)
    • Tomcatをstop

例 – L7 ヘルスチェック up / down

#!/bin/bash

ESTB_CHECK_MAX=3
RETRY_MAX=5
SLEEP_TIME=3 #second

SERVICE_PORTS=("80" "443")
L4_ENABLE_URL="http://127.0.0.1/actuator/health/up"
L4_DISABLE_URL="http://127.0.0.1/actuator/health/down"
SERVICE_CHECK_URL="http://127.0.0.1/actuator/health"

L4_HEALTH_CHECK_IPS=("10.0.0.253" "10.0.0.254")

func_disable_l7()
{
    curl -XPUT $L4_DISABLE_URL
}

func_enable_l7_repeat()
{
    retry=0
    while [ $retry -lt $RETRY_MAX ]
    do
        curl -XPUT $L4_ENABLE_URL # Enable health check
        sleep $SLEEP_TIME
        [[$(curl -LI $SERVICE_CHECK_URL -o /dev/null -w '%{http_code}' -s | grep 200 | wc -l) -eq 1]] && break
        ((retry++))
        echo "Service is down, retrying..."
    done

    if [ $retry == $RETRY_MAX ];
    then
        echo "FAILED to enable L7 healthcheck" >&2
        exit 1
    else
        echo "SUCCESS to start"
        exit 0
    fi
}

func_check_no_conn_repeat()
{
    estb_check_count=0
    retry_count=0
    grep_cond_ports=$(printf ":%s |" ${SERVICE_PORTS[*]} | head -c -1)
    grep_cond_exclude_ips=$(IFS='|'; echo "${L4_HEALTH_CHECK_IPS[*]}") #exclude; because L4 request continuously regardless of health status
    while [ $estb_check_count -lt $ESTB_CHECK_MAX -a $retry_count -lt $RETRY_MAX ]
    do
        sleep $SLEEP_TIME
        echo "Checking established connection..."
        conn=$(netstat -nt | grep -E "$grep_cond_ports" | grep -vE "$grep_cond_exclude_ips" | grep ESTABLISHED)
        if [ $(echo $conn | wc -l) -gt 0 ];
        then
            echo "connection exist"
            echo -e "$conn"
            estb_check_count=0
            ((retry_count++))
        else
            echo "NO connection"
            ((estb_check_count++))
        fi
    done

    if [ $retry_count == $RETRY_MAX ];
    then
        echo "User connection still exist" >&2
        exit 1 #exit with error
    else
        exit 0
    fi
}



case "$1" in
        down)
                func_disable_l7
                sleep 10
                func_check_no_conn_repeat
                ;;
        up)
                func_enable_l7_repeat
                ;;
        *)
                echo $"Usage: $0 {down|up}"
esac

まとめ

ループバック(ex. lo:0)インターフェースを落とさなくても、L4スイッチからサービスを除外することができます。

  • 最近はL7 ヘルスチェック方式がよく使用されていますが、以前のポートヘルスチェック方式では、ループバックインタフェースを落とすことがたまにありました。DSR(Direct Server Return)方式でループバックインタフェースを落とした場合、クライアント側でパケットをドロップするため、リクエストの失敗が発生する可能性があります。
  • (クライアントがL4 VIPに送信すると、ループバックがないサーバーでは、サーバーのローカルIPでクライアントにレスポンスパケットを送信するはず。そうなると、クライアントは混乱に陥り、残りのパケットをDROPしてしまう。)

パケット損失を最小限にするには、ESTABLISHEDされたTCP コネクションまで確認する必要があります。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop