Apache Mesos / Marathon を本番で運用するための5つのTips

こんにちは。 インフラエンジニアの光野です。

先日のブログ記事でご紹介したとおり、弊社のクローラーはDockerコンテナ化されています。このコンテナはApache MesosとMarathonのクラスタ上で動いています。

先日の記事はクローラーシステム全体を取り扱いましたが、本記事ではMesos/Marathonを導入するにあたって必要だった設定について「〜したい」という形で紹介いたします。 Tips集として導入や検討の参考にしていただければ何よりです。

記事中の用語については先頭の前提知識・用語まとめにまとめています。また、Tipsは各見出しごとに独立させていますので、お好きな部分を参照ください。

シリーズ一覧

Tips一覧

前提知識・用語まとめ

本記事で使う用語や、コマンドを簡単にまとめます。 なお、Apache MesosとMarathon自体については先日のブログ記事で触れておりますので説明を省略します。

登場する用語

名称 概要
Mesosクラスタ Apache Mesosのマスタとスレーブから成る。zookeeperによって管理される
Mesosマスタ Apache Mesosのマスタノード。スレーブに対してタスクを投入する。Web UIもここにある。
Mesosスレーブ Apache Mesosのスレーブノード。クラスタのリソースを担う。
Mesosエージェント Mesosスレーブの各ノードで動くデーモン。起動オプションによってMesosでできることが変わる。
タスク Apache Mesosで実行する処理。シェルコマンドからコンテナまでなんでもよい。本記事中ではdocker runされるもの。

ソフトウェアのバージョン

  • Ubuntu 16.04.1 LTS
  • Apache Mesos 1.1.0
  • Marathon 1.4.1

Mesos / Marathonの起動

Ubuntuの場合は、Mesosphare社がパッケージ化してくれており、 リポジトリを追加することでapt-getを使ってインストールが可能です。

# master
sudo apt-get install mesos marathon
sudo systemctl start mesos-master
sudo systemctl start marathon

# slave
sudo apt-get install mesos
sudo systemctl start mesos-slave

初期設定については以下の記事がとても参考になります。リポジトリについても記載されています。 なおOSバージョン毎に存在するパッケージが若干異なるためご注意下さい。

How To Configure a Production-Ready Mesosphere Cluster on Ubuntu 14.04 | DigitalOcean

Mesosエージェントの設定方法

  1. 起動時にオプションとして与える
    • mesos-agent --resources=ports:[80-80, 31000-32000]
  2. 設定ファイルに記述する
    • オプション名 = ファイル名
    • ファイルの内容 = 引数
    • echo 'ports:[80-80, 31000-32000]' > /etc/mesos-slave/resources

本記事では2の方法を使っています。

ref. Apache Mesos - Configuration

sudo systemctl restart mesos-slave

なお、リスタートに失敗するようであればlatestディレクトリを削除して下さい。

sudo rm -rf /var/lib/mesos/meta/slaves/latest

Mesosマスタはzookeeperを介して各スレーブを識別しており、その情報がlatestディレクトリに記録されています。 記録されている情報と新しい設定が食い違うとリスタートに失敗するため、latestを削除し新しいスレーブとして認識させます。

ref. Apache Mesos - Slave Recovery in Apache Mesos

Marathonのタスク宣言

Marathonには大きく3つのタスク宣言方法がありますが、編集の手段が異なるだけで最終的なリクエストは同じJSONです。

  1. Web UIで宣言する
  2. Web UIのJSONモードで宣言する
  3. Web APIで宣言する

本記事でも文中にMarathon用のJSONを記述します。 ただ、説明に必要な部分だけを抜粋しているため、コピー&ペーストでは動作しません。

MesosのTips

Tips 1. ホストポートをコンテナに割り当てたい(Mesos編)

実行するタスクによってはホストのポートを専有したいことがあるかもしれません。 その場合、予めMesosエージェントに起動オプションを与えておく必要があります。 デフォルトで[31000-32000]が専有可能ですが、これに80番を追加する場合は次のように指定します。

echo 'ports:[80-80, 31000-32000]' > /etc/mesos-slave/resources
sudo systemctl restart mesos-slave

Tips 2. プライベートサブネット環境下でMesos UIを使いたい

MesosはWeb UIを持っており、ここからクラスタやタスクの状況、またタスクごとのサンドボックスを確認することができます。

f:id:vasilyjp:20170510164003p:plain

サンドボックス内には、fetch済みのファイルやログファイルがあるためデバッグ時に大変便利です。

f:id:vasilyjp:20170510164828p:plain

この情報は、Web UIがMesosスレーブに対して直接リクエストを行い収集しています。

f:id:vasilyjp:20170510170355p:plain

一方、AWSのベストプラクティスに従うとMesosクラスタはプライベートサブネットに構築されることが多いと思います。 実際に、弊社のMesosクラスタは次の構成になっています。

f:id:vasilyjp:20170510173027p:plain

MesosのWeb UIでサンドボックスの中を確認するためには、手元からプライベートサブネットにあるMesosスレーブへアクセスする必要があります。 間にELBとnginxによるプロキシを挟みこれを解決します。

f:id:vasilyjp:20170510174118p:plain

Mesos Config

Web UIがスレーブにアクセスする場合、その問い合わせはMesosエージェントに起動オプションとして与えられたホストネームに対して行われます。

まず、nginxで扱いやすいユニークな名前を設定してください。

echo "$(hostname).mesos-slave.xxxxx.yyyyy" > /etc/mesos-slave/hostname
sudo systemctl restart mesos-slave

xxxxx / yyyyyは、適宜ご自身で所有されているドメインへ読み替えてください。

DNS Record

ELBに対するAliasレコードとして*.mesos-slave.xxxxx.yyyyyを設定してください。

nginx

nginxを使って、特定のルールに基づくホストネームから名前解決を行い、 プライベートサブネットに存在する各スレーブへプロキシします。 なお、コメントにもありますが、5051ポートはMesosエージェントが利用するためnginxは別ポートでListenしています。

server {
    set_real_ip_from   10.0.0.0/8;
    real_ip_header     X-Forwarded-For;

    # [NOTE] 5051はMesosエージェントがbindする
    # ELBでポートを変えて送信。
    # Web UI -> 5051 ELB -> 15051 nginx -> 5051 Mesosスレーブ
    listen 15051;
    server_name .mesos-slave.xxxxx.yyyyy;

    location / {
        # [NOTE] Route53
        resolver 10.0.0.2;

        # [NOTE] 定期的に名前解決を行えるようにsetする
        # 直接書くとnginx restartでしか名前解決が行われない
        if ($host ~* (.*)\.mesos-slave\.xxxxx\.yyyyy) {
            set $mesos_slave_server "$1.YOUR_AWS_REGION.compute.internal";
        }

        proxy_pass http://${mesos_slave_server}:5051;
    }
}

ここではRoute53をVPC内のprivate DNSとして使っています。

MarathonのTips

Tips 3. ホストポートをコンテナに割り当てたい(Marathon編)

Marathonにおいて特定のホストポートを専有するタスクはスケジューリングに制約を与えることから非推奨になっています。 とはいえ実行するタスクによってはホストのポートを専有したいことがあるかもしれません。 この場合、Marathonでタスクを宣言する際にオプションを与える必要があります。

ポートを割り当てる(入門編)

まずは公式ドキュメントに従って単純に設定します。

{
  "container": {
    "type": "DOCKER",
    "docker": {
      "network": "BRIDGE",
      "requirePorts": true,
      "portMappings": [
        { "containerPort": 3000, "hostPort": 80, "protocol": "tcp"}
      ]
    }
  }
}
  1. requirePortsをtrueに設定
  2. portMappingsでhostPortを0ではない値に設定

hostPortで指定するポート番号は、予めMesosエージェントに起動オプションで許可されている必要があります。  これらは、公式ドキュメントのトラブルシューティングにわかりやすくまとめられています。

ポートを割り当てる(実践編)

入門編の内容で、ポートを割り当てる事自体は完了です。実践編ではデプロイ時の問題を解決します。

{
  "container": {
    "type": "DOCKER",
    "docker": {
      "network": "BRIDGE",
      "requirePorts": true,
      "portMappings": [
        { "containerPort": 3000, "hostPort": 80, "protocol": "tcp"}
      ]
    }
  },
  "constraints": [
    ["hostname", "UNIQUE"], // あるタスクがMesosスレーブあたり高々1コンテナになるようにする
  ],
  "upgradeStrategy": {
    "minimumHealthCapacity": 0, // デプロイ時、旧コンテナ数が0になることを許容する
    "maximumOverCapacity": 0 // デプロイ時、旧タスクをkillしてから新タスクをrunする
  } 
}

requirePortshostPortに加えて、constraintsupgradeStrategyを設定しています。

Marathonはタスクを更新する際、upgradeStrategyとに基づいてタスクをローリングリスタートしてくれます。 新しいタスクが何らかのバグで起動しない場合も、古いタスクがそのまま動き続けるため安全です。 しかし、既存のタスクがホストポートを専有するタスクの場合、新しいタスクはポートをバインドできずデプロイが必ず失敗するという状況に陥ります。

その為、constraintsを使ってタスクのスケジューリングに制約を与えた上で、upgradeStrategyを変更して旧タスクが無い状況を作り出すことで問題を回避します。 幸いにも弊社で運用されているポートを専有するタスクは、最悪瞬断しても良いという類のものでした。 もし、瞬断が許されない条件で動かす場合は、別の工夫が必要になります。

Tips 4. タスクでUserDefinedNetworkを使いたい

DockerのUDNを使いたい場合は、3箇所の宣言が必要です。

{
  "container": {
    "type": "DOCKER",
    "docker": {
      "network": "USER"
    }
  },
  "ipAddress": {
    "networkName": "mesos_slave_host_nw"
  },
  "ports": []
}
  1. networkにUSERを指定
  2. ipAddressにUDN名を指定
    • docker network createしたときの名前
  3. portsに空配列を指定

空配列を明示的に指定しないと、エラーになります。 portsはオプショナルな項目のため、ついつい忘れがちです。ご注意ください。

Tips 5. タスクの宣言をWeb APIで行いたい

Marathonは整理されたWeb APIをもっています。 Web APIにはコンソールも用意され、各パラメータの詳細を確認することが可能です。

Web APIはRESTfulに設計されており、状況に応じてPOST/PUT/PATCH/DELETEを使い分けます。

  • POST: 新タスクの宣言
  • PUT: 既存タスクの更新 / もし既存タスクがなければ作成される
  • PATCH: 既存タスクの更新(1.4.1時点で/v2/apps以下のエントリポイントのみ)
  • DELETE: タスクの削除

操作したいリソースに対するエントリポイントさえ分かれば自然に利用できるかと思いますが、 その中で/v2/appsに対するPUTについては注意が必要です。

curl -XPUT -H "Accept: application/json" -H "Content-type: application/json" <Mesosマスター>/v2/apps/ -d@app.json

PUTはとても便利でPOST/PATCHの両方を兼ねてくれるのですが、 Marathon 1.4系からPATCHのように振る舞うPUTについてDeprecatedになりました。

後方互換性を守るため、1.4.1時点ではPUTとPATCHに挙動の差はありません。ただし、次のバージョン(おそらく1.5.0)で変更されるという宣言がされています。

For backward compatibility, we will not change this behaviour, but let users opt in for a proper PUT.
The next version of Marathon will use PATCH and PUT as two separate actions.

将来のバージョンアップを考えると、PATCHを積極的に利用するのが望ましいです。

余談ですが、1.4.0には「PATCHのように振る舞うPUTがすべてエラーになる」という不具合がありUIも一部動作しません。そのためアップデートの際には1.4.1以降を選択下さい。

おわりに

Apache MesosとMarathonを本番運用するにあたって必要になるであろう内容をTipsの形でご紹介いたしました。

Apache MesosとMarathonは実際に運用している情報が少なく、発生する問題に対しては自力で解決する必要があります。 とはいえ、両者とも機能そのものは豊富ですし、なにより公式の情報がとても丁寧に整備されています。 そのため、ドキュメントさえ読み込めば大抵のことはフレームワーク上で解決できるというのが、実際に運用してみての感想です。

今後もまた問題が発生するとは思いますが、都度ドキュメントとにらめっこして解決して解決していこうと考えています。

最後に

VASILYにはこんなトライ&エラーを繰り返しながら成長できる環境があります。皆様の応募をお待ちしております。