CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: line_profiler でボトルネックを調べる

前回は Python の標準ライブラリに用意されているプロファイラの profile/cProfile モジュールについて書いた。

blog.amedama.jp

今回は、同じ決定論的プロファイリングを採用したプロファイラの中でも、サードパーティ製の line_profiler を使ってみることにする。 line_profiler は profile/cProfile モジュールに比べるとコード単位でプロファイリングが取れるところが魅力的なパッケージ。 また、インターフェースについても profile/cProfile に近いものとなっている。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31
$ python --version
Python 3.5.2

インストール

まずは line_profiler をインストールする。 サードパーティ製のパッケージなので pip を使う。

$ pip install line_profiler

題材とするプログラム

ひとまず、前回も使った FizzBuzz を表示するソースコードをプロファイリングの対象にしよう。

次のようなソースコードを用意する。

$ cat << 'EOF' > fizzbuzz.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    for i in range(1, 100):
        message = fizzbuzz(i)
        print(message)


if __name__ == '__main__':
    main()
EOF

このプログラムは実行すると 1 から 99 までの FizzBuzz を表示する。

$ python fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz

プロファイリングしてみる

さて、それでは用意したソースコードを早速プロファイリングしてみよう。

ここでは Python の REPL を使って動作を確認していく。 先ほどのソースコードを用意したのと同じディレクトリで REPL を起動しよう。

$ python

REPL が起動したら、まずはプロファイリング対象の fizzbuzz モジュールをインポートしておく。 補足しておくと Python ではソースコード (*.py) が、そのままモジュールに対応している。 例えば先ほどの fizzbuzz.py であれば fizzbuzz モジュールとして使えるわけだ。

>>> import fizzbuzz

続いて line_prpfiler モジュールをインポートして LineProfiler クラスのインスタンスを生成しよう。 このインスタンスを使ってプロファイリングをする。

>>> import line_profiler
>>> pr = line_profiler.LineProfiler()

プロファイリング対象の関数を LineProfiler#add_function() メソッドを使って登録しよう。 ここでは、前述の fizzbuzz モジュールの中にある fizzbuzz 関数を登録した。

>>> pr.add_function(fizzbuzz.fizzbuzz)

そして、続いて LineProfiler#runcall() メソッドでプロファイリングを実行する。 このメソッドで実行した処理の中で、先ほど LineProfiler#add_function() メソッドで登録した関数が呼び出されることを期待する。 もし呼びだされた場合には、それが処理される過程が line_profiler によって逐一記録されるという寸法だ。

>>> pr.runcall(fizzbuzz.main)
1
2
Fizz
...(省略)...
97
98
Fizz

ちなみに、上記の LineProfiler#runcall() メソッドは、代わりに次のようにしても構わない。 こちらのやり方では LineProfiler#enable() メソッドと LineProfiler#disable() メソッドによってプロファイリングのオン・オフを切り替えている。 LineProfiler#enable() メソッドと LineProfiler#disable() メソッドの間に実行された処理がプロファイリングの対象ということになる。

>>> pr.enable()
>>> fizzbuzz.main()
1
2
Fizz
...(省略)...
97
98
Fizz
>>> pr.disable()

プロファイリングが終わったら LineProfiler#print_stats() メソッドで結果を表示しよう。 すると、次のようにプロファイリング結果が得られる。

>>> pr.print_stats()
Timer unit: 1e-06 s

Total time: 0.000292 s
File: /Users/amedama/Documents/temporary/fizzbuzz.py
Function: fizzbuzz at line 5

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     5                                           def fizzbuzz(n):
     6        99           95      1.0     32.5      if n % 3 == 0 and n % 5 == 0:
     7         6            0      0.0      0.0          return 'FizzBuzz'
     8
     9        93           57      0.6     19.5      if n % 3 == 0:
    10        27           13      0.5      4.5          return 'Fizz'
    11
    12        66           46      0.7     15.8      if n % 5 == 0:
    13        13           29      2.2      9.9          return 'Buzz'
    14
    15        53           52      1.0     17.8      return str(n)

上記の表のカラムは次のような意味がある。

  • Line

    • ソースコードの行番号
  • Hits

    • その行が実行された回数
  • Time

    • その行の実行に費やした時間の合計
    • 単位は先頭行の Timer unit で示されている
  • Per Hit

    • その行を 1 回実行するのに費やした時間
  • % Time

    • その行の実行に費やした時間の全体に対する割合
  • Line Contents

    • 行番号に対応するソースコード

とても分かりやすい。

コマンドラインから調べる

line_profiler には、もうひとつコマンドラインからプロファイリングを実行するインターフェースがある。 むしろ、公式の説明を見るとこちらを推している雰囲気がある。 ただ、個人的には先ほどのやり方が便利なので、こちらをあまり使うことはないかな…という感じ。

コマンドラインからの処理では、まずプロファイリングを実行したい関数に一工夫が必要になる。 具体的には、プロファイリングを実行したい関数やメソッドを @profile デコレータで修飾する。

以下は fizzbuzz() 関数に @profile デコレータをつけたサンプルコード。

$ cat << 'EOF' > fizzbuzz.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


@profile
def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    for i in range(1, 100):
        message = fizzbuzz(i)
        print(message)


if __name__ == '__main__':
    main()
EOF

ちなみに、するどい人は上記が @profile デコレータがインポートされていないのでエラーになると気づくかもしれない。

その通りで、実はこのソースコードは通常の Python インタプリタでは実行できなくなる。

$ python fizzbuzz.py 
Traceback (most recent call last):
  File "fizzbuzz.py", line 5, in <module>
    @profile
NameError: name 'profile' is not defined

ただ、line_profiler を使ってコマンドラインでプロファイリングするときは専用のコマンドを使うことになる。 具体的には kernprof というコマンドを使う。 このコマンドではソースコードの中の @profile デコレータがちゃんと動作するように細工して実行するので問題ない。

早速 kernprof コマンドで fizzbuzz.py をプロファイリングしてみよう。

$ kernprof -l fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz
Wrote profile results to fizzbuzz.py.lprof

実行すると、プロファイリング対象のファイル名に .lprof という拡張子のついたファイルができる。

そして、そのできたファイルを line_profiler モジュールをコマンドライン経由で読み込ませる。 すると、解析結果が表示される。

$ python -m line_profiler fizzbuzz.py.lprof
Timer unit: 1e-06 s

Total time: 0.000275 s
File: fizzbuzz.py
Function: fizzbuzz at line 5

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     5                                           @profile
     6                                           def fizzbuzz(n):
     7        99          116      1.2     42.2      if n % 3 == 0 and n % 5 == 0:
     8         6            3      0.5      1.1          return 'FizzBuzz'
     9
    10        93           53      0.6     19.3      if n % 3 == 0:
    11        27           15      0.6      5.5          return 'Fizz'
    12
    13        66           33      0.5     12.0      if n % 5 == 0:
    14        13            5      0.4      1.8          return 'Buzz'
    15
    16        53           50      0.9     18.2      return str(n)

ただ、上記のやり方だとソースコードに手を加えないといけないから面倒くさい。 それに、モジュールが実行可能になっていないとプロファイリングできないところも汎用性が低いと感じた。

まとめ

  • 標準ライブラリには profile/cProfile というプロファイラがある
  • それ以外にもサードパーティ製で line_profiler というパッケージがある
  • line_profiler はコード一行単位でプロファイリング結果が見えるので分かりやすい

Python: profile/cProfile モジュールでボトルネックを調べる

プログラムを作ってみたは良いけれど、想定していたよりもパフォーマンスが出ないという事態はよくある。 そんなときはパフォーマンス・チューニングをするわけだけど、それにはまず測定が必要になる。 具体的に何を測定するかというと、プログラムの中のどこがボトルネックになっているかという点だ。 この測定の作業は一般にプロファイリングと呼ばれる。

プロファイリングにはふたつの種類がある。 ひとつ目は今回紹介する profile/cProfile モジュールが採用している決定論的プロファイリング。 そして、もうひとつが統計的プロファイリングだ。

決定論的プロファイリングでは、プログラムが実行されていく過程を逐一記録していく。 そのため、時間のかかっている処理を正確に把握することができる。 ただし、測定自体がプログラムに与える影響 (プローブ・エフェクト) も大きくなる。

それに対し統計的プロファイリングでは、プログラムが実行されていく過程をランダムに記録していく。 これは、統計的にいえばプログラムが実行されていく過程の全体を母集団と捉えて、そこから標本を抽出している。 そして、抽出した標本から母集団を推定する形で、呼び出しの多い処理や時間のかかっている処理を推定することになる。 この場合、結果はあくまで推定 (一定の割合で誤りを許容すること) になるものの、測定自体が与える影響は比較的小さくなる。

profile/cProfile モジュール

前置きが長くなったけど、今回は Python の標準ライブラリに用意されているプロファイリング用のモジュールを紹介する。 具体的には profile と cProfile というふたつの実装で、どちらも前述した通り決定論的プロファイリングを採用している。 ふたつある理由は profile が Pure Python の実装で、cProfile が C 拡張モジュールを使った実装になっている。 それぞれの特徴は、profile に比べると cProfile は測定にかかるオーバーヘッドが少ない代わりに、ユーザの拡張性が低くなっている。 使い分けについては、一般的なユースケースでは cProfile を常に使っていれば良さそうだ。

今回使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31
$ python --version
Python 3.5.1

まずは使ってみる

何はともあれ profile/cProfile を使ってみることにしよう。 まずは Python の REPL を使って動作を確認していくことにする。

$ python

起動したら cProfile をインポートする。

>>> import cProfile

もちろん profile を使っても構わない。

>>> import profile

動作確認には time モジュールを使うことにしよう。 このモジュールを使うとスレッドを一定時間停止させることができる。

>>> import time

最もシンプルな使い方としてはモジュールの run() 関数に呼び出したい Python のコードを渡す形になる。 こうすると、渡した Python のコードが cProfile 経由で実行されて、プロファイリング結果が得られる。

>>> cProfile.run('time.sleep(1)')
         4 function calls in 1.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    1.001    1.001 <string>:1(<module>)
        1    0.000    0.000    1.001    1.001 {built-in method builtins.exec}
        1    1.001    1.001    1.001    1.001 {built-in method time.sleep}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

結果として得られる表の読み方は次の通り。

  • ncalls

    • 呼び出し回数
  • tottime

    • その関数の実行にかかった時間の合計
    • (別の関数呼び出しにかかった時間を除く)
  • percall

    • かかった時間を呼び出し回数で割ったもの
    • (つまり 1 回の呼び出しにかかった平均時間)
  • cumtime

    • その関数の実行にかかった時間の合計
    • (別の関数呼び出しにかかった時間を含む)
  • filename

    • lineno(function): 関数のファイル名、行番号、関数名

再帰的な処理が含まれる場合

上記の ncalls (呼び出し回数) は、再帰呼び出しがあるときとないときで表示が異なる。 例えば、次のような再帰を使ってフィボナッチ数を計算する関数を用意する。

>>> def fib(n):
...     if n == 0:
...         return 0
...     if n == 1:
...         return 1
...     return fib(n - 2) + fib(n - 1)
...

これをプロファイリングにかけると 177/1 といったように分数のような表示が得られる。

>>> cProfile.run('fib(10)')
         180 function calls (4 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

これは、元々は 1 回の呼び出しから関数が再帰的に 176 回呼びだされたことを意味している。 右側が再帰によるものでない呼び出し回数、左側は再帰を含む呼び出し回数になっているため。

より実践的な使い方

先ほどの例では Python のコードを文字列で渡すことでプロファイリングした。 とはいえ、より柔軟に一連の Python のコードをプロファイリングしたいという場合も多いはず。 次は、そんなときはどうしたら良いかについて。

まずは Profile クラスのインスタンスを用意する。

>>> pr = cProfile.Profile()

そして、プロファイリングを開始するために enable() メソッドを呼びだそう。

>>> pr.enable()

その上で、プロファイリングしたいコードを実行する。

>>> fib(10)
55

そして、プロファイリングを終了するために disable() メソッドを呼び出す。

>>> pr.disable()

ちなみに、これは代わりに runcall() メソッドを使っても構わない。

>>> pr.runcall(fib, 10)
55

プロファイリングが終わったら print_stats() メソッドで結果を出力する。

>>> pr.print_stats()
         179 function calls (3 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <stdin>:1(<module>)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

結果を詳しく見る

これまでの例では、プロファイリングの対象となるコードがシンプルだったので結果もシンプルだった。 とはいえ、実際にプロファイリングしたいようなコードは色々な処理が呼び出しあうような複雑なものになるはず。 結果も自ずと重厚長大なものになるはずだ。 そんなときは、プロファイリングの結果を pstats モジュールで詳しく調べることができる。

まずは pstats モジュールをインポートする。

>>> import pstats

そして、先ほどの Profile クラスのインスタンスを渡して Stats クラスをインスタンス化しよう。

>>> stats = pstats.Stats(pr)

Stats クラスのインスタンスでは、例えば特定のカラムで結果をソートできる。 ここでは ncalls (呼び出し回数) で降順ソートしてみよう。

>>> stats.sort_stats('ncalls')
<pstats.Stats object at 0x102518278>

>>> stats.print_stats()
         178 function calls (2 primitive calls) in 0.000 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


<pstats.Stats object at 0x102518278>

これで、呼び出し回数の多い fib() 関数が上にくる。

プロファイリング結果をファイルに残す

プロファイリングの結果はデータとして残せるようになっていると色々と便利なはず。

そんなときは Profile クラスの dump_stats() というメソッドが使える。 このメソッドは、ファイル名を指定してプロファイリング結果をファイルに保存できる。

>>> pr.dump_stats('fib.profile')

上記であれば Python の REPL を起動したディレクトリに fib.profile という名前のファイルができる。

$ ls | grep ^fib
fib.profile

上記のファイルは pstats モジュールから読み込んで使うことができる。 Stats クラスをインスタンス化するときにファイル名を文字列で指定すれば結果を読み込むことができる。

>>> import pstats
>>> stats = pstats.Stats('fib.profile')
>>> stats.print_stats()
Tue Aug 30 19:30:07 2016    fib.profile

         178 function calls (2 primitive calls) in 0.000 seconds

   Random listing order was used

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


<pstats.Stats object at 0x10ec9acf8>

コマンドラインでプロファイリングする

次は、そんなに使う場面はないかもしれないけどコマンドラインでプロファイリングする方法について。

例えば、次のような FizzBuzz を実行するソースコードを用意しておこう。

$ cat << 'EOF' > fizzbuzz.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    for i in range(1, 100):
        message = fizzbuzz(i)
        print(message)


if __name__ == '__main__':
    main()
EOF

このソースコードは実行すると 1 から 99 までの FizzBuzz を出力する。

$ python fizzbuzz.py 
1
2
Fizz
...(省略)...
97
98
Fizz

このとき -m cProfile というオプションをつけて cProfile モジュールが実行されるようにしてみよう。 これで、自動的にプロファイリングが実行される。

$ python -m cProfile fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz
         202 function calls in 0.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:18(main)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:5(<module>)
       99    0.000    0.000    0.000    0.000 fizzbuzz.py:5(fizzbuzz)
        1    0.000    0.000    0.001    0.001 {built-in method builtins.exec}
       99    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

この使い方では -s オプションを使って結果のソートができる。

$ python -m cProfile -s ncalls fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz
         202 function calls in 0.001 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       99    0.000    0.000    0.000    0.000 {built-in method builtins.print}
       99    0.000    0.000    0.000    0.000 fizzbuzz.py:5(fizzbuzz)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:5(<module>)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:18(main)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.001    0.001 {built-in method builtins.exec}

同じように -o オプションを使えばプロファイリング結果をファイルに保存できる。

$ python -m cProfile -o fizzbuzz.profile fizzbuzz.py
$ ls | grep .profile$
fizzbuzz.profile

呼び出し関係を調べる

profile/cProfile は処理の呼び出し回数や時間などの他に、呼び出し関係も調べることができる。 例えば、先ほどの FizzBuzz のプロファイリング結果を例に使ってみよう。

まずは改めて Python の REPL を起動する。

$ python

そしてプロファイリング結果をファイル経由で読み込む。

>>> import pstats
>>> p = pstats.Stats('fizzbuzz.profile')

そして print_callers() メソッドで、呼び出し元が分かる。 左が呼び出された関数で、右が呼び出し元の関数になっている。

>>> p.print_callers()
   Random listing order was used

Function                                          was called by...
                                                      ncalls  tottime  cumtime
{built-in method builtins.print}                  <-      99    0.000    0.000  fizzbuzz.py:18(main)
fizzbuzz.py:5(<module>)                           <-       1    0.000    0.001  {built-in method builtins.exec}
{built-in method builtins.exec}                   <-
{method 'disable' of '_lsprof.Profiler' objects}  <-
fizzbuzz.py:18(main)                              <-       1    0.000    0.001  fizzbuzz.py:5(<module>)
fizzbuzz.py:5(fizzbuzz)                           <-      99    0.000    0.000  fizzbuzz.py:18(main)


<pstats.Stats object at 0x100d24a58>

その逆が print_callees() メソッドで得られる。 今度は左が呼び出し元で、右が呼び出し先になっている。

>>> p.print_callees()
   Random listing order was used

Function                                          called...
                                                      ncalls  tottime  cumtime
{built-in method builtins.print}                  ->
fizzbuzz.py:5(<module>)                           ->       1    0.000    0.001  fizzbuzz.py:18(main)
{built-in method builtins.exec}                   ->       1    0.000    0.001  fizzbuzz.py:5(<module>)
{method 'disable' of '_lsprof.Profiler' objects}  ->
fizzbuzz.py:18(main)                              ->      99    0.000    0.000  fizzbuzz.py:5(fizzbuzz)
                                                          99    0.000    0.000  {built-in method builtins.print}
fizzbuzz.py:5(fizzbuzz)                           ->


<pstats.Stats object at 0x100d24a58>

まとめ

  • パフォーマンスチューニングには、まずプロファイリングが必要になる
  • プロファイリングには決定論的プロファイリングと統計的プロファイリングのふたつがある
  • Python には profile/cProfile という決定論的プロファイリングができるモジュールがある
  • このモジュールを使うと特定の処理における関数の呼び出し回数や、かかった時間を調べることができる

参考

27.4. Python プロファイラ — Python 3.6.5 ドキュメント

CentOS7 で Docker Swarm を試してみる

先日リリースされた Docker 1.12 から Docker Swarm が本体に同梱されるようになった。 この Docker Swarm というのは、複数の Docker ホストを束ねて使えるようにするオーケストレーションツールになっている。 今回は、その Docker Swarm がどういったものなのかを一通り触って試してみることにする。

今回使った環境は次の通り。 CentOS 7 を Docker ホストの OS に使う。

$ cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
$ uname -r
3.10.0-327.el7.x86_64

なお、Docker ホストは 3 台構成で、それぞれ node1, node2, node3 と呼ぶことにする。 各ノードはお互いに通信する必要があるので IP アドレスを 192.168.33.11, 192.168.33.12, 192.168.33.13 と振っておく。

ただ、この初期構築は結構めんどくさい。 なので、今回は Vagrant で途中まで自動化したものも用意した。 具体的には Docker のインストールと必要なポートを開けるところまで。

Vagrantfile for Docker Swarm · GitHub

使い方は次の通り。

$ git clone https://gist.github.com/1dc33c45e47c75d03408a44e63c7daa7.git vagrant-docker-swarm
$ cd vagrant-docker-swarm
$ vagrant up

以下は手動で構築する場合の手順になっている。 また、上記で構築できるのは Docker のインストールまで。 だから、クラスタの構築やサービスの定義はいずれにせよ手動で実施する必要がある。

初期設定

それでは各ホストを手動で構築していこう。 まずはホスト名を設定する。

$ sudo hostname node1.example.com
$ cat << 'EOF' | sudo tee /etc/hostname > /dev/null
node1.example.com
EOF

上記で設定する値 (nodeX) は各ホストごとに異なるので注意してほしい。

通信用のポートを開ける

次に Docker Swarm がクラスタ内で通信するのに使うポートを開ける。 具体的には 2377/TCP, 7946/TCP, 4789/TCP, 7946/UDP, 4789/UDP を開ける必要がある。

ただし、ひとつ注意点があって CentOS7 標準の firewalld ではなく iptables を直接使う必要があるようだ。

github.com

そこで、まずは firewalld のサービスを止める。

$ sudo systemctl stop firewalld
$ sudo systemctl disable firewalld

そして、代わりに iptables をインストールする。

$ sudo yum -y install iptables-services
$ sudo systemctl start iptables
$ sudo systemctl enable iptables

必要なポートを開けていこう。 先ほど挙げたポート以外に 80/TCP も開けている。 これは後ほど Docker コンテナ内で立ち上げるサービスへのアクセスに使う。

$ for i in 80 2377 7946 4789; do sudo iptables -I INPUT -j ACCEPT -p tcp --dport $i; done;
$ for i in 7946 4789; do sudo iptables -I INPUT -j ACCEPT -p udp --dport $i; done;

ポートを開けることができたら設定を保存しておく。

$ sudo service iptables save

Docker をインストールする

次に Docker 本体をインストールする。

その前に、まずはパッケージを最新の状態にしておこう。

$ sudo yum -y update

次に Docker の提供するリポジトリを yum に登録する。 CentOS7 が標準で使える Docker は、公式が提供するそれよりも古いため。

$ cat << 'EOF' | sudo tee /etc/yum.repos.d/docker.repo > /dev/null
[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/7/
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg
EOF

一旦 yum のキャッシュをクリアする。

$ sudo yum clean all

そして Docker 本体をインストールする。 Docker Swarm はバージョン 1.12 から本体に同梱されるようになった。 そのため、これだけで使えるようになる。

$ sudo yum -y install docker-engine

インストールできたら Docker のサービスを起動する。

$ sudo systemctl start docker
$ sudo systemctl enable docker

インストールできたら docker version コマンドを実行しよう。 次のようにクライアントとサーバの両方でエラーが出ていなければ問題ない。

$ sudo docker version
Client:
 Version:      1.12.0
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   8eab29e
 Built:
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.0
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   8eab29e
 Built:
 OS/Arch:      linux/amd64

ついでに Docker コンテナが実行できることも確認しておこう。 hello-world イメージを使って、次のようなメッセージになれば大丈夫。

$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world

c04b14da8d14: Pull complete
Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

上記の作業をすべての Docker ホストで実行しよう。 そして、先ほど紹介した Vagrantfile で実行できるのは、ここまで。

また、次の作業からは各 Docker ホストで実行する内容が異なる。

クラスタを作る

さて、ここからはいよいよ Docker Swarm の機能を使っていく。 各ホストで実行する内容が異なるのでターミナルの先頭にノード名を記述することにする。

まずは Docker ホストを束ねたクラスタを作る。 クラスタにはマネージャとワーカというふたつの役割がある。 ワーカは Docker コンテナが動作するだけの Docker ホストになっている。 マネージャは、それに加えてクラスタの管理などを行う。

今回は node1 をマネージャにして、その他のホスト node2 と node3 をワーカにしてクラスタを組んでみよう。

まずはマネージャとして動作する node1 で docker swarm init コマンドを実行する。 これでクラスタを作成できる。 オプションの --listen-addr と --advertise-addr にはクラスタの通信で使う IP アドレスとポートを指定する。

node1 $ sudo docker swarm init --listen-addr 192.168.33.11:2377 --advertise-addr 192.168.33.11:2377
Swarm initialized: current node (37v1n4u827kqfv2iwuh0u395r) is now a manager.

To add a worker to this swarm, run the following command:
    docker swarm join \
    --token SWMTKN-1-1q6s0gycuxtt3hjqlrd02pmnu555fju2m62vx0ivvmnytbp9hs-83fccs5u4bb9lsb40fo9gbd69 \
    192.168.33.11:2377

To add a manager to this swarm, run the following command:
    docker swarm join \
    --token SWMTKN-1-1q6s0gycuxtt3hjqlrd02pmnu555fju2m62vx0ivvmnytbp9hs-c2co7kyo54yc9z3vnvz0ykbhf \
    192.168.33.11:2377

何やら色々と表示されているけど、これは他のノードがクラスタに参加するためのコマンドになっている。 このトークンを知らないとクラスタには参加できないというわけ。

トークンにはマネージャ用とワーカ用のふたつがある。 つまりマネージャもワーカも複数台をクラスタに追加できるということ。

クラスタに参加しているノードの状態は docker node ls コマンドで確認できる。 今はクラスタを作成した直後なので、参加しているノードは node1 だけ。

node1 $ sudo docker node ls
ID                           HOSTNAME           STATUS  AVAILABILITY  MANAGER STATUS
37v1n4u827kqfv2iwuh0u395r *  node1.example.com  Ready   Active        Leader

クラスタにノードを追加する

先ほど作ったクラスタにノードを追加しよう。 追加するには、先ほどクラスタを作成したときに表示されたコマンドを使う。

まずは node2 をクラスタに参加させる。

node2 $ sudo docker swarm join --token SWMTKN-1-1q6s0gycuxtt3hjqlrd02pmnu555fju2m62vx0ivvmnytbp9hs-83fccs5u4bb9lsb40fo9gbd69 192.168.33.11:2377
This node joined a swarm as a worker.

そして node3 も同じようにクラスタに参加させる。

node3 $ sudo docker swarm join --token SWMTKN-1-1q6s0gycuxtt3hjqlrd02pmnu555fju2m62vx0ivvmnytbp9hs-83fccs5u4bb9lsb40fo9gbd69 192.168.33.11:2377
This node joined a swarm as a worker.

上記が実行できたら node1 で再び docker node ls コマンドを実行してみよう。 今度は 3 台が表示されるはず。

node1 $ sudo docker node ls
ID                           HOSTNAME           STATUS  AVAILABILITY  MANAGER STATUS
33mmomv1up2azt0e38h959a48    node2.example.com  Ready   Active
37v1n4u827kqfv2iwuh0u395r *  node1.example.com  Ready   Active        Leader
5opby7sfnpovoqshnjkymxe0s    node3.example.com  Ready   Active

動作確認用の Docker イメージをダウンロードする

次に Docker Swarm の動作を確認するための Docker イメージをダウンロードしよう。 このイメージは Docker コンテナの ID を HTTP (80/TCP) で返すだけのシンプルなものになっている。

https://hub.docker.com/r/momijiame/greeting/

なぜ、このようなものが必要かというと Docker Swarm にはロードバランス機能が備わっているため。 詳しくは後述するが、クラスタ内で動作する各コンテナにアクセスを振り分けることができる。 そこで、アクセス先のコンテナを確認するために上記のようなイメージを用意した。

このイメージを、それぞれの Docker ホストでダウンロードしておこう。 一応、これはやらなくても必要なときにダウンロードされるので動作はする。 とはいえ、あらかじめやっておいたほうが必要な時間の短縮につながるはず。

まずは node1 でダウンロードする。 バージョン (タグ) が複数あるのは、あとで Docker Swarm のローリングアップデート機能を試すため。

node1 $ sudo docker pull momijiame/greeting:1.0
node1 $ sudo docker pull momijiame/greeting:2.0
node1 $ sudo docker pull momijiame/greeting:latest

同じように node2 でもダウンロードする。

node2 $ sudo docker pull momijiame/greeting:1.0
node2 $ sudo docker pull momijiame/greeting:2.0
node2 $ sudo docker pull momijiame/greeting:latest

そして node3 でもダウンロードする。

node3 $ sudo docker pull momijiame/greeting:1.0
node3 $ sudo docker pull momijiame/greeting:2.0
node3 $ sudo docker pull momijiame/greeting:latest

サービスを定義する

さて、ここまでで Docker Swarm を使う準備が整った。 次はクラスタに対してサービスを定義する。 サービスというのは、ようするにクラスタの上で提供してほしい機能をいう。 これは、例えば HTML の静的なホスティングかもしれないし、Rails のアプリケーションだったりするかもしれない。 その内容は、サービスで使う Docker イメージに依存する。

それでは docker service create コマンドを使ってサービスを定義しよう。 名前は helloworld にする。 --replicas オプションは、いくつの Docker ホスト上でコンテナを動作させたいかを表している。 -p オプションは docker run コマンドのときと同じように、提供するポートフォワーディングの設定だ。 そして最後がサービスで使用する Docker イメージになっている。 この他にもオプションは多数あるけど、ここで使うのは上記だけ。

node1 $ sudo docker service create --name helloworld --replicas=2 -p 80:8000 momijiame/greeting:1.0
f44y32qiu9mlma2gcffi0o4xs

docker service ls コマンドでクラスタが提供しているサービスが見られる。

node1 $ sudo docker service ls
ID            NAME        REPLICAS  IMAGE                       COMMAND
f44y32qiu9ml  helloworld  2/2       momijiame/greeting:1.0

上記は概要なので、より詳しい内容は docker service inspect コマンドを使おう。

node1 $ sudo docker service inspect --pretty helloworld
ID:     f44y32qiu9mlma2gcffi0o4xs
Name:       helloworld
Mode:       Replicated
 Replicas:  2
Placement:
UpdateConfig:
 Parallelism:   1
 On failure:    pause
ContainerSpec:
 Image:     momijiame/greeting:1.0
Resources:
Ports:
 Protocol = tcp
 TargetPort = 8000
 PublishedPort = 80

サービスの稼働状況を調べるには docker service ps コマンドを使うと良い。

$ sudo docker service ps helloworld
ID                         NAME          IMAGE                       NODE               DESIRED STATE  CURRENT STATE           ERROR
7uaq5jpd7zu3wx4y0t4qrxzqr  helloworld.1  momijiame/greeting:1.0  node3.example.com  Running        Running 42 seconds ago
634or7vt454kja1xyzb5fxcqb  helloworld.2  momijiame/greeting:1.0  node2.example.com  Running        Running 41 seconds ago

上記は helloworld サービスにおいて Docker コンテナが node2 と node3 で実行されていることを表している。

アクセス

さて、これだけでコンテナが各 Docker ホスト上にばらまかれて稼働し始めている。 実際にコンテナにアクセスしてみよう。 アクセスする先はポートフォワーディングをしている Docker ホストになる。

また、アクセスする先の IP アドレスは、コンテナが実際に稼働している Docker ホストを意識する必要がない。 なぜなら、クラスタ内のどの Docker ホストにアクセスしても、そのアクセスは実際に稼働している Docker ホストに振り分けられる。 また、アクセス内容もロードバランシングされる。

実際に curl コマンドを使って Docker ホストにアクセスしてみよう。

$ curl http://192.168.33.11
Hello, 1cfe277ecd10

これで、ロードバランス先のコンテナの ID が得られる。

続けて実行すると、ラウンドロビン方式でアクセスがロードバランスされていることが見て取れる。

$ curl http://192.168.33.11
Hello, 094ab7b026d0
$ curl http://192.168.33.11
Hello, 1cfe277ecd10
$ curl http://192.168.33.11
Hello, 094ab7b026d0

オートヒーリング

さて、Docker Swarm には一部のコンテナが落ちたときに別の場所で上げなおす機能もある。

例えば node2 で稼働しているコンテナを docker kill コマンドで止めてみよう。

node2 $ sudo docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS               NAMES
1cfe277ecd10        momijiame/greeting:1.0   "/usr/local/bin/gunic"   4 minutes ago       Up 4 minutes        8000/tcp            helloworld.2.634or7vt454kja1xyzb5fxcqb
node2 $ sudo docker kill 1cfe277ecd10
1cfe277ecd10

そして node1 で docker service ps コマンドを実行してサービスの稼働状況を確認する。 すると、node2 で動作していたコンテナがシャットダウンした代わりに node3 で新しいコンテナが動き始めている。

node1 $ sudo docker service ps helloworld
ID                         NAME              IMAGE                       NODE               DESIRED STATE  CURRENT STATE          ERROR
7uaq5jpd7zu3wx4y0t4qrxzqr  helloworld.1      momijiame/greeting:1.0  node3.example.com  Running        Running 4 minutes ago
9eeuo9ug1og7txh2stq4m8css  helloworld.2      momijiame/greeting:1.0  node1.example.com  Running        Running 5 seconds ago
634or7vt454kja1xyzb5fxcqb   \_ helloworld.2  momijiame/greeting:1.0  node2.example.com  Shutdown       Failed 10 seconds ago  "task: non-zero exit (137)"

node3 で確認すると、たしかにコンテナが生まれている。

node3 $ sudo docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS               NAMES
15d48e89915a        momijiame/greeting:1.0   "/usr/local/bin/gunic"   30 seconds ago      Up 27 seconds       8000/tcp            helloworld.2.9eeuo9ug1og7txh2stq4m8css

レプリカ数を増やす

同時に稼働するコンテナ数 (レプリカ) を動的に増減させることもできる。 これには docker service scale コマンドを使う。

ここでは数を 2 から 3 に増やしてみよう。

node1 $ sudo docker service scale helloworld=3
helloworld scaled to 3

先ほどと同じように docker service ps コマンドを使って状況を確認しよう。 すると、すべてのノードでコンテナが稼働しはじめたことがわかる。

node1 $ sudo docker service ps helloworld
ID                         NAME              IMAGE                       NODE               DESIRED STATE  CURRENT STATE               ERROR
7uaq5jpd7zu3wx4y0t4qrxzqr  helloworld.1      momijiame/greeting:1.0  node3.example.com  Running        Running 5 minutes ago
9eeuo9ug1og7txh2stq4m8css  helloworld.2      momijiame/greeting:1.0  node1.example.com  Running        Running about a minute ago
634or7vt454kja1xyzb5fxcqb   \_ helloworld.2  momijiame/greeting:1.0  node2.example.com  Shutdown       Failed about a minute ago   "task: non-zero exit (137)"
43gr58ha2a2f44eyy146wccvi  helloworld.3      momijiame/greeting:1.0  node2.example.com  Running        Running 11 seconds ago

ちなみに、レプリカ数はクラスタのノード数よりも増やすことができる。 その場合は、ひとつのノードで複数のコンテナが起動することになる。

ローリングアップデート

Docker Swarm には、サービスで使うイメージを順番に更新する機能もある。 更新している最中は、そこにアクセスが振り分けられることがない。

試しに helloworld サービスで使うイメージのタグを 1.0 から 2.0 に変更してみよう。 これには docker service update コマンドを使う。

node1 $ sudo docker service update --image momijiame/greeting:2.0 helloworld
helloworld

実行してから docker service ps コマンドを使うと、タグ 1.0 を使っていたコンテナはシャットダウンされている。 そして、代わりにタグ 2.0 を使ったコンテナが各ノードで稼働し始めていることがわかる。

node1 $ sudo docker service ps helloworld
ID                         NAME              IMAGE                       NODE               DESIRED STATE  CURRENT STATE            ERROR
5qg8rxo0sw6ywsgs1jkdf22lo  helloworld.1      momijiame/greeting:2.0  node1.example.com  Running        Running 25 seconds ago
7uaq5jpd7zu3wx4y0t4qrxzqr   \_ helloworld.1  momijiame/greeting:1.0  node3.example.com  Shutdown       Shutdown 28 seconds ago
dr9wpc6kpftiet87m1y60nkpg  helloworld.2      momijiame/greeting:2.0  node3.example.com  Running        Running 22 seconds ago
9eeuo9ug1og7txh2stq4m8css   \_ helloworld.2  momijiame/greeting:1.0  node1.example.com  Shutdown       Shutdown 24 seconds ago
634or7vt454kja1xyzb5fxcqb   \_ helloworld.2  momijiame/greeting:1.0  node2.example.com  Shutdown       Failed 8 minutes ago     "task: non-zero exit (137)"
5sffoje8rnjg2uhpowoutvxk1  helloworld.3      momijiame/greeting:2.0  node2.example.com  Running        Running 28 seconds ago
43gr58ha2a2f44eyy146wccvi   \_ helloworld.3  momijiame/greeting:1.0  node2.example.com  Shutdown       Shutdown 35 seconds ago

もちろんサービスの提供は継続している。

node1 $ curl http://192.168.33.11
Hello, 330a354fa8ea
node1 $ curl http://192.168.33.11
Hello, cfd58c37c40d
node1 $ curl http://192.168.33.11
Hello, 189668ee58a6

ちなみに、各ノードの更新間隔を指定することもできる。 これには docker service update コマンドで --update-delay オプションを指定する。 これはサービスを定義するときに指定することもできる。

$ sudo docker service update --update-delay 1m helloworld
helloworld

更新間隔を 1 分に広げてイメージを変更してみよう。

$ sudo docker service update --image momijiame/greeting:latest helloworld
helloworld

すると node1 が即座に更新される。

$ sudo docker service ps helloworld
ID                         NAME              IMAGE                          NODE               DESIRED STATE  CURRENT STATE           ERROR
8co9b2rt9a5u1q1xnpkmnr4xo  helloworld.1      momijiame/greeting:latest  node3.example.com  Running        Running 2 seconds ago
5qg8rxo0sw6ywsgs1jkdf22lo   \_ helloworld.1  momijiame/greeting:2.0     node1.example.com  Shutdown       Shutdown 3 seconds ago
7uaq5jpd7zu3wx4y0t4qrxzqr   \_ helloworld.1  momijiame/greeting:1.0     node3.example.com  Shutdown       Shutdown 8 minutes ago
dr9wpc6kpftiet87m1y60nkpg  helloworld.2      momijiame/greeting:2.0     node3.example.com  Running        Running 8 minutes ago
9eeuo9ug1og7txh2stq4m8css   \_ helloworld.2  momijiame/greeting:1.0     node1.example.com  Shutdown       Shutdown 8 minutes ago
634or7vt454kja1xyzb5fxcqb   \_ helloworld.2  momijiame/greeting:1.0     node2.example.com  Shutdown       Failed 16 minutes ago   "task: non-zero exit (137)"
5sffoje8rnjg2uhpowoutvxk1  helloworld.3      momijiame/greeting:2.0     node2.example.com  Running        Running 8 minutes ago
43gr58ha2a2f44eyy146wccvi   \_ helloworld.3  momijiame/greeting:1.0     node2.example.com  Shutdown       Shutdown 9 minutes ago

この状態では新しいコンテナの node1 と、古いコンテナの node2 にアクセスが振り分けられているようだ。 コンテナの更新中の node3 にはアクセスが振り分けられない。

$ curl http://192.168.33.11
Hello, e1c8d02b9b98
$ curl http://192.168.33.11
Hello, 189668ee58a6
$ curl http://192.168.33.11
Hello, e1c8d02b9b98
$ curl http://192.168.33.11
Hello, 189668ee58a6

しばらくすると node3 の更新がおわる。

$ sudo docker service ps helloworld
ID                         NAME              IMAGE                          NODE               DESIRED STATE  CURRENT STATE                ERROR
8co9b2rt9a5u1q1xnpkmnr4xo  helloworld.1      momijiame/greeting:latest  node3.example.com  Running        Running about a minute ago
5qg8rxo0sw6ywsgs1jkdf22lo   \_ helloworld.1  momijiame/greeting:2.0     node1.example.com  Shutdown       Shutdown about a minute ago
7uaq5jpd7zu3wx4y0t4qrxzqr   \_ helloworld.1  momijiame/greeting:1.0     node3.example.com  Shutdown       Shutdown 9 minutes ago
dr9wpc6kpftiet87m1y60nkpg  helloworld.2      momijiame/greeting:2.0     node3.example.com  Running        Running 9 minutes ago
9eeuo9ug1og7txh2stq4m8css   \_ helloworld.2  momijiame/greeting:1.0     node1.example.com  Shutdown       Shutdown 9 minutes ago
634or7vt454kja1xyzb5fxcqb   \_ helloworld.2  momijiame/greeting:1.0     node2.example.com  Shutdown       Failed 17 minutes ago        "task: non-zero exit (137)"
4jwq9mphyed0zzc4pvaxrxu4i  helloworld.3      momijiame/greeting:latest  node1.example.com  Running        Preparing 1 seconds ago
5sffoje8rnjg2uhpowoutvxk1   \_ helloworld.3  momijiame/greeting:2.0     node2.example.com  Shutdown       Shutdown 1 seconds ago
43gr58ha2a2f44eyy146wccvi   \_ helloworld.3  momijiame/greeting:1.0     node2.example.com  Shutdown       Shutdown 10 minutes ago

この状況では新しいコンテナの node1 と node3 にアクセスが振り分けられている。 更新中の node2 にはアクセスが振り分けられない。

$ curl http://192.168.33.11
Hello, 0734903b8f27
$ curl http://192.168.33.11
Hello, e1c8d02b9b98
$ curl http://192.168.33.11
Hello, 0734903b8f27
$ curl http://192.168.33.11
Hello, e1c8d02b9b98

そして、さらにしばらくするとすべてのコンテナの更新がおわる。

$ sudo docker service ps helloworld
ID                         NAME              IMAGE                          NODE               DESIRED STATE  CURRENT STATE                ERROR
8co9b2rt9a5u1q1xnpkmnr4xo  helloworld.1      momijiame/greeting:latest  node3.example.com  Running        Running 2 minutes ago
5qg8rxo0sw6ywsgs1jkdf22lo   \_ helloworld.1  momijiame/greeting:2.0     node1.example.com  Shutdown       Shutdown 2 minutes ago
7uaq5jpd7zu3wx4y0t4qrxzqr   \_ helloworld.1  momijiame/greeting:1.0     node3.example.com  Shutdown       Shutdown 11 minutes ago
838oiqxd69e5xkf12inr4hnfw  helloworld.2      momijiame/greeting:latest  node2.example.com  Running        Preparing 1 seconds ago
dr9wpc6kpftiet87m1y60nkpg   \_ helloworld.2  momijiame/greeting:2.0     node3.example.com  Shutdown       Shutdown 2 seconds ago
9eeuo9ug1og7txh2stq4m8css   \_ helloworld.2  momijiame/greeting:1.0     node1.example.com  Shutdown       Shutdown 10 minutes ago
634or7vt454kja1xyzb5fxcqb   \_ helloworld.2  momijiame/greeting:1.0     node2.example.com  Shutdown       Failed 18 minutes ago        "task: non-zero exit (137)"
4jwq9mphyed0zzc4pvaxrxu4i  helloworld.3      momijiame/greeting:latest  node1.example.com  Running        Running about a minute ago
5sffoje8rnjg2uhpowoutvxk1   \_ helloworld.3  momijiame/greeting:2.0     node2.example.com  Shutdown       Shutdown about a minute ago
43gr58ha2a2f44eyy146wccvi   \_ helloworld.3  momijiame/greeting:1.0     node2.example.com  Shutdown       Shutdown 11 minutes ago

この状況では、すべてのコンテナにアクセスが振り分けられる。

$ curl http://192.168.33.11
Hello, 5df4881d10a3
$ curl http://192.168.33.11
Hello, 0734903b8f27
$ curl http://192.168.33.11
Hello, e1c8d02b9b98

サービスを削除する

最後に、サービスが不要になったら docker service rm コマンドで削除しよう。

$ sudo docker service rm helloworld
helloworld

まとめ

Docker 1.12 からオーケストレーションツールの Docker Swarm が本体に同梱されるようになった。 運用に必要そうな機能も一通り揃っていて、単独ですぐに使い始められるところが魅力的だと感じた。

GNU Privacy Guard でファイルを気軽に暗号化する

このファイルは平文のまま置いておきたくないなーっていうようなファイルがたまにある。 例えば、何らかのトークンや個人情報などが書き込まれているもの。 そんなときは GNU Privacy Guard を使うとサクッと暗号化しておくことができて便利そう。 これは OpenPGP という暗号化ソフトウェアの仕様を実装したものだ。

今回使った環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"
$ uname -r
4.4.0-21-generic

下準備

まずは apt で GnuPG をインストールする。

$ sudo apt-get -y install gnupg2

次に、暗号化・復号する対象となるファイルを用意しておく。 これは、ただのテキストファイルにした。

$ cat << 'EOF' > greeting.txt
Hello, World!
EOF

暗号化する

早速、ファイルを暗号化してみよう。

gpg2 コマンドに -c オプションをつけて、暗号化したいファイルを指定する。

$ gpg2 -c greeting.txt
gpg: directory '/home/vagrant/.gnupg' created
gpg: new configuration file '/home/vagrant/.gnupg/dirmngr.conf' created
gpg: new configuration file '/home/vagrant/.gnupg/gpg.conf' created
gpg: keybox '/home/vagrant/.gnupg/pubring.kbx' created

上記を実行すると、暗号化に使うパスワードを 2 回聞かれるので入力しよう。

上手くいけば拡張子に .gpg のついたファイルができあがる。

$ ls
greeting.txt  greeting.txt.gpg
$ file greeting.txt.gpg
greeting.txt.gpg: GPG symmetrically encrypted data (AES cipher)

復号する

次は、先ほど暗号化したファイルを復号してみよう。

まずは、まぎらわしいので元の平文は削除しておく。

$ rm greeting.txt
$ ls
greeting.txt.gpg

そして、今回は何もオプションをつけずに gpg2 コマンドを実行して、引数には暗号化されたファイルを指定する。

$ gpg2 greeting.txt.gpg
gpg: AES encrypted data
gpg: encrypted with 1 passphrase

すると復号されたファイルができあがる。

$ ls
greeting.txt  greeting.txt.gpg

中身を見ると、ちゃんと元のファイルに戻っている。

$ cat greeting.txt
Hello, World!

ちなみに、上記を実行するとパスワードを聞かれずに復号される。 これは、どうやら暗号化したときにキャッシュ的なものがシステムに残るからのようだ。 残る先は ~/.gnupg ディレクトリの中のようなので、これを削除してから復号を試みるとちゃんとパスワードを聞かれる。

$ rm -rf ~/.gnupg/

CentOS 7 の場合

ちなみに、上記では Ubuntu 16.04 LTS を使っていたけど CentOS 7 でも同じように使える。

$ cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
$ uname -r
3.10.0-327.el7.x86_64

インストールに使うのが apt ではなく yum になるだけでパッケージ名は同じだ。

$ sudo yum -y install gnupg2

Python: データセットの標準化について

今回は機械学習とか統計で扱うデータセットの標準化について。

まずは、標準化されていない生のデータセットについて考えてみよう。 それらの多くは、次元によって数値の単位がバラバラだったり、あるいは大きさが極端に異なったりする。 これをそのまま扱ってしまうと、各次元を見比べたときにそれぞれの関係が分かりにくい。 また、機械学習においては特定の次元の影響が強く (または反対に弱く) 出てしまったりすることもあるらしい。 そこで、それぞれの次元のスケールを同じに揃えてやりたい。 これを標準化というようだ。

今回は「Zスコア」という標準化のやり方を扱う。 これは、一言で言ってしまえばデータセットの各要素から平均を引いて、標準偏差で割ったもの。 これをすると、データセットは平均が 0 で標準偏差・分散が 1 になる。

使った環境は次の通り。

$ python --version
Python 3.5.1

NumPy を使った標準化

まずは一番単純な NumPy の配列を直接使ったやり方から。

ひとまず pip を使って NumPy をインストールしておこう。

$ pip install numpy

Python の REPL を起動しよう。

$ python

NumPy をインポートしたら、次にZスコア化する対象となる配列を変数 l に代入する。 ようするに、これがデータセットを模したものということ。

>>> import numpy as np
>>> l = np.array([10, 20, 30, 40, 50])

この時点でデータセットの算術平均は 30 になっている。

>>> l.mean()
30.0

Zスコアによる標準化の第一段階では、まずデータセットの各要素から、データセットの平均値を引く。

>>> l2 = l - l.mean()

データセットの平均値は、これで 0 になった。

>>> l2.mean()
0.0

各要素から平均値を引いたので、中身はこんなことになる。

>>> l2
array([-20., -10.,   0.,  10.,  20.])

次にZスコア化の第二段階では標準偏差を扱う。 今のデータセットの標準偏差は約 14.14 だ。

>>> l2.std()
14.142135623730951

この標準偏差で、同じくデータセットの各要素を割る。

>>> l3 = l2 / l2.std()

これでデータベースの標準偏差は 1 になった。 きれいに 1 になっていないのは浮動小数点の計算誤差だろうね。

>>> l3.std()
0.99999999999999989

中身はこんなことになる。

>>> l3
array([-1.41421356, -0.70710678,  0.        ,  0.70710678,  1.41421356])

そして、これこそが標準化されたデータセットだ。 各要素の単位は、もはや元々のデータセットで扱われていたそれではなくなっている。 代わりに、単に各要素の相対的な位置関係を表したZスコアになっている。

SciPy を使った標準化

さっきは NumPy の配列を自分で標準化してみたけど、実際にはこの作業はライブラリが代わりにやってくれる。 ここでは SciPy を使った例を挙げておこう。

先ほどと同じように、今度は SciPy を pip コマンドでインストールする。

$ pip install scipy

インストールできたら Python の REPL を起動する。

$ python

さっきと同じようにデータセットを模した NumPy 配列を変数 l として用意しておく。

>>> import numpy as np
>>> l = np.array([10, 20, 30, 40, 50])

SciPy では、そのものずばり zscore() という名前の関数が用意されている。

>>> from scipy.stats import zscore

これに NumPy の配列を渡せば一発でZスコアに標準化できる。

>>> zscore(l)
array([-1.41421356, -0.70710678,  0.        ,  0.70710678,  1.41421356])

あっけない。

scikit-learn を使った標準化

同じように scikit-learn にも標準化のユーティリティが用意されている。

まずは scikit-learn を pip でインストールしておく。

$ pip install scikit-learn

Python の REPL を起動したら…

$ python

データセットを模した NumPy 配列を用意して…

>>> import numpy as np
>>> l = np.array([10, 20, 30, 40, 50])

scikit-learn では StandardScaler というクラスを使って標準化する。

>>> from sklearn.preprocessing import StandardScaler

インスタンス化したら、データセットを模した配列を渡す。 これでデータセットの情報が StandardScaler のインスタンスにセットされる。

>>> sc = StandardScaler()
>>> sc.fit(l)

そして StandardScaler#transform() メソッドを使ってデータセットを標準化する。 返り値として標準化されたデータセットが返る。

>>> sc.transform(l)
array([-1.41421356, -0.70710678,  0.        ,  0.70710678,  1.41421356])

ばっちり。 ちなみに、上記を実行すると NumPy 配列の型が int から float に変換されたよ!っていう警告が出る。 けど、意図したものなので無視しておっけー。

まとめ

  • データセットは標準化してから扱ったほうが良いらしい
  • 標準化は「Zスコア」というものを使うのがメジャーっぽい
  • 標準化のやり方には NumPy / SciPy / scikit-learn それぞれにやり方がある

Python: シーケンスアンパックについて

Python では、右辺がタプルなどのとき、左辺に複数の変数を置くことで、その中身を展開できる。 これをシーケンスアンパック (sequence unpack) という。

シーケンスアンパックを試してみる

まずは python コマンドを実行して REPL を起動しておこう。

$ python

例えば example という変数にタプルで 3 つの要素を代入しておく。

>>> example = (1, 2, 3)

これを右辺に置いて、左辺には同じ数の変数 a b c をカンマ区切りで置く。

>>> a, b, c = example

するとタプルの中身が、各変数に代入される。 これがシーケンスアンパックの挙動だ。

>>> a
1
>>> b
2
>>> c
3

シーケンスアンパックできるオブジェクト・できないオブジェクト

シーケンスアンパックは、例えばリストでもできる。

>>> example = [1, 2, 3]
>>> a, b, c = example
>>> a
1
>>> b
2
>>> c
3

シーケンスアンパックできるか否かは、そのオブジェクトがイテラブル (Iterable) か否かによる。 例えば object クラスのインスタンスはイテラブルでないので、シーケンスアンパックできない。

>>> a, b, c = object()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'object' object is not iterable

ユーザ定義クラスでシーケンスアンパックできるようにする

次は、ユーザ定義クラスでシーケンスアンパックできるようにしてみよう。 これは、ようするにそのユーザ定義クラスのインスタンスをイテラブルにするということ。

ユーザ定義クラスをイテラブルにするには、イテレータプロトコルという特殊メソッドを実装する必要がある。 これは具体的には __iter__() と __next__() という、ふたつのメソッドだ。 ただし、イテレータのコンテナオブジェクトであれば、このうち __iter__() さえあればいい。 コンテナオブジェクトというのは、自身のメンバなどを元にイテレータオブジェクトを生成して返すオブジェクトのことをいう。

次のサンプルコードでは User クラスをイテラブルにすることでシーケンスアンパックできるようにしている。 具体的には __iter__() メソッドを実装することで、クラスをイテレータのコンテナオブジェクトにしている。 このメソッドの中では、自身のメンバである name と age をリストにラップした上で iter() 組み込み関数でイテレータオブジェクトを取り出して返している。

>>> class User(object):
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
...     def __iter__(self):
...         return iter([self.name, self.age])
...

それでは、上記のクラスをインスタンス化しよう。

>>> u = User('Spam', 20)

そして、右辺に上記のインスタンスを、左辺は name と age という変数でそれを受ける。

>>> name, age = u

すると、シーケンスアンパックされて各変数に内容が代入された。

>>> name
'Spam'
>>> age
20

ばっちり。

まとめ

  • タプルやリストの内容を複数の変数に一度に代入することをシーケンスアンパックという
  • シーケンスアンパックできるオブジェクトはイテラブルなオブジェクト
  • ユーザ定義クラスをイテラブルにするにはイテレータプロトコルを実装する

参考

5. データ構造 — Python 3.5.2 ドキュメント

Python: Ellipsis について

今回は Python の特殊な定数 Ellipsis について調べてみた。

Ellipsis ってなんだ

Ellipsis というのは、主に拡張スライス文と共に使われる特殊な定数のこと。 これを使うと、例えば配列などのスライスで「...」を指定できるようになる。

3. 組み込み定数 — Python 3.5.2 ドキュメント

具体的な使用例

例えば、高速な数値計算のための配列ライブラリである NumPy の配列には、この Ellipsis を使うことができる。

ひとまず NumPy をインストールしよう。

$ pip install numpy

そして Python の REPL を起動する。

$ python

array という名前で NumPy の配列を作る。

>>> import numpy as np
>>> array = np.array([1, 2, 3])

この配列のスライスに「...」を指定してみよう。 これが Ellipsis だ。

>>> array[...]
array([1, 2, 3])

NumPy では (その次元の) すべての要素を返すことを表すために使われている。

ユーザ定義クラスで Ellipsis を使ってみる

ユーザ定義クラスで Ellipsis を指定できるようにしてみよう。 まず、ユーザ定義クラスでは __getitem__() という特殊メソッドを実装すると、そのインスタンスにスライス ([]) が使えるようになる。 そのメソッドに渡されるキーとして Ellipsis が指定されたら「...(snip)...」という値を返すようにしてみる。

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


class MyClass(object):

    def __getitem__(self, key):
        if key is Ellipsis:
            return '...(snip)...'

        return None


def main():
    obj = MyClass()
    print(obj[...])


if __name__ == '__main__':
    main()

上記を ellipsis.py という名前で保存して実行してみよう。

$ python ellipsis.py
...(snip)...

ばっちり。

Python 2 と 3 における Ellipsis の振る舞い

実は Ellipsis は Python 2.x と 3.x で振る舞いが結構違っている。 それぞれのインタプリタで挙動の違いを確かめてみよう。

まずは Python 3.x から。

$ python --version
Python 3.5.1
$ python

Python 3.x では「...」が単独で Ellipsis オブジェクトとして使える。

>>> ...
Ellipsis

それ対し Python 2.x ではどうなるだろうか。

$ python --version
Python 2.7.10
$ python

なんと「...」だけでは文法エラーになってしまった。

>>> ...
  File "<stdin>", line 1
    ...
    ^
SyntaxError: invalid syntax

Python 2.x では Ellipsis の使えるシチュエーションが、とても限られていることがわかる。

例えば、次のように通常のメソッドの引数として Ellipsis を受け取るようにしてみよう。

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


class MyClass(object):

    def mymethod(self, value):
        if value is Ellipsis:
            return '...(snip)...'

        return None


def main():
    obj = MyClass()
    print(obj.mymethod(...))


if __name__ == '__main__':
    main()

これを、先ほどと同じように ellipsis.py という名前で保存する。 まずは Python 3.x で動かしてみよう。

$ python --version
Python 3.5.1
$ python ellipsis.py
...(snip)...

ちゃんと動いた。

それに対し Python 2.x だと、どうなるだろうか。

$ python --version
Python 2.7.10
$ python ellipsis.py
  File "ellipsis.py", line 16
    print(obj.greeting(...))
                       ^
SyntaxError: invalid syntax

こちらは文法エラーになってしまった。 通常のメソッドの呼び出しに Ellipsis は渡すことができないらしい。

まとめ

  • Ellipsis は「...」を表す特殊な定数
  • Python 2 と 3 では挙動が異なる
  • Python 3 では「...」が Ellipsis オブジェクトになっている
  • そのため、通常のメソッドの引数などにも Ellipsis が使える
  • それに対し Python 2 では拡張スライス文でしか使えない