CUBE SUGAR CONTAINER

技術系のこと書きます。

docker-machine で "Error response from daemon: client is newer than server" になったときの対処方法

今回のネタは、おそらく docker-machine を使っていればいつか遭遇するエラーについて。 結論から先に言うと docker-machine を使っていて、ある日 Docker 本体のバージョンを更新するとこうなる。 まず前提として Docker は Linux コンテナが動作する Docker ホストと、それを制御する Docker クライアントに分かれている。 通常は Docker ホスト = Docker クライアントなんだけど docker-machine を使っているとその状況が崩れる。 Docker クライアントがハイパーバイザを動かしているマシンで、Docker ホストがハイパーバイザ上で動く仮想マシンに分かれるから。 その状況で Docker クライアントのバージョンだけを更新してしまうと、このようなエラーになる。

実際に起こしてみる

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21

Mac OS X なので Homebrew Cask で Docker と Docker Toolbox をインストールできる。 このコマンドを入力している時点で、既に両者は古いバージョンがインストール済みで docker-machine の仮想マシンも作られているという想定。

$ brew cask install docker dockertoolbox

こうなると Docker クライアントだけが新しくなって API のバージョンがミスマッチを起こしてしまう。

$ docker version
Client:
 Version:      1.10.0
 API version:  1.22
 Go version:   go1.5.3
 Git commit:   590d5108
 Built:        Thu Feb  4 18:18:11 2016
 OS/Arch:      darwin/amd64
Error response from daemon: client is newer than server (client API version: 1.22, server API version: 1.21)

対処方法

このエラーに対処するには落ち着いて docker-machine upgrade サブコマンドを実行する。 これで対象の仮想マシンが最新のバージョンに更新される。 ここで指定している「default」は docker-machine で使っている仮想マシンの名前。

$ docker-machine upgrade default
Waiting for SSH to be available...
Detecting the provisioner...
Upgrading docker...
Stopping machine to do the upgrade...
Upgrading machine "default"...
Copying /Users/amedama/.docker/machine/cache/boot2docker.iso to /Users/amedama/.docker/machine/machines/default/boot2docker.iso...
Starting machine back up...
(default) Check network to re-create if needed...
(default) Waiting for an IP...
Restarting docker...

これでエラーが解消できた。

$ docker version
Client:
 Version:      1.10.0
 API version:  1.22
 Go version:   go1.5.3
 Git commit:   590d5108
 Built:        Thu Feb  4 18:18:11 2016
 OS/Arch:      darwin/amd64

Server:
 Version:      1.10.0
 API version:  1.22
 Go version:   go1.5.3
 Git commit:   590d5108
 Built:        Thu Feb  4 19:55:25 2016
 OS/Arch:      linux/amd64

めでたしめでたし。

Python: concurrent.futures を使った並行・並列処理

Python の concurrent.futures はバージョン 3.2 で追加された並行・並列処理用のパッケージ。 似たようなパッケージにはこれまでにも threading や multiprocessing があったんだけど、これはそれよりも高レベルの API になっている。 デフォルトでスレッド・プロセスプールが使えたり、マルチスレッドとマルチプロセスがほとんどコードを変えずに使い分けられるメリットがある。

下準備

使う Python のバージョンが 3.2 未満のときは PyPI にあるバックポート版のパッケージをインストールする必要がある。

$ pip install futures

ただし、今回使う環境は Python 3.5 なので関係ない。

$ python --version
Python 3.5.1
$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21

シングルスレッドの場合

今回は扱うものが並行・並列処理なので、計算量がたくさん必要になるものを題材とする。 試しに、いくつかの大きな数を素数か判定するプログラムをシングルスレッドで書いてみよう。 アルゴリズムとしては単純な試し割り法を使った。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math


def _is_prime(n):
    """数値が素数か判定する関数"""
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False

    return True


def _calc():
    # 調査対象の数値
    values = [
        112272535095293,
        112582705942171,
        112272535095293,
        115280095190773,
        115797848077099,
        1099726899285419,
    ]
    for target in values:
        # 素数かチェックして結果を表示する
        result = _is_prime(target)
        msg = '{n}: {prime}'.format(n=target, prime=result)
        print(msg)


def main():
    import time
    start = time.time()
    _calc()
    end = time.time()
    # 計算にかかった時間を表示する
    elapsed_time = end - start
    msg = 'Time: {elapsed_time} sec'.format(elapsed_time=elapsed_time)
    print(msg)


if __name__ == '__main__':
    main()

これを実行してみると、それぞれの数をリストに収まっている順番で判定していくことになる。

$ python singlethread.py
112272535095293: True
112582705942171: True
112272535095293: True
115280095190773: True
115797848077099: True
1099726899285419: False
Time: 3.3549559116363525 sec

シングルスレッド版は 3.35 秒かかった。

マルチスレッドの場合

次は先ほどのサンプルをマルチスレッドにしてみよう。 ソースコードの細かい説明はコメントで行っている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math
from concurrent import futures


def _is_prime(n):
    """数値が素数か判定する関数"""
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False

    return True


def _calc():
    # 調査対象の数値
    values = [
        112272535095293,
        112582705942171,
        112272535095293,
        115280095190773,
        115797848077099,
        1099726899285419,
    ]
    with futures.ThreadPoolExecutor() as executor:
        # 並行処理する対象とそれに対応する値 (処理の引数) を辞書で用意する
        mappings = {executor.submit(_is_prime, n): n for n in values}
        # futures.as_completed() は処理がおわったものから結果を返していくジェネレータ
        for future in futures.as_completed(mappings):
            # 完了した処理に対応する引数を辞書から取得する
            target = mappings[future]
            # 処理結果を取得する
            result = future.result()
            # 結果を表示する
            msg = '{n}: {prime}'.format(n=target, prime=result)
            print(msg)


def main():
    import time
    start = time.time()
    _calc()
    end = time.time()
    # 計算にかかった時間を表示する
    elapsed_time = end - start
    msg = 'Time: {elapsed_time} sec'.format(elapsed_time=elapsed_time)
    print(msg)


if __name__ == '__main__':
    main()

次も同じように実行してみよう。

$ python multithread.py
1099726899285419: False
112582705942171: True
112272535095293: True
112272535095293: True
115280095190773: True
115797848077099: True
Time: 3.3795218467712402 sec

先ほどとかかっている時間はほとんど変わっていない。 マルチスレッドにしたのに何故かと不思議に思われるかもしれない。 ただ、これにはちゃんとした原因がある。 CPython には GIL (Global Interpretor Lock) というものがあって、マルチスレッドにしても同時に実行できるスレッドはひとつだけだからだ。 むしろスレッドの生成コストの分だけ遅くなっても何ら不思議ではない。 とはいえ同時に実行できるスレッドがひとつなだけで、処理は「並行」に進められている。 その証拠に、先ほどとは数が出力されている順番が異なることに気づくことだろう。

マルチプロセスの場合

次は先ほどのサンプルをマルチプロセスにした場合。 なんと修正するところはたったひとつ。 先ほど使った ThreadPoolExecutor を ProcessPoolExecutor に変えるだけ。 これでマルチスレッドの実行がマルチプロセスでの実行に変わる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math
from concurrent import futures


def _is_prime(n):
    """数値が素数か判定する関数"""
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False

    return True


def _calc():
    # 調査対象の数値
    values = [
        112272535095293,
        112582705942171,
        112272535095293,
        115280095190773,
        115797848077099,
        1099726899285419,
    ]
    with futures.ProcessPoolExecutor() as executor:
        # 並行処理する対象とそれに対応する値 (処理の引数) を辞書で用意する
        mappings = {executor.submit(_is_prime, n): n for n in values}
        # futures.as_completed() は処理がおわったものから結果を返していくジェネレータ
        for future in futures.as_completed(mappings):
            # 完了した処理に対応する引数を辞書から取得する
            target = mappings[future]
            # 処理結果を取得する
            result = future.result()
            # 結果を表示する
            msg = '{n}: {prime}'.format(n=target, prime=result)
            print(msg)


def main():
    import time
    start = time.time()
    _calc()
    end = time.time()
    # 計算にかかった時間を表示する
    elapsed_time = end - start
    msg = 'Time: {elapsed_time} sec'.format(elapsed_time=elapsed_time)
    print(msg)


if __name__ == '__main__':
    main()

同じように実行してみよう。

$ python multiprocess.py
112272535095293: True
112582705942171: True
112272535095293: True
115280095190773: True
1099726899285419: False
115797848077099: True
Time: 1.3142600059509277 sec

今度は複数のプロセスで「並列」に処理が進んだから実行時間が短縮されている。

まとめ

今回は Python の標準ライブラリ concurrent.futures を使った並行・並列処理について紹介した。 このライブラリを使うとマルチスレッドとマルチプロセスでほとんどソースコードを変えずに使い分けることができる。 ただし、マルチプロセスを使うときはひとつだけ注意が必要だ。 マルチプロセスの場合、プロセス間でのオブジェクトのやり取りは Pickle にもとづいて行われる。 そのため、並列化する関数の引数と返り値には Pickle 化できないオブジェクトを使うことができない。 Pickle についてはこのブログで以前に書いたことがあるのであわせて読んでもらいたい。

blog.amedama.jp