こんにちは。
キャスレーコンサルティングのSD(システムデザイン)部の 小松 です。

社内でミドルウェアのチューニングやシステム基盤の設計等を担当しています。
今回は、Go言語で書かれたインテリジェントな負荷分散を実現する Traefik を試した話を紹介します。

はじめに

システムデザイン部の開発では Vagrant を使っており、
開発用イメージを Packer で予めビルドして配布するケースが多いです。
最近は 業務で Docker を使う機会もありレジストリにで共有するなど、積極的に Docker の利用を進めています。

Docker コンテナはデプロイが容易であるため、
複数のマシン上でコンテナを並列に実行させて負荷分散構成が取れるメリットがあります。

しかし、Docker をしばらく利用してみて感じたことは、クラスタ上のコンテナに対してリバースプロキシを使い
バランシングしようとすると、動的にスケールアウトさせたり縮退させる数に制限が出来てしまうという
困った部分がある事に気が付きました。

コンテナをスタティックポートでEXPOSE(ポートフォワーディング)(図1) していることが根本的な問題ですが、
リバースプロキシの特性としてダイナミックポートを指定することができないため、クラスタの難しさを感じました。

本稿では問題を解決するために取り組んたことや個人の見解を記載します。

▼ 図1 EXPOSEオプションスタティックポート指定例

$ docker run –privileged -d –expose 8081:80 –name httpd centos:centos7 /sbin/init

スタティックポートで転送するリバースプロキシの制限について

リバースプロキシの転送先変更は、設定ファイルの修正を必要とするためサービスの停止を伴います。
このため動的にスケールアウトをするには、予めスケールアウトするコンテナの上限数を決めて設定を定義するなどの
工夫が必要となります。

しかし、スタティックな設定を持つことはDockerホストへの依存性が高くなり、
利点である瞬時に起動させて気軽に環境を捨てられる俊敏性を損なうことになるため避けたいところです。(図2)

▼ 図2 スタティックポートを指定したDockerクラスタ

Traefik

Dockerホストとコンテナ間のダイナミックポートをうまい具合に束ねて負荷分散する機能が実現できる良いプロダクト探していたところ Traefik という負荷分散ソフトウェアがあることを知りました。
サイトの Traefik 概要(図3)によれば Traefik はマイクロサービス向けモダンHTTPリバースプロキシ・ロードバランサとのことで Docker にも対応しており、目的にはピッタリそうです。
またプロダクトのイメージを伝えるロゴは、Traefik がGo言語で書かれていることもあり言語のマスコットである Gopher君が、手信号で飛行機を誘導するマーシャラーに扮したデザインでプロダクトの直感的なイメージを伝えています。

▼ 図3 Traefik概要

“Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. It supports several backends (Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, Zookeeper, BoltDB, Rest API, file…) to manage its configuration automatically and dynamically.”
Træfɪkは、マイクロサービスを容易に展開できるように作られたモダンなHTTPリバースプロキシ・ロードバランサです。 自動的かつダイナミックに構成を管理するために、いくつかのバックエンド(Docker、Swarm、Kubernetes、Marathon、Mesos、Consul、Etcd、Zookeeper、BoltDB、Rest API、file …)をサポートしています。※意訳

利用方法は、実行バイナリからの起動方式と Docker コンテナからの起動方式の2つが紹介されており、
Docker 以外の利用も想定された製品でありなかなか良さそうな印象です。

実行バイナリからの起動

リリースページから最新のバイナリを入手し、設定ファイル引数に指定して実行

$ ./traefik -c traefik.toml

Dockerイメージからの起動

Docker Hub から最新のイメージを入手し、設定ファイル引数に指定して実行

$ docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik

機能とアーキテクチャ

Træfɪkは分散Key Value Store(以降KVS)をサービスレジストリとして動的に設定を保持し、
サービスの追加、削除、更新されるたびにKVSを介して検知し、動的に構成を変化させることができます。

モダンと謳っていることもあり、機能はリバースプロキシとして必要な機能は揃っているだけでなく
新しい技術もサポートされています。

Version 1.1.1からは、セッション・アフィニティ機能も入りより使いやすくなった様です。

  • バックエンドのサーキットブレーカ
  • ラウンドロビン、リーストコネクション ロードバランシング
  • セッションアフィニティ
  • 構成のホットリロード
  • HTTP/2サポート
  • Websocketのサポート
  • REST Metrics
  • SSLバックエンドのサポート
  • SSLフロントエンドのサポート
  • ネットワークエラーの場合は再試行要求
  • クラスタモードによる高可用性

インテリジェント負荷分散の定義

定義として、ロード・バランサー・グループを使用して、メンバーシップ、メンバーの重み、
およびセッション・アフィニティーに関する構成データを動的に変更できる機能を提供することにより、
ワークロードをさらに効率的にアプリケーション・サーバー全体に分散させる機能を持つものを指す様です。

Traefik は、EntryPoints, FrontEnds, BackEnds の3つのゾーンにグループやメンバーシップを定義し、
メンバーの重みや転送先の設定などを定義することができます。

GUI ダッシュボードについて

Traefik は AngularJS を使ったシンプルなGUIを装備しています。
左側の黄色のテーブルは登録された frontend-rule が表示されるエリアとなり、
右側の緑のテーブルは backend-list に登録されたリダイレクト可能なサービスの一覧が表示されます。(図4)

▼ 図4 Traefik ダッシュボード コンフィグレーション

シンプルなヘルスチェック画面により、発生しているエラー継続時間をリアルタイムに確認することができます。(図5)

▼ 図5 Traefik ダッシュボード ヘルスチェック

開発体制

開発主体は 2016年に設立されたフランスのContainous社で、CEOの Emile Vauge 氏を中心として
オープンな体制で開発されています。

GitHubやSlack通して議論が行われ、ときおり新しい機能がコミュニティを通して取り込まれています。

またContainous社は、最新のデータセンター用のインフラストラクチャソフトウェアをオープンソースで開発することを
ポリシーに掲げており、追加されるプロダクトにも注目して行きたいところです。

ほかのツールとの比較

自動ディスカバリや動的ロード算出によるロードバランス機能で言えば、
Consul-template+HAproxyで出来ることに近い印象をうけます。

Traefikがほかのツールと違うのは、まず非常にシンプルに構成できるという点です。
Traefikは一つのバイナリを置き、いくつかのコマンドを実行するだけで構成することが可能です。

また設定をファイルに持たせずKVSへ持たせて連携することが可能であり、
イベントを継起に動的な設定変更を行えることは大きな特徴だと言えます。

使い方

これまでは Traefik の概要や機能の説明が中心でした。
次は、実際に Traefik を使って動きを確認してみたいと思います。

Traefik はバイナリファイルを一つ置くだけで動作するため、基本的には追加でほかのアプリケーションを
セットアップする必要がありませんが、ここではサービスディスカバリである Consul と連携させて動作させてみたいと思います。

検証環境の概要

検証では DockerHost として CentOS7 を使います。
VagrantのBoxイメージが公開されているので、こちらを使いたいと思います。

Traefik はバイナリをインストールしてデーモン化させて使います。
Consul はサービスディスカバリ機能以外に、REST API で管理できる KVS(Key Value Store)機能をもっています。

KVS に Traefik の設定を保持させて動的にコンテナの追加と削除を実行したいと思います。
また、立ち上げたコンテナに関する情報を Consul へ登録させるには Registrator が必要となるため
組み合わせて動作させます。検証環境の構成は以下とします。(図6)

▼ 図6 環境構成図

本検証にて導入したプロダクトについては以下となります。(表1)
▼ 表1 導入プロダクト一覧

プロダクト名 補足
 CentOS Linux release 7.3.1611 (Core) VirtualBox向けBoxイメージ
 Docker version 1.10.3, build cb079f6-unsupported
 gliderlabs/consul-server gliderlabsが公開している Consul イメージ Version は 0.6.4 と古め
 gliderlabs/registrator gliderlabsが公開しているRegistrator イメージ。
 traefik_linux-amd64

検証の流れ

検証は以下の流れで進める。

  1. Linux版バイナリのダウンロードとインストール
  2. Traefikの基本設定
  3. Consulコンテナの起動
  4. nginxコンテナの起動
  5. Registratorコンテナの起動
  6. Consul KVSへ設定を登録

1. Linux版バイナリのダウンロードとインストール

バイナリはダウンロード用のページ(https://github.com/containous/traefik/releases)から取得できます。

現在のバージョン1.1.2(2016年12月時点)では、Linuxのほか、MacOSX、Windowsに対応しています。

リスト1はスクリプトによるインストール例です。
Traefikは頻繁にバージョンアップしていますので、最新版の取得更新はこのスクリプトを使うことで
対応することができます。(図7)

リリースノートなど確認する際はダウンロードページを確認ください。

▼図7 Traefikのインストール

[vagrant@centos-el7 ~]$ sudo sh install-traefik.sh

インストール後は、“traefik version”と入力することでバージョンが表示されます。(図8)

▼図8 Traefikのバージョンを表示する

[vagrant@centos-el7 ~]$ traefik version
Version: v1.1.2
Codename: camembert
Go version: go1.7.4
Built: 2016-12-15_10:27:40AM
OS/Arch: linux/amd64

▼リスト1 Linux版Traefikインストールスクリプト(install-traefik.sh)

#!/usr/bin/env bash
# grobal definition.
url='https://github.com'
path='/containous/traefik/releases'
regex='s/[^"]*"\([^"]*\)".*/\1/'
path_list=$(curl -sS ${url}${path} | grep '/download/' |sed -e ${regex})

# cpu archtechture list.
arch_list='1,i386,linux-386\n2,x86_64,linux-amd64\n3,arm,linux-arm'

# filtering process.
arch=$(echo -e "${arch_list}" | grep $(uname -m) | awk -F, '{print $3}';)
path=$(echo -e "${path_list}" | grep -e ${arch} | head -1 )
fname=$(echo -e ${path} | sed -E 's#/#\n#g' | tail -1)

# get traefik binary.
wget -P /tmp ${url}${path} && {

# install traefik binary.
install -o root -g root -m 755 /tmp/${fname} /usr/local/bin/traefik
rm /tmp/${fname}

}

# create configuration directory for traefik.
install -o root -g root -m 755 -d /etc/traefik
install -o root -g root -m 755 -d /var/log/traefik

# create daemon systemd script for traefik.
cat << 'EOS' > /etc/systemd/system/traefik.service
[Unit]
Description=traefik proxy
Requires=network-online.target
After=network-online.target

[Service]
Restart=on-failure
ExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.toml
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
EOS

# enable traefik daemon.
systemctl daemon-reload
systemctl enable traefik

2. Traefikの基本設定

Traefik の設定ファイルは、toml形式の設定ファイルを作成します。

tomlは、GitHub の中の人が提案した、
設定ファイルを記述するための小さな言語となっており、Traefikではこの形式で設定ファイルを作成します。

EntryPointには HTTP と HTTPS を設定しています。
ログの出力先やダッシュボード接続用のポートなど変更しない設計値などは、
このtomlファイルに定義することが望ましいです。

以下スクリプトは、toml形式の設定ファイルをジェネレートします。(リスト2)

▼リスト2 Linux版Traefikプロビジョニングスクリプト(provision-traefik.sh)

#!/usr/bin/env bash
# create traefik configreation.
cat << 'EOF' > /etc/traefik/traefik.toml
traefikLogsFile = "/var/log/traefik/traefik.log"
accessLogsFile = "/var/log/traefik/access.log"
logLevel = "DEBUG"
defaultEntryPoints = ["http", "https"]
# traefik web console.
[web]
address = ":8888"
EOF

Consulとの連携設定

リバースプロキシ設定を動的に変化させるためにConsulと連携するための設定を追加します。(リスト3)

▼リスト3 Consulバックエンド設定追加スクリプト(provision-consul-traefik.sh)

#!/usr/bin/env bash
cat << 'EOF' >> /etc/traefik/traefik.toml
# add consule configuretion.
[consul]
endpoint = "172.17.0.2:8500"
watch = true
prefix = "traefik"
EOF

3. Consulサーバの起動

Consulサーバクラスタの起動に3ノード以上で起動させる制約があることから、
手順の簡略化も兼ねてConsulサーバもDockerで動作させてみたいと思います。

Consulサーバは、DockerHubでGliderLabsが公開しているイメージを利用します。

  1. DockerHubで公開されているイメージをpullします。

    [vagrant@centos-el7 ~]$ sudo docker pull gliderlabs/consul-server

  2. Consul サーバはクラスタは冗長性・耐障害性を高めるために 奇数ノードでの運用が推奨されています。
    3ノードで構成した場合は、その中の 1ノードがリーダーとなる必要があるため、
    ここでリーダーとして起動させます。

    [vagrant@centos-el7 ~]$ sudo docker run -d -p 8500:8500 –name node01 \
    -h node01 gliderlabs/consul-server:latest -server -bootstrap-expect 3

    この時ブラウザからに Consul UIにアクセスするとメンバーが未登録の初期状態が表示されます(図9)

    ▼ 図9 Consul UI ダッシュボードの初期表示

  3. 起動した node01 のIPアドレスを変数に格納

    [vagrant@centos-el7 ~]$ JOIN_IP=”$(docker inspect -f ‘{{.NetworkSettings.IPAddress}}’ node01)”

  4. Consul メンバーノード node02 を起動

    [vagrant@centos-el7 ~]$ sudo docker run -d –name node02 -h node02 gliderlabs/consul-server:latest -server -join $JOIN_IP

  5. Consul メンバーノード node03 を起動

    [vagrant@centos-el7 ~]$ sudo docker run -d –name node03 -h node03 gliderlabs/consul-server:latest -server -join $JOIN_IP

  6. Consulクラスタ状態確認
    クラスタにメンバーが追加されていることが確認できる。同様に Consul UIでも確認できる。(図10)

    [vagrant@centos-el7 ~]$ sudo docker exec -it node01 consul members
    Node Address Status Type Build Protocol DC
    node01 172.17.0.2:8301 alive server 0.6.4 2 dc1
    node02 172.17.0.3:8301 alive server 0.6.4 2 dc1
    node03 172.17.0.4:8301 alive server 0.6.4 2 dc1

    ▼ 図10 Consul UI ダッシュボードのノード一覧

    Services の覧は Consul だけの表示となる。(図11)

    ▼ 図11 Services 一覧その1

4. nginxコンテナの起動

DockerHubで公開されているイメージをpullします。

[vagrant@centos-el7 ~]$ sudo docker pull docker.io/nginx

nginx コンテナを起動します。

[vagrant@centos-el7 ~]$ sudo docker run –privileged -itd -p 80 –name web01 \
-h web01 docker.io/nginx /sbin/init JOIN_IP=$JOIN_IP

5. Registratorコンテナの起動

EXPOSEされたダイナミックポートを取得するために利用します。

[vagrant@centos-el7 ~]$ sudo docker run -d -v /var/run/docker.sock:/tmp/docker.sock \
-h registrator-1 –name registrator gliderlabs/registrator consul://$JOIN_IP:8500

Registrator により Consul-Server-8500 と nginx-80 が Consul に登録される。(図12)

▼ 図12 Services 一覧その2

Registratorは、コンテナの情報をWebクエリのリクエストに合わせてJSON形式で応答してくれます。
これにより、EXPOSEされたポートを取得することもできます。(リスト4)

▼ リスト4 ホスト名を引数に指定するだけでEXPOSEされたダイナミックポートを取得する関数

# Get docker expose port.
function getPort() {
  eval $(ps aux|grep init|awk '{print $12}'|head -1)
  typeset consul="${JOIN_IP}:8500/v1/catalog/service/consul-server-8500"
  typeset leader=$(curl -s ${consul}|jq -r '.[] | [.ServiceID]|@csv'|awk -F: '{print $2}')
  typeset csv_text=$(curl -s ${JOIN_IP}:8500/v1/catalog/node/${leader}|jq -r '.Services[]|[.ID,.Port]|@csv')
  typeset expose=$(echo -e "${csv_text}"|grep ${1}|awk -F, '{print $2}')
  echo "${expose}"
}

6. Consul KVSへ設定を登録

ここまで Consulサーバの起動等を説明してきましたが、ここからは実際に Consul KVSへTraefikの設定を登録を行います。
まず、traefikを再起動しておきましょう。

再起動

▼ 図13 traefik デーモン再起動

[vagrant@centos-el7 ~]$ sudo systemctl restart traefik

backendの設定を登録

ターミナル上でcurlコマンドでAPIに対して直接設定を登録していきます。

▼ リスト5 backend設定の登録

# backend 1
curl -i -H "Accept: application/json" -X PUT -d "10" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/maxconn/amount
curl -i -H "Accept: application/json" -X PUT -d "request.host" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/maxconn/extractorfunc
curl -i -H "Accept: application/json" -X PUT -d "wrr" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/loadbalancer/method
curl -i -H "Accept: application/json" -X PUT -d "true" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/loadbalancer/sticky
curl -i -H "Accept: application/json" -X PUT -d "NetworkErrorRatio() > 0.5" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/circuitbreaker/expression
curl -i -H "Accept: application/json" -X PUT -d "http://127.0.0.1:$(getPort web01)" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server1/url
curl -i -H "Accept: application/json" -X PUT -d "10" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server1/weight
curl -i -H "Accept: application/json" -X PUT -d "http://127.0.0.1:$(getPort web02)" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server2/url
curl -i -H "Accept: application/json" -X PUT -d "10" http://127.0.0.1:8500/v1/kv/traefik/backends/backend1/servers/server2/weight

設定反映後のTraefik ダッシュボード(図14)を確認するとbackend1が追加されているのがわかります。

▼ 図14 backendの設定が反映された結果

backendの要素説明

▼ 表2 Traefik backend設定概要

分類 パラメータ名 概要
 maxconn  amount 最大接続数を定義する
 extractorfunc 最大接続数の評価に対するリクエスト分類方法を定義
 loadbarancer  method ロードバランシング方式を定義
 sticky セッション・アフィニティの有無を定義
 circuitbreaker  expression サーキットブレーカ起動条件を定義
 servers.server1  url リバースプロキシ対象のURLを定義
 weight 負荷分散の重みを定義

frontendグループの登録

ターミナル上でcurlコマンドでAPIに対して直接設定を登録していきます。
振り分けルールとして example.com のサブドメインでリクエストされたケースを
正規表現で対処できる機能を持っている様で、実用的な部分も備わっていることが確認できました。
▼ リスト6 frontend設定の登録

# frontend 1
typeset hRegexp="HostRegexp:192.168.121.10,localhost,{subdomain:([a-z]|[0-9])+}.example.com"
curl -i -H "Accept: application/json" -X PUT -d "backend1" http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/backend
curl -i -H "Accept: application/json" -X PUT -d "true" http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/passHostHeader
curl -i -H "Accept: application/json" -X PUT -d "http" http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/entrypoints
curl -i -H "Accept: application/json" -X PUT -d "${hRegexp}" http://127.0.0.1:8500/v1/kv/traefik/frontends/frontend1/routes/192.168.121.10/rule

設定反映後のTraefik ダッシュボード(図15)を確認するとfrontend1が追加されているのがわかります。

▼ 図15 frontendの設定が反映された結果

frontendの要素説明

▼ 表3 Traefik frontend設定概要

分類 パラメータ名 概要
 frontends.frontend1  backend backend転送先定義
 passHostHeader クライアントのホストヘッダーをバックエンドに転送の有無定義
 entrypoints Træfɪkへのネットワークエントリポイントの定義
 routes.proxy.rule リクエストヘッダを基準に転送評価ルールの定義

終わりに

一通り、味見した程度ですが体験することは出来ました。
実際の課題としてこれを動的にスケールアウトや縮退への動きについては、
Consul Watch を使い動的にAPIをコールして動かしたりしています。

Traefik自体はすばらしい製品だとは思いますが、クラウド環境においてはこの製品の特性と同じ機能を持つものが
出ており、それらを置き換えるほどの存在になれるのか細い目で見ています。