失敗は一時の恥

パッケージソフト開発をしているプログラマが気の赴くままに何かを投稿するブログ.

Ruby で ZooKeeper の四文字コマンドを呼ぶ

Ruby で ZooKeeper の四文字コマンドを呼ぶ

About

Ruby で ZooKeeper の四文字コマンドを呼ぶ方法の備忘録. 単に TCP ソケットで入出力するだけです.

やり方

TCPSocket を開いて,四文字を送信して出力を読み出します.

require 'socket'

socket = TCPSocket.open('localhost', 2181)
socket.write('stat')
socket.each_line do |line|
  puts line
end

これだけ!

Automator でスキャン画像を PDF にする

Automator でスキャン画像を PDF にする

概要

macOSAutomator を使って, スキャンした複数ページの文書を PDF にして連結する方法の備忘録です.

Automator のワークフローを作る

Automator のワークフローを作ります.

フローの内容は,次のようになります:

  • 指定された Finder 項目を取得
  • フォルダの内容を取得
  • Finder 項目を並べ替える
  • イメージから新規 PDF を作成

実際に作ったワークフローはこんな感じです.

f:id:snobutaka:20180421094020p:plain
workflow

ワークフローを実行

ワークフローを実行します.

ここでは例として,猫の画像がいくつか (複数ページのスキャン文書だと思ってください) を連結して,猫のフォトブック (連結された複数ページの文書だと思ってください) を作ります.

猫の画像:

f:id:snobutaka:20180421094016j:plainf:id:snobutaka:20180421094013j:plainf:id:snobutaka:20180421094009j:plain

ワークフローを実行するとこんな PDF が得られます.

おわり

手軽だからという理由で Automator を使いましたが, Ruby とか Python あたりののスクリプト言語でサクッと実装できたらかっこいいですね.

Docker コンテナからホストにアクセスする

Docker コンテナからホストにアクセスする

概要

Docker コンテナからホスト (macOS) にアクセスする方法を探していたら, host.docker.internal という名前を使えばよいということが公式リファレンスに書いてありました.

試してみるといとも簡単にアクセスできました.

※ちなみに Docker for Windows の公式リファレンスにも同じことが書いてあるのですが,こちらは試していません.(職場で試してみようかな……)

環境

  • ホスト OS: macOS High Sierra 10.13.4
  • Docker: Docker for Mac 18.03 CE,詳細は以下の通り
$ docker version
Client:
 Version:   18.03.0-ce
 API version:   1.37
 Go version:    go1.9.4
 Git commit:    0520e24
 Built: Wed Mar 21 23:06:22 2018
 OS/Arch:   darwin/amd64
 Experimental:  false
 Orchestrator:  swarm

Server:
 Engine:
  Version:  18.03.0-ce
  API version:  1.37 (minimum version 1.12)
  Go version:   go1.9.4
  Git commit:   0520e24
  Built:    Wed Mar 21 23:14:32 2018
  OS/Arch:  linux/amd64
  Experimental: false

検証

コンテナから macOS にアクセスできることを試すのに, まずは適当な Web サーバを起動しておきます. Rubysinatra を使って適当なサーバを起動してみました.

$ gem install sinatra
$ ruby -r sinatra -e 'get "/" do "Hello world from macOS!"; end' > /dev/null 2>&1 &
$ curl http://localhost:4567
Hello world from macOS!

この "Hello world from macOS!" というメッセージをコンテナから得られるということを確認します.

適当に alpine のイメージと curl を使います.

$ docker run --rm -it alpine:3.7 /bin/sh
(以下コンテナ内の世界)
$ wget -O - http://host.docker.internal:4567 2>/dev/null
Hello world from macOS!

コンテナから macOS にアクセスできました!

Docker Machine の場合

安直に「docker-machine でも同じことできないの?」と思い試してみましたが,できないようでした.

$ eval $(docker-machine env default)
$ docker run --rm -it alpine:3.7 /bin/sh
(以下コンテナ内の世界)
$ ping host.docker.internal
ping: bad address 'host.docker.internal'

おわり

host.docker.internal というものを見つけたおかげで,あまりにも簡単に目的を達成できてしまった感があります. 正直にいうと,Docker のネットワーク周りはきちんと理解できていないのでちゃんと勉強したい.

参考

Java で空の JSON を作る

Java で空の JSON を作る

概要

Java でテストのために空の JSON {} を作ろうとしてちょっと悩んだのでその備忘録.

空の JSON が必要なら,単純に "{}" と書けばいいじゃないかとも思いますが,何かしらのオブジェクトを ObjectMapper を介してシリアライズしなければいけないというシチュエーションだったのです.

作り方

まず JSON にすべき空のクラスを用意しておきます.

public class EmptyClass {}

ObjectMapper#disable(SerializationFeature) メソッドを使う

JSON を生成する ObjectMapper にインスタンスを直接触れられるならば,

objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
objectMapper.writeValueAsString(new EmptyClass());

で OK です.

そもそも,空のクラスをシリアライズしようとすると次のような例外が出るので,これがすぐに見つかる解決策です.

com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class EmptyClass and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) )

@JsonSerialize アノテーションを使う

ObjectMapper のインスタンスに直接触れられない場合 (ライブラリやフレームワークの奥の方で ObjectMapper が使われている等),上述の方法は使えません. ライブラリやフレークワークに「この ObjectMapper 使ってね」みたいなことができればそれでもいいですが,シリアライズされる側のクラスにアノテーション @JsonSerialize を付けてしまうことでも解決できます.

@JsonSerialize
public class EmptyClass {}

おまじない感は少々あります.

参考

ZooKeeper の管理コマンド

ZooKeeper の管理コマンド

概要

Apache ZooKeeper には,稼働状況などを取得するための管理コマンドがあります.

この管理コマンドでどのようなことができるのか試してみたいと思います.

管理コマンド

管理コマンドの内容は,ZooKeeper Administrator's Guide で説明されています.

管理コマンドは,クライアントライブラリ等を使用するのではなく,TCP 接続でメッセージを送ることで呼び出します. 送信するメッセージが 4 文字なので,4 文字コマンド (Four Letter Worlds commands) と呼ばれます.

最も簡単な例として,"ruok" コマンドを呼んでみます.これは "Are you OK?" の意味で,ZooKeeper が稼働していれば "imok" を返すというだけのものです.

$ echo ruok | nc localhost 2181
imok

このようなコマンドが十数個提供されています.

  • conf
  • cons
  • crst
  • dump
  • envi
  • ruok
  • srst
  • srvr
  • stat
  • wchs
  • wchc
  • wchp
  • mntr

管理コマンドを有効にする

呼び出せる管理コマンドは,プロパティファイル zoo.cfg の 4lw.commands.whitelist で指定することができます (あるいは,Javaシステムプロパティ zookeeper.4lw.commands.whitelist でも可).

このプロパティを指定しなかった場合はデフォルトで,"wchp" と "wchc" を除くすべてのコマンドが呼び出せる状態となっています.

注意!: ZooKeeper 3.5 ではこのデフォルトが変更されています.プロパティ未指定の場合に呼び出せるコマンドは srvr のみです.

各コマンドの内容

各コマンドを実際に呼んでみます. 数がそこそこあるので,ゆるい感じでサクっと試します.

ここで挙げる例は,Docker で起動した ZooKeeper から得られたものです.

conf

適用されている設定 (zoo.cfg に記述するもの) を取得します.

$ echo conf | nc localhost 2181
clientPort=2181
dataDir=/data/version-2
dataLogDir=/datalog/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=0

上記の出力について,実際の zoo.cfg には dataDir=/data と指定しているのですが,それとは異なる値が取れてきています. 細かい挙動は注意して使ったほうがいいのかもしれません.

cons

現在サーバーに接続してるすべてのクライアントのコネクション/セッションを表示します. 送受信したパケット数,セッション ID,レイテンシ等さまざまな情報が付随します.

echo cons | nc localhost 2181
/127.0.0.1:37251[0](queued=0,recved=1,sent=0)
/127.0.0.1:58686[1](queued=0,recved=4,sent=4,sid=0x100088f2a4e0000,lop=PING,est=1523313273401,to=30000,lzxid=0x3,lresp=144169653,llat=1,minlat=0,avglat=0,maxlat=1)
/127.0.0.1:58740[1](queued=0,recved=4,sent=4,sid=0x100088f2a4e0001,lop=PING,est=1523313322911,to=30000,lzxid=0x3,lresp=144164469,llat=0,minlat=0,avglat=0,maxlat=0)

crst

コネクション/セッションに関する統計をすべてリセットします (Connection reset の略?). コネクションを切断するわけではありません.

echo crst | nc localhost 2181
Connection stats reset.

dump

リファレンスには

Lists the outstanding sessions and ephemeral nodes. This only works on the leader.

とあります.試しに zkcli から短命ノード /ephemeral_node を作成して,コマンドを実行してみると……

$ echo dump | nc localhost 2181
SessionTracker dump:
Session Sets (6):
0 expire at Sat Jan 03 04:32:32 GMT 1970:
0 expire at Sat Jan 03 04:32:34 GMT 1970:
0 expire at Sat Jan 03 04:32:42 GMT 1970:
0 expire at Sat Jan 03 04:32:44 GMT 1970:
1 expire at Sat Jan 03 04:32:52 GMT 1970:
    0x1000b42a5440001
1 expire at Sat Jan 03 04:32:54 GMT 1970:
    0x1000b42a5440000
ephemeral nodes dump:
Sessions with Ephemerals (1):
0x1000b42a5440000:
    /ephemeral_node

一見よくわからないものもありますが (← おい),0x1000b42a54400010x1000b42a5440000 は zkcli で接続しているコネクションです. また,ID が 0x1000b42a5440000 のセッションが /ephemeral_node という短命ノードを作成していることが読み取れます.

envi

サーバ環境に関する情報を取得します. Javaシステムプロパティのようなものでしょうか.

$ echo envi | nc localhost 2181
Environment:
zookeeper.version=3.4.11-37e277162d567b55a07d1755f0b31c32e93c01a0, built on 11/01/2017 18:06 GMT
host.name=7de453fdb571
java.version=1.8.0_151
java.vendor=Oracle Corporation
java.home=/usr/lib/jvm/java-1.8-openjdk/jre
java.class.path=/zookeeper-3.4.11/bin/../build/classes:/zookeeper-3.4.11/bin/../build/lib/*.jar:/zookeeper-3.4.11/bin/../lib/slf4j-log4j12-1.6.1.jar:/zookeeper-3.4.11/bin/../lib/slf4j-api-1.6.1.jar:/zookeeper-3.4.11/bin/../lib/netty-3.10.5.Final.jar:/zookeeper-3.4.11/bin/../lib/log4j-1.2.16.jar:/zookeeper-3.4.11/bin/../lib/jline-0.9.94.jar:/zookeeper-3.4.11/bin/../lib/audience-annotations-0.5.0.jar:/zookeeper-3.4.11/bin/../zookeeper-3.4.11.jar:/zookeeper-3.4.11/bin/../src/java/lib/*.jar:/conf:
java.library.path=/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
java.io.tmpdir=/tmp
java.compiler=<NA>
os.name=Linux
os.arch=amd64
os.version=4.9.87-linuxkit-aufs
user.name=zookeeper
user.home=/home/zookeeper
user.dir=/zookeeper-3.4.11

ruok

ZooKeeper が正常に稼働していれば「imok」と応答します.それ以外は何も返しません.(Are you OK の略)

$ echo ruok | nc localhost 2181
imok

srst

サーバの統計情報をリセットします.(Server reset の略?)

$ echo srst | nc localhost 2181
Server stats reset.

srvr

サーバの全詳細情報を取得します. サーバがリーダーなのかフォロワーなのか,それともスタンドアローンなのかもこれでわかります.

$ echo srvr | nc localhost 2181
Zookeeper version: 3.4.11-37e277162d567b55a07d1755f0b31c32e93c01a0, built on 11/01/2017 18:06 GMT
Latency min/avg/max: 0/0/0
Received: 16
Sent: 16
Connections: 3
Outstanding: 0
Zxid: 0x3
Mode: standalone
Node count: 5

stat

サーバとコネクションに関する情報の要約を出力します.

echo stat | nc localhost 2181
Zookeeper version: 3.4.11-37e277162d567b55a07d1755f0b31c32e93c01a0, built on 11/01/2017 18:06 GMT
Clients:
 /127.0.0.1:58902[1](queued=0,recved=4421,sent=4421)
 /127.0.0.1:58904[1](queued=0,recved=4418,sent=4418)
 /127.0.0.1:40145[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/0/10
Received: 89
Sent: 89
Connections: 3
Outstanding: 0
Zxid: 0x3
Mode: standalone
Node count: 5

wchs

設定されている監視の要約を取得します.

echo wchs | nc localhost 2181
1 connections watching 1 paths
Total watches:1

wchc

設定されている監視を,それを設定しているコネクションごとに取得します.

$ echo wchc | nc localhost 2181
0x1000b42a5440000
    /watch

wchp

監視が設定されているパスと,そこを監視しているコネクションを取得します.

$ echo wchp | nc localhost 2181
/watch
    0x1000b42a5440000

mntr

ZooKeeper アンサンブルのヘルスをモニタリングするのに使えそうな値を取得します.

echo mntr | nc localhost 2181
zk_version  3.4.11-37e277162d567b55a07d1755f0b31c32e93c01a0, built on 11/01/2017 18:06 GMT
zk_avg_latency  0
zk_max_latency  10
zk_min_latency  0
zk_packets_received 302
zk_packets_sent 302
zk_num_alive_connections    3
zk_outstanding_requests 0
zk_server_state standalone
zk_znode_count  6
zk_watch_count  1
zk_ephemerals_count 1
zk_approximate_data_size    93
zk_open_file_descriptor_count   29
zk_max_file_descriptor_count    1048576

Java から管理コマンドを呼ぶ

通常クライアントとして使用する クラス ZooKeeper からコマンドを呼ぶことはできないので,ソケットを繋いで取得します.

Socket socket = new Socket("localhost", 2181)
OutputStream out = socket.getOutputStream()
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
out.write("ruok".getBytes())
in.readLine() // 正常に稼働中なら "imok" が返る

コマンド用の簡単なクライアントをこちらで実装 (途中) しています: https://github.com/snobutaka/zk-command

おわり

dump など見方がパッと分からないものもありましたが…… ruok, srvr, mntr あたりは ZooKeeper のステータスチェックに利用できそうに感じました.

ZooKeeper 3.5 では Dynamic Reconfiguration という機能も追加されているので,管理コマンドと組み合わせてアンサンブルを動的に運用したり……がうまくできると面白そうです.

ZooKeeper の動的再構成

ZooKeeper 3.5 の動的再構成

概要

Apache ZooKeeper 3.5 では,Dynamic Reconfiguration という機能が追加されています.

この機能を使うと,いままでは適用に再起動を要していた設定変更を,再起動なしで即時反映させることができるようになります.

この記事では,この Dynamic Recofiguration を試してみたいと思います.

ZooKeeper 3.5 を使用する際の注意点

ZooKeeper 3.5 で Dynamic Reconfiguration を試す前に,いくつか注意しておいたほうがいいことを挙げます.

3.5 系の最新バージョンは 3.5.3-beta

2018-04-07 時点で,ZooKeeper 3.5 の最新版は 3.5.3-beta です. (ちなみに 3.5.3-beta のリリースはおよそ一年前の 2017-04-17 です……)

ちなみに,Apache Curator という ZooKeeper のライブラリがあるのですが,そこの説明では「ZooKeeper の開発チームは 3.5 を "beta" としているが,実際には多くのユーザーがプロダクションで使用している」と言われています. 実際どうなのでしょう……?

設定の変更

ZooKeeper 3.5 では,アンサンブルを構成するサーバのアドレスを指定する方法が 3.4 から変わっています.

従来の clientPort というプロパティは非推奨となり,代わりにサーバーアドレス指定の一部に指定するなりました.

clientPort=1236 # このプロパティは廃止
server.5=125.23.63.23:1234:1235;1236 # セミコロンに続けてポートを指定する

加えて,従来のプロパティ clientPortAddress もサーバーアドレス指定の一部として移行されており,さらに participant OR observer の指定もあるため,指定方法の例として次の 5 通りの記述が紹介されています.

# どれも有効な指定方法
server.5=125.23.63.23:1234:1235;1236
server.5=125.23.63.23:1234:1235:participant;1236
server.5=125.23.63.23:1234:1235:observer;1236
server.5=125.23.63.23:1234:1235;125.23.63.24:1236
server.5=125.23.63.23:1234:1235:participant;125.23.63.23:1236

3.4 からのデータ移行

サービスを無停止でローリングアップデートする場合,3.4.6 以降を使用しているならばそのまま 3.5 に移行できます. そうでない場合は,一度 3.4.6 へアップデートする必要があります.

ただし,一度サービスを全停止するならば,3.4.6 を経ずに直接 3.5 へ移行できるようです.

Dynamic Recofiguration の設定内容

機能的な設定

Dynamic Reconfiguration を行うには,大きく次の 2 つのプロパティが関係してきます. (Administrator's Guide, Advanced Configuration を参照)

  • reconfigEnabled
  • standaloneEnabled

まず,Dynamic Reconfiguration を行うためにはプロパティに reconfigEnabled=true を指定してやる必要があります.

次に,standaloneEnabled=false を指定すると,ZooKeeper を 1 台構成から複数台構成へ拡張することや,逆に,複数台構成から 1 台構成へ縮退させることができます.

standaloneEnabled=true に設定した場合は,これらの操作はできません.つまり,ZooKeeper を起動する時点で複数台の構成を取っておくことが前提となります.

この standaloneEnabled については false の指定が望ましく,レガシーなスタンドアローンモードは将来的に非推奨になるとのことです.

セキュリティに関する設定

Dynamic Reconfiguration はセキュリティの観点から,ACL を適切に設定した上で,認証されたユーザーしか実行できないようになっています.

ただし,悪意ある者がアクセスできないような環境であることが保証されているならば,プロパティに skipACL=yes を指定しておくことで認証を不要とすることもできます. (Administrator's Guide, Encryption, Authentication, Authorization Options を参照.)

設定ファイルの分割

3.4 までの設定ファイルは zoo.cfg があるのみでしたが,3.5 では

  • 起動後に変更できないプロパティは今まで通り zoo.cfg
  • 起動後に変更できるプロパティは zoo.cfg.dynamic.{zxid}

というように分割されます.

起動時は今まで通りに zoo.cfg にすべての設定を書いておいても OK ですが, 起動すると ZooKeeper がプロパティファイルを書き換えて上記の 2 ファイルに分割されます.

zoo.cfg.dynamic.{zxid} のファイルは,基本的に自動生成されるものであって,人間が手を加えるべきものではありません. Dynamic Configuration を実行した際に ZooKeeper が取り扱い,その際の zxid がファイルの末尾に付加されます.

ファイルたちをコマンドで確認すると,例えば次のような構成になります:

$ ls /conf
zoo.cfg                    zoo.cfg.dynamic.100000000
log4j.properties           zoo.cfg.bak
$ cat /conf/zoo.cfg
initLimit=5
syncLimit=2
maxClientCnxns=60
tickTime=2000
dataDir=/data
dataLogDir=/datalog
dynamicConfigFile=/conf/zoo.cfg.dynamic.100000000
$ cat /conf/zoo.cfg.dynamic.100000000 
server.1=localhost:2888:3888:participant;0.0.0.0:2181

zoo.cfg.bak は,分割される前のファイルがバックアップとして残されているものです.

zoo.cfg の中で dynamicConfigFile=/conf/zoo.cfg.dynamic.100000000 というように,ダイナミックな方のファイルパスが記述されています.

Dynamic Reconfiguration の動作仕様

incremental モードと bulk モード

Dynamic Reconfiguratin の操作には incremental モードと bulk モードの 2 種類があります.

incremental モードでは,現在の状態に対して追加・削除を行います.

bulk モードでは,新たに適用させたい状態を指定して構成の変更をかけます.

サーバの除去

アンサンブル内のどのサーバも,除去することができます.リーダーも除去できますが,多少のサービスダウンが発生します.

除去されたサーバは直ちに停止するわけではなく,"non-voting follower" という状態になります. この状態では,データの読み書きはできるのですが,クオラムへの投票は行わないという動作になります.

non-voting follower の動作は observer に似ているのですが,内部の動作が observer よりもコストのかかるものとなっており,この状態で稼働し続けるのは好ましくありません. あくまでも,管理者からの停止や何らかの次の操作を受けるまでの一時的な状態です.

サーバの追加

サーバを追加する際は,これから追加されるサーバが既存のアンサンブルのリーダーに接続し同期が取れている必要があります.

Dynamic Reconfiguration を実行してみる

それでは Dynamic Reconfiguration を試してみます.

Docker イメージ snobutaka/zookeeper:3.5.3-beta で,ZooKeeper を起動します. このイメージは,公式の ZooKeeper イメージ では考慮されていないプロパティを追加でいくつか設定できるようにしたものです.

docker-compose を使って,ZooKeeper を 3 台立ち上げます. 次に示す docker-compose.yml では,

  • zoo1 と zoo2 がアンサンブルを構成する
  • zoo3 が上記のアンサンブルに接続する (参加はしていない)

という状況が作られます. 想定としては,2 台のアンサンブルを運用しているところに,新たに 3 台目を追加しようとしているといったシチュエーションです (3 台目も docker-compose で同時に立ち上がりはするのですが……).

version: '3.1'

services:
  zoo1:
    image: snobutaka/zookeeper:3.5.3-beta
    restart: always
    hostname: zoo1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181
      ZOO_STANDALONE_ENABLED: 'false'
      ZOO_RECONFIG_ENABLED: 'true'
      ZOO_4LW_COMMANDS_WHITELIST: '*'
      ZOO_SKIP_ACL: 'yes'

  zoo2:
    image: snobutaka/zookeeper:3.5.3-beta
    restart: always
    hostname: zoo2
    ports:
      - 2182:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181
      ZOO_STANDALONE_ENABLED: 'false'
      ZOO_RECONFIG_ENABLED: 'true'
      ZOO_4LW_COMMANDS_WHITELIST: '*'
      ZOO_SKIP_ACL: 'yes'

  zoo3:
    image: snobutaka/zookeeper:3.5.3-beta
    restart: always
    hostname: zoo3
    ports:
      - 2183:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888:observer;2181
      ZOO_STANDALONE_ENABLED: 'false'
      ZOO_RECONFIG_ENABLED: 'true'
      ZOO_4LW_COMMANDS_WHITELIST: '*'
      ZOO_SKIP_ACL: 'yes'

これでコンテナを起動します.

$ docker-compose up -d

zoo1 または zoo2 に zkcli で接続して作業を行います.

$ docker-exec -it (zoo1 または zoo2 のコンテナ名) /bin/bash
$ /zookeeper-3.5.3-beta/bin/zkCli.sh

zkcli から config コマンドを実行すると,現在の構成が取得できます.

[zk: localhost:2181(CONNECTED) 0] config
server.1=zoo1:2888:3888:participant;0.0.0.0:2181
server.2=zoo2:2888:3888:participant;0.0.0.0:2181
version=100000000

まだ zoo3 は参加していないので,2 台分の情報が出力されます.

それでは,Dynamic Reconfiguration を実行しましょう!

まず,現在のアンサンブルに接続しているが参加はしていない zoo3 を,正式にアンサンブルに参加させます.

[zk: localhost:2181(CONNECTED) 1] reconfig -add 3=zoo3:2888:3888;2181
Committed new configuration:
server.1=zoo1:2888:3888:participant;0.0.0.0:2181
server.2=zoo2:2888:3888:participant;0.0.0.0:2181
server.3=zoo3:2888:3888:participant;0.0.0.0:2181
version=100000004

地味ですが zoo3 がアンサンブルに参加しました!

次に,サーバを除外してみましょう. 今度は 2 台を同時に始末します.

[zk: localhost:2181(CONNECTED) 4] reconfig -remove 2,3
Committed new configuration:
server.1=zoo1:2888:3888:participant;0.0.0.0:2181
version=100000005

zkcli から quit し元のシェルに戻り,ついでにコンテナも停止してしまいましょう.

$ docker stop (zoo2 のコンテナ名) (zoo3 のコンテナ名)

これで 3 台中 1 台のみが残っている状態なので,アンサンブルが 3 台の設定のままならばサービスが停止しているところです. しかし,Dynamic Reconfiguration を使ってアンサンブルを 1 台の設定にしておいたため,今でも zoo1 はサービスを継続できています!

Dynamic Reconfiguration でできないこと

別々に稼働してる 2 つのアンサンブルを統一することはできません. (注: ドキュメントを読んでいる限りできないようにしか思えませんでした.もしもできる方法があるのならご教示いただきたいです……)

先の実行例は,「zoo1 と zoo2 がアンサンブルを構成済みのところに,zoo3 が接続する」という状況でした.

そうではなく,「zoo1 と zoo2 がアンサンブルを構築済みで,zoo3 は単独のアンサンブルとして起動済みである.この zoo3 を zoo1, zoo2 のアンサンブルに追加する」ということはできるのでしょうか?

答えは,「できない」になります.

実際に zoo3 を単独で起動してから,zoo1, zoo2 に接続しようとしても次のようなエラーとなります.

$ reconfig -add 1=zoo1:2888:3888;2181,2=zoo2:2888:3888;2181
No quorum of new config is connected and up-to-date with the leader of last commmitted config - try invoking reconfiguration after new servers are connected and synced

Dynamic Reconfiguration でサーバを追加するときは,新たなサーバーが既存のアンサンブルのリーダーに接続した状態で行う必要があるのでした. zoo3 で上記の操作を行った場合,zoo3 からすれば「zoo1 と zoo2 が自分につながってきていない」ということになります.

逆に,zoo1, zoo2 の側で reconfig -add zoo3 をしても同じことになるので,別個に稼働してしまったアンサンブルは合流できないということです.

おわり

Dynamic Reconfiguration を簡単に紹介しましたが,説明しきれていない設定やコマンド,注意事項がまだまだあります.実運用などで使用する際は公式のドキュメントをよく確認する必要があります.

公式ドキュメントの中で何度も参照されているのですが,Dynamic Reconfiguration の詳細については論文を参照しろとされています. 深く正確に理解するにはこちらも読んだほうがよいのでしょう (僕もまだ読んでないです).

Docker swarm チュートリアル

Docker swarm チュートリアル

この記事について

Docker swarm を公式ドキュメントのチュートリアルに沿って試してみた内容をまとめています.

使うもの

Set up に書いてある通り, Docker が動く互いにネットワーク接続できるマシン (仮想でもよい) が 3 台必要になります.

僕は macOS 上で docker-machine コマンドと VirtualBox を利用して……

$ docker-machine create manager1
$ docker-machine create worker1
$ docker-machine create worker2

仮想マシンを用意しました.

f:id:snobutaka:20180401092232p:plain
チュートリアルで使用する仮想マシン

Docker swarm is 何?

Docker swarm は,複数のマシンからなる Docker のクラスターを構築するための仕組みです.

swarm の基本概念は Swarm mode key concepts にまとめられています. 大まかにかいつまむと,次のようなキーワードが登場します.

  • Node: swarm のクラスターを構成する各マシン (Docker engine) のことを Node と呼びます.Node には manager と worker の 2 通りがあり,
    • manager はクラスターの状態管理を行います.また,worker にタスクをアサインします.manager 自身も worker と同様にタスクを実行することもできます.
    • worker は manager からアサインされたタスクを実行します.
  • Service と Task:
    • Task は Docker コンテナとそこで実行するコマンドのことです.
    • Service はクラスターで実行するタスクの定義で,レプリカ数の設定に応じた数の Task が実行されるように指定すること等ができます.
  • Load Balancing: クラスターのノード外へサービスを公開する際に,クラスター内で負荷分散が行われるようになります.クラスターを構成しているいずれのノードにリクエストしても (そのノードでは Task が実行されていなくても),サービスにアクセスすることが可能です.

コンテナのオーケストレーションといえば Kubernetes が有名ですが,swarm はもともと Docker の一部として提供されているのもあり,より簡易にトライできそうな雰囲気です.

swarm を構成する

manager のセットアップ

まず,manager として稼働させるマシンに接続します

$ docker-machine ssh manager1

manager として Docker swarm を初期化します.

docker@manager1:~$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (tul5yq5nqs72u8mkvvd947w9m) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-20egj6uc7b1q03t93updrwlfi2m7082nsddxvc9sgeywqvqd4l-14kopj72ycqvysdv2lsy6epm5 192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

出力されるメッセージの中に,今初期化した swarm に対して新たに他の Node を参加させるコマンドも記述されています.

  • worker を追加する: docker swarm join --token SWMTKN-1-20egj6uc7b1q03t93updrwlfi2m7082nsddxvc9sgeywqvqd4l-14kopj72ycqvysdv2lsy6epm5 192.168.99.100:2377
  • manager を追加する: docker swarm join-token manager を実行し,案内に従う.

swarm に参加している Node を確認するには,docker node コマンドを使用します.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
tul5yq5nqs72u8mkvvd947w9m *   manager1            Ready               Active              Leader              18.03.0-ce

ID の隣に * 印がついているのは,現在コマンドを実行している Node 自身を表しています.

worker の追加

先ほど manager を初期化した際に出力されたコマンドを使って,worker1 を swarm に参加させます.

ssh 接続をしてから……

$ docker-machine ssh worker1

docker swarm join コマンドを実行します

docker@worker1 $ docker swarm join --token SWMTKN-1-20egj6uc7b1q03t93updrwlfi2m7082nsddxvc9sgeywqvqd4l-14kopj72ycqvysdv2lsy6epm5 192.168.99.100:2377

同じことを worker2 でも行います.

そして,manager1 から docker node ls を実行すると 3 台の Node が swarm に参加できたことがわかるでしょう.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
tul5yq5nqs72u8mkvvd947w9m *   manager1            Ready               Active              Leader              18.03.0-ce
py07mcf6fk9ino9hctipnwpyj     worker1             Ready               Active                                  18.03.0-ce
mf9qqqgr8uk212lz5a837zccf     worker2             Ready               Active                                  18.03.0-ce

docker node ls は,manager からしか実行できないので注意が必要です.

swarm にサービスをデプロイする

Node 3 台からなる swarm のクラスターを構成できたので,サービスをデプロイしてみます.

サービスをデプロイするには,manager からdocker service create コマンドを実行します.

docker@manager1:~$ docker service create --replicas 1 --name helloworld alpine ping docker.com

docker run とかなり似ています. 見慣れないオプションの "--replicas 1" は,読んで字のごとく docker プロセスを (レプリカを) 1 つ走らせるということです.

起動しているサービスは,docker service ls コマンドで確認します.

docker@manager1:~$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
lljs26w9vi5x        helloworld          replicated          1/1                 alpine:latest       

docker service inspect コマンドを使えば,サービスの使用するイメージや実行するコマンドといった詳細を得ることができます.

docker@manager1:~$ docker service inspect --pretty helloworld

ID:     lljs26w9vi5xhhy7jgr1kjx6w
Name:       helloworld
Service Mode:   Replicated
 Replicas:  1
Placement:
UpdateConfig:
 Parallelism:   1
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Update order:      stop-first
RollbackConfig:
 Parallelism:   1
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Rollback order:    stop-first
ContainerSpec:
 Image:     alpine:latest@sha256:7df6db5aa61ae9480f52f0b3a06a140ab98d427f86d8d5de0bedab9b8df6b1c0
 Args:      ping docker.com 
Resources:
Endpoint Mode:  vip

(--pretty オプションを指定しない場合は,JSON 形式で出力されます)

サービスとして動いている docker プロセスは,docker service ps コマンドで取得できます.

docker@manager1:~$ docker service ps helloworld
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
rtvl9wo07nwn        helloworld.1        alpine:latest       worker2             Running             Running 7 minutes ago                       

どの Node で何のイメージが実行されているのかが分かります.

サービスをスケールする

サービスをデプロイできたので,今度はサービスをスケールしてみましょう. クラスターらしいですね.

サービスのスケールとは,サービスを構成するタスク (稼働するコンテナ) の数を増減することです.次のようなコマンドを実行します.

docker@manager1:~$ docker service scale helloworld=5
helloworld scaled to 5
overall progress: 5 out of 5 tasks 
1/5: running   [==================================================>] 
2/5: running   [==================================================>] 
3/5: running   [==================================================>] 
4/5: running   [==================================================>] 
5/5: running   [==================================================>] 
verify: Service converged 

実際にタスクが増えていることを確認しましょう.

docker@manager1:~$ docker service ps helloworld
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
rtvl9wo07nwn        helloworld.1        alpine:latest       worker2             Running             Running 14 minutes ago                       
syl5rksdy779        helloworld.2        alpine:latest       manager1            Running             Running 34 seconds ago                       
o3c3oa99a5te        helloworld.3        alpine:latest       manager1            Running             Running 34 seconds ago                       
szf1qy7flz8j        helloworld.4        alpine:latest       worker2             Running             Running 39 seconds ago                       
5xyffmmtgahj        helloworld.5        alpine:latest       worker1             Running             Running 34 seconds ago                       

タスクが 3 ノードに適当に分散されていることも分かります. なぜか manager1 が 2 タスク,worker1 が 1 タスクという割り振りになっていますが,まぁそういうこともあるのでしょう……

サービスを削除する

helloworld サービスの役目はここで終わりになります.サービスを停止するには docker service rm を実行します.削除できたことは docker service ls や,各ノードで docker ps を実行すれば確認できます.

docker@manager1:~$ docker service rm helloworld
helloworld
docker@manager1:~$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
# 何もなし!
docker@manager1:~$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
# 何もなし!すべてのコンテナが消えるのには何秒かかかります.

サービスをローリングアップデートする

サービスを構成するタスクを,全て一斉にではなく一部ずつアップデートしていくことをローリングアップデートといいます.

アップデートに伴って問題が発生した場合に,影響範囲を絞ることができるといったメリットがあります.

まずは redis:3.0.6 の 3 タスクからなるサービスを作成します.

docker@manager1:~$ docker service create --replicas 3 --name redis --update-delay 10s redis:3.0.6

--update-delay は,タスクのアップデート同士の間隔を設定します.

デフォルトでは 1 度にアップデートされるタスクは 1 つですが,--update-parallelism オプションにより同時にアップデートを行うタスクの最大数を設定することもできます.

もしもアップデートが失敗した場合の動作は,--update-failure-action で指定が可能です.デフォルトでは,アップデートを中断します.

それでは,アップデートを実行しましょう.

docker@manager1:~$ docker service update --image redis:3.0.7 redis
redis
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

無事にコマンドが完了したら,アップデートされていることを確認しましょう.

docker@manager1:~$ docker service inspect --pretty redis

ID:     ms5ooar390takwiill06y6ey8
Name:       redis
Service Mode:   Replicated
 Replicas:  3
UpdateStatus:
 State:     completed
 Started:   2 minutes ago
 Completed: About a minute ago
 Message:   update completed
Placement:
UpdateConfig:
 Parallelism:   1
 Delay:     10s
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Update order:      stop-first
RollbackConfig:
 Parallelism:   1
 On failure:    pause
 Monitoring Period: 5s
 Max failure ratio: 0
 Rollback order:    stop-first
ContainerSpec:
 Image:     redis:3.0.7@sha256:730b765df9fe96af414da64a2b67f3a5f70b8fd13a31e5096fee4807ed802e20
Resources:
Endpoint Mode:  vip
docker@manager1:~$ docker service ps redis
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
ofy9j61ayidp        redis.1             redis:3.0.7         worker1             Running             Running 3 minutes ago                        
6nq293pf2jtz         \_ redis.1         redis:3.0.6         worker1             Shutdown            Shutdown 3 minutes ago                       
y9kxv1cwsxpe        redis.2             redis:3.0.7         worker2             Running             Running 2 minutes ago                        
ifuzzfy80igc         \_ redis.2         redis:3.0.6         worker2             Shutdown            Shutdown 2 minutes ago                       
wmybv3j8w5jd        redis.3             redis:3.0.7         manager1            Running             Running 2 minutes ago                        
yfmnr47k8wt0         \_ redis.3         redis:3.0.6         manager1            Shutdown            Shutdown 2 minutes ago                       

イメージが redis:3.0.6 から redis:3.0.7 に切り替わっていることが分かります.

ノードを停止する

サービスを稼働中でも,計画メンテナンスなどでマシンを停止させる必要性は発生します. swarm のノードを "drain" ステータスにすることで,その間はタスクの割り当てなどを受け付けないようにするということができます.

(この "drain" というステータスはあくまでも swarm のステータスであり,docker run コマンドなどで起動されたコンテナの管理については関知しません.)

いま,worker1 をメンテナンスのため停止したいものとします. 稼働しているタスクはこのようになっているとします.

docker@manager1:~$ docker service ps redis
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
uw92a8l1w1sq        redis.1             redis:3.0.6         manager1            Running             Running 3 minutes ago                       
pv988a7ecmdt        redis.2             redis:3.0.6         worker1             Running             Running 3 minutes ago                       
ec4z7huzr27h        redis.3             redis:3.0.6         worker2             Running             Running 3 minutes ago                       

manager1 から次のようなコマンドを実行することで,worker1 を停止させることができます.

docker@manager1:~$ docker node update --availability drain worker1

dokcer node コマンドを確認してみましょう.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
o67397jwrzch9ccxkhre3sgrh *   manager1            Ready               Active              Leader              18.03.0-ce
xpeqk27zg8x2rnf1qpcail6de     worker1             Ready               Drain                                   18.03.0-ce
9i8v2ked45bm572ti34oyu3r5     worker2             Ready               Active                                  18.03.0-ce

さらに docker node inspect コマンドを使えば,worker1 のより詳細な情報を取得できます.

docker@manager1:~$ docker node inspect --pretty worker1
ID:         xpeqk27zg8x2rnf1qpcail6de
Hostname:               worker1
Joined at:              2018-03-31 11:27:28.770241361 +0000 utc
Status:
 State:         Ready
 Availability:          Drain
... (以下省略) ...

たしかに worker1 が Drain ステータスになっています. このとき,稼働していたタスクはどうなっているのでしょうか? docker service ps コマンドで確認してみましょう.

docker@manager1:~$ docker service ps redis
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
uw92a8l1w1sq        redis.1             redis:3.0.6         manager1            Running             Running 10 hours ago                        
31vg14mao63n        redis.2             redis:3.0.6         manager1            Running             Running 10 hours ago                        
pv988a7ecmdt         \_ redis.2         redis:3.0.6         worker1             Shutdown            Shutdown 10 hours ago                       
ec4z7huzr27h        redis.3             redis:3.0.6         worker2             Running             Running 10 hours ago                        

worker1 でう退いていたタスク redis.2 が,manager1 に再割り当てされていることが分かります.

worker1 のメンテナンスが完了したら,元に戻すことができます.

$ docker node update --availability active worker1

ステータスを確認してみましょう.

docker@manager1:~$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
o67397jwrzch9ccxkhre3sgrh *   manager1            Ready               Active              Leader              18.03.0-ce
xpeqk27zg8x2rnf1qpcail6de     worker1             Ready               Active                                  18.03.0-ce
9i8v2ked45bm572ti34oyu3r5     worker2             Ready               Active                                  18.03.0-ce

ステータスが active に戻っており,worker1 は再びタスクを実行できるようになります.

注意として,ステータスが active に戻ったとしても,drain 時に他ノードへ再割り当てされたタスクはそのままとなります. つまり,タスク redis.2 はまだ manager1 で実行されています.

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
uw92a8l1w1sq        redis.1             redis:3.0.6         manager1            Running             Running 10 hours ago                        
31vg14mao63n        redis.2             redis:3.0.6         manager1            Running             Running 10 hours ago                        
pv988a7ecmdt         \_ redis.2         redis:3.0.6         worker1             Shutdown            Shutdown 10 hours ago                       
ec4z7huzr27h        redis.3             redis:3.0.6         worker2             Running             Running 10 hours ago                        

サービスを公開する

swarm のスラスターにはルーティンメッシュが構築されており,外部から任意のノードに送られたリクエストは,swarm 内で動作する適切なタスクへと振り分けられます.

公式チュートリアルの図が分かり易いです.

service ingress image

サービスを外部に公開するには,--publish オプションを指定します. 試しに,nginx をポート 8080 で公開するようにしてみましょう.

docker@manager1:~$ docker service create --name my-web --replicas 2 --publish published=8080,target=80 nginx

サービスの公開状況は,docker service inspect コマンドで確認することができます.

docker@manager1:~$ docker service inspect --format="{{json .Endpoint.Spec.Ports}}" my-web
[{"Protocol":"tcp","TargetPort":80,"PublishedPort":8080,"PublishMode":"ingress"}]

swarm の外部 (ここでは,swarm のノードを VirtualBox で動作させている macOS) からアクセスしてみます.

f:id:snobutaka:20180401092228p:plain
外部からのサービスへのアクセス

ポイントは,swarm の全てのノード (192.168.99.100, 192.168.99.101, 192.168.99.102) からサービスにアクセスできるということです. このサービスは,--replicas 2 で起動しているため,実際にタスクが動作しているのは 2 ノードのみとなっています. しかし,swarm 内部のロードバランサーによって,タスクを実行していないノードからでもサービスへのアクセスができるようになっているのです.

おわり

swarm のチュートリアルをさらっとやってみましたが,かなり面白い内容でした. ロードバランサーを勝手に立ち上げて良きに計らってくれるところとか,かなり良いですね.

いまのところ個人的には swarm を使って実際の製品・サービスを提供するというよりかは,クラスター化したテスト環境の作成などに利用してみたいと思っています.

いずれ Kubernetes とかより本格的 (?) なオーケストレーションも試してみたいです.