Neinvalli

研究テーマは ”IT 環境でのリソース最適化” です

日本語 / 英語 両方に最適化した IT エンジニア向けキー配列 Astarte を作りました


キー配列 Astarte

キー配列 Astarte

Keyboard Layout Analyzer にて英語/日本語で解析した際のヒートマップです。
青から赤になるにつれて使用頻度が高くなります。

アルファベット以外のキーは無視してください。
(特にホームポジション付近の記号については仮で配置してあるだけです)

ヒートマップではないほうの画像も貼っておきます。

キー配列 Astarte


メジャーな配列とのスコア比較 (英語 / 日本語 混在)

Keyboard Layout Analyzer にてメジャーな配列とのスコア比較した結果です。

  • 対象テキストは 20-30 ほどのサイトから 10-15 種類程度の分野からピックアップしました。
  • 英語はそのまま使用、日本語はヘボン式ローマ字へ変換して使用しています。
  • 割合はほぼ半分で合計 36 万文字程度。

The optimal layout score is based on a weighed calculation that factors in the distance your fingers moved (33%), how often you use particular fingers (33%), and how often you switch fingers and hands while typing (34%).

スコアの詳細については上記のとおりです。

  • 指の移動量
  • 特定の指の使用頻度
  • タイプ中の指や手のスイッチ具合

でスコアが決まるようです。

詳細は下記の記事が詳しいです。

Keyboard Layout Analyzerを使ったキー配列12種類の比較


Astarte 配列の特徴

  • 日本語/ 英語 両方に最適化
  • プログラミングのキーワードやサーバーでのコマンド等の文字列に対しても最適化
  • 指の移動距離を最小化
  • 同じ指の連続使用頻度を最小化
  • 左右の手の交互打鍵頻度を最大化
  • ls が打ちづらくない

Astarte 配列を作成した経緯

1. Dvorak 配列への移行の挫折

おそらくこのページを見ている人は一度は Qwerty から Dvorak や Colemak に乗り換えることを考えたことがあるのではないでしょうか?
乗り換えがうまくいった人や何らかの理由で Qwerty に引き返した人がいるかと思います。 私も 8年ほど前に一度 Dvorak へ乗り換えようと試したことがあり、後者のパターンで Qwerty に引き返して来ました。
仕事で IT エンジニアをしており、仕事上 Linux のサーバーの操作やプログラミングをする機会が多かったため、年末年始の休みを利用して Dvorak 配列に慣れておき年明けに一気に乗り換える作戦でした。
ただ、これはうまくいかず、結局 Qwerty に引き返すことにしました。 直接の引き金は「MySQL で急ぎの作業があった時に "show processlist" とすぐにタイプ出来なかった」ことです。
他にも下記のような不安要素もあり、直感的に Qwerty に戻ることを決めました。

  • Dvorak は英語には最適化されているが日本語に対してはどこまで有効か判断出来なかった
  • 使っているソフトのキーバインド(特にテキストエディタ)をうまく変更できる確信がなかった
  • コピー&ペーストのようないろんなソフトで使われるキーバインドが変わることに対しての対応方法の見込みがなかった
  • シェルコマンド等は Qwerty でそれほで打ちづらくないような名前が選ばれる傾向がある気がしていた

なんとも苦い経験ですが似たような理由で Dvorak への移行を見送った人は他にもいるのではないかと思っています(特に "ls" が打ちづらい、というのはよく言われる話かと思います)

2. Keyboard Layout Analyzer, MTGAP 配列との出会い

Dvorak への移行の挫折から 8年ほどたった去年の秋、下記の記事で Keyboard Layout Analyzer, MTGAP 配列 のことを知りました。

https://qiita.com/ZeptByteS/items/6e6a3e46552dcb105948

MTGAP についてはプログラムで自動生成されたもので、Dvorak, Colemak 等のメジャーな配列に対して圧倒的なスコアを出していたことがとても印象的だったのを覚えています。

3. どうせなら確信を持って新しい配列を試したい

MTGAP でも Dvorak の時のような不安がつきまとうことに変わりはありません。 どうせなら移行するなら、確信を持って移行ができる自分で作成した配列にしようと思い立ち、私自身、AI 関連のプログラムを作った経験があったため、英語と日本語両方に最適化されたキー配列を作成することを決意しました。
Dvorak の ls のようにならないように、プログラミング言語SQL 等で出てくるキーワードもある程度重視するようにしました。

※ Astarte 以外だと ゆかり さんによる Eucalyn配列 があります。Astarte と同様の特徴と「ZXCV のキーはそのまま」「HJKL のキーはあまり動かさない」が特徴です。


キー配列の移行についての注意点

Astarte 配列のアルファベット部分は去年末に完成していたのですが、他にも考慮点が複数あったため Astarte への移行に時間がかかってしまいましたがようやく私自身 Astarte への移行が終わりました。
現状、キー配列は Astarte のみで過ごしています。
キー配列を移行した経験から、注意点がいくつか見えて来ましたので今後の記事で注意点やキー配列について言及していきたいと考えています。


最後に英語・日本語それぞれ単体の時のスコア比較とヒートマップを載せておきます。

メジャーな配列とのスコア比較とヒートマップ (英語)


メジャーな配列とのスコア比較とヒートマップ (日本語)


Keyboard Layout Analyzer で使える Astarte の json ファイルです。 気になる方は Keyboard Layout Analyzer にていろいろ配列やいろいろなテキストでスコア比較してみてください。

Astarte の json (クリックで開く)

{
    "label": "Astarte",
    "fingerStart": {
        "1": 29,
        "2": 30,
        "3": 31,
        "4": 32,
        "5": 56,
        "6": 56,
        "7": 35,
        "8": 36,
        "9": 37,
        "10": 38,
        "11": 56
    },
    "keyboardType": "standard",
    "author": "Neinvalli",
    "authorUrl": "http://neinvalli.hatenablog.com/",
    "moreInfoUrl": "http://neinvalli.hatenablog.com/",
    "moreInfoText": "A keylayout optimized for both of English and Japanese, and some of programming keywords",
    "keys": [
        {
            "primary": 96,
            "shift": 126,
            "finger": 1,
            "id": 0
        },
        {
            "primary": 49,
            "shift": 33,
            "finger": 1,
            "id": 1
        },
        {
            "primary": 50,
            "shift": 64,
            "finger": 2,
            "id": 2
        },
        {
            "primary": 51,
            "shift": 35,
            "finger": 3,
            "id": 3
        },
        {
            "primary": 52,
            "shift": 36,
            "finger": 4,
            "id": 4
        },
        {
            "primary": 53,
            "shift": 37,
            "finger": 4,
            "id": 5
        },
        {
            "primary": 54,
            "shift": 94,
            "finger": 7,
            "id": 6
        },
        {
            "primary": 55,
            "shift": 38,
            "finger": 7,
            "id": 7
        },
        {
            "primary": 56,
            "shift": 42,
            "finger": 8,
            "id": 8
        },
        {
            "primary": 57,
            "shift": 40,
            "finger": 9,
            "id": 9
        },
        {
            "primary": 48,
            "shift": 41,
            "finger": 10,
            "id": 10
        },
        {
            "primary": 59,
            "shift": 58,
            "finger": 10,
            "id": 11,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 61,
            "shift": 43,
            "finger": 10,
            "id": 12
        },
        {
            "primary": 8,
            "finger": 10,
            "id": 13
        },
        {
            "primary": 9,
            "finger": 1,
            "id": 14
        },
        {
            "primary": 113,
            "shift": 81,
            "finger": 1,
            "id": 15,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 112,
            "shift": 80,
            "finger": 2,
            "id": 16,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 117,
            "shift": 85,
            "finger": 3,
            "id": 17,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 121,
            "shift": 89,
            "finger": 4,
            "id": 18,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 44,
            "shift": 60,
            "finger": 4,
            "id": 19,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 106,
            "shift": 74,
            "finger": 7,
            "id": 20,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 100,
            "shift": 68,
            "finger": 7,
            "id": 21,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 104,
            "shift": 72,
            "finger": 8,
            "id": 22,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 103,
            "shift": 71,
            "finger": 9,
            "id": 23,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 119,
            "shift": 87,
            "finger": 10,
            "id": 24,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 91,
            "shift": 123,
            "finger": 10,
            "id": 25
        },
        {
            "primary": 93,
            "shift": 125,
            "finger": 10,
            "id": 26
        },
        {
            "primary": 92,
            "shift": 124,
            "finger": 10,
            "id": 27
        },
        {
            "primary": 20,
            "finger": 1,
            "id": 28
        },
        {
            "primary": 105,
            "shift": 73,
            "finger": 1,
            "id": 29,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 111,
            "shift": 79,
            "finger": 2,
            "id": 30,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 101,
            "shift": 69,
            "finger": 3,
            "id": 31,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 97,
            "shift": 65,
            "finger": 4,
            "id": 32,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 46,
            "shift": 62,
            "finger": 4,
            "id": 33,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 107,
            "shift": 75,
            "finger": 7,
            "id": 34,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 116,
            "shift": 84,
            "finger": 7,
            "id": 35,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 110,
            "shift": 78,
            "finger": 8,
            "id": 36,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 115,
            "shift": 83,
            "finger": 9,
            "id": 37,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 114,
            "shift": 82,
            "finger": 10,
            "id": 38,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 39,
            "shift": 34,
            "finger": 10,
            "id": 39
        },
        {
            "primary": 13,
            "finger": 10,
            "id": 40
        },
        {
            "primary": 16,
            "finger": 1,
            "id": 41
        },
        {
            "primary": 122,
            "shift": 90,
            "finger": 1,
            "id": 42,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 120,
            "shift": 88,
            "finger": 2,
            "id": 43,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 45,
            "shift": 95,
            "finger": 3,
            "id": 44,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 99,
            "shift": 67,
            "finger": 4,
            "id": 45,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 47,
            "shift": 63,
            "finger": 4,
            "id": 46,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 109,
            "shift": 77,
            "finger": 7,
            "id": 47,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 108,
            "shift": 76,
            "finger": 7,
            "id": 48,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 102,
            "shift": 70,
            "finger": 8,
            "id": 49,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 98,
            "shift": 66,
            "finger": 9,
            "id": 50,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": 118,
            "shift": 86,
            "finger": 10,
            "id": 51,
            "altGr": -1,
            "shiftAltGr": -1
        },
        {
            "primary": -16,
            "finger": 10,
            "id": 52
        },
        {
            "primary": 17,
            "finger": 5,
            "id": 53
        },
        {
            "primary": -91,
            "finger": 5,
            "id": 54
        },
        {
            "primary": 18,
            "finger": 5,
            "id": 55
        },
        {
            "primary": 32,
            "finger": 5,
            "id": 56
        },
        {
            "primary": -18,
            "finger": 6,
            "id": 57
        },
        {
            "primary": -91,
            "finger": 6,
            "id": 58
        },
        {
            "primary": -93,
            "finger": 6,
            "id": 59
        },
        {
            "primary": 17,
            "finger": 6,
            "id": 60
        }
    ]
}

Prometheus, Grafana を利用して MySQL の各種パフォーマンスをモニタリングする

前回(http://neinvalli.hatenablog.com/entry/2017/10/31/020644)は Munin についての話でしたが、今回は Prometheus についての話です。

  • Prometheus + Grafana では 15秒ごとのグラフが簡単に作成出来ますので Munin と比較してこの点が強みです。
  • 前回述べたように、5分ごとのグラフだとどうしても瞬発的な現象を捕捉しきれないからです。

Prometheus のインストール関連の情報は下記サイトを参考にしました。

https://knowledge.sakura.ad.jp/11633/

http://mmorita44.hateblo.jp/entry/2017/08/24/123438

http://d.hatena.ne.jp/wyukawa/20160214/1455439298

http://ngyuki.hatenablog.com/entry/2017/11/14/203554

https://libetrada.com/prometheus_healthcheck/

下記インストール関連はすべて Ubuntu 16.04 向けの情報です 下記サンプルのホスト名と役割はそれぞれ下記のとおりです

  • fronttest011 監視される側のサーバー。MySQL やウェブサーバーが動作している全部入りウェブサーバー。このサーバーで Prometheus のエージェントが動作。
  • admintest011 監視する側のサーバー。このサーバーで Prometheus 本体部分が動作。

まずは Node Exporter のインストールと起動

  • Munin でいうところの munin-node です
  • ただし Node Exporter はシステム系のメトリクスしか扱っていません。

https://prometheus.io/download/

公式サイトからダウンロードしてバイナリを起動するだけです

[root@fronttest011 prom]# wget "https://github.com/prometheus/node_exporter/releases/download/v0.15.2/node_exporter-0.15.2.linux-amd64.tar.gz"
[root@fronttest011 prom]# (cd node_exporter-0.15.2.linux-amd64/;  ./node_exporter &)

Prometheus 本体のインストールと起動

Node Exporter とほぼ同じで公式サイトからダウンロードしてバイナリを起動するだけですが、設定ファイルの修正が必要です(下記サンプルは以降の設定に合うようにすでに修正済みのものです)

[root@admintest011 prom]# wget "https://github.com/prometheus/prometheus/releases/download/v2.2.1/prometheus-2.2.1.linux-amd64.tar.gz"
[root@admintest011 prom]# tar xzf prometheus-2.2.1.linux-amd64.tar.gz

[root@admintest011 prom]# cat prometheus-2.2.1.linux-amd64/prometheus.yml
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
#      - localhost:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
#  - "alert_rules.yml"
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    static_configs:
      - targets: ['localhost:9090']
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']
      - targets: ['fronttest011:9100']
  - job_name: 'mysql'
    static_configs:
      - targets: ['fronttest011:9104']



[root@admintest011 prom]# (cd prometheus-2.2.1.linux-amd64/;  ./prometheus --config.file=prometheus.yml &)

その後下記 URL にてアクセス

http://admintest011:9090

Grafana のインストールと起動

公式サイトからダウンロードしてバイナリを起動するだけです

https://grafana.com/grafana/download

[root@admintest011 prom]# wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3.linux-x64.tar.gz
[root@admintest011 prom]# tar xzf grafana-5.0.3.linux-x64.tar.gz
[root@admintest011 prom]# (cd grafana-5.0.3/;  ./bin/grafana-server web &)

http://admintest011:3000/dashboard/

Data Source で Prometheus を設定。 ここで Name に Prometheus と設定している箇所はのちのダッシュボードが参照している名前と関係ありますのでここではこの通りにする必要があります。

f:id:neinvalli:20180319005520p:plain

すでに作成されたダッシュボードのインポートをします。

システム系のグラフでは Node Exporter Server Metrics というダッシュボードが複数台対応もしており使いやすいかと思います https://grafana.com/dashboards/405

インポート方法はインポートのページにて上記ダッシュボードの ID を入力するだけです

http://admintest011:3000/dashboard/import

f:id:neinvalli:20180319005854p:plain

Data Source で Prometheus を選択することを忘れずに。

Node の箇所で複数サーバーを選択すればグラフが横に並んで表示されます

f:id:neinvalli:20180319011932p:plain

他に入れる場合はこちらのページから検索できます

https://grafana.com/dashboards?dataSource=prometheus&search=node

MySQL Exporter のインストールと起動

Prometheus 公式サイトからダウンロードできます

https://prometheus.io/download/#mysqld_exporter

[root@fronttest011 prom]# wget "https://github.com/prometheus/mysqld_exporter/releases/download/v0.10.0/mysqld_exporter-0.10.0.linux-amd64.tar.gz"

MySQL にて下記を実行
CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'pass' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';

起動
[root@fronttest011 prom]# (cd mysqld_exporter-0.10.0.linux-amd64;  export DATA_SOURCE_NAME='exporter:pass@(localhost:3306)/';  ./mysqld_exporter &)

MySQL 向けのダッシュボードのインポート

Percona のものを利用します

https://www.percona.com/blog/2016/02/29/graphing-mysql-performance-with-prometheus-and-grafana/

こちらの情報少し古いのか、うまく行きませんでしたので下記の方法で対応しました

https://github.com/percona/grafana-dashboards/tree/master/dashboards

こちらからインポートしたいダッシュボードの JSON ファイルの内容をコピー&ペーストで Prometheus のインポートのページに貼り付けてインポートします

f:id:neinvalli:20180319010202p:plain

表示されるようになりました

f:id:neinvalli:20180319012009p:plain

まとめ

ざっとインストール手順をメインで書きました 私自身他のウェブサイトを参考にインストールしようとしましたが、情報が古かったのかうまく行かない箇所もありましたので、今回私が試した手順をまとめました ここの情報は 2018年3月19日 時点では動作確認とれている手順ですので、何かの参考になれば嬉しいです。

Prometheus の導入理由が「Munin では捕捉しづらい負荷情報を捕捉する」でしたので、次回はその点を言及できればと思います。

サーバーの負荷状態を把握するために見ておくべき Munin グラフとは

今回は Munin グラフについて考えます

インフラエンジニアの皆さんであればサーバーの状態を把握するために、Cacti, Munin, Zabbix 等のツールを使いロードアベレージ等の数値をグラフ化していることと思います。

今回はどういったグラフを見ておけばよいかを考えます。 私の場合は普段は Munin を使っているため、今回は Munin に特化した話です。 ただ、グラフに対する考え方についてはすべてのツールで共通ですね。

グラフ化する目的は下記の 2点だと思われます

  • トラブル発生時の状態把握を迅速に行うため
  • 中長期でのリソースの消費量増加具合を把握し、パフォーマンスチューニングやリソース増強の前提データとする

グラフの用途その1 (トラブル発生時の状態把握を迅速に行うため)

何かサーバートラブルが発生した際には、どこかの箇所で何かの変化が起き、その結果としてサーバートラブルとなっている、ということです。 ハードの故障の場合はともかく、急なアクセス増やプログラム修正に伴う負荷増、アタックによる負荷増、時限発火するような落とし穴での負荷増等等、いろいろとあります。

こういった原因でのサーバー負荷状態の変化を把握するためにはどういったグラフを設定しておけばいいのか、考えていきます。

グラフの用途その2 (中長期でのリソースの消費量増加具合を把握)

こちらは逆に長期トレンドでのリソース消費量の変化を見るために使います。

  • CPU 使用率の増加 (半年で倍になっているので、そろそろサーバー増強が必要)
  • ディスク使用率の増加 (3ヶ月で 10% 増えているため、ディスク増強等が必要)

Ubuntu 16.04 LTS でデフォルトで設定されている Munin グラフについて

デフォルトでそこそこの数のグラフが有効になっています。

デフォルトは下記の通りです (munin-node パッケージをインストールするとこれらの設定が最初から入ると思われます)

# ls -l /etc/munin/plugins
total 0
lrwxrwxrwx 1 root root 28 Oct 29 12:32 cpu -> /usr/share/munin/plugins/cpu
lrwxrwxrwx 1 root root 27 Oct 29 12:32 df -> /usr/share/munin/plugins/df
lrwxrwxrwx 1 root root 33 Oct 29 12:32 df_inode -> /usr/share/munin/plugins/df_inode
lrwxrwxrwx 1 root root 34 Oct 29 12:32 diskstats -> /usr/share/munin/plugins/diskstats
lrwxrwxrwx 1 root root 32 Oct 29 12:32 entropy -> /usr/share/munin/plugins/entropy
lrwxrwxrwx 1 root root 30 Oct 29 12:32 forks -> /usr/share/munin/plugins/forks
lrwxrwxrwx 1 root root 43 Oct 29 12:32 fw_forwarded_local -> /usr/share/munin/plugins/fw_forwarded_local
lrwxrwxrwx 1 root root 35 Oct 29 12:32 fw_packets -> /usr/share/munin/plugins/fw_packets
lrwxrwxrwx 1 root root 28 Oct 29 12:32 if_enp0s3 -> /usr/share/munin/plugins/if_
lrwxrwxrwx 1 root root 32 Oct 29 12:32 if_err_enp0s3 -> /usr/share/munin/plugins/if_err_
lrwxrwxrwx 1 root root 35 Oct 29 12:32 interrupts -> /usr/share/munin/plugins/interrupts
lrwxrwxrwx 1 root root 33 Oct 29 12:32 irqstats -> /usr/share/munin/plugins/irqstats
lrwxrwxrwx 1 root root 29 Oct 29 12:32 load -> /usr/share/munin/plugins/load
lrwxrwxrwx 1 root root 31 Oct 29 12:32 memory -> /usr/share/munin/plugins/memory
lrwxrwxrwx 1 root root 32 Oct 29 12:32 netstat -> /usr/share/munin/plugins/netstat
lrwxrwxrwx 1 root root 35 Oct 29 12:32 open_files -> /usr/share/munin/plugins/open_files
lrwxrwxrwx 1 root root 36 Oct 29 12:32 open_inodes -> /usr/share/munin/plugins/open_inodes
lrwxrwxrwx 1 root root 33 Oct 29 12:32 proc_pri -> /usr/share/munin/plugins/proc_pri
lrwxrwxrwx 1 root root 34 Oct 29 12:32 processes -> /usr/share/munin/plugins/processes
lrwxrwxrwx 1 root root 29 Oct 29 12:32 swap -> /usr/share/munin/plugins/swap
lrwxrwxrwx 1 root root 32 Oct 29 12:32 threads -> /usr/share/munin/plugins/threads
lrwxrwxrwx 1 root root 31 Oct 29 12:32 uptime -> /usr/share/munin/plugins/uptime
lrwxrwxrwx 1 root root 30 Oct 29 12:32 users -> /usr/share/munin/plugins/users
lrwxrwxrwx 1 root root 31 Oct 29 12:32 vmstat -> /usr/share/munin/plugins/vmstat

追加で入れておくといいと思われるグラフについて

私の場合は、これらに加えて下記のグラフを追加しています。

さらに使っているミドルウェアに応じてそれぞれのグラフを追加しておくと良いと思います。 たとえば以下のとおりです。

その他、Redis, Memcached 等を使っている場合は対応するグラフを入れておくといいです。

私が使っている VPS の設定具合は下記のとおりです。 nginx, MySQL, 自前のアプリケーションサーバーが動作しています。

アクセス数がそれほど多くないため、nginx 関連のグラフについてはまだ入れていません。

# ls -l /etc/munin/plugins/
total 0
lrwxrwxrwx 1 root root 28 Oct 28 21:28 cpu -> /usr/share/munin/plugins/cpu
lrwxrwxrwx 1 root root 27 Oct 28 21:28 df -> /usr/share/munin/plugins/df
lrwxrwxrwx 1 root root 33 Oct 28 21:28 df_inode -> /usr/share/munin/plugins/df_inode
lrwxrwxrwx 1 root root 34 Oct 28 21:28 diskstats -> /usr/share/munin/plugins/diskstats
lrwxrwxrwx 1 root root 32 Oct 28 21:28 entropy -> /usr/share/munin/plugins/entropy
lrwxrwxrwx 1 root root 30 Oct 28 21:28 forks -> /usr/share/munin/plugins/forks
lrwxrwxrwx 1 root root 37 Oct 28 23:29 fw_conntrack -> /usr/share/munin/plugins/fw_conntrack
lrwxrwxrwx 1 root root 35 Oct 28 21:28 fw_packets -> /usr/share/munin/plugins/fw_packets
lrwxrwxrwx 1 root root 28 Oct 28 21:28 if_ens3 -> /usr/share/munin/plugins/if_
lrwxrwxrwx 1 root root 28 Oct 28 21:28 if_ens4 -> /usr/share/munin/plugins/if_
lrwxrwxrwx 1 root root 28 Oct 28 21:28 if_ens5 -> /usr/share/munin/plugins/if_
lrwxrwxrwx 1 root root 32 Oct 28 21:28 if_err_ens3 -> /usr/share/munin/plugins/if_err_
lrwxrwxrwx 1 root root 32 Oct 28 21:28 if_err_ens4 -> /usr/share/munin/plugins/if_err_
lrwxrwxrwx 1 root root 32 Oct 28 21:28 if_err_ens5 -> /usr/share/munin/plugins/if_err_
lrwxrwxrwx 1 root root 35 Oct 28 21:28 interrupts -> /usr/share/munin/plugins/interrupts
lrwxrwxrwx 1 root root 33 Oct 28 21:28 irqstats -> /usr/share/munin/plugins/irqstats
lrwxrwxrwx 1 root root 29 Oct 28 21:28 load -> /usr/share/munin/plugins/load
lrwxrwxrwx 1 root root 31 Oct 28 21:28 memory -> /usr/share/munin/plugins/memory
lrwxrwxrwx 1 root root 31 Oct 28 22:46 mysql_commands -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 23:44 mysql_connections -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_files_tables -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_bpool -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_bpool_act -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_insert_buf -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_io -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_io_pend -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_log -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_rows -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_semaphores -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_innodb_tnx -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_network_traffic -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:48 mysql_qcache -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:49 mysql_qcache_mem -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:49 mysql_select_types -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:49 mysql_slow -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:49 mysql_sorts -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:49 mysql_table_locks -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 31 Oct 28 22:49 mysql_tmp_tables -> /usr/share/munin/plugins/mysql_
lrwxrwxrwx 1 root root 32 Oct 28 21:28 netstat -> /usr/share/munin/plugins/netstat
lrwxrwxrwx 1 root root 38 Oct 28 22:38 netstat_multi -> /usr/share/munin/plugins/netstat_multi
lrwxrwxrwx 1 root root 35 Oct 28 21:28 open_files -> /usr/share/munin/plugins/open_files
lrwxrwxrwx 1 root root 36 Oct 28 21:28 open_inodes -> /usr/share/munin/plugins/open_inodes
lrwxrwxrwx 1 root root 33 Oct 28 21:28 proc_pri -> /usr/share/munin/plugins/proc_pri
lrwxrwxrwx 1 root root 34 Oct 28 21:28 processes -> /usr/share/munin/plugins/processes
lrwxrwxrwx 1 root root 29 Oct 28 21:28 swap -> /usr/share/munin/plugins/swap
lrwxrwxrwx 1 root root 28 Oct 28 22:38 tcp -> /usr/share/munin/plugins/tcp
lrwxrwxrwx 1 root root 32 Oct 28 21:28 threads -> /usr/share/munin/plugins/threads
lrwxrwxrwx 1 root root 31 Oct 28 21:28 uptime -> /usr/share/munin/plugins/uptime
lrwxrwxrwx 1 root root 30 Oct 28 21:28 users -> /usr/share/munin/plugins/users
lrwxrwxrwx 1 root root 31 Oct 28 21:28 vmstat -> /usr/share/munin/plugins/vmstat

グラフごとの例

CPU

f:id:neinvalli:20171031020211p:plain

あまり説明はいらないかと思います。

  • user が上がっている場合はプログラムが CPU を使っています
  • system が上がっている場合は、通信やメモリの確保等のカーネルランドの処理が CPU を使っています
  • iowait が上がっている場合は、ディスク IO が発生しています

メモリ

f:id:neinvalli:20171031020304p:plain

  • apps がアプリが使っている実メモリ量です。ここが一番重要。
  • cache はページキャッシュです。これが実際にどの程度有効活用されているかどうかは echo 3 > /proc/sys/vm/drop_caches してディスク IO がどうなるか確認する必要があります
  • active, inactive についてはそれが cache, apps どちらなのかわかるため、その点は改善点かもしれません
  • swap の量が増減している場合は注意が必要です。

ロードアベレージ

f:id:neinvalli:20171031020325p:plain

これも説明不要かと思います。 急に増加している場合は、アクセス増やプロセスの暴走やディスク IO のどれかが起こっていることが大半です。

ディスク IO

f:id:neinvalli:20171031020348p:plain f:id:neinvalli:20171031020404p:plain f:id:neinvalli:20171031020416p:plain

特に HDD を使っている場合はディスク IO がボトルネックとなる場合が多いです。 急にロードアベレージが上がった場合等はチェックすると良いグラフかもしれません。 大体の場合がサーバー上にログインして vmstat, iostat で iowait が多いことに気づくことのほうが多いです...

swap については、swapin, swapout の際のディスク IO が原因でロードアベレージが爆発するケースがあるため、見ておくと良いかもしれません。 ただ、ほとんどの場合はメモリのグラフで swap が増加しているのを見て「うわー swap 出てますねー」となるケースが多いです...

プロセス数、コンテキストスイッチ

f:id:neinvalli:20171031020431p:plain f:id:neinvalli:20171031020442p:plain

これらの数も負荷高騰時に確認する項目です。

  • プロセス数が増加するということは、どこかで何かの処理が詰まっている可能性があります。
  • コンテキストスイッチが増加している場合は大量のプロセスが処理をしている際に見られる現象です。

ただしこれらのグラフも負荷高騰後にサーバーログインして vmstat, ps で気づく場合が多いです。

TCP ソケット

f:id:neinvalli:20171031020458p:plain

負荷増ではなく、HTTP のエラー件数が多くなったり、サーバーのレスポンスが悪化した際に確認することが多いです。 アプリケーションサーバーから Memcached, Redis, MySQL 等のミドルウェアへ接続している場合で、 ウェブサーバーのレスポンスが悪化した場合は、最初にどこかが詰まり、その後他の箇所も接続がたまってしまうため、 原因ではないサーバーの established が大量に出ることがあります。

特に重要なのが、サーバー発の TCP コネクション(Memcached, MySQL 宛等)を大量に張っている場合です。 TIME_WAIT 状態のソケットが溜まり、TCP ローカルポートが枯渇してしまう可能性があります。 まずはローカルポートの範囲を広げるのが望ましいです(net.ipv4.ip_local_port_range) これについては機会があれば言及したいと思います。

アクセスを受けるだけのサーバーであっても TIME_WAIT の最大値がありますので、これは上げておくのが望ましいです。(net.ipv4.tcp_max_tw_buckets)

netfilter conntrack テーブル

f:id:neinvalli:20171031020512p:plain

iptables state モジュールで iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 等の設定を入れた場合、直近でどういったパケットのやりとりがあったかを記憶しておくテーブルの使用量です。 これが大量にある場合は、最大値を増やしたり(net.netfilter.nf_conntrack_max)、タイムアウト値を短くすること(net.netfilter.nf_conntrack_generic_timeout)を検討したほうがいいかもしれません。 サーバーにつながりにくくなって、dmesg や syslog にて ip_conntrack: table full, dropping packet のメッセージが出るあれです。

Ubuntu 14.04 までだと net.netfilter.nf_conntrack_tcp_timeout_time_wait 等の項目ごとにタイムアウト値があったのですが、16.04 のカーネルだと net.netfilter.nf_conntrack_generic_timeout の項目しか見当たりません。要調査です。

ディスク使用量

f:id:neinvalli:20171031020529p:plain f:id:neinvalli:20171031020539p:plain

使用量と言っても

  • ディスクサイズ
  • inode 使用量

の 2パターンがあるため、注意が必要です。

ディスクサイズのほうは監視していることが大半ですが、inode 使用量の監視を忘れていて、ハマることがたまにあります。 「あれ、書き込みエラーになる。ディスクまだ空きあるのに。あー、inode 枯渇してる」というようなやつです。 デバッグ向けのログファイル等が大量に出来てしまい、ディスクサイズはまだまだ余裕なのに inode が枯渇してしまうというようなことがあるので油断はできません。

MySQL 関連

f:id:neinvalli:20171031020554p:plain f:id:neinvalli:20171031020606p:plain

例として MySQL のグラフも貼っておきます。 システムの修正や機能追加で急にクエリが増えたりした場合はこのグラフでわかります。 MySQL は見ておくべき箇所が多いため、出せるだけ出しておいて、トラブルの種類に応じて使い分けしていくのがいいかと思います。

uptime

f:id:neinvalli:20171031020622p:plain

最後は毛色を変えて、uptime のグラフです。 勝手にリブートがかかり、メモリやキャッシュが暖まっていないために負荷が高騰する、といようなパターンもあるため、どのタイミングでリブートがかかったかを簡単に知れるのは一応メリットがあります。

トラフィック

私のサーバーではグラフがエラーで出ていなかったため、グラフサンプルはないのですが、非常に重要なグラフです。 トラフィックを把握しているとネットワーク IO にどの程度変化があったかわかるため、トラブルの原因にある程度あたりをつけることができます。

例: DB サーバーのトラフィック増 → アクセス増 OR クエリ増加 OR クエリ変更があった OR データのサイズが増えた等

その他出しておくといいと思われるグラフについて

  • CPU スレッドごとの負荷 (特定 CPU に負荷が偏って詰まりが出ていないか)
  • ローカルポートの消費数 (ローカルポート枯渇対策)
  • MySQL InnoDB Commit の回数 (Commit 回数が多いとディスク負荷になりやすい)
  • MySQL InnoDB チェックポイント処理されていないデータ数 (チェックポイント処理がどの程度行われているかの把握 参考)

Munin は向いていないけど、出しておくべきグラフについて

  • 単位時間毎の HTTP リクエスト数、レスポンスコードの数 (増減があるとすぐにわかるように)
  • 単位時間毎の HTTP レスポンスのタイムの平均 (どこかで詰まりが出た場合にすぐにわかるように)
  • プログラムやシステムのログの単位時間毎の件数 (エラー件数が増えているタイミングの把握等)
  • サービスの特定のログの単位時間毎の件数 (ログイン回数等の急激な変化の把握)

Munin の弱点について

  • ノードの数が多くなるとサーバーからのクロール処理が間に合わなくなり、5分では更新できなくなる。
  • 5分間隔の更新のため、それよりも細かいサンプリングができない (設定変更で短くすることは可能)

特に2点目が大きいです。数秒〜10秒単位で瞬間的に発生する負荷に対してはどうしても捕捉することができないため、別のグラフ化の仕組みが必要になるケースがあります。 これについては機会があれば言及したいと思います。

まとめ

グラフ化する目的を再掲します。

  • トラブル発生時の状態把握を迅速に行うため
  • 中長期でのリソースの消費量増加具合を把握し、パフォーマンスチューニングやリソース増強の前提データとする

  • トラブル発生時の調査にいつも時間がかかる

  • 知らない間にサーバー負荷が慢性的に高くなっていて、突貫メンテでサーバー増強を行うことがある

というようなことで困っている場合は、グラフの充実を検討したほうがいいかもしれませんね。

nginx upstream パッシブヘルスチェックはかなり使える (max_fails, fail_timeout)

今回は nginx upstream のパッシブヘルスチェックの挙動について調べます。

前回の consistent hash (http://neinvalli.hatenablog.com/entry/2017/10/29/235721) の調査でも出てきた下記の設定について、 今度はヘルスチェックの挙動がどうなっているのか調査しました。

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.3:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
}

ヘルスチェックの仕様について

公式のドキュメントによると、下記のようになっています。

http://nginx.org/en/docs/http/ngx_http_upstream_module.html#server https://www.nginx.com/resources/admin-guide/load-balancer/

max_fails=number

sets the number of unsuccessful attempts to communicate with the server that should happen in the duration set by the fail_timeout parameter to consider the server unavailable for a duration also set by the fail_timeout parameter. By default, the number of unsuccessful attempts is set to 1. The zero value disables the accounting of attempts.

fail_timeout=time

the time during which the specified number of unsuccessful attempts to communicate with the server should happen to consider the server unavailable; and the period of time the server will be considered unavailable.

ドキュメントを読む限りは fail_timeout の秒数の間にバックエンドへの通信が max_fails の回数以上失敗した場合に fail_timeout の時間そのバックエンドは使われなくなる。というように読み取れます。

nginx のヘルスチェックの問題点

バックエンドへの通信がどうなると失敗と評価されるのかは proxy_next_upstream のパラメーターで設定します。

例えば、proxy_next_upstream error timeout; と設定した場合で、バックエンドサーバーがダウンしてしまった場合、proxy_connect_timeout で設定した秒数まで失敗したかどうかわからないことにあるため、大量に通信が来ている場合は該当バックエンドがダウンしたと評価されるまでにそれなりの数のリクエストがタイムアウトしてしまいます。

proxy_next_upstream_tries を 2以上に設定している場合は、次のバックエンドへ通信をプロキシしてくれるのですが、どちらにしてもタイムアウトの時間を待っている間はエンドユーザーへのレスポンスが大幅に遅くなってしまいます。

上記のように考えると、この挙動は品質的にはよろしくないです。

下記のようになると考えられるからです。

  1. バックエンドの1台がダウン(バックエンドA とする)
  2. プロキシからバックエンドA への通信が max_fails 回数タイムアウトするまでの間、バックエンドA へ流した通信(かなりの数になると思われる)がタイムアウトしてしまう
  3. max_fails 回数タイムアウトし、バックエンドA がダウンしていると評価される。
  4. fail_timeout の時間の間はバックエンドA には通信は流れない
  5. fail_timeout の時間経過後、バックエンドA の「ダウンしている評価」が解除される
  6. バックエンドA は実際はダウンしたままなので、上記 2から繰り返しとなる

ここで問題なのが、fail_timeout の時間ごとにバックエンドA がダウンしていると評価されるまでの感のバックエンドA へ流しているコネクションがタイムアウトしてしまい、エンドユーザーに待たせてしまうことになることです。

アクティブヘルスチェックは有料版のみ

これは品質的によくないです。この形式の「実際のユーザーの通信を利用してヘルスチェックを行う方法」をパッシブヘルスチェックと言うそうです。 逆に通常のロードバランサーのように、バランサー自身が専用のコネクションでヘルスチェックだけを行う機能があればいいですね。

パッシブチェックに対して、こういうヘルスチェックをアクティブヘルスチェックと言います。 先ほどの nginx 公式のページにも「Active Health Monitoring」なる項目があります。

https://www.nginx.com/resources/admin-guide/load-balancer/

ただ、残念なことにアクティブヘルスチェックは NGINX Plus という有料版のみの機能のようです。

パッシブチェックは本当に使えないのか、実験で確かめる

nginx のパッシブチェックの挙動が上で公式ドキュメントから読み取った内容通りなのかどうか、実験で確認します。

今回の実験で使用する設定は下記のとおりです

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.3:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
}

log_format  chash-proxy-v2  'uri:$uri\t'
                            'upstream_addr:$upstream_addr\t';

server {
    listen 80 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name _;

    access_log /var/log/nginx/chash-access-v2.log chash-proxy-v2;
    error_log /var/log/nginx/chash-error.log;

    location / {
        proxy_pass http://myapp1;
        proxy_next_upstream error timeout;
        proxy_next_upstream_tries 0;
        proxy_next_upstream_timeout 10;
        proxy_connect_timeout 4s;
        proxy_send_timeout 5s;
        proxy_read_timeout 5s;
    }
}
  1. この設定の nginx へ 前回 使用した 10000パターンの URL に対してアクセスしてみます。
  2. アクセスを流しつつ、iptables -A INPUT -d 127.0.0.3 -j DROP を流し込み、127.0.0.3:10080 へつながらないようにします。
  3. ただ、max_fails=100 fail_timeout=10 の設定を入れているため、10秒間の間に 100回タイムアウトすれば該当バックエンドがダウンしたと評価され、別のサーバーへアクセスが振られるようになります。
  4. その 10秒後に「ダウン状態が解除」された時にどの程度アクセスが流れているのかチェックします。

チェックはエラーログを確認します。バックエンドへの通信が失敗した場合には、下記のようなエラーメッセージがエラーログに出ます。

2017/10/29 18:16:11 [error] 8692#8692: *27312 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /4992 HTTP/1.1", upstream: "http://127.0.0.3:10080/4992", host: "127.0.0.1"

テストの結果は以下のとおりです。エラーログを見れば一目瞭然です。(大量のエラーが出ているので、途中を省略します。)

2017/10/29 18:16:11 [error] 8692#8692: *27312 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /4992 HTTP/1.1", upstream: "http://127.0.0.3:10080/4992", host: "127.0.0.1"
2017/10/29 18:16:11 [error] 8692#8692: *27324 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /3498 HTTP/1.1", upstream: "http://127.0.0.3:10080/3498", host: "127.0.0.1"
2017/10/29 18:16:11 [error] 8692#8692: *27334 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /6045 HTTP/1.1", upstream: "http://127.0.0.3:10080/6045", host: "127.0.0.1"
2017/10/29 18:16:11 [error] 8692#8692: *27338 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /3243 HTTP/1.1", upstream: "http://127.0.0.3:10080/3243", host: "127.0.0.1"
2017/10/29 18:16:11 [error] 8692#8692: *27360 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /9629 HTTP/1.1", upstream: "http://127.0.0.3:10080/9629", host: "127.0.0.1"
...(140行ほど同じタイミングでの同じエラーが出ている。省略)...
2017/10/29 18:16:11 [error] 8692#8692: *28306 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /7639 HTTP/1.1", upstream: "http://127.0.0.3:10080/7639", host: "127.0.0.1"
2017/10/29 18:16:16 [error] 8692#8692: *28309 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /4503 HTTP/1.1", upstream: "http://127.0.0.3:10080/4503", host: "127.0.0.1"
2017/10/29 18:16:16 [error] 8692#8692: *28314 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /4960 HTTP/1.1", upstream: "http://127.0.0.3:10080/4960", host: "127.0.0.1"
2017/10/29 18:16:16 [error] 8692#8692: *28316 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /0120 HTTP/1.1", upstream: "http://127.0.0.3:10080/0120", host: "127.0.0.1"
2017/10/29 18:16:16 [error] 8692#8692: *28320 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /2747 HTTP/1.1", upstream: "http://127.0.0.3:10080/2747", host: "127.0.0.1"
2017/10/29 18:16:16 [error] 8692#8692: *28336 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /5761 HTTP/1.1", upstream: "http://127.0.0.3:10080/5761", host: "127.0.0.1"
2017/10/29 18:16:32 [error] 8692#8692: *92392 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /5822 HTTP/1.1", upstream: "http://127.0.0.3:10080/5822", host: "127.0.0.1"
2017/10/29 18:16:48 [error] 8692#8692: *158836 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /7273 HTTP/1.1", upstream: "http://127.0.0.3:10080/7273", host: "127.0.0.1"
2017/10/29 18:17:04 [error] 8692#8692: *225872 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /3417 HTTP/1.1", upstream: "http://127.0.0.3:10080/3417", host: "127.0.0.1"
2017/10/29 18:17:20 [error] 8692#8692: *291484 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /7083 HTTP/1.1", upstream: "http://127.0.0.3:10080/7083", host: "127.0.0.1"
2017/10/29 18:17:36 [error] 8692#8692: *358310 upstream timed out (110: Connection timed out) while connecting to upstream, client: 127.0.0.1, server: _, request: "GET /1997 HTTP/1.1", upstream: "http://127.0.0.3:10080/1997", host: "127.0.0.1"

公式ドキュメントの仕様からの考察再掲

公式ドキュメントの仕様からの考察を以下に再掲します。

  1. バックエンドの1台がダウン(バックエンドA とする)
  2. プロキシからバックエンドA への通信が max_fails 回数タイムアウトするまでの間、バックエンドA へ流した通信がタイムアウトしてしまう
  3. max_fails 回数タイムアウトし、バックエンドA がダウンしていると評価される。
  4. fail_timeout の時間の間はバックエンドA には通信は流れない
  5. fail_timeout の時間経過後、バックエンドA の「ダウンしている評価」が解除される
  6. バックエンドA は実際はダウンしたままなので、上記 2から繰り返しとなる

実験結果では、最後の「上記 2から繰り返しとなる」以降が違います。 具体的には一度ダウンしたと評価されたバックエンドへのパッシブチェックは fail_timeout の時間ごとに 1コネクションだけ流されるようです。

上記考察を実験結果で修正したものは下記のとおりです。

実験結果を見て判明した挙動

  1. バックエンドの1台がダウン(バックエンドA とする)
  2. プロキシからバックエンドA への通信が max_fails 回数タイムアウトするまでの間、バックエンドA へ流した通信がタイムアウトしてしまう (実験では max_fails=10 だが、10通信がタイムアウトしたと判明するまでに合計 150 ほどタイムアウト)
  3. max_fails 回数タイムアウトし、バックエンドA がダウンしていると評価される。
  4. fail_timeout の時間の間はバックエンドA には通信は流れない
  5. fail_timeout の時間経過後、バックエンドA が復活したかどうか確認するため、1コネクションだけ流される
  6. バックエンドA は実際はダウンしたままなので、再度 fail_timeout の間はダウンと評価したままとなる。
  7. 上記 5 から繰り返しとなる。

ポイントは 5 の箇所です。 考察では fail_timeout の時間ごとに大量のコネクションがタイムアウトしてしまう、という内容でしたが、想定外にかなり許容できる挙動になっています。

ちなみに、バックエンドのパッシブヘルスチェックの結果は nginx のワーカーごとに持っているようで、ワーカーが多い場合は fail_timeout ごとのチェックのための 1コネクションがワーカー数分発生します

consistent hash の挙動はどうなっているのか

前回 挙動を確認した consistent hash ですが、パッシブヘルスチェックでバックエンドがダウン状態となった場合、consistent hash がちゃんと効くのかどうか、ですが、同じようにしっかりと効いていました。

ちなみに下記のような設定でバックエンドへの通信がタイムアウトやエラーとなった場合に次のバックエンドが選ばれますが、これについても consistent hash が効いていました。

proxy_next_upstream error timeout;
proxy_next_upstream_tries 2;

この「効いていた」というのは、サーバーを外したときの consistent hash と同じ振り分け先となっていた、という意味です。 表にすると下記のとおりです

前回 の記事のデータを再掲します。

基準データ

サーバー URL パターン数
1 2295
2 2302
3 2977
4 2426

各種ダウン状態の consistent hash の数字

サーバー 基準データ サーバー3 を事前に外す サーバー3 が max_fails 以上のエラーでダウン サーバー3 への通信失敗で次のバックエンドへ 備考
1 2295 3259 3259 3259 3つともサーバー3 から 964 件移動
2 2302 3574 3574 3574 3つともサーバー3 から 1272 件移動
3 2977 0 0 0 0
4 2426 3167 3167 3167 3つともサーバー3 から 741 件移動

上記表の意味は下記のとおりです。

  • サーバー3 を事前に外す → server 127.0.0.3:10080 max_fails=100 fail_timeout=10 down; の設定で予め外した状態
  • サーバー3 が max_fails 以上のエラーでダウン評価 → サーバー3 がダウン評価を受けた場合に、代わりに振り分け先となるバックエンドがどれか
  • サーバー3 への通信失敗で次のバックエンドへ → proxy_next_upstream error timeout; proxy_next_upstream_tries 2; 等の設定で、次の振り分け先のバックエンドがどれになるか

アクティブヘルスチェックとの差を考える

アプライアンスのバランサー等でアクティブチェックしている場合でも、 timeout=5s, interval=30s, retry=3, retry-interval=3s というような設定にしているかと思うのですが、この場合でも最初にバックエンドがダウン状態と評価されるまでの 10秒程度の間は該当バックエンドに流れているコネクションはタイムアウトしてしまいます。 そう考えると nginx のパッシブチェックも意外に頑張っていると思われます。

まとめ (nginx のパッシブヘルスチェックはかなり使える)

NGINX Plus へユーザーを流すためにわざと機能を弱めにしてるというイメージだったのですが、 nginx のパッシブヘルスチェックは意外にもかなり使い物になることがわかりました。 コネクションの取りこぼしができるだけ起きないようにしないといけないシビアな環境でなければ気軽に使ってもよさそうです。

curl を利用してウェブサーバー側の処理時間を推計する

curl を利用してウェブサーバー側の処理時間を推計しようというのが今回のテーマです。

前回の記事 ウェブでのリバースプロキシ導入目的は消費メモリ量を抑えるため ではこのスクリプトを利用してメジャーなサイトの速度を計測しました。

以下の例は前回の記事の test.php に対して計測スクリプトを実行した結果です。

[root @st-webclient001 ~]# ./check-speed.sh 'http://10.1.0.2/test.php'
response_size:                             36263  bytes
msec_namelookup:                           0      msec
msec_connect:                              50     msec
msec_ssl_handshake_overhead:               0      msec
msec_client_transfer_request(estimated):   25     msec
msec_server_generate_response(estimated):  151    msec
msec_server_transfer_response(estimated):  76     msec
msec_total:                                302    msec

 

curl の -w オプション

curl には -w, --write-out <format> というオプションがあります。

format の部分では、curl の変数の使うことができ、これを利用して curl の各ステップでかかった時間を表示することが可能です。

例えば、下記のような使い方です。

# curl -s -o /dev/null -w 'code:%{http_code} size_download:%{size_download} time_total:%{time_total}\n' "https://www.google.co.jp/"
code:200 size_download:10897 time_total:0.502

今回使うのは下記の変数です。

  size_download  The total amount of bytes that were downloaded.

  time_connect   The time, in seconds, it took from the start until the TCP connect to the remote host (or proxy) was completed.

  time_namelookup
                 The time, in seconds, it took from the start until the name resolving was completed.

  time_pretransfer
                 The  time, in seconds, it took from the start until the file transfer was just about to begin. This includes all pre-transfer commands and negotiations that are specific to the particu-
                 lar protocol(s) involved.

  time_starttransfer
                 The  time,  in seconds, it took from the start until the first byte was just about to be transferred. This includes time_pretransfer and also the time the server needed to calculate the
                 result.

  time_total     The total time, in seconds, that the full operation lasted. The time will be displayed with millisecond resolution.

ざっと挙げると下記のとおりです。

  • size_download - ダウンロードしたバイト数
  • time_namelookup - 開始から DNS 名前解決完了までの時間
  • time_connect - 開始からリモートホストへの TCP コネクションが完了するまでの時間
  • time_pretransfer - 開始からデータ転送開始までの時間
  • time_starttransfer - 開始から最初の 1バイトが転送されるまでの時間
  • time_total - トータルの時間

これだけだと意味がわかりません。

 

tcpdump の結果と比較

tcpdump の結果と比較した限りでは下記のとおりです

  • size_download - ダウンロードしたバイト数
  • time_namelookup - 開始から DNS 名前解決完了までの時間
  • time_connect - 開始から TCP 3ウェイハンドシェイクの SYN -> SYN/ACK -> ACK の 3つ目の ACK を送信した瞬間までの時間
  • time_pretransfer - 開始からクライアントが HTTP リクエストを送信する瞬間までの時間
  • time_starttransfer - 開始からクライアントがサーバー側からの最初の 1バイトを受信した瞬間までの時間
  • time_total - 開始から、TCP コネクション終了に FIN をクライアントが送信した瞬間までの時間
[root @st-webclient001 speed-tools]# ./check-speed.sh 'http://10.1.0.2/test.php'
---- curl raw variable values ----
code:                200
size_download:       36263
time_namelookup:     0.000
time_connect:        0.051
time_appconnect:     0.000
time_pretransfer:    0.051
time_starttransfer:  0.251
time_total:          0.352

---- calculated each phase times ----
response_size:                             36263  bytes
msec_namelookup:                           0      msec
msec_connect:                              51     msec
msec_ssl_handshake_overhead:               0      msec
msec_client_transfer_request(estimated):   25     msec
msec_server_generate_response(estimated):  149    msec
msec_server_transfer_response(estimated):  126    msec
msec_total:                                351    msec

[root @st-webclient001 ~]# tcpdump -nnn -i enp0s8 '((dst 10.1.0.2) and (dst port 80)) || ((src 10.1.0.2) and (src port 80))'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes
22:01:27.341618 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [S], seq 2615431876, win 29200, options [mss 1460,sackOK,TS val 2832563 ecr 0,nop,wscale 9], length 0
22:01:27.392671 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [S.], seq 77093785, ack 2615431877, win 28960, options [mss 1460,sackOK,TS val 2863731 ecr 2832563,nop,wscale 9], length 0
22:01:27.392699 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 1, win 58, options [nop,nop,TS val 2832576 ecr 2863731], length 0
22:01:27.392849 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [P.], seq 1:420, ack 1, win 58, options [nop,nop,TS val 2832576 ecr 2863731], length 419: HTTP: GET /test.php HTTP/1.1
22:01:27.443610 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], ack 420, win 59, options [nop,nop,TS val 2863744 ecr 2832576], length 0
22:01:27.592097 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], seq 1:11585, ack 420, win 59, options [nop,nop,TS val 2863781 ecr 2832576], length 11584: HTTP: HTTP/1.1 200 OK
22:01:27.592118 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 11585, win 103, options [nop,nop,TS val 2832626 ecr 2863781], length 0
22:01:27.592297 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], seq 11585:14481, ack 420, win 59, options [nop,nop,TS val 2863781 ecr 2832576], length 2896: HTTP
22:01:27.592306 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 14481, win 114, options [nop,nop,TS val 2832626 ecr 2863781], length 0
22:01:27.642365 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], seq 14481:20273, ack 420, win 59, options [nop,nop,TS val 2863794 ecr 2832626], length 5792: HTTP
22:01:27.642380 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 20273, win 137, options [nop,nop,TS val 2832638 ecr 2863794], length 0
22:01:27.693085 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [P.], seq 20273:26065, ack 420, win 59, options [nop,nop,TS val 2863806 ecr 2832638], length 5792: HTTP
22:01:27.693106 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 26065, win 159, options [nop,nop,TS val 2832651 ecr 2863806], length 0
22:01:27.693373 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [P.], seq 26065:36485, ack 420, win 59, options [nop,nop,TS val 2863806 ecr 2832638], length 10420: HTTP
22:01:27.693384 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 36485, win 200, options [nop,nop,TS val 2832651 ecr 2863806], length 0
22:01:27.693491 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [F.], seq 420, ack 36485, win 200, options [nop,nop,TS val 2832651 ecr 2863806], length 0
22:01:27.744300 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [F.], seq 36485, ack 421, win 59, options [nop,nop,TS val 2863819 ecr 2832651], length 0
22:01:27.744312 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 36486, win 200, options [nop,nop,TS val 2832664 ecr 2863819], length 0
^C

 

わかりやすいように図にしました。

f:id:neinvalli:20171030000208p:plain

ここでは SSL を使用していないため、SSL のハンドシェイクのオーバーヘッドと DNS 名前解決の時間は入っていません。

図の中に書いてしまったのですが、特筆事項は下記のとおりです。

  • time_connect は SYN -> SYN/ACK -> SYN を送信直後の時間なので、実質はラウンドトリップタイムだと考えることができます。
  • HTTP Request の送信時間はラウンドトリップタイムの半分と考えることができます。
  • 図を見ていただくとわかりやすいかと思いますが、time_starttransfer - time_pretransfer には HTTP リクエストと HTTP レスポンスを送信する時間が含まれるため、この数字からラウンドトリップタイムを引くことでサーバーがレスポンスを生成するのにかかった時間を推計できます
  • time_total - time_starttransfer に HTTP レスポンスの1パケット目の送信時間を加算するとレスポンスの転送時間が推計できます。

 

スクリプトソースコード

#!/bin/bash


# Usage
# 
# ./check-speed.sh 'http://example.com/'
# 
# ./check-speed.sh -tsv 'http://example.com/'
#
# ./check-speed.sh -ltsv 'http://example.com/'
# 

if echo "$1" | egrep -q '^-';  then
    case "$1" in
        "-tsv") mode="tsv";;
        "-ltsv") mode="ltsv";;
    esac
    shift;
fi


url="$1"


data=$(curl -s \
 -H 'accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' \
 -H 'accept-encoding:gzip, deflate, br' \
 -H 'accept-language:en-US,en;q=0.8' \
 -H 'cache-control:no-cache' \
 -H 'pragma:no-cache' \
 -H 'upgrade-insecure-requests:1' \
 -H 'user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/61.0.3163.79 Chrome/61.0.3163.79 Safari/537.36' \
 -m 15 \
 -o /dev/null \
 -w 'code:%{http_code} size_download:%{size_download} time_namelookup:%{time_namelookup} time_connect:%{time_connect} time_appconnect:%{time_appconnect} time_pretransfer:%{time_pretransfer} time_starttransfer:%{time_starttransfer} time_total:%{time_total} \n' \
 "$url")

if [[ "$?" != 0 ]];  then # it means prior curl command failed
    echo "curl failed"
    exit 2
fi

size_download=$(echo "$data" | perl -ne 's|^.*size_download:([.0-9]+) .*$|$1|; print;')
msec_time_namelookup=$(echo "$data" | perl -ne 's|^.*time_namelookup:([.0-9]+) .*$|$1|; print ($_ * 1000);')
msec_time_connect=$(echo "$data" | perl -ne 's|^.*time_connect:([.0-9]+) .*$|$1|; print ($_ * 1000);')
msec_time_pretransfer=$(echo "$data" | perl -ne 's|^.*time_pretransfer:([.0-9]+) .*$|$1|; print ($_ * 1000);')
msec_time_starttransfer=$(echo "$data" | perl -ne 's|^.*time_starttransfer:([.0-9]+) .*$|$1|; print ($_ * 1000);')
msec_time_total=$(echo "$data" | perl -ne 's|^.*time_total:([.0-9]+) .*$|$1|; print ($_ * 1000);')

# debug
# echo "$msec_time_namelookup $msec_time_connect $msec_time_pretransfer $msec_time_starttransfer $msec_time_total"

response_size=$size_download
msec_namelookup=$msec_time_namelookup
msec_connect=$(($msec_time_connect - $msec_time_namelookup))
msec_ssl_handshake_overhead=$(($msec_time_pretransfer - $msec_time_connect))
msec_request_to_first_byte=$(($msec_time_starttransfer - $msec_time_pretransfer))
msec_client_transfer_request=$(($msec_connect / 2))
msec_server_generate_response=$(($msec_request_to_first_byte - $msec_connect))
msec_server_transfer_response=$(($msec_time_total - $msec_time_starttransfer + (msec_connect / 2)))
msec_total=$(($msec_namelookup + $msec_connect + $msec_ssl_handshake_overhead + $msec_client_transfer_request + $msec_server_generate_response + $msec_server_transfer_response))


case "$mode" in
    "tsv")
        echo -e "$response_size\t$msec_namelookup\t$msec_connect\t$msec_ssl_handshake_overhead\t$msec_client_transfer_request\t$msec_server_generate_response\t$msec_server_transfer_response\t$msec_total";;
    "ltsv")
        echo -e "response_size:$response_size\tmsec_namelookup:$msec_namelookup\tmsec_connect:$msec_connect\tmsec_ssl_handshake_overhead:$msec_ssl_handshake_overhead\tmsec_client_transfer_request:$msec_client_transfer_request\tmsec_server_generate_response:$msec_server_generate_response\tmsec_server_transfer_response:$msec_server_transfer_response\tmsec_total:$msec_total";;
    *)
        echo "---- curl raw variable values ----"
        echo "$data" | perl -ne 's| |\n|g; s|:|: |g; print;' | column -t
        echo
        echo "---- calculated each phase times ----"
        echo "response_size: $response_size bytes
msec_namelookup: $msec_namelookup msec
msec_connect: $msec_connect  msec
msec_ssl_handshake_overhead: $msec_ssl_handshake_overhead msec
msec_client_transfer_request(estimated): $msec_client_transfer_request msec
msec_server_generate_response(estimated): $msec_server_generate_response msec
msec_server_transfer_response(estimated): $msec_server_transfer_response msec
msec_total: $msec_total msec" | column -t;;
esac

 

実行結果サンプル

$ ./check-speed.sh 'http://example.com/'
---- curl raw variable values ----
code:                200
size_download:       606
time_namelookup:     0.060
time_connect:        0.222
time_appconnect:     0.000
time_pretransfer:    0.222
time_starttransfer:  0.391
time_total:          0.391

---- calculated each phase times ----
response_size:                             606  bytes
msec_namelookup:                           60   msec
msec_connect:                              162  msec
msec_ssl_handshake_overhead:               0    msec
msec_client_transfer_request(estimated):   81   msec
msec_server_generate_response(estimated):  7    msec
msec_server_transfer_response(estimated):  81   msec
msec_total:                                391  msec

TCP コネクションの部分でインターネット上の途中経路でパケットロスや遅延があった場合に、 ラウンドトリップタイムが大きくなってしまった場合に、msec_server_generate_response がマイナスになってしまうケースがあるので、その点だけ注意点です。 複数回実行し、極端な数字を排除した上で平均値を出すのが適切かと思います。

nginx の consistent hash は本当に consistent なのか

nginx の upstream, consistent hash の挙動について

nginx でリバースプロキシを作る際に、同じ URL へのアクセスは同じサーバーに流したい場合があります。 バックエンド側でコンテンツのキャッシュをしている場合等です。

nginx で以下のように設定した場合、サーバーの追加や削除で選ばれる対象がどのように変化するのか調査しました。

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.3:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
}

Consitent hash 自体については下記の URL が詳しいです。

http://alpha.mixi.co.jp/entry/2008/10691/

期待する挙動について

便宜上、上記設定の 127.0.0.1 はサーバー1、127.0.0.2 はサーバー2 ... 127.0.0.4 はサーバー4 と呼称することにします。

Consistent hash について、インフラ担当の希望としては、以下のようになってくれることを期待します。

  • サーバー3 がダウンした(もしくは外された)場合 1, 2, 4 に振られていた URL はそのままでサーバー3 に振っていた URL だけが均等に 1, 2, 4 のサーバーに振られる
  • サーバー3 が復活した(もしくは増設された)場合、もともとサーバー3 に振られていた URL のみがサーバー3 へ振られるようになる
  • サーバー1~4 のところへ新規でサーバー5 を増設した場合、サーバー1~4 から 1/4 ずつ均等にサーバー5 へ振られるようになる
  • サーバー1~4 のところからサーバー3 を外し、新規でサーバー5 を増設した場合、残っているサーバーは URL の一部が再配置されるが、それほど変わらず、サーバー5 にはサーバー3 から多めに振られる

結論から言うと、多少の誤差はあります、このような挙動になっていました。

nginx を使ってのテスト

今回はマシン 1台でテストします。 同じマシンに Apache(ポート10080)、nginx(ポート80) を同時に稼働させ、Apache にはバックエンドの役割をさせます。 Apache が返す結果は 404 でも何でもいいので、デフォルト設定のまま、ポートだけ 10080 に変更しました。

[root @nv003-nginx ~]# netstat -npl | egrep 'apache|nginx'
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      10014/nginx -g daem
tcp        0      0 0.0.0.0:10080           0.0.0.0:*               LISTEN      3450/apache2    

nginx の設定は下記のとおりです。Ubuntu の sites-available 直下のファイルに以下の設定を入れる感じです。

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.3:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
}

log_format  chash-proxy-v2  'uri:$uri\tupstream_addr:$upstream_addr\t';

server {
    listen 80 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name _;

    access_log /var/log/nginx/chash-access-v2.log chash-proxy-v2;
    error_log /var/log/nginx/chash-error.log;

    location / {
        proxy_pass http://myapp1;
#        proxy_next_upstream error timeout;
        proxy_next_upstream off;
        proxy_next_upstream_tries 0;
        proxy_next_upstream_timeout 0;
        proxy_connect_timeout 5s;
        proxy_send_timeout 5s;
        proxy_read_timeout 5s;
    }
}

テスト内容について

上記設定の log_format chash-proxy-v2 'uri:$uri\tupstream_addr:$upstream_addr\t'; のログを利用して、アクセスした URL と選ばれたバックエンドの関係を調べることにします。 URL のパターンは

http://127.0.0.1:80/0000
http://127.0.0.1:80/0001
...
http://127.0.0.1:80/9998
http://127.0.0.1:80/9999

という1万パターンの URL を作成し、ウェブベンチマークツール(ここでは siege を使いました)を使い、アクセスすることを考えます。

  1. アクセスをし、アクセスログを入手
  2. アクセスログからバックエンド(サーバー1~4)に振られた URL を抽出する -> データA とする
  3. バックエンドに変化を起こす(サーバー3 を止める等)
  4. 再度アクセスをし、アクセスログを入手
  5. アクセスログからバックエンド(サーバー1~4)に振られた URL を抽出する -> データB とする
  6. データA とデータB を比較して URL とバックエンドの関係の変化を確認する

基準データ(バックエンドはサーバー1~4)

それぞれ下記のような件数となりました。

サーバー URL パターン数
1 2295
2 2302
3 2977
4 2426

これを基準データとします。 全て足すと 10000 となります。 意外にもこの段階から均等とはなっていません。

テスト1 (サーバー3 をダウンさせ、選ばれ方がどうのようになるか)

  • サーバー3 がダウンした(もしくは外された)場合 1, 2, 4 に振られていた URL はそのままでサーバー3 に振っていた URL だけが均等に 1, 2, 4 のサーバーに振られる
  • サーバー3 が復活した(もしくは増設された)場合、もともとサーバー3 に振られていた URL のみがサーバー3 へ振られるようになる

上記の期待についての検証です。 サーバー3 をダウンさせ、選ばれ方がどうのようになるか確認します。 ダウン前の状態を「サーバー1, 2, 4 の状態にサーバー3 を追加した」と考え、上記2つについての検証をします。

サーバー 基準データ 今回のテスト結果 基準データとの一致件数 残りの件数 残りの件数のソース
1 2295 3259 2295 964 サーバー3 から 964 件移動
2 2302 3574 2302 1272 サーバー3 から 1272 件移動
3 2977 0 0 0 0
4 2426 3167 2426 741 サーバー3 から 741 件移動

数字は URL パターン件数です

基準データはそのままにサーバー3 に振られていた URL のみほぼ均等に分散しています。 期待通りの動作です。

ちなみに nginx の設定ファイルの書き方としては下記の 2パターンどちらも同じでした。

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.3:10080  max_fails=100 fail_timeout=10 down;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
}
upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
#    server  127.0.0.3:10080  max_fails=100 fail_timeout=10 down;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
}

テスト2 (サーバー1~4 のところへ新規でサーバー5 を増設)

  • サーバー1~4 のところへ新規でサーバー5 を増設した場合、サーバー1~4 から 1/4 ずつ均等にサーバー5 へ振られるようになる

上記期待についての検証です。 サーバー1~4 のところへ新規でサーバー5 を増設し、変化を確認します。

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.3:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.5:10080  max_fails=100 fail_timeout=10;
}
サーバー 基準データ 今回のテスト結果 基準データとの一致件数 残りの件数 残りの件数のソース
1 2295 1846 1846 449
2 2302 1860 1860 442
3 2977 2362 2362 615
4 2426 1967 1967 459
5 - 1965 - - 449+442+615+459 = 1965 他のサーバーからほぼ均等に割り振り

数字は URL パターン件数です

これも期待する結果となりました

テスト3 (サーバー3 を外し、サーバー5 追加して変化を見ます。)

  • サーバー1~4 のところからサーバー3 を外し、新規でサーバー5 を増設した場合、残っているサーバーは URL の一部が再配置されるが、それほど変わらず、サーバー5 にはサーバー3 から多めに振られる

上記期待への検証です。

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
#    server  127.0.0.3:10080  max_fails=100 fail_timeout=10 down;
    server  127.0.0.4:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.5:10080  max_fails=100 fail_timeout=10;
}
サーバー 基準データ 今回のテスト結果 基準データとの一致件数 残りの件数 残りの件数のソース
1 2295 2374 1846 528 サーバー3 から 528 件移動
2 2302 2584 1860 724 サーバー3 から 724 件移動
3 2977 0 0 0 0
4 2426 2411 1967 444 サーバー3 から 444 件移動
5 - 2631 - - サーバー3 から 1281 件移動。残りの 1350 件はサーバー1, 2, 4 から移動(それぞれ 449, 442, 459 件)

数字は URL パターン件数です

サーバー3 からサーバー5 への移動は半分くらいとなり、残り半分は他のサーバーからの再配置となりました。 もう少し頑張って欲しかったですが、ある程度期待している挙動となりました。

おまけ

server にホスト名を使っている場合は、DNS 名前解決結果のアドレスではなく、ハッシュの決定にはホスト名が使われているようです。 下記の 2パターンのテストをした結果、全く同じ URL の振られ方となりました。 IP が変わるけど、同じ URL を降ってほしい時は最初からホスト名を使うほうがいいかもしれません。

/etc/hosts にこれを追加
127.0.0.4 myhost4

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  myhost4:10080    max_fails=100 fail_timeout=10;
    server  127.0.0.5:10080  max_fails=100 fail_timeout=10;
}
/etc/hosts にこれを追加
127.0.0.3 myhost4

upstream myapp1 {
    hash $host$uri consistent;
    server  127.0.0.1:10080  max_fails=100 fail_timeout=10;
    server  127.0.0.2:10080  max_fails=100 fail_timeout=10;
    server  myhost4:10080    max_fails=100 fail_timeout=10;
    server  127.0.0.5:10080  max_fails=100 fail_timeout=10;
}

まとめ

nginx upstream の consistent hash は使えることがわかりました。 ほぼアルゴリズムの理論通りに動作するため、consistent hash が必要なところでは頼ってよさそうです。

インフラ視点で見た時のリバースプロキシの必要性について

インフラ視点で見た時のリバースプロキシの必要性について

3年前の記事ですが、ふとしたことがきっかけでこちらの記事が目に入ってきました。

Reverse Proxy がなぜ必要か
http://d.hatena.ne.jp/naoya/20140826/1409024573

ウェブのインフラの経験がある私としてはとても共感できました。 その中で自分なりに掘り下げることができる箇所があったので、私の考えを述べたいと思います。

この「重い」アプリケーションサーバーと「軽い」Reverse Proxy を組み合わせてそれぞれ自分が得意なものだけ担当することで、システム全体の系でみたときにリソース効率を全体最適させましょう・・・というのがインフラ視点で Reverse Proxy を導入したい一番の理由である。

上のブログで言われている通り、インフラ視点で見た場合にリバースプロキシを導入し、リソース効率を最適化する場合、

  1. 画像や CSS のような静的なファイルを返すだけの処理はリバースプロキシで返す
  2. アプリケーションの処理が必要なものだけをアプリケーションサーバープロセスへ流す

という考え方になるかと思います。

さらに下記のことも言われています。

Reverse Proxy はネットワーク的に遅いクライアントや、KeepAlive リクエスト、あるいは大きなファイルをアップロードしてくるクライアントなど「あまり仕事はしないけどネットワーク接続の維持は要求される」されるような側面でも役に立つ。それらは Reverse Proxy 側で処理しておき、アプリケーションサーバーには本当に必要なときだけにしかリクエストを転送しない・・・ たとえば遅いクライアントのHTTPリクエストがすべて着信してから転送、KeepAlive はフロントだけ有効にしておきバックエンドは応答が終わったらそく切断、アップロードファイルは終わるまでフロント側でバッファリング・・・などである。

これもそのとおりで、前述の部分も含めて同感なのですが、大量にアクセスが来る環境だとこの部分が非常に大きいと思われますのでもうちょっと掘り下げてみたいと思います。

 

今回考える環境について

ここでは掘り下げる箇所に特化するため、下記のような条件を考えます

静的ファイルは別サーバーから配信されるため、このサーバーは完全にアプリケーションサーバーとしての役割しかない状態ですが、こういった場合でもリバースプロキシは配置したほうがリソース面でのメリットがあります。

この環境でリバースプロキシの有無でのサーバーリソース消費量の差を見ていきます。

なお、通信や処理時間については、下記のとおりとします。(夜間 22時ころに有名な国内サイトへアクセスしたものの計測結果を平均したものです)

  • HTTP レスポンスサイズ 36KB (gzip 圧縮)
  • TCP コネクト SYN 送信開始から、SYN/ACK を経て、ACK を送信直後までが 51msec
  • HTTP リクエストの送信に 25msec
  • サーバーでの HTTP レスポンス結果生成が 150msec
  • HTTP レスポンスの送信にかかる時間が 312msec

 

メモリ消費量の差が大きい

結論から言うと、大量アクセスをさばく環境では、画像や CSS 等の静的ファイルを別のサーバーから配信していた場合であっても、 アプリケーションサーバープロセスの前段にリバースプロキシを導入するのとしないのとではメモリ消費量に差が出ます。
そして、現状のウェブシステムのサーバーリソース(CPU、メモリ、HDD、通信帯域)の使用具合では、そのメモリの差の部分がリソース面で一番最初にボトルネックとなる可能性が非常に高いです。 そのため、リバースプロキシを導入しボトルネックを解消しましょう、というのがインフラ視点で見た時の考え方だと思われます。

 

リバースプロキシ有り無しでのシーケンス図

まずはこの部分のシーケンス図を見ます。 リバースプロキシを導入しない場合はクライアントとの通信の最初から最後までアプリケーションサーバープロセスが拘束されることになります。

図中には手元で計測した各処理の大体の所要時間を記載してあります。
特に図中の「Apache PHP プロセスの拘束時間: 約○○msec」という箇所にご注目ください。

リバースプロキシ無し

f:id:neinvalli:20171009160135p:plain

リバースプロキシ有り

f:id:neinvalli:20171009161427p:plain

同じアクセス数を処理する場合であっても、リバースプロキシを利用しない場合はアプリケーションサーバープロセスの拘束時間が長くなり、プロセスの並列稼働数が多くなってしまいます。 リバースプロキシを導入することで、この拘束時間を最小化しプロセスの同時実行数を減らし、メモリ消費量を抑えることができます。

 

VM 環境にて検証

まずは下記環境での実験結果を見ていただきたいと思います。

f:id:neinvalli:20171009172456p:plain

  • リバースプロキシとして、nginx が 80番ポートで稼働。受けたアクセスはすべて、localhost の 81番ポートへプロキシ
  • アプリケーションサーバープロセスとして、Apache PHP(Prefork) が 81番ポートで稼働。試験用に test.php を用意。今回のテストのため、81番ポートで nginx 経由なしでアクセスを受け付けるようになっている。
  • test.php 内部では 144 ミリ秒 sleep した後に 150KB(mod_deflate で圧縮して 36KB) ほどの HTML を返す usleep(1000 * 144); echo file_get_contents('./contents.html');
  • Linux ルーター(st-router001)を間に配置し、tc を使って片道 25msec, 往復 50msec の遅延を発生させる
  • Apache, nginx でそれぞれ、コネクションごとの帯域制限をかける(800Kbps 程度に絞ってあります)

アプリケーションの処理と結果の HTML のサイズをある程度実環境に近づけるため、test.php には上記の処理を入れました。 国内のラウンドトリップタイムをある程度実環境に近づけるために tc で通信遅延を発生させています。 また、tc での遅延 25msec を考慮した上で HTTP レスポンス転送時間を 312msec に近づけるために、コネクションごとに帯域制限をかけています。

この構成でクライアントから、サーバーに対して大量に HTTP アクセスを投げた場合にどういった結果となるのか、確認してみましょう。

負荷の生成には httperf を使うことにします。 秒間同時接続数 100 で負荷をかけます。 同時に大量にアクセスを投げられるツールであれば何でも構いません。

テストは VM 環境で 1~2CPU, 1GB Ram 等の環境で行うため、大量アクセスとはいきませんが、リバースプロキシ有無の違いを見ることがテストの目的ですので、問題ないと考えています。

 

負荷をかけつつ、サーバーリソース情報を Munin でグラフ化

テストのパターンは3つ

  1. Apache PHP へ直接アクセスする (Apache プロセス数は 100, Apache の設定でコネクションごとの帯域制限 800Kbps)
  2. nginx 経由で Apache PHP へアクセス (Apache プロセス数は 100, Apache 帯域制限 Off, nginx の設定でコネクションごとの帯域制限 800Kbps)
  3. nginx 経由で Apache PHP へアクセス (Apache プロセス数は 40, Apache 帯域制限 Off, nginx の設定でコネクションごとの帯域制限 800Kbps)

この3つのパターンのテストを実施し、その間の下記の項目の状態を確認します。

  • CPU 使用率
  • メモリ使用状態
  • nginx のコネクション数(stub_status)
  • Apache status(mod_status)

これとは別に負荷をかけつつ、Apache mod_status のページ状態をコマンドで確認しておきます。 サーバー状態をより正確に把握するため、Munin は設定を変更し、1分毎のサンプリングを元にグラフ化しています。 負荷をかける前後で下記コマンドでメモリやプロセスの状態をクリアしています。
systemctl restart apache2; systemctl restart nginx; swapoff -a && swapon -a; sync; echo 3 > /proc/sys/vm/drop_caches

 

テストのコマンドとその結果 (テスト1~3 ともにエラーや大きな遅延はなし)

Test 1 (直接 Apache へアクセス、Apache プロセス数 100、Apache 帯域制限 800Kbps)
[root @st-webclient001 ~]# /usr/local/bin/httperf --add-header 'accept-encoding:gzip, deflate, br\n' --server 10.1.0.2 --port 81 --uri /test.php --rate 100 --num-conn 30000 --num-call 1 --timeout 30
httperf --timeout=30 --client=0/1 --server=10.1.0.2 --port=81 --uri=/test.php --rate=100 --send-buffer=4096 --recv-buffer=16384 --add-header='accept-encoding:gzip, deflate, br\n' --num-conns=30000 --num-calls=1
Maximum connect burst length: 5

Total: connections 30000 requests 30000 replies 30000 test-duration 300.492 s

Connection rate: 99.8 conn/s (10.0 ms/conn, <=61 concurrent connections)
Connection time [ms]: min 498.3 avg 503.2 max 689.1 median 501.5 stddev 6.5
Connection time [ms]: connect 50.9
Connection length [replies/conn]: 1.000

Request rate: 99.8 req/s (10.0 ms/req)
Request size [B]: 104.0

Reply rate [replies/s]: min 90.0 avg 99.8 max 101.0 stddev 1.3 (60 samples)
Reply time [ms]: response 200.5 transfer 251.8
Reply size [B]: header 217.0 content 36263.0 footer 0.0 (total 36480.0)
Reply status: 1xx=0 2xx=30000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 112.24 system 186.98 (user 37.4% system 62.2% total 99.6%)
Net I/O: 3566.8 KB/s (29.2*10^6 bps)

Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0
[root @st-webapp001 ~]# curl -s 'http://localhost/server-status?auto'
~省略~
BusyWorkers: 46
IdleWorkers: 54
Scoreboard: CC_CWWW__W_____WW_WWW_______W_W_W_WW____W_WCCW__C_C___C__C__WWW_W_W__W_WWWW_WWW____W_WW____WW__C_W__
Test 2 (nginx 経由でアクセス、Apache プロセス数 100、Apache 帯域制限 Off、nginx 帯域制限 800Kbps)
[root @st-webclient001 ~]# /usr/local/bin/httperf --add-header 'accept-encoding:gzip, deflate, br\n' --server 10.1.0.2 --port 80 --uri /test.php --rate 100 --num-conn 30000 --num-call 1 --timeout 30
httperf --timeout=30 --client=0/1 --server=10.1.0.2 --port=80 --uri=/test.php --rate=100 --send-buffer=4096 --recv-buffer=16384 --add-header='accept-encoding:gzip, deflate, br\n' --num-conns=30000 --num-calls=1
Maximum connect burst length: 3

Total: connections 30000 requests 30000 replies 30000 test-duration 300.524 s

Connection rate: 99.8 conn/s (10.0 ms/conn, <=61 concurrent connections)
Connection time [ms]: min 379.5 avg 529.5 max 661.6 median 532.5 stddev 20.9
Connection time [ms]: connect 50.7
Connection length [replies/conn]: 1.000

Request rate: 99.8 req/s (10.0 ms/req)
Request size [B]: 104.0

Reply rate [replies/s]: min 89.6 avg 99.8 max 101.0 stddev 1.4 (60 samples)
Reply time [ms]: response 200.9 transfer 277.9
Reply size [B]: header 221.0 content 36263.0 footer 0.0 (total 36484.0)
Reply status: 1xx=0 2xx=30000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 99.83 system 199.25 (user 33.2% system 66.3% total 99.5%)
Net I/O: 3566.8 KB/s (29.2*10^6 bps)

Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0
[root @st-webapp001 ~]# curl -s 'http://localhost/server-status?auto'
~省略~
BusyWorkers: 16
IdleWorkers: 84
Scoreboard: _____________________________________W_____________W_______W_WW_W_WW_W__WW_________W_W___W_W__W_____
Test 3 (nginx 経由でアクセス、Apache プロセス数 40、Apache 帯域制限 Off、nginx 帯域制限 800Kbps)
[root @st-webclient001 ~]# /usr/local/bin/httperf --add-header 'accept-encoding:gzip, deflate, br\n' --server 10.1.0.2 --port 80 --uri /test.php --rate 100 --num-conn 30000 --num-call 1 --timeout 30
httperf --timeout=30 --client=0/1 --server=10.1.0.2 --port=80 --uri=/test.php --rate=100 --send-buffer=4096 --recv-buffer=16384 --add-header='accept-encoding:gzip, deflate, br\n' --num-conns=30000 --num-calls=1
Maximum connect burst length: 2

Total: connections 30000 requests 30000 replies 30000 test-duration 300.523 s

Connection rate: 99.8 conn/s (10.0 ms/conn, <=61 concurrent connections)
Connection time [ms]: min 380.1 avg 529.7 max 698.0 median 532.5 stddev 20.3
Connection time [ms]: connect 50.7
Connection length [replies/conn]: 1.000

Request rate: 99.8 req/s (10.0 ms/req)
Request size [B]: 104.0

Reply rate [replies/s]: min 89.4 avg 99.8 max 101.2 stddev 1.4 (60 samples)
Reply time [ms]: response 200.8 transfer 278.2
Reply size [B]: header 221.0 content 36263.0 footer 0.0 (total 36484.0)
Reply status: 1xx=0 2xx=30000 3xx=0 4xx=0 5xx=0

CPU time [s]: user 110.14 system 188.94 (user 36.6% system 62.9% total 99.5%)
Net I/O: 3566.8 KB/s (29.2*10^6 bps)

Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
Errors: fd-unavail 0 addrunavail 0 ftab-full 0 other 0
[root @st-webapp001 ~]# curl -s 'http://localhost/server-status?auto'
~省略~
BusyWorkers: 16
IdleWorkers: 24
Scoreboard: W_WWWW_WWWW__W________WW___W___W______WW

Test 1~3 の区間を図中に書き入れています。

CPU 使用状態

f:id:neinvalli:20171009161443p:plain

メモリ使用状態

f:id:neinvalli:20171009161448p:plain

Apache プロセスのステータス

f:id:neinvalli:20171009161439p:plain

nginx コネクション数

f:id:neinvalli:20171009161452p:plain

 

リバースプロキシ有りの時は Apache の同時稼働プロセス数が少ない

Test 1 の nginx コネクション数がないのは nginx を経由していないからで当然なのですが、Test 2, 3 の Apache プロセス数が少ないことに気がつくと思います。 Test 1 では 50 弱程度のプロセスが常時仕事をしているのに対し、Test 2, 3 では 20 弱です。

Test 1 の Apache プロセスのステータス別に確認すると、Closing, Sending reply がほぼすべてを占めています。 明確に調査したわけではないですが、Sending reply は PHP 処理+クライアントへレスポンスを送信中。Closing は接続のクローズ処理中だと思われます。

ローカルの通信と違い、クライアントとの間にはインターネットがあるため、パケットのやりとりに時間がかかります。(今回のテストでは tc とコネクション事の帯域制限で再現) リバースプロキシを挟まない場合は Apache がクライアントとの通信を担当するため、その間 Apache PHP のプロセスが拘束されてしまいます。 リバースプロキシを挟んだ場合は nginx がこの部分の処理をすべて担当し、Apache PHP はローカルの nginx との通信だけで済むため、プロセスの拘束時間が短くなります。

プロセスの同時稼働数は下記の計算式で大体近い数が出るかと思います。
1 リクエストを処理するためのプロセスの拘束時間(msec) / 1000 * 秒間リクエスト数

今回のテストの結果を当てはめてみると下記のようになります。
Test 1 462(msec) / 1000 * 100(req/sec) -> 46プロセスが同時稼働
Test 2, 3 151(msec) / 1000 * 100(req/sec) -> 15プロセスが同時稼働

 

Apache 同時稼働プロセスが少ないためプロセス数を少なくすることが可能

見てきたとおり、リバースプロキシを挟んだ場合は Apache PHP のプロセス数が少なくて済みます。 Test 3 は Test 2 の結果を確認し、Apache PHP のプロセス数を 40 まで下げています。 その結果、メモリの消費量も抑えることができました。

Apache PHP は HTTP 通信をしたらいいだけの nginx プロセスと違い、メモリを多く消費します。 イメージとしては、Apache PHP が 1プロセス(1コネクション)あたり数MB〜数十MBに対して、nginx で 1コネクション担当するのに必要なメモリはせいぜい数十KB〜数百KB程度もしくはもっと少ないと思われます。

これについても上のブログで言われているとおりです。

ご存知のようにアプリケーションサーバーはメモリを大量に消費する。メモリ使用量という文脈で「重い」と言える。そのため、32GB とかどんなに潤沢にメモリが確保できる状況でも、せいぜい最大プロセス数は、すなわち並行処理性能は 50 とか 100 とかそのぐらいである。

 

大量アクセスをさばくシステムだとこのメモリの差が大きく響いてくる

今回のテストだと test.php が内部で処理をほとんどしていないためメモリ消費量が少なかったのと、負荷の秒間接続数が 100 程度であったため、それほど大きな差は出ていませんでした。

これが大規模なアプリだと、PHP 1 プロセスあたりメモリ 20~40MB消費、秒間接続数 500 というようなことになり、Apache のプロセス数を 350 でいくのか、150 に下げれるのかで、20MB(~40MB) * 200プロセス = 4GB(~8GB) ほどの差が出ます。

環境によりますが、カーネルやシステム部分に必要なメモリ、nginx 動作に必要なメモリ、ページキャッシュ用に必要なメモリ、安全マージン、をそれぞれ考慮すると2, 3GB 程度のメモリが必要だと思われます。 これに加えて上記の Apache PHP に必要なメモリを考慮すると、下記のようなメモリの違いになるかと考えられます。

  • 3GB(システム、nginx、その他) + 6GB(Apache PHP 40MB * 150 プロセス) + 安全マージン 2GB= 11GB
  • 3GB(システム、nginx、その他) + 14GB(Apache PHP 40MB * 350 プロセス) + 安全マージン 5GB = 22GB

リバースプロキシ導入で、メモリが 16GB で足りるのか、32GB 必要なのかという違いです。メモリが潤沢にある環境であればいいのですが、そうでない環境だとこの違いは大きいのではないでしょうか。
アプリケーションサーバープロセスのメモリ消費量がもっと多い場合や、通信に時間がかかる場合、秒間リクエスト数が多い場合は、メモリ消費量の差はもっと広がります。

 

リバースプロキシ導入のデメリット

インフラ視点で見た時に、逆にリバースプロキシを導入することで発生するデメリットもあります。

  • リバースプロキシのプロセスが増える分、管理コストが増加する(パフォーマンス、死活監視、セキュリティアップデート等)
  • リバースプロキシとアプリケーションサーバープロセス間の通信のためのリソースが消費される

2点目のリソース部分が注意点です。特に TCP/IP で通信している場合は、リソースの枯渇に気をつける必要があります。 これについてはまたの機会があれば言及したいと思います。

 

まとめ

リバースプロキシを導入し アプリケーションサーバープロセスが仕事をしている時間を最小化する ことでサーバーリソース(主にメモリ)を最適化しましょう、ということでした。