Saturday, November 22, 2014

docker用の超絶シンプルDNS

fig使って遊んでいたら、どうしても欲しくなり、Goでプログラム書きたかったこともあり、作ってみた。

実際にはこんな回りくどいことしなくても、既存のDNS使った、マシなソリューションがあると思う。参考

まあ200行くらいで済んだし、これはこれでいいかなー。

go-docker-dns

Github

原理は死ぬほど単純で、dockerのリモートAPIをひたすら見守り続けて、コンテナの作成イベントを捉えたら、そのホスト名とIPの組を登録。逆にコンテナ停止のイベントを捉えたら、その情報を削除する。これとは別のDNSのスレッドが動いていて(ほとんど流用だけどっ)、dockerコンテナからの要求に応じて、上で登録したホスト名-IPの組を見つけたら、そのIPを回答している。

逆引きは実装していない。結構簡単に実装できそうだけど、現状でも用途には応えているから作る理由もない…。

もともとfigを使って、普段使っているPC上で比較的大きなシステム(WebAPI x MQ x KVS x RDB x LB)のはりぼてみたいなのを作り、その動作を検証するといったことをやりたかった。

この時、DNSがいないとクラスタを組む類のサービス(典型的には、今回変なDNSを実装するきっかけになったRabbitMQがスケール時にまともに動かない。

RabbitMQの場合:masterノードとslaveノードを用意し、masterノードに対して複数のslaveを登録する仕組みを今回作ったが、fig scale mqslave=3としたときに、docker --linkの仕組みでmasterノードのIPアドレスは解決できるが、他のslaveノードのIPアドレスはDNSがいないと解決のしようがない。

他の人のアプローチは、もっとslaveノードが欲しかったら、

fig.yml

rabbit:
    image: bijukunjummen/rabbitmq-server
    hostname: rabbit_1
    ports:
        - "5672:5672"
        - "15672:15672"
mqnode1:
    image: bijukunjummen/rabbitmq-server
    hostname: mqnode_1
    environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit_1
        - RAM_NODE=true
    links:
        - rabbit
mqnode2:
    image: bijukunjummen/rabbitmq-server
    hostname: mqnode_2
    environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit_1
        - RAM_NODE=true
    links:
        - rabbit
        - mqnode1
...

こんな感じに増やしていけばいいじゃない!って言ってるけど、流石に「ス、スケールとは一体」…って言いたくなる。fig scale mqnode=3ってやったら1 master x 3 slaves の構成でクラスタが構築できてほしい。

コンテナ用のDNSサーバーを立てることで、わざわざfig.ymllinksを記載しなくても、scale時に名前解決してくれるようになる。

ここでもう一つ面倒な話があって、(RabbitMQでは)コンテナのホスト名がクラスタを組むときに大事になってくる。具体的にいうとfigは、scaleして増やすコンテナのホスト名をfig.ymlで記述されたものを使う。fig scale mqnode=3とやると、mqnodeというホスト名を持つコンテナが3つできるorz じゃあ、どやって名前解決してるの?っていうと、これとは別に環境変数envdocker exec <コンテナID> echo envで覗ける)と/etc/hostsに、linkのところで指定された、関係のあるコンテナのIPと外側向けホスト名みたいのを列挙している。(この辺はdocker側の実装。)

この状況だと、RabbitMQのmasterノードはslaveノードをクラスタに参加させる時に、「お前、slave_1っていう名前で参加しようとしてきたのに、実際にはslaveじゃん。」みたいな事を指摘して参加が上手くいかない。頑張って参加するときの名前とホスト名を一致させても、今度は2つめ以降のslaveが参加するときにホスト名が被る。つまり、1ノードだけが運よく参加できて、他は今参加しているノードが落ちるまで参加できない…\(^o^)/ いや、ある意味、可用性あるけど、そうじゃないいいいいいい。

fig改悪?版

という訳でfig本体にもしぶしぶ手を入れた。具体的には上に書いたようにホスト名をscaleを考慮した形で生成する。

以下のようなfig.ymlがあったとする。

mqnode:
    image: bijukunjummen/rabbitmq-server
    hostname: mqnode
    environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit_1
        - RAM_NODE=true

fig up -d mqnodeとやると、コンテナが1つだけ立ち上がる。ホスト名はmqnodeではなく、mqnode_1。続いてfig scale mqnode=3。これで新しいコンテナが2つ立ち上がる。ホスト名はそれぞれmqnode_2, mqnode_3。自作したDNSサーバーは、コンテナが作成されたイベントを検知すると、このホスト名とIPの組をレコードとして登録する。IPアドレスが10.10.0.1から始まってたとすると、こういうレコード。

mqnode_1 10.10.0.1
mqnode_2 10.10.0.2
mqnode_3 10.10.0.3

RabbitMQのクラスタ構築時の動きが

  1. slaveノードが起動する
  2. slaveノードがmasterノードに自身の存在を通知
  3. masterノードが参加してきたslaveノードに他のslaveの存在を教える
  4. slaveノードが教えてもらった他のslaveノードにアクセスを試みる

ってな感じになってるから、fig側の修正とDNSサーバの合わせ技で、この振る舞いが正しく機能するようになった。

オマケ

開発中は、必要に応じてコンテナを頻繁に作り直すことになると思うんだけど、fig up serviceの現在の実装がとても気に入らない。

fig up -d service
fig scale service=3

こうすると、ホスト名、およびコンテナ名がservice_1, service_2, service_3となり、これはいい。でも、この後、もう一度

fig up -d service

ってやると、service_1, service_2, service_3が削除され、service_3, service_4, service_5が新たに立ち上がる。なんで番号が増えんのよorz

別途立ち上げていたnginxコンテナにロードバランスの設定を

upstream webapp {
        server service_1;
        server service_2;
        server service_3;
    }

こんな感じに書いてるものだから、コンテナを作り直す度にnginxの設定を書き直さなきゃならなくなる。流石に気が滅入ってくるorz ので、fig up -d serviceした時に既存のサービスが立ち上がっているなら、そのホスト名で作り直すように変更。better_recreateって名前のブランチで、ここまでに書いたfig側の変更を全て反映している。

まとめ

  • Goの練習も兼ねてdocker用の超絶シンプルなDNSを作成
  • figを使ってRabbitMQクラスタを自動構築できるようにした
  • figのコードをちょっと変えてrecreate時にホスト名を引き継げるようにした

ここまでやって、メモリ4G程度のゲストマシン(CoreOS)上に計18のサービス(Rails App 2種類, KVS, RDB, Node.js App, Nginx)が協調動作する環境を構築できた。全部動いていると、だいたい3.8Gくらいメモリを消費。

なんやかんや問題も多いわけだけど、figみたいのを使うと、これまで鯖を何台も用意、provisonerなんかを使って構築していた比較的大きいシステムを、(ハリボテだけど)各開発者が自分のマシン上に持てるようになる、これで開発効率上がるといいな、とか思ったり思わなかったり。ここまでできると、本番環境へのデプロイを同じ設定ファイル使いまわしで実現したくなるね。

Friday, November 14, 2014

Docker remote API

とある事情からDockerデーモン内で発生したイベントをストリームで取得したいと思ったら、remote APIとかいう便利なものがあったので触ってみた。

ホストはOSXでboot2dockerを使っている。

いつからか分からないが、最新版のboot2dockerだとゲストマシンで起動するdockerデーモンがremote APIを受け付けるオプション付きで動いている模様。

 1055 root     /usr/local/bin/docker -d -D -g /var/lib/docker -H unix:// -H tcp://0.0.0.0:2376 --tlsverify --tlscacert=/var/lib/boot2docker/tls/ca.pem --tlscert=/var/lib/boot2docker/tls/server.pem --tlskey=/var/lib/boot2docker/tls/serverkey.pem

curlを使ってアクセスできるようなので、ホストマシン側からテストがてらGET images/jsonを叩いてみる。

yukaarybox:~ yukaary$ curl --insecure --cert $DOCKER_CERT_PATH/cert.pem --key $DOCKER_CERT_PATH/key.pem https://192.168.59.103:2376/images/json
curl: (35) Unknown SSL protocol error in connection to 192.168.59.103:-9825

なんと…。繋がらないので色々調べてたらOSX版curlのバグじゃない?って話題が上がっている。Certificate Authentication Fails

ちなみにboot2docker sshして同じ意味のコマンドを打つと、結果が返ってくる。

docker@boot2docker:~$ curl --insecure --cert ~/.docker/cert.pem --key ~/.docker/
key.pem https://boot2docker:2376/images/json
[{"Created":1414247260,"Id":"806930947ad909768835b8311a061e630ee95e7cb0d3420ec2c815abd21182f1","ParentId":"4271258c1b3fc4f96e8d7ddbf2893ff7f9dfbfcf5d97b5dcae25685930c8645a","RepoTags":["dockerfile/redis:latest"],"Size":0,"VirtualSize":434170370}

解決方法が泣けるけど、セキュリティリスクを承知でTLSオプション無しでdockerd起動すればいいじゃない!というもの。ゲストマシンにTLSオプションを無効にする設定を書く。

docker@boot2docker:~$ cat /var/lib/boot2docker/profile 
DOCKER_TLS=no
docker@boot2docker:~$ sudo /etc/init.d/docker restart

ホストマシンからremote APIを叩いてみる。

yukaarybox:boot2docker-vm yukaary$ curl http://192.168.59.103:2375/images/json
[{"Created":1414247260,"Id":"806930947ad909768835b8311a061e630ee95e7cb0d3420ec2c815abd21182f1","ParentId":"4271258c1b3fc4f96e8d7ddbf2893ff7f9dfbfcf5d97b5dcae25685930c8645a","RepoTags":["dockerfile/redis:latest"],"Size":0,"VirtualSize":434170370}

イメージの一覧は取れた。が、ストリーム関係はだめぽ…。OSXのcurlが駄目なんじゃないかって気がしてきた。

yukaary$ curl -N http://192.168.59.103:2375/events?since1415972290
シーン...

ゲストマシンからだとストリームAPIも正常に動作する。

docker@boot2docker:~$ curl http://boot2docker:2375/events?since=1415972290
{"status":"die","id":"688f24305b55bb8587635e5392e664e1ec50591c5d13be66e5040a6d6a36d2de","from":"dockerfile/redis:latest","time":1415972323}{"status":"stop","id":"688f24305b55bb8587635e5392e664e1ec50591c5d13be66e5040a6d6a36d2de","from":"dockerfile/redis:latest","time":1415972323}{"status":"die","id":"1811fbe97840253a797c0fac6a33d84bf74eeac084793c3d68f157a36e892199","from":"dockerfile/redis:latest","time":1415972339}{"status":"stop","id":"1811fbe97840253a797c0fac6a33d84bf74eeac084793c3d68f157a36e892199","from":"dockerfile/redis:latest","time":1415972339}{"status":"destroy","id":"688f24305b55bb8587635e5392e
...

追記

mac側に入れていたファイアウォールソフトが全力で邪魔していたっぽい。

yukaarybox:CoreServices yukaary$ curl -N http://192.168.59.103:2375/images/json
[{"Created":1414247260,"Id":"806930947ad909768835b8311a061e630ee95e7cb0d3420ec2c815abd21182f1","ParentId":"4271258c1b3fc4f96e8d7ddbf2893ff7f9dfbfcf5d97b5dcae25685930c8645a","RepoTags":["dockerfile/redis:latest"],"Size":0,"VirtualSize":434170370}
yukaarybox:CoreServices yukaary$ curl -N http://192.168.59.103:2375/events?since=1415972290
{"status":"create","id":"d0d362d2d7b9a1fb61aec059007b7aa902148cb67796ffb01e98840cdf7ba7f5","from":"dockerfile/redis:latest","time":1415975558}{"status":"start","id":"d0d362d2d7b9a1fb61aec059007b7aa902148cb67796ffb01e98840cdf7ba7f5","from":"dockerfile/redis:latest","time":1415975558}

繋がったからよしとしよう…。

Sunday, November 9, 2014

Python part2

思うところあってPython(3.4.2)を勉強中。公式サイトのドキュメントを見よう真似で

4. More Control Flow Tools

4.1 if Statements

最もよく知られたステートメント型はifで間違いない。

x = int(input("Please enter an interger: "))

if x < 0:
    x = 0
    print('Negative changed to zero')
elif x == 0:
    print('Zero')
elif x == 1:
    print('Single')
else:
    print('More')

実行結果。

$ py ifstmt.py
Please enter an interger: 100
More
[c:python]
$ py ifstmt.py
Please enter an interger: 1
Single
[c:python]
$ py ifstmt.py
Please enter an interger: 0
Zero
[c:python]
$ py ifstmt.py
Please enter an interger: -19
Negative changed to zero

elifは0あるいは複数あっていい。elseはオプションだ。if, elif,elif…のシークエンスはswitch, caseの代替手段になっている。

4.2 for Statement

Pythonでのfor文は、CPascalとは若干異なる。Pascalのような数学的な数の繰り返し、あるいはCのような繰り返しのステップとその停止条件の定義というよりは、あらゆるシークエンス(ListやString)内の要素の繰り返しをする。

words = ['yukari', 'maki', 'zunko']
for w in words:
    print(w, len(w))

実行結果。

$ py iterate.py
yukari 6
maki 4
zunko 5

ループの内側でシークエンスに対する改変を行いたい場合、まずコピーを作ることをお勧めする。シークエンス上での繰り返しはcopyを暗黙的に作るわけではない。スライス表記がこの目的で使えるだろう。

words = ['yukari', 'maki', 'zunko']
for w in words:
    print(w, len(w))

for w in words[:]: # Loop over slice copy of the entire list.
    if len(w) > 5:
        words.insert(0, w)
print('words', words)

実行結果。

$ py iterate.py
yukari 6
maki 4
zunko 5
words ['yukari', 'yukari', 'maki', 'zunko']

4.3 The range() Function

数列上(0,1,2,…)での繰り返しを実装したい場合は、ビルトイン関数range()を使うといい。数学的なprogression(直訳で増殖。訳すのメンドイ)を生成してくれる。

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

与えた終点は生成されたシーケンスに含まれない。range(10)は10個の値を生成する。レンジの開始点を別の値に設定することができるほか、増分(減産分)を指定することもできる。(ステップということもある)

>>> for i in range(5, 10):
...     print(i)
...
5
6
7
8
9
>>> for i in range(0, 10, 3):
...     print(i)
...
0
3
6
9
>>> for i in range(-10, -100, -30):
...     print(i)
...
-10
-40
-70

シーケンスのインデックスを使った繰り返しを実現したい場合はrange()len()を組み合わせることができる。

words = ['yukari', 'maki', 'zunko']
for w in words:
    print(w, len(w))

for w in words[:]: # Loop over slice copy of the entire list.
    if len(w) > 5:
        words.insert(0, w)
print('words', words)

for i in range(len(words)):
    print(i, words[i])

実行結果。

$ py iterate.py
yukari 6
maki 4
zunko 5
words ['yukari', 'yukari', 'maki', 'zunko']
0 yukari
1 yukari
2 maki
3 zunko

大半のこういうケースでは、enumerate()を使うと便利だ。ループのテクニックを参照してほしい。(for i, v in enumerate(['tic', 'tac', 'toe']):。なるほど理解。)

単にrangeを出力するとおかしなことになるはずだ。

>>> print(range(10))
range(0, 10)

range()によって返されるオブジェクトはリストのように振る舞うが、実際のところは違う。このオブジェクトは、繰り返しの中で期待するシークエンスの次のアイテムを返すものだ。リストを作っているわけではない。つまりスペースを削減している。

こういったオブジェクトをiterableと呼び、供給が可能な限り継続的に次のアイテムを生産する。for文は繰り返しであることを見てきたが、関数listは別だ。これはiterableオブジェクトからリストを作り出す。

>>> list(range(5))
[0, 1, 2, 3, 4]

後ほど、iterableオブジェクトを返す関数や、iterableを引数に取る関数を見ていく。

4.4 break and continue Statements, and else Caluses on Loops

break文はCのようにforwhileループの処理部から脱出する。

ループ文はelseの節を持つことがある。この節は、ループの中でリストが枯渇(for文)、または条件がfalseになる(while文)時に実行される。ただし、ループがbreakにより中断したときは実行されない。下記の例では、この特性を使って素数をピックアップしている。

primes.py

for n in range(2,10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'equals', x, '*', n//x)
            break
    else:
        print(n, 'is a prime number.')

実行結果。

$ py primes.py
2 is a prime number.
3 is a prime number.
4 equals 2 * 2
5 is a prime number.
6 equals 2 * 3
7 is a prime number.
8 equals 2 * 4
9 equals 3 * 3

よく見てほしい。elseは、ifではなくforに属している。

ループと一緒に使うとき、elseは、if文とセットで使用するより、try文のelse部として利用するのがより一般的だ。try文のelse部は、例外が発生しなかった時に実行される。または、ループ文のelse部は、breakが発生しなかったときに実行される。try文に関する詳細はHandling Exceptionsで見ていく。

continue文は、C同様、ループ内の次の繰り返しを実行する。

even.py

for num in range(2, 10):
        if num % 2 == 0:
            print("Found an even number", num)
            continue
        print("Found a number", num)

実行結果。

$ py even.py
Found an even number 2
Found a number 3
Found an even number 4
Found a number 5
Found an even number 6
Found a number 7
Found an even number 8
Found a number 9

3.4 pass Statements

pass文は何もしない。これはステートメントが構文的に必要だが、プログラムは何もする必要が無い場合に使える。たとえば、

>>> while True:
...     pass # Busy-wait for keyboard interrupt (Ctrl+C)
...

passは、一般に最少のクラスを作るために使われている。

>>> class MyEmptyClass:
...     pass
...
>>>

passが使われている他の場所は、関数のプレースホルダ、あるいは新しいコードを書いているときの条件付きの本体だ。これにより、もっと抽象的なレベルで検討を続けることができる。passは特にわーわー言われることなく無視される。

>>> def initlog(*args):
...     pass # remember to implement this!!
...
>>> initlog
<function initlog at 0x000000000296CBF8>
>>>
>>> initlog('a')
>>>

4.6 Defining Functions

任意の上限までフィボナッチ数列を生成する関数を作ってみよう。

def fib(n):
    """Print a Fibonacci series up to n."""
    a, b = 0, 1
    while b < n:
        print(b, end=' ')
        a, b = b, a + b
    print()

fib(2000)

$ py fibonacchi.py
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

キーワードdefで関数を定義する。続いて関数名と丸括弧内にパラメータのリストが続かなければならない。関数の処理本体は次の行から始まる。インデントされている必要がある。

関数の最初の行はオプションであり文章だ。この文章は関数のドキュメント(docstring)だ。docstringに関する詳細はDocumentation Stringsで扱う。docstringで、オンラインまたは印刷用文書を自動的に生成するツールがある。また、利用者にコードを通して情報を与える。書いたコードに対してdocstringを付与しておくようにしよう。

関数の実行は関数のローカル変数用にシンボルテーブルを導入するところから始まる。もう少し正確にいうと、関数内で割り当てられた全ての変数はローカルシンボルテーブル内に値を格納する。一方で変数の参照はローカルシンボルテーブル内に最初に見つかる。続いて上位関数のローカルシンボルテーブル、グローバルシンボルテーブル、そして最後にビルトイン名のテーブル。従ってグローバル変数は関数の中では、(global文無しでは)直接、値を割り当てることはできない(関数内で参照できるシンボルテーブルにはないってことか)。

関数コールでの実際のパラメータ(引数)は呼び出された関数のローカルシンボルテーブル内に、それがコールされた時に導入される。引数はcall by value(値は常にオブジェクトへの参照であり、オブジェクトの値ではない)で渡される。関数が別の関数を呼び出す場合、新たなローカルシンボルテーブルが呼び出し時に作られる。

関数定義は現在のシンボルテーブル内に関数名を導入する。関数名の値はユーザ定義関数としてインタプリタによって認識される型を持っている。この値には別の名前を割り当てることができ。これも関数として使える。一般的なリネームの仕組みだ。

>>> def fib(n):
...     pass
...
>>> fib
<function fib at 0x0000000002AAC9D8>
>>> f = fib
>>> f(100)

シンボルテーブルってのは現在のコンテキストが持つオブジェクトを格納しているテーブルのことか。locals(), globals()で中身を覗けた。

>>> locals()
{'f': <function fib at 0x0000000002AAC9D8>, '__doc__': None, '__name__': '__main__', '__spec__': None, '__package__': None, '__builtins__': <module 'builtins' (built-in)>, 'MyEmptyClass': <class '__main__.MyEmptyClass'>, 'initlog': <function initlog at 0x000000000296CBF8>, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, 'fib': <function fib at 0x0000000002AAC9D8>, 'fin': <function fin at 0x0000000002AACAE8>}
>>> globals()
{'f': <function fib at 0x0000000002AAC9D8>, '__doc__': None, '__name__': '__main__', '__spec__': None, '__package__': None, '__builtins__': <module 'builtins' (built-in)>, 'MyEmptyClass': <class '__main__.MyEmptyClass'>, 'initlog': <function initlog at 0x000000000296CBF8>, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, 'fib': <function fib at 0x0000000002AAC9D8>, 'fin': <function fin at 0x0000000002AACAE8>}

他の言語から来た人にとって、fibは関数ではなく、それは値を返さないので手続きのように見えるだろう。実際は、return文を持たない関数も値を返す。この値はNone(これはビルトインの名前だ)と呼ばれている。Noneの書き出しはインタプリタによって抑止されているが、print()を使うと確認できる。

>>> print(fib(0))
None

フィボナッチ数列のリストを返すよう関数を直すのは単純だ。

def fib2(n):
    """Return a list containing the FIbonacci series up to n. """
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

print(fib2(100))

実行結果。

$ py fibonacchi.py
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

この例では次のPythonの特性を紹介した。

  • return文で関数から値を返す。単にreturnとした場合、または、returnの指定がない場合はNoneが返る。
  • result.append(a)はリストオブジェクトresultのメソッドを呼び出す。メソッドはオブジェクトに属する関数であり、obj.methodnameの形で呼び出す。objは何らかのオブジェクト、methodnameはオブジェクトの型によって定義されたメソッドの名前だ。異なる型は、異なるメソッドを定義する。違った型のメソッドは曖昧性をもたらすことなく同じ名前を持てる(classesを使って、独自のオブジェクト型とメソッドを定義することが可能だ。Classesのセクションを参照。) appendメソッドはリストオブジェクトで定義されている。これはリストの終端に新たな要素を追加する。この例ではresult = result + [a]と等価であるが、より効率的だ。

4.7. More on Defining Functions

引数の数が可変である関数を定義することもできる。同時に利用できる3つの形態がある。

4.7.1 Default Argument Values

最も使いやすい形態は引数のデフォルト値を指定するものだ。より少ない引数で関数を呼び出すことができる。(そりゃそうだ)

def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise OSError('uncooperative user')
        print(complaint)

ask_ok('Yukarisan maji tenshi')

実行結果。

$ py askok.py
Yukarisan maji tenshia
Yes or no, please!
Yukarisan maji tenshib
Yes or no, please!
Yukarisan maji tenshid
Yes or no, please!
Yukarisan maji tenshie
Yes or no, please!
Yukarisan maji tenshif
Traceback (most recent call last):
  File "askok.py", line 13, in <module>
    ask_ok('Yukarisan maji tenshi')
  File "askok.py", line 10, in ask_ok
    raise OSError('uncooperative user')
OSError: uncooperative user

この関数は幾つかの方法で呼び出すことができる。

  • 最低限、必要な引数だけ:ask_ok('Yukarisan maji tenshi:')
  • オプション引数を1つだけ: ask_ok('Yukarisan maji tenshi:', 2)
  • オプション全部指定: ask_ok('Yukarisan maji tenshi:', 2, 'use yes or no, plz.')

inキーワードは、シークエンスが正しい値を含むかチェックする。

デフォルト値は、definingのスコープで関数の定義位置で評価される。つまり

i = 5

def f(arg=i):
    print(arg)
i = 6
f()

この結果は5だ。

重大な警告: デフォルト値は1回のみ評価される。つまり、デフォルト値がlist, dictionary, クラスインスタンスのような可変オブジェクトの時に違いが発生する。以下の例を見てみよう。

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

$ py default.py
[1]
[1, 2]
[1, 2, 3]

まさかのデフォルト値を設定したリストが共有されてしまう。このようにしたくない場合、以下のようにする。

def g(a, L=None):
    if(L is None):
        L = []
    L.append(a)
    return L

$ py default.py
[1]
[2]
[3]

うわ、めんどくさ…。

4.7.2. Keyword Arguments

関数はkwarg=valueという形式を持つkeyword argumentを使って呼び出すことができる。以下の例で説明する。

def parrot(voltage, state='a stif', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("If you put", voltage, "volts though it.")
    print("-- Lovely plumage, the", type)
    print("It's", state, "!")

# These calls are valid
parrot(1000)
parrot(voltage=1000)
parrot(voltage=10000000, action="VOOOOOOOM")
parrot(action="VOOOOOOOM", voltage=10000000)
parrot('a million', 'bereft of life', 'jump')
parrot('a thousand', state='pushing up the daisies')

# Invalid calls
parrot() # required argument missing
parrot(voltage=5.0, 'dead') # non-keyword argument after a keyword argument
parrot(110, voltage=220) # duplicate value for the same argument
parrot(actor='Makimaki') # unknown keyword argument

関数parrotは1つの必須引数と3つのオプション数を取る。関数を呼び出す際、キーワード引数は実際に定義されている引数に従わなければならない。ただ、その順序を気にする必要はない。同時に同じキーワードに対して複数の値を設定することはできない。この制約は簡単にチェックできる。

>>> def func(a):
...     pass
...
>>> func(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() got multiple values for argument 'a'

**nameという形の形式パラメータがあるとき、それは形式パラメータに関連するものを除いた全てのキーワード引数が含まれている辞書を受け取る。これは、*nameという形をした形式パラメータ(こっちは形式パラメータリストの上位の引数を格納したタプルだ)と一緒に使うこともあるだろう。*nameは、**nameの前に置かなければならない。関数を例に示す。

def cheeeshop(kind, *arguments, **keywards):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    keys = sorted(keywards.keys())
    for kw in keys:
        print(kw, ":", keywards[kw])


cheeeshop("Limburger", "It's very runny, sir",
        "It's very very runny, sir.",
        shopkeeper="Maki Tsurumaki",
        client="Yukari Yuzuki",
        sketch="Cheese Shop Sketch")

実行結果。

$ py formalparam.py
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir
It's very very runny, sir.
----------------------------------------
client : Yukari Yuzuki
shopkeeper : Maki Tsurumaki
sketch : Cheese Shop Sketch

キーワード引数の名前のリストは、それの中身を書き出す前にkeywards dictionaryのkeys()メソッドの出力をソートしている点に注意してほしい。もしこれをしない場合、引数の書き出し順序は確定しない。

4.7.4 Arbitary Argument Lists

最後に、任意の数の引数をもって関数を呼び出すことができる最も頻度が低い方法について触れておく。これらの引数はタプル内にラップされる。可変の引数の前に、0個ないし通常の引数の引数が並ぶ。

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

通常、これらのvariadic(可変な)な引数は形式パラメータのリストの最後に置かれる。関数に渡される残りの引数全てが、この可変パラメータとして扱われるので、まあ仕方がない。*argsの後ろに置かれるどんな形式パラメータも、keyward-onlyな引数だ。つまり、位置が確定した引数というより、キーワードでのみ利用する。

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

4.7.4. Unpacking Argument Lists

引数がリストやタプルに含まれているときは逆の必要が生じるだろう。つまり、それらをばらして、個別の引数を必要とする関数呼び出しに対応させる必要性だ。例えばビルトイン関数range()は、別々にstartstopの引数を必要とする。これらの情報が分かれていない場合は、*オペレータを用いてリスト/タプルの外に引数を引き出すことができる。

>>> list(range(3,6))
[3, 4, 5]
>>> args = [3,6]
>>> list(range(*args))
[3, 4, 5]

同じように、dictionaryも**オペレータを使って引数を引き出すことができる。


d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
parrot(**d)

4.7.5. Lambda Expressions

小さな無名関数をキーワードlamdaを使って作成できる。lambda a, b: a +b、この関数は2つの引数a,bの合計値を返すものだ。ラムダ関数は関数オブジェクトを使えるところなら、どこでも使える。単一の表現しか使えないという文法上の制約がある。意味的には、ラムダ関数は通常の関数定義のシンタックスシュガーだ。ネストされた関数定義のように、ラムダ関数はそのスコープから変数を参照できる。

def make_incrementor(n):
    return lambda x: x + n

f = make_incrementor(42)
print(f(0))
print(f(1))

この例は関数を返り値で返す時にラムダ表現を使っている。次の例は引数にラムダ式を与える。

pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
print(pairs)

4.7.5. Documentation Strings

ドキュメントのお作法の話。関数の宣言の直後に"""で初めて複数行に渡って色々書いたら"""で閉じる。

def my_function():
    """Do nothing, but document it.

    No really, it doen't do anything.
    """
    pass

print(my_function.__doc__)

__doc__で参照できるのが面白いw

4.7.7. Function Annotations

これは完全にオプションだ。ユーザ定義関数についての任意のメタ情報を含めることができる。サードパーティ製のプロジェクトではアノテーションをドキュメント、型チェック、その他の用途、何に使っても自由だ。

アノテーションは関数の__annotations__属性にdictionaryとして格納されている。関数の他の部分に何の影響も与えない。パラメータアノテーションはパラメータ名の後ろにコロンを付けて定義する。この後、アノテーションの値を評価する文が続く。リターンアノテーションはリテラル->で定義する。下記の例は、positional argumentとkeyward argument、および返り値に特に意味のないアノテーションを付けたものだ。

def f(ham: 42, eggs: int = 'spam') -> "Nothing to see here":
    print("Annotations:", f.__annotations__)
    print("Arguments:", ham, eggs)

f("wonderful")

$ py annotation.py
Annotations: {'return': 'Nothing to see here', 'ham': 42, 'eggs': <class 'int'>}
Arguments: wonderful spam

4.8. Intermezzo: Coding Style

タブ使うな、とかそんな話。

Written with StackEdit.

Saturday, November 8, 2014

DockerとFig クラスタ対応の話

figをぽちぽち触っていると、こいつDBなんかのサービスを含むアプリケーション全体を自動構築できるのに、なぜデプロイターゲットはDOCKER_HOST一台だけなのん?と疑問に思う。

少し調べてみたら、当然、外人さん達も同じことを思うらしく、githubでぼちぼち議論されていた。

中身を覗いてみると単純にhostを複数管理できるようにしているだけらしい。そんでfig.ymlにどのホストにデプロイするかの情報を付与する。

web1:
  build: .
  command: python app.py
  docker_host: tcp://192.168.0.101
  docker_cert_path: /path/to/your/cert1/directory
  docker_tls_verify: 1
  ports:
   - "8000:8000"

web2:
  build: .
  command: python app.py
  docker_host: tcp://192.168.0.102
  docker_cert_path: /path/to/your/cert2/directory
  docker_tls_verify: 1
  ports:
   - "8000:8000"

これ、ホスト決め打ちだから、そのホスト落ちたら(^o^)/なんじゃないかな…。

Dockerクラスタ内のどこかにデプロイされるが、それがどこかはリクエスト側は知る必要が無い/仮にクラスタを構成するインスタンスがクラッシュした場合、そのインスタンスが実行していたDockerプロセスは別の健全なインスタンスに引き継がれる。無理言ってる気もするけど、このくらいの機能がないとマルチホスト構成をサポートする意味がないように思える。

上に挙げた特性はkubernetesがそこそこ対応しているように思えるけど、googleにべったりなのもなぁ…docker本体でなんとかしないのかなーと期待している。

この話、figdockerに取り込まれたこともあり、内部ではdockerfigどっちに持たせる?って話になっているように見える。

docker本体側の議論。

  • Docker Clustering: Design proposal
  • Proposal: Host management

  • 8859 dockerのクラスタ対応、master x 1 slave x Nのモデル?

    • 詳しく読んでないけど「masterが死んだときにslaveのどれかがmasterになる/masterが定期的に交代する」くらいの特性が欲しいな!それなんてcoreosって話だけども。
  • 8681 dockerホスト立てるのメンドイ。dockerクライアントにdockerホスト立てる機能も持たせようぜ!みたいな話

という感じで、上の機能が盛り込まれたら今までのdockerとは位置づけが異なった
ものになりそう。figをアプリケーションの構成を管理する環境構築ツールと位置付けて、クラスタ管理はdockerが担うっていうのが住み分けとしてはいいのかな。

クラスタ機能がサポートされるのを待つのもあれだし、PR中のブランチ引っ張ってきて自分で試せないか調べてみようかな…。

Written with StackEdit.