初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

スポンサーリンク

Python3でのSIGNAL受信時の挙動が実行するコンテナイメージで変わる罠



Dockerこのやろう!と思ったけど、実はDockerさん悪くなかった。

PythonやJavaなどはバージョンさえ同じであれば、ホスト実行でもコンテナ実行でも挙動は変わらないものだと思っていたのだが、コンテナイメージによって挙動が変わる場合があるというのは知らんかったのでメモ。

環境

コンテナ実行環境は以下。

OS Ubuntu 14.04 LTS
Docker 1.11.1

コンテナで実行するコンテナイメージは公式で提供されている以下の2パターン

https://hub.docker.com/_/python/

イメージ OS Python
python:latest Debian(8.4) 3.5.1
python:3-alpine Alpine(4.3) 3.5.1

Debianは知らない人はいないだろうがAlpineって何よ?

Alpine Linux

Alpine Linux | Alpine Linux

Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.

コンテナ向きの軽量ディストリビューション。どれくらい軽量なのかというと、

$ docker images
REPOSITORY      TAG       IMAGE ID       CREATED         SIZE
python      3-alpine    a35dc90b7d05    3 days ago    71.46 MB
python      latest      a00e9008965a    3 days ago   697.7  MB

なんと、通常(Debian)のPythonコンテナイメージの

10分の1程度。

なので、pullも速ければコンテナ起動も速い。デメリットとしては、軽量ゆえにライブラリ類が必要最小限しか導入されていないので、

C/C++で記述されているPythonライブラリを導入する時はちょっと面倒。

なだけだと思っていたのだが、実際はちょっと挙動も違ったので泣けた。

サンプルコード

今回試したコードは以下のもの。

シグナルのハンドリングがPython2系と3系で違うから注意しようぜーという記事を書いた時のもの。

osa030.hatenablog.com

hoge.py

import logging
import signal
import threading
import time

class Worker(threading.Thread):
    def __init__(self):
        self._running = True
        super(Worker, self).__init__()

    def run(self):
        cnt = 0
        while self._running:
            logging.info('hoge[%s]' % cnt)
            cnt += 1
            time.sleep(1)

    def shutdown(self):
        self._running = False

if __name__ == '__main__':
    logging.basicConfig(
        format='[%(threadName)s][%(levelname)s]%(message)s',
        level=logging.DEBUG
    )

    worker = Worker()

    logging.debug('-- START --')
    worker.start()

    def stop_handler(signum, frame):
        logging.debug('GET signal[%s]' % signum)
        worker.shutdown()

    signal.signal(signal.SIGTERM, stop_handler)
    signal.signal(signal.SIGINT, stop_handler)
    worker.join()
    logging.debug('-- STOP --')


で、これが実行させるコンテナイメージで挙動が異なるという悪夢。

試してガッテン

ホスト(Ubuntu)

まずはホスト上で動作確認(Pythonのバージョンは3.4.3)。

  • 実行
$ python3 hoge.py
[MainThread][DEBUG]-- START --
[Thread-1][INFO]hoge[0]
[Thread-1][INFO]hoge[1]
[Thread-1][INFO]hoge[2]
  • 実行中にSIGTERM送信
$ kill -TERM `pgrep python`
  • SIGTERM受けて終了
[Thread-1][INFO]hoge[3]
[MainThread][DEBUG]GET signal[15]
[MainThread][DEBUG]-- STOP --
$

問題なし。

python:latest(Debian)

今度は普通のDebianベースなPythonイメージでコンテナ実行して試す。

コンテナ停止時にコンテナ上で実行しているプロセスにはSIGTERMが通知されてそれでも一定時間死なないとKILLされるのだ。

stop

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL.

  • 実行
$ docker run -v "$PWD":/app -w /app python:latest python hoge.py
[MainThread][DEBUG]-- START --
[Thread-1][INFO]hoge[0]
[Thread-1][INFO]hoge[1]
[Thread-1][INFO]hoge[2]
  • 実行中にコンテナ停止
$ docker stop `docker ps -q`
7fd414df81b8
  • コンテナ停止(コンテナ上のプロセスはSIGTERM受けて終了)
[Thread-1][INFO]hoge[3]
[MainThread][DEBUG]GET signal[15]
[MainThread][DEBUG]-- STOP --
$ 

これも全く問題ナッシング。そりゃコンテナで動作変わったら困るがな。

python:3-alpine(Alpine)

さて、今度はAlpine Linux版だ。

  • 実行
$ docker run -v "$PWD":/app -w /app python:3-alpine python3 hoge.py
[MainThread][DEBUG]-- START --
[Thread-1][INFO]hoge[0]
[Thread-1][INFO]hoge[1]
[Thread-1][INFO]hoge[2]
  • 実行中にコンテナ停止
$ docker stop `docker ps -q`
(しばらく応答がない)
82efe801680f
  • コンテナ停止(コンテナ上のプロセスはSIGTERMを受けずに最終的にKILLで終了)
[Thread-1][INFO]hoge[3]
[Thread-1][INFO]hoge[4]
[Thread-1][INFO]hoge[5]
[Thread-1][INFO]hoge[6]
[Thread-1][INFO]hoge[7]
[Thread-1][INFO]hoge[8]
[Thread-1][INFO]hoge[9]
[Thread-1][INFO]hoge[10]
[Thread-1][INFO]hoge[11]
[Thread-1][INFO]hoge[12]
[Thread-1][INFO]hoge[13]
$

うーん、まいっちんぐ。

Dockerfileを見るとPython3をソースからビルドしてインストールしているのだが、特に特別なオプションを指定している訳でもない。

python/Dockerfile at 0610d9ccc2dc8ad4ab6038f775e7a28cadf12114 · docker-library/python · GitHub

となるとPythonではなく、Alpineの場合はDebianと挙動が異なるのかなとか思うわけだけど、そこまで調べるのも正直辛いというか面倒くさい。

対策

Python2系の時と同様にjoin()でシグナルを待たないでsignal.pause()で待つ。

     signal.signal(signal.SIGTERM, stop_handler)
     signal.signal(signal.SIGINT, stop_handler)
+    signal.pause()
     worker.join()
     logging.debug('-- STOP --')

これでどのパターンでも正常にSIGTERMを受信して終了する。この問題は3系で解決したのかと思っていたけどなかなか奥が深いのであるな。
ということで、

signal.pause()大事。
オジサンも流石に覚えました。

Docker実戦活用ガイド

Docker実戦活用ガイド

実践 Python 3

実践 Python 3

まいっちんぐマチコ先生  【コミックセット】

まいっちんぐマチコ先生 【コミックセット】

スポンサーリンク