dockerとdocker-composeの使い方メモ

プライベートで使ってるVPSの環境をdocker-composeで動かしたのだけど、忘れてもいいようにメモを残す。
ホストOSは、Ubuntu16.04。

インストール

dockerは手順通りaptでインストールすればよい。
Installation on Ubuntu
docker-composeは、dockerとは別途インストールが必要(1ファイルのコマンドをダウンロードして配置するだけ)
Install Compose
dockerコマンドを実行するユーザーはdockerグループに属している必要があるので、インストール後にusermodで設定しておく(usermodで設定したグループは再ログインで反映される)

Dockerイメージの準備

DockerHubからpullしてくるか、Dockerfileを作成してビルド(docker build)する。
ビルドの際に、名前の指定をしとかないと、無名になりハッシュ値で指定することになる。
なるべくオフィシャルのイメージを使って、自前ビルドや野良ビルドのイメージは避けたほうがよさそう。メンテが面倒。
野良のイメージを使う場合は、セキュリティに注意する。バックドアが仕込まれたイメージなどもあるらしい。

コンテナの作成と起動

docker runコマンドで起動されるが、これはコンテナ作成+プロセス実行であって、実行が終わってもコンテナが消えないで残るので注意。
コンテナ一覧はdocker psで見れる。-aオプションを指定すると、実行中以外のコンテナも表示される。

docker ps -a

終了したプロセスの再実行はdocker startでできる。
runの際にポートマッピングやvolumeのマウントなど、全部指定する。コンテナ名をちゃんと指定したほうがよい。
「-d」 (detouch) オプションを指定しないとforegroundで実行される(systemdやsupervisorなどから監視させるならdetouchしなくていい)
docker-composeで管理するなら、docker-compose.ymlにrunのオプションを書いてしまう。

コンテナの停止

docker stopで停止できる。停止してもコンテナは消えないので、startで再度実行できる。

再起動

docker restartでプロセスを再起動できる。runで指定したオプションを変更するならコンテナの再生成が必要かも?
設定ファイルが入ったディレクトリをVOLUMEでマウントさせとけば、restartでプロセスに読み込ませるのも可能か。

プロセスの自動再起動

docker runで実行するときに --restart オプションでalwaysを指定しておくと、プロセスが死んでもdocker-engineが自動で再起動してくれる。
また、alwaysの場合は、OSを再起動したときも自動で起動される。
Docker run reference -- Restart policies (--restart)
例:

$ docker run -d -v /tmp/ftp/:/srv/soloftpd/ \
  -p 21:21 -p 50000-50009:50000-50009 \
  --env OPTIONS="--masquerade-address=192.168.0.2" \
  --name=soloftpd --restart=always tokibito/soloftpd

復数の依存するサービスを起動する

docker-composeを使って依存を記述すればいい。

docker-compose

docker-compose.ymlにdocker runのオプションなどを書いといて、複数のイメージをまとめてビルド、コンテナを作って実行するところまでやってくれるラッパーツール。

docker-compose up

でイメージをビルドしてコンテナを生成して起動までやってくれる。 -dオプションを指定すればバックグラウンドで実行できる。

docker-compose down

でコンテナを停止して破棄してくれる。

docker-composeの管理

docker-compose.ymlと各Dockerイメージの管理とかディレクトリ構成どうするか。
単一リポジトリで管理するのもありだけど、gitを使ってるなら、Dockerfileはそれぞれ別リポジトリに置いて、submoduleでdocker-compose用のリポジトリから参照すれば、個別に再利用しやすく、バージョン管理も楽そう。

通信料金の計算2016夏

前回の計算以降、プランの見直しや通信回線の解約、契約を行ったので、メモを残す。

現状の使い方

通信料金

  • au使用料(ガラケー): 1,334円/月
  • iijmio(ミニマムスタートプラン): 900円/月
  • 0 SIM: 0円/月

合計: 2,234円/月

感想

前回から5000円近く下がった。乗り換えに伴い、解約手数料や端末購入代金が発生したので、ペイするのは半年後ぐらいから。
ガラケーを辞めればもう少し下げれるけど、頑丈な通話用の端末をどうするかに悩む。
自宅のインターネット回線は、マンション一括契約のもので1,500円/月ぐらいなので、計算には入れてない。

pyftpdlibを使って1ユーザー専用のFTPサーバーを作った

pyftpdlibを使って、1ユーザー専用のFTPサーバーを作ってました。
でかいデータの受け渡しなんかに使ったりするのにFTPサーバーがやっぱり便利なんで。
ついでにDockerを試したりしてた。

DjangoのDeprecationWarningを確認する

Djangoフレームワークでは、APIが変更、廃止される場合には、次のバージョンですぐに変わってしまうのではなく、警告を上げた上で2つ先のバージョンで削除されることになっています。
Deprecation policy
Pythonの組み込み例外であるDeprecationWarningを継承したクラスがDjangoでは定義されており、必要に応じてこれを確認できます。

確認方法

Pythonの実行オプションとして -Wd のように指定するとDeprecationWarningを表示できるので、これを「manage.py test」や「manage.py check」の実行時に指定すればよいです。
試したバージョンはPython3.5, Django1.9.5です。

$ python -Wd manage.py test

実行結果:

$ python -Wd manage.py test
Creating test database for alias 'default'...
/home/tokibito/sandbox/django_dep/myproject/myapp/urls.py:5: RemovedInDjango110Warning: Support for string view arguments to url() is deprecated and will be removed in Django 1.10 (got index). Pass the callable instead.
  url('^$', 'index'),
/home/tokibito/sandbox/django_dep/myproject/myapp/urls.py:5: RemovedInDjango110Warning: django.conf.urls.patterns() is deprecated and will be removed in Django 1.10. Update your urlpatterns to be a list of django.conf.urls.url() instances instead.
  url('^$', 'index'),
/home/tokibito/sandbox/django_dep/myproject/myproject/urls.py:20: RemovedInDjango20Warning: Passing a 3-tuple to django.conf.urls.include() is deprecated. Pass a 2-tuple containing the list of patterns and app_name, and provide the namespace argument to include() instead.
  url(r'^admin/', include(admin.site.urls)),
.
----------------------------------------------------------------------
Ran 1 test in 0.021s

OK

RemovedInDjango110WarningとRemovedInDjango20Warningの両方が表示されますが、20が不要な場合は、-Wオプションに指定する文字列を変更すれば除外できます。

$ python -W::DeprecationWarning manage.py test

これはRemovedInDjango20WarningがPendingDeprecationWarningを継承しているからです。
Pythonの実行オプションについては、ドキュメントを参照してください。
110を表示せずに20のみを表示したい場合は、PendingDeprecationWarningを指定するとできます。

$ python -W::PendingDeprecationWarning manage.py test

アップグレード作業をどのように進めるか

今、使っているバージョンでのDeprecationWarningを解消してからアップグレードすると、コードを大きく変更しなくても1つ次のバージョンでなら動作します。
先にDjangoのバージョンを上げてしまうと、アプリケーションは「修正しないと動かない」です。
アプリケーションが「動いてる状態」で警告を解消するほうが、手間は少ないです。
そのため、DeprecationWarningを修正しつつ、1つずつバージョンを上げていくのが結果的には楽なのかもしれないなと思いました。
もちろんテストコードは必須です。

cx_Oracleをマルチスレッドで使う

PythonOracle Databaseに接続するには、cx_Oracleを使う。
マルチスレッド環境下でcx_Oracleを使う場合にハマったのでメモを残す。
Oracleのクライアントライブラリは、マルチスレッドで使う際には、OCI_THREADEDというモードで使わないといけないらしい。
cx_OracleをOCI_THREADEDモードで使うには、connect関数のthreadedオプションにTrueを指定する。

>>> import cx_Oracle
>>> connection = cx_Oracle.connect(
...     "testuser",
...     "testpassword",
...     "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracledb.example.com) (PORT=1521))(CONNECT_DATA=(SID=ORCL)))",
...     threaded=True)

cx_Oracleのドキュメントにも説明があった。

The threaded argument is expected to be a boolean expression which indicates whether or not Oracle should use the mode OCI_THREADED to wrap accesses to connections with a mutex.
Doing so in single threaded applications imposes a performance penalty of about 10-15% which is why the default is False.

Module Interface — cx_Oracle 5.2.1 documentation

DjangoOracleを使っていて、マルチスレッドで動作させる場合だと、settingsのデータベース設定に指定すればよい。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.oracle',
        'NAME': 'oracledb.example.com:1521/orcl',
        'USER': 'testuser',
        'PASSWORD': 'testpassword',
        'OPTIONS': {
            'threaded': True,
        }
    }
}

この指定をしない状態で、gunicornのThreadWorkerで動かしていた際に、低頻度でワーカープロセスがログを出力せずに死ぬ、という現象が発生していた。
シングルスレッドの場合は、threaded=Falseのほうがパフォーマンスが良いそうなので、使いどころに気をつけたい。

concurrent.futures.ThreadPoolExecutorを使ってみる

Python 3.2から追加されたconcurrent.futuresモジュール。
ThreadPoolExecutorを使うと、スレッドプールで処理を実行できる。

コード

main.py

import time
import threading
from concurrent.futures import ThreadPoolExecutor


def spam():
    for i in range(3):
        time.sleep(1)
        print("thread: {}, value: {}".format(threading.get_ident(), i))


def main():
    tpool = ThreadPoolExecutor(max_workers=3)
    for i in range(6):
        print("Threads: {}".format(len(tpool._threads)))  # スレッド数を表示
        tpool.submit(spam)
    print("main thread exit.")

実行結果

(venv) tokibito@tokibito-MacBookAir:~/sandbox$ python -c "import main;main.main()"
Threads: 0
Threads: 1
Threads: 2
Threads: 3
Threads: 3
Threads: 3
main thread exit.
thread: 140208052463360, value: 0
thread: 140208044070656, value: 0
thread: 140208035677952, value: 0
thread: 140208052463360, value: 1
thread: 140208044070656, value: 1
thread: 140208035677952, value: 1
thread: 140208052463360, value: 2
thread: 140208044070656, value: 2
thread: 140208035677952, value: 2
thread: 140208052463360, value: 0
thread: 140208044070656, value: 0
thread: 140208035677952, value: 0
thread: 140208044070656, value: 1
thread: 140208052463360, value: 1
thread: 140208035677952, value: 1
thread: 140208044070656, value: 2
thread: 140208052463360, value: 2
thread: 140208035677952, value: 2
  • スレッド数は必要になったときに増える
  • max_workersを超えるタスクを投げた場合は、キューに入れられて、ワーカースレッドが暇になったら次が実行される

スレッドが終了するタイミングについて検証

Pythonでスレッドをしばらく使っていなくて、どういう動きするのだったか忘れてたので、検証したメモを残す。
試した環境は、Ubuntu 14.04、Python 3.5.1。

検証に使ったコード

main.py:

import threading
import os
import sys
import time
import signal


def spam():
    for i in range(5):
        time.sleep(1)
        print("value: {}".format(i))


def main_1():
    t = threading.Thread(target=spam)
    t.start()
    print("Thread: {} started.".format(t.ident))
    sys.exit()


def main_2():
    t = threading.Thread(target=spam)
    t.start()
    print("Thread: {} started.".format(t.ident))
    t.join()  # スレッドの処理が終わるのを待つ
    print("Thread end.")
    sys.exit()


def main_3():
    t = threading.Thread(target=spam)
    t.start()
    print("Thread: {} started.".format(t.ident))
    # 自身のプロセスにSIGKILLを送って強制終了
    os.kill(os.getpid(), signal.SIGKILL)

実行結果

pythonの-cオプションを使って、各検証の関数を実行。

(venv) tokibito@tokibito-MacBookAir:~/sandbox$ python -c "import main;main.main_1()"
Thread: 140258192140032 started.
value: 0
value: 1
value: 2
value: 3
value: 4
(venv) tokibito@tokibito-MacBookAir:~/sandbox$ python -c "import main;main.main_2()"
Thread: 139913801033472 started.
value: 0
value: 1
value: 2
value: 3
value: 4
Thread end.
(venv) tokibito@tokibito-MacBookAir:~/sandbox$ python -c "import main;main.main_3()"
Thread: 140079644133120 started.
強制終了
  • sys.exit()を呼んでも、処理中のスレッドがいると、プロセスは終了しない
  • Thread.join()を呼ぶと、スレッドが終了するまで待つ
  • SIGKILLをプロセスに送ると、スレッドごとプロセスが強制終了される

2018/4/14追記

  • daemon=True を指定してデーモンモードでスレッドを作った場合は、スレッドの終了を待たずに強制終了する。