このエントリーをはてなブックマークに追加

Docker swarmを試してみた

TD; LR

docker swarmは気軽に使えてとても良いが、実運用は自分でちゃんと試してからの方がよさげ。

まえがき

Dockerの1.12.0からDocker swarmがdockerと統合され、クラスタ環境でのDockerが簡単に使えるようになりました。この記事は夏休みの自由研究として、このDocker swarm modeについて調べてみた記録です。

長いです。細かいです。前提をいろいろ飛ばしています。全部読むことはおすすめしません。

間違っている点は指摘していただけると助かります。

リポジトリ

この文章に関するリポジトリ
https://github.com/shirou/docker-swarm-test
docker engine: docker engineのCLI的なやつ。
https://github.com/docker/docker
swarmkit: この中にswarmの実際の中身が入っている
https://github.com/docker/swarmkit
swarm: 1.12までのswarm実装。今回は対象外
https://github.com/docker/swarm

docker swarm mode

docker swarm modeは、複数のdocker hostを束ねて使えるようになるモードです。従来はdocker swarmとして別物でしたが、1.12からdocker engineに統合されました。

とかいう概要はばさっと飛ばします。以下は用語やコマンドのメモです。

登場人物

manager node
管理ノード。3〜7がmanager nodeの最適個数
worker node
task実行ノード
node
一つのdocker engineが動いているサーバー
service
複数のdocker engineが協調して、一つのportでサービスを提供する一つのswarm clusterは複数のserviceを持てる
https://docs.docker.com/engine/swarm/images/swarm-diagram.png

使いかた

  • swarm
    • docker swarm init
      • swarm clustrを初期化します。つまり、このコマンドを実行したdocker hostが一番最初のmanager nodeとなります。
    • docker swarm join
      • 指定したmanager nodeが管理するswarm clusterにjoinします。--tokenでswarm cluster tokenを指定します。manager tokenを指定すればmanagerとして、worker tokenを指定すればworkerとしてjoinします。あるいは--managerで明示的にmanagerとしてjoinさせることもできます。
    • docker swarm leave
      • clusterから抜けます
  • node
    • docker node ls
      • nodeの状態を見ます
    • docker node ps
      • taskの状態を見ます
    • docker node update
      • nodeをupdate
    • docker node demote/docker node promote
      • workerに降格(demote) / managerに昇格(promote)
  • service
    • docker service create
      • serviceを作成
    • docker service ls
      • serviceの状態を見ます
    • docker service ps
      • taskの状態を見ます
    • docker service update
      • rolling updateをします
  • network
    • docker network create
      • overray networkを作成

今回使うプロセス

今回実行するプロセスは以下の通り。コードはこちら

package main

import (
     "fmt"
     "net/http"
     "strings"
     "time"
)

var wait = time.Duration(1 * time.Second)

func handler(w http.ResponseWriter, r *http.Request) {
     rec := time.Now()

     time.Sleep(wait)
     rep := time.Now()

     s := []string{
             rec.Format(time.RFC3339Nano),
             rep.Format(time.RFC3339Nano),
     }

     fmt.Fprintf(w, strings.Join(s, ","))
}

func main() {
     http.HandleFunc("/", handler)
     http.ListenAndServe(":8080", nil) // fixed port num
}

単に1秒待ってからリクエスト時とリプライ時の時刻をCSVを8080ポートで返しているだけ。1秒をblockingして待つという最悪な処理なのです。

今回buildはCircleCIに任せてあって、tar.gzを作ってあるので、各nodeでは以下のようにimportすればいい。tagは適当。

$ docker import https://<circleci artifact URL>/docker-swarm-test.tar.gz docker-swarm-test:1

ヒント

golangはglibcなどが必要なく1バイナリで実行できるので、Dockerfileとか別に要らなくてtar.gzで十分。linuxでnetを使うとdynamic linkになる件はGo 1.4でstatic binaryを作成するgolangで書いたアプリケーションのstatic link化をみてください。

$ docker service create --name web --replicas 3 --publish 8080:8080 docker-swarm-test:1 "/docker-swarm-test"
$ docker service ps web
ID                         NAME   IMAGE                NODE       DESIRED STATE  CURRENT STATE          ERROR
18c1hxqoy3gkaavwun43hyczw  web.1  docker-swarm-test:1  worker-2   Running        Running 3 minutes ago
827sjn1t4nrj7r4c0eujix2it  web.2  docker-swarm-test:1  manager-1  Running        Running 3 minutes ago
2xqzzwf2bte3ibj2ekccrt6nv  web.3  docker-swarm-test:1  worker-3   Running        Running 3 minutes ago

この状態で、コンテナが動いている worker-2,3, manager-1に対して、curlでGETすると返してくれます。それ以外に、コンテナが動いていないworker-1に対してcurlで聞いても、きちんと答えてくれます。これは、中でリクエストが転送されているからです。

rolling update

swarm clusterで--publishを付けてserviceを提供すると、一つのportで複数のnodeに対してリクエストを割り振るload balancingをやってくれます。従って、今までdockerでportが動的に変わったり、一つのnodeで同じport番号を使ってしまったりするのが問題になる場合がありましたが、その問題は生じません。また、swarm cluster内でLoad balancingを行ってくれるので、rolling updateも容易です。

% sudo docker service update --image "docker-swarm-test:1" web

というわけで、試してみました。先ほどのプロセスは1秒待つので、下手に切るとリクエストを取りこぼすはずです。

ツールにはabを使います。今回は処理能力ではなく、リクエストを取りこぼさないかどうかのテストなので、abで十分と判断しました。

% ab -rid -c 10 -n 500 http://45.76.98.219:8080/

Concurrency Level:      10
Time taken for tests:   50.146 seconds
Complete requests:      500
Failed requests:        8
   (Connect: 0, Receive: 4, Length: 0, Exceptions: 4)

ということで、取りこぼしてしまいました。残念ですね。--update-delayは次のコンテナを起動するまでの時間なので関係がなさそうです。--restart-delayも組み合わせてみましたがだめでした。nodeのステータスをdrainに手動で変更していけばうまく動くかもしれませんが、手間がかかるので試してません。

調べてみると、この辺りが関連してそうです。

次のpatch releaseで修正されるとのこと。ちょっとまだlibnetworkまで調査が足りてないのでこれで本当に直るのかは分かりませんが、今はまだ本番環境上で使うのは早そうです。

というより、nginxとか使え

というより、そもそもingress overlay networkは内部で使う用途で、外部サービスに公開する用途ではない、ということのようです。外部に公開する場合は、nginxなりを使って後述のDNS service discoveryに従って使うコンテナを決定しろ、とのことのようです。

この辺りはこれからもうちょっと調べないといけない感じです。

network

docker network createでnetworkを作成します。あとからdocker service update --network-addでnetworkを追加しようとしたら

Error response from daemon: rpc error: code = 2 desc = changing network in service is not supported

と怒られたのでserviceを作り直します。

docker service create --replicas 3 --name web --network webnet ...

その後、shellとしてalpineを立ち上げます。

$ docker service create --name shell --network webnet alpine sleep 3000
$ sudo docker service ls
ID            NAME        REPLICAS  IMAGE                COMMAND
1f9jj2izi9gr  web         3/3       docker-swarm-test:1  /docker-swarm-test
expgfyb6yadu  my-busybox  1/1       busybox              sleep 3000

execで中に入ってnslookupで同じnetworkに属しているwebserviceをDNSで探します。

$ docker exec -it shell.1.3x69i44r6elwtu02f1nukdm2v /bin/sh
/ # nslookup web

Name:      web
Address 1: 10.0.0.2

/ # nslookup tasks.web

Name:      tasks.web
Address 1: 10.0.0.5 web.3.8y9qbba8eknegorxxpqchve76.webnet
Address 2: 10.0.0.4 web.2.ccia90n3f4d2sr96m2mqa27v3.webnet
Address 3: 10.0.0.3 web.1.44s7lqtil2mk4g47ls5974iwp.webnet

webつまりservice名を聞けばVIPを、tasks.webを聞けば各nodeを直接DNS RoundRobinで答えてくれます。

このように、同じnetworkに属しさえすれば、他のServiceを名前で引くことができるので、コンテナ間の連携がしやすいと思います。

プロトコル

raft

docker swarmでは、複数のmanager node間でのLeader選出にはRaft consensusを使っています。raftの実装はetcdのraft libraryです。docker node lsでどのManagerがLeaderかが分かります。

$ docker node ls
ID                           HOSTNAME   STATUS  AVAILABILITY  MANAGER STATUS
5g8it81ysdb3lu4d9jhghyay3    worker-3   Ready   Active
6td07yz5uioon7jycd15wf0e8 *  manager-1  Ready   Active        Leader
91t9bc366rrne19j37d0uto0x    worker-1   Ready   Active
b6em4f475u884jpoyrbeubm45    worker-2   Ready   Active

raftですので、きちんとした耐故障性を持つためにはmanager nodeは最低限3台必要ですし、3台中2台が落ちたらLeader選出ができなくなります。この場合、docker swarmでは、新規のtaskを受け付けられない状態になります。

heat beat

swarm node間はheatbeatで死活監視をしています。heatbeatは通常5秒間隔ですが、docker swarm init時に--dispatcher-heartbeat durationで指定することも出来ます。死活監視の結果はgossipで分配されます。

疑問

serviceを消したらcontainerはどうなるの?

docker service rmでserviceを消すと、containerも全部消えます。消えるまでは時間がかかるので注意

worker node以上のtaskを振られたら?

nodeが3つしかない場合に、 docker swarm scale web=10とかしたらどうなるか?

答えは一つのnodeに複数のコンテナが立ち上がります。

podの概念

なさそう。 serviceを作成するときに--constraintで配置制約としてaffinityを使うとかなのかな。

あとがき

もはやコンテナ技術そのものはどうでも良くて、Kubernetesなどの複数Node管理が重要だと個人的には思っています。Docker swarm自体は今までもあったけれども、それをDocker Engineに統合することで、コンテナだけではなくその上の管理も事実上の標準を取っていくぞという意気込みが感じられます。しかも、kubernetesは使いはじめるまでが大変ですが、その手間がほぼないため、優位と感じます。

swarm clusterを組むのは簡単ですし、cluster自体はとても安定しているように思いました。もちろん、split brainなど凝ったテストをしていないのでなんともですが、raft関係はetcdのを使っていますので安定しているのではないかと思っています。

ただ、networkについてはまだ不安定なようで、serviceを作ったり消したりnetworkを作ったり消したりしていると、名前が引けなくなったりしました(詳細は追っていません)。

networkやgracefulなど、まだこなれていない点はありますが、今後Docker swarmは普及していくのではないかな、と思います。