Automator でスキャン画像を PDF にする
Automator でスキャン画像を PDF にする
概要
macOS の Automator を使って, スキャンした複数ページの文書を PDF にして連結する方法の備忘録です.
Automator のワークフローを作る
Automator のワークフローを作ります.
フローの内容は,次のようになります:
- 指定された Finder 項目を取得
- フォルダの内容を取得
- Finder 項目を並べ替える
- イメージから新規 PDF を作成
実際に作ったワークフローはこんな感じです.
ワークフローを実行
ワークフローを実行します.
ここでは例として,猫の画像がいくつか (複数ページのスキャン文書だと思ってください) を連結して,猫のフォトブック (連結された複数ページの文書だと思ってください) を作ります.
猫の画像:
ワークフローを実行するとこんな PDF が得られます.
おわり
手軽だからという理由で Automator を使いましたが, Ruby とか Python あたりののスクリプト言語でサクッと実装できたらかっこいいですね.
Docker コンテナからホストにアクセスする
Docker コンテナからホストにアクセスする
概要
Docker コンテナからホスト (macOS) にアクセスする方法を探していたら,
host.docker.internal
という名前を使えばよいということが公式リファレンスに書いてありました.
試してみるといとも簡単にアクセスできました.
※ちなみに Docker for Windows の公式リファレンスにも同じことが書いてあるのですが,こちらは試していません.(職場で試してみようかな……)
環境
$ 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 サーバを起動しておきます. Ruby で sinatra を使って適当なサーバを起動してみました.
$ 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
一見よくわからないものもありますが (← おい),0x1000b42a5440001
と 0x1000b42a5440000
は 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
仮想マシンを用意しました.
Docker swarm is 何?
Docker swarm は,複数のマシンからなる Docker のクラスターを構築するための仕組みです.
swarm の基本概念は Swarm mode key concepts にまとめられています. 大まかにかいつまむと,次のようなキーワードが登場します.
- Node: swarm のクラスターを構成する各マシン (Docker engine) のことを Node と呼びます.Node には manager と worker の 2 通りがあり,
- 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 内で動作する適切なタスクへと振り分けられます.
公式チュートリアルの図が分かり易いです.
サービスを外部に公開するには,--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) からアクセスしてみます.
ポイントは,swarm の全てのノード (192.168.99.100, 192.168.99.101, 192.168.99.102) からサービスにアクセスできるということです.
このサービスは,--replicas 2
で起動しているため,実際にタスクが動作しているのは 2 ノードのみとなっています.
しかし,swarm 内部のロードバランサーによって,タスクを実行していないノードからでもサービスへのアクセスができるようになっているのです.
おわり
swarm のチュートリアルをさらっとやってみましたが,かなり面白い内容でした. ロードバランサーを勝手に立ち上げて良きに計らってくれるところとか,かなり良いですね.
いまのところ個人的には swarm を使って実際の製品・サービスを提供するというよりかは,クラスター化したテスト環境の作成などに利用してみたいと思っています.
いずれ Kubernetes とかより本格的 (?) なオーケストレーションも試してみたいです.