NGINXによるgRPC通信のロードバラシング

はじめに

技術部基盤グループの高橋です。2回目の投稿です。 表題にある通りNGINX での gRPC 通信の負荷分散について実装したのでご紹介です。

NGINXとは

NGINX(「エンジンエックス」のように発音[3])は、フリーかつオープンソースなWebサーバである。 処理性能・高い並行性・メモリ使用量の小ささに焦点を当てて開発されており、HTTP, HTTPS, SMTP, POP3, IMAPのリバースプロキシの機能や、ロードバランサ、HTTPキャッシュなどの機能も持つ。 1.13.10以降、gRPC通信のロードバランシングが可能となっている。

https://www.nginx.com/blog/nginx-1-13-10-grpc/

Consulとは

APIやDNS Interfaceを通じてサービスディスカバリ機能を提供する。 VaultのStorageとしても動作する。 こちらの詳しい説明は別の機会に。

背景

とある案件でサーバ間通信にgRPCの採用が決まり、Dockerなどのコンテナは使わず従来のWindows系(.net)アプリケーションの資産を流用するということで、サービスメッシュではなくアプリケーション間にロードバランサーを配置したClient/Server構成を整える事になりました。
従来のオンプレミス環境における負荷分散は、主にF5社のBIGIP(LTM)もしくは、Microsoft NLBを利用していました。前者は単独でのHTTP/2の負荷分散実装が難しいこと、後者はL4ロードバランサーでありHTTP/2での負荷分散による偏りの発生に懸念が生じたため、今回L7ロードバランサーとしてNGINXを採用しました。HAProxyやTraefikといった選択肢も有るようですが、採用例・事例等が豊富なNGINXを選定しています。 なお、無償版NGINXです。

要件

今回、アプリケーションから提示された前提は以下でした。

  • ClientはIIS(ASP.NET)アプリケーション、ServerはWindows Service(C#)
  • 非コンテナであるためサービスメッシュは不要。固定IP・ポートでの負荷分散
  • gRPC の通信方式は Unary RPC
  • 秒間最大1,300リクエスト。レイテンシ3秒以内。

全体構成

項目 内容
構成 NGINX 3台構成
スペック 2vCPU , 4GiB
負荷分散方式 ラウンドロビン
冗長化 Act/Act構成 , Consulによるサービスディスカバリ

f:id:UT__TA:20201121165842p:plain
全体概要図

NGINXの冗長化

  • NGINXはコンテナ化せず、ホストOSに直接インストールし稼働しています。
  • HashiCorpのConsulを利用したサービス化を行い、Clientはサービス名(.service.consul)で接続し、生存している何れかのNGINXに着信します。
  • 有償版NGINXではHA構成を取ることが出来るようですが、今回無償版ということでConsulでの冗長化としています。
  • Consulによる冗長化はDNSラウンドロビンでもあるためある程度の偏りは発生する前提として台数、スペックを検討する必要があります。

f:id:UT__TA:20201121211055p:plain
Consulによるサービス化

NGINXのサーバヘルスチェック

  • 無償版NGINXにはパッシブヘルスチェックが実装されており、下位サーバへのリクエスト転送の失敗とその回数を検知し、自動的に切り離しを行います。
  • 有償版NGINXにはアクティブヘルスチェックが実装されており、擬似的なリクエストを発行しヘルスチェックを行えるため、より下位サーバの障害の検知精度・速度が良いと思われるが、アクセス頻度が高い場合、障害検知から切り離しまでのリクエスト失敗を完全に防ぐことは、何れの場合も困難です。
  • パッシブヘイルスチェックには「fail_timeout」と「max_fails」というパラメータで障害検知と切り離しの制御を行う。今回はデフォルト値を採用します。

f:id:UT__TA:20201121165943p:plain
下位サーバのパッシブヘルスチェック

ヘルスチェックの概要

種類 概要
パッシブヘルスチェック 実際のリクエストの失敗とその回数を検知し、下位サーバの切り離しを自動的に行う。失敗の例として下位サーバへのリクエストタイムアウトが考えられるが、タイムアウト値は下位サーバ上のアプリケーションの仕様に準ずる。(nginx側で打ち切らはない)。
アクティブヘルスチェック 疑似的なリクエストを発行し失敗とその回数を検知し、下位サーバの切り離しを自動的に行う。

パラメータの概要

パラメータ 説明
fail_timeout 切り離されたサーバが利用できないとみなす期間。切り離された場合、当該値の秒数はリクエストが送信されない。デフォルトは10秒
max_fails 障害と判定するリクエスト失敗の回数。デフォルトは1回

続いてセットアップに進みます。

インストール

公式リポジトリの追加

今回su -で作業を実施しています。(Linuxお作法が素人ですみません。。) yum.repos.dにnginx.repoを作成します。

# touch /etc/yum.repos.d/nginx.repo
# vi /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1

# ls -l /etc/yum.repos.d/
total 40
-rw-r--r--. 1 root root 1664 Sep  5  2019 CentOS-Base.repo
-rw-r--r--. 1 root root 1309 Sep  5  2019 CentOS-CR.repo
-rw-r--r--. 1 root root  649 Sep  5  2019 CentOS-Debuginfo.repo
-rw-r--r--. 1 root root  314 Sep  5  2019 CentOS-fasttrack.repo
-rw-r--r--. 1 root root  630 Sep  5  2019 CentOS-Media.repo
-rw-r--r--. 1 root root 1331 Sep  5  2019 CentOS-Sources.repo
-rw-r--r--. 1 root root 6639 Sep  5  2019 CentOS-Vault.repo
-rw-r--r--  1 root root 2424 Oct 18  2019 docker-ce.repo
-rw-r--r--  1 root root   98 Aug 20 05:33 nginx.repo
~

バージョン確認

今回、バージョンは1.18.0を採用しています。

# yum info nginx
Loaded plugins: fastestmirror
Determining fastest mirrors
 * base: d36uatko69830t.cloudfront.net
 * extras: d36uatko69830t.cloudfront.net
 * updates: d36uatko69830t.cloudfront.net
base                                                     | 3.6 kB     00:00
docker-ce-stable                                         | 3.5 kB     00:00
extras                                                   | 2.9 kB     00:00
nginx                                                    | 2.9 kB     00:00
updates                                                  | 2.9 kB     00:00
(1/3): extras/7/x86_64/primary_db                          | 206 kB   00:00
(2/3): updates/7/x86_64/primary_db                         | 3.8 MB   00:00
(3/3): nginx/x86_64/primary_db                             |  55 kB   00:01
Available Packages
Name        : nginx
Arch        : x86_64
Epoch       : 1
Version     : 1.18.0
Release     : 1.el7.ngx
Size        : 772 k
Repo        : nginx/x86_64
Summary     : High performance web server
URL         : http://nginx.org/
License     : 2-clause BSD-like license
Description : nginx [engine x] is an HTTP and reverse proxy server, as well as
            : a mail proxy server.

NGINXインストール

yum install nginx
~~~~
Installed:
  nginx.x86_64 1:1.18.0-1.el7.ngx
Complete!

自動起動設定

コンピュータ再起動時に自動的にNginxが起動するように変更します。

# systemctl enable nginx
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.

テストアクセス

一度起動し、テストアクセスしてみます。「Welcome to nginx!」というページが開けばインストール成功です。

# systemctl start nginx

f:id:UT__TA:20201121171732p:plain
NGINXテストページ

一度停止します

# systemctl stop nginx

LogFormatに処理時間と処理データサイズを追加する

初期ログフォーマットではnginx自身の処理時間がログに出力されないため、log_format main に $request_length と $request_time のログフィールドを追加します。

vi /etc/nginx/nginx.conf
~~~
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" '
                      '$request_length $request_time';

パフォーマンス・チューニング

今回、それほど非機能要件の要求が高くないこともありますが、基本的なチューニングのみとしています。

# vi /etc/nginx/nginx.conf

worker_processes  auto;
worker_rlimit_nofile 150000;
~~
events {
    worker_connections  65535;
    multi_accept on;
    use epoll;
}

gRPCの負荷分散設定

NGINXによるgRPC通信のリクエストは、grpc_pass を設定することで実現できます。また、転送先についてはIP/Portが固定ということも有り、直接Configに記載しています。

vi /etc/nginx/conf.d/default.conf

upstream backend-api {
    # backend server
    server 転送先IP:転送先ポート;
    server IP:port;
    server IP:port;
    server IP:port;
    server IP:port;
 ・・・追記する
}
server {
    listen [NGINXがリッスンするポート番号] http2;
    location / {
        grpc_pass grpc://backend-api;
        grpc_set_header X-Real-IP $remote_addr;
    }
}

Consulによるサービス化

Consulの細かい説明は省きます。Consul Client がセットアップサ¥されている前提のため、ConsulAgentにNGINX自体をサービス化するための設定のみ記載します。 簡単に言うと、NGINXをインストールしたサーバにConsulを導入し、自身の80ポートへのヘルスチェックを設定します。 Consulのヘルスチェックに合格すると、登録した名前(例だと backend-api.service.consul)でDNS名前解決ができるようになります。 Consulのヘイルチェックに不合格(障害)の場合、DNS名前解決で当該サーバのIPアドレスが解決できなくなることでサービスディスカバリによるNGINXの冗長化を実現しています。

# vi /etc/consul.d/consul.hcl

~~~
service {
    name = "backend-api"
    port = 80
    check = [
        {
            id = "backend-api-health"
            http = "http://localhost:80/"
            method = "GET"
            interval = "10s"
            timeout = "3s"
        }
    ]
}
~~~

NGINXのサーバヘルスチェック試験

パッシブヘルスチェックの動作がいまいちイメージ出来なかったため、下位サーバの障害発生による負荷分散の挙動確認のため、テストプログラムを用意して以下のような試験を実施しました。

切り離し条件

  • 今回、デフォルト値を採用。
  • nginxの下位サーバからの応答失敗が1回発生した場合切り離す。
  • 切り離し他サーバは10秒間リクエストを送信しない。

テスト環境

SeverとClientと、負荷リクエストを発生させるJMeter、NGINXの構成です。

役割 説明
JMeterサーバ 1台あたり2req/serc の負荷を3分間(180秒)発生させる(ラウンドロビンにより大凡均等に分散)
gRPC - Webサーバ JMeterからリクエストを受付るサーバ。BE(バックエンド)サーバとgrpc通信を行う
gRPC - backendサーバ (4台) gRPC通信受信し、自分のIPアドレスを返却する処理を行う。
NGINX Webサーバとbackendサーバの間でgRPC通信を中継する proxyサーバ。

f:id:UT__TA:20201121211448p:plain
パッシブヘルスチェックテスト構成

シナリオ

  • 1BEサーバあたり2req/secの負荷を180秒間発生させる
  • 開始30秒後にBE3号機を停止する。
  • 開始1分後にBE4号機を停止する。
  • 開始1分30秒後にBE3号機を起動する。
  • 開始2分後にBE4号機を起動する。

結果

  • 停止時に停止サーバで処理中のリクエストは502エラーがクライアント(jmeter)へ応答された。
  • 次のリクエストからは、オンライン中のBEサーバに処理が分散された。
  • 想定とことなり、デフォルト10秒の切り離し時間を超えて、停止したBEサーバに処理が振り分けられてないように見える。

最後の挙動がfail_timeout=10秒の設定と若干食い違う気がしますが、結果的にリクエストがエラーになっていないことから、テストアプリケーションのタイムアウト内に、存命する他のサーバに振り分けられたためと考えれば、辻褄が合いそうです。

各BEサーバの処理件数の推移は以下

f:id:UT__TA:20201121185700p:plain
テスト時系列

NGINXの処理性能試験

スペック選定のため以下簡単な性能試験を実施しました。結果、t3.medium(2vCPU / 4GiB)で、今回の応答スループットであれば、難なくこなせることがわかりました。 使用したアプリケーションは、サーバヘルスチェック試験で利用したものを利用します。

テスト環境

  • サーバヘルスチェック試験で利用した構成と同とします。

シナリオ

  • 非機能要件の数値に対し、3倍の秒間3,600リクエストを100秒間発生させ続ける。
  • NGINXサーバのスペックを3段階用意し、t3.medium(2vCPU/4GiB) → t3.xlarge(4vCPU/16GiB) → t3.2xlarge(8vCPU/32GiB)の順に実施。 徐々にスペックを上げていく。
  • 偏りの発生を前提にNGINXサーバのシステムリソースの使用率・性能は上限はCPU使用率40%以下、応答時間100msec以内とする。

結果

t3.medium(2vCPU/4GiB)の時点で、テスト中ののCPU使用率平均6% , RT平均15msec程度で処理可能であることを確認した。

項目 平均 最大
CPU使用率(%) 5.51 26.00
メモリ使用率(GiB) 1.11 1.15
応答時間(msec) 15.20 470.87

ボツ案-DNSラウンドロビンでのbackendserviceの負荷分散

当初、NGINXを利用せず、backend service を直接Consulでサービス化し、DNSラウンドロビンによる水平負荷分散を行うことを検討しました。が偏りが激しくボツ。

f:id:UT__TA:20201121205747p:plain
ボツ案

最後に

いかがでしたでしょうか?すでに、Container周りのサービスメッシュにおけるサイドカープロキシなどでも利用があるNGINXということで、特に実装に関して不安はありませんでしたが、若干出力ログ項目が少ないように感じます。(振り分け先のIPアドレス等が出力できるとトレースしやすいと感じましたが、設定項目がなさそうでした。) もともとMicrosoft系中心としたインフラ構築をしていたため、Linux系・OSS系に触れられ非常に楽しく実装ができました。

auカブコム証券では一緒に働く仲間を募集しています。 私が所属している システム技術部 基盤グループでは、システム基盤のモダナイゼーションをミッションとし、走攻守を掲げ、「走:スピードに拘る」、「攻:最新技術に拘る」、「守:品質に拘る」ことをモットーに、オンプレミス・クラウド両基盤を提供しています。

採用についてはこちら auカブコム証券株式会社の会社情報 - Wantedly