子育てしながらエンジニアしたい

現在 2 歳女の子の子育て中エンジニアによる、技術系 + 日常系ブログ。

Python: マルチプロセスで並列処理をさせるには multiprocessing.Pool が超便利

独立した並列処理を高速化したい

ふつう並列処理といえば、GUI と処理を切り離すなどが思い当たります。
GUI と処理を切り離すといっても、相互にメッセージなどをやり取りして協調動作する必要があります。

今回やりたいのは、そういう協調動作を必要としない、独立した並列処理です。
たとえば、大量のファイルをすべて別個に圧縮するという場合、それぞれのファイルは独立に処理することができます。
これはスレッドを分けても意味が無いので、プロセスを分けて処理する必要があります。
こんなときに、Python の multiprocessing.Pool がとても便利だったので記録として残しておきます。

課題

20 個の処理をマルチプロセスで並列処理したいとします。
CPU がクアッドコアであれば、4 個を同時に走らせることができます。
普通に考えると、プロセスの終了状況を監視して、終了したら次のタスクを割り当てるという流れが必要になります。
実際にこういう方法でプログラムを書いていたのですが、これはけっこう面倒でミスをしやすいプログラムでした。

multiprocessing モジュールの Pool クラスは、なんとこれを 1 行でやってくれます。
はじめから知っておきたかった...

サンプルプログラム

1 秒かけて与えられた数字の 2 乗を返すだけのサンプルプログラムです。
数字は range(20) で与えるようにしていますが、ここはリストなどでも大丈夫です。

出力結果は以下のようになります。

param 0 is being processed
param 2 is being processed
param 4 is being processed
param 6 is being processed
param 1 is being processed
param 3 is being processed
param 5 is being processed
param 7 is being processed
param 8 is being processed
param 10 is being processed
param 12 is being processed
param 14 is being processed
param 9 is being processed
param 11 is being processed
param 13 is being processed
param 15 is being processed
param 16 is being processed
param 18 is being processed
param 17 is being processed
param 19 is being processed
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]

プロセスを並列で走らせ、返り値は順番どおりに返してくれる、ほんとに便利なモジュールです!

注意点としては、Pool.map は range(20) の値を 0 から順番に与えるわけではない、ということです。
出力結果を見ると、0,2,4,6 が最初に来ていることがわかります。
この順番を 1 ずつにしたい!という場合は、Pool.map にもう一つ引数を追加する必要があります。

results = pool.map(parafunc, range(20), 1)

この最後の引数 "1" は chunksize と呼ばれる引数で、まとまりを意味するようです。
これを 1 にすると 0,1,2,3,...、2 だと 0,2,4,6,...、3 なら 0,3,6,9... という順番で値が渡されます。
なお、ここを変えたとしても、渡される結果の順番は同一なので安心です。

Raspberry pi を NAT ルーター兼 DHCP + DNS サーバーにしたい

やっぱり外にもつなぎたい...

前回の記事で、クローズネットワーク内に Raspberry pi を置いて、DHCP + DNS サーバーにしました。

edosha.hatenablog.jp

当然ながらクローズネットワークなので、外には出られません。
しかし開発をしていると、どうしても外につなぎたいことがあります。

pip とか apt-get とか...

そのときにいちいち回線をつなぎ直すのは面倒です。
なので、前回使った Raspberry pi に NAT ルーターの機能を追加することにしました。

ネットワークの完成イメージ

f:id:edosha:20170706112816p:plain:w500

すでに外につながる PPPoE ルーターがあり、その配下に Raspberry pi を置くことにします。
そのまた下にクローズネットワークを構築します。

このとき、Raspberry pi 配下にある端末へのアクセスに対しては Raspberry piDNS サーバーとして動作し、
そうでない端末 (google.com とか) には、Google DNS (8.8.8.8) を使うことにします。

必要な部品

Raspberry piEthernet の口が一つしかありません。
しかし上記のようなネットワークを組むには、LAN 側と WAN 側の 2 つの Ethernet が必要になります。

そこで、WAN 側には USB-Ethernet 変換を使うことにしました。
下記の製品であれば、ドライバーがすでに Raspbian の中に入っているので、これにしました。

自分の環境では、USB ポートにさすだけで eth1 として認識されました。

Raspberry pi の設定

それでは Raspberry pi の設定をします。
前回設定した Dnsmasq の一部書き換えと、IP 転送の設定をします。

Dnsmasq の書き換え

一行だけ書き換えれば OK です。
DHCP でアドレスを配るときに、アクセスすべき DNS サーバーのアドレスを追加するようにします。

/etc/dnsmasq.more.conf を開いて、前回の記事では以下のようになっているところを...

# DHCPクライアントに通知するDNSサーバのIPアドレス
dhcp-option = option:dns-server, 192.168.0.1

192.168.0.1 のあとに 8.8.8.8 を追加するだけです。

# DHCPクライアントに通知するDNSサーバのIPアドレス
dhcp-option = option:dns-server, 192.168.0.1, 8.8.8.8

そして Dnsmasq サービスを再起動しましょう。

$ sudo service dnsmasq restart

IP 転送の許可

Stack overflow の回答を参考にしました。

raspberrypi.stackexchange.com

まずは /etc/sysctl.conf を開きます。

$ sudo emacs /etc/sysctl.conf

この中で、#net.ipv4.ip_forward=1 となっているところがあるので、その # を消します。

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1  # <-- コメントアウトを解除する

次に、以下のように IPv4 の転送を有効にします。

$ sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"

iptables の設定

最後に iptables の設定をします。
なお以下に記載する設定は、eth0 がクローズネットワーク側、eth1 が外側なので、ご留意ください。

まず iptables に残っているエントリを全部消去します。

$ sudo iptables -F
$ sudo iptables -X
$ sudo iptables -t nat -F
$ sudo iptables -t nat -X
$ sudo iptables -t mangle -F
$ sudo iptables -t mangle -X
$ sudo iptables -P INPUT ACCEPT
$ sudo iptables -P FORWARD ACCEPT
$ sudo iptables -P OUTPUT ACCEPT

次に eth0 - eth1 間の通信を有効にします。

$ sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$ sudo iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
$ sudo iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE

これで、外側への通信ができるようになったはずです。

このままではリブートしたあと設定が消えてしまうので、この設定をセーブし、起動時に読み込めるようにします。
まず、設定をファイルに書き出します。

$ sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"

このファイルを起動時に読み込むようにするために、/etc/rc.local の exit 0 の上に以下の記述を追加します。

sudo iptables-restore < /etc/iptables.ipv4.nat

これで再起動しても大丈夫になりました。

Raspberry pi を DHCP + DNS サーバーにしたい

f:id:edosha:20170622083157j:plain:w400

クローズネットワークの台数増加

クローズネットワークを組んで使っています。
最初は台数が少なかったので固定 IP でよかったのですが、台数増加に伴い DHCP サーバーが欲しくなってきました。
ついでに名前もつけてあげれば IP を気にする必要がなくなります。
そこで、Raspberry piDHCPDNS サーバーに仕立て上げることにしました。

使用したのは Raspberry pi 3 Model B です。

OS は Raspbian Jessie Lite の April 2017 バージョンを使用しました。

Raspberry pi の初期設定

SSH を有効にする

通常 Raspberry pi はディスプレイと USB マウス/キーボードを接続して操作しますが、DHCP + DNS サーバーにそんなものは贅沢です。
どうせローカルネットワークに入るので、SSH を有効にして SSH 経由で操作できるようにしました。

以下のリンクを参考に。
qiita.com

IP アドレスの設定

SSH でつなぐには IP アドレスを知らないといけません。
というわけで Raspberry pi に固定アドレスを振ることにしました。

2017/07/06 追記
OS Raspbian Jessie からは、/etc/network/interfaces ではなく /etc/dhcpcd.conf をいじるのが正しいようです。
なぜか /etc/network/interfaces でうまくいっていたのですが、Raspberry pi を 2 台導入して同じことをやろうとしたら、できませんでした。
なので /etc/dhcpcd.conf の書きかえ方も記載しておきます。

sudo nano /etc/dhcpcd.conf

最後の方に以下の記述を入れます。

interface eth0
static ip_address=192.168.0.1/24
static domain_name_servers=192.168.0.1


こちらは古い記述です。

アドレスは /etc/network/interfaces の編集で行います。

sudo nano /etc/network/interfaces

eth0 インターフェースに 192.168.0.1 を割り当てます。

auto eth0
iface eth0 inet static
    address 192.168.0.1
    network 192.168.0.0
    netmask 255.255.255.0
    broadcast 192.168.0.255
    dns-nameservers 127.0.0.1 192.168.0.1
    dns-search local
SSH を有効にする

SSH を有効にするには raspi-config から行います。

pi@raspberrypi:~ $ sudo raspi-config

以下のような設定画面が出てきます。

f:id:edosha:20170622090958p:plain:w600

ここで、"5 Interfacing Options" を選択、"P2 SSH" を選択して、Yes を選択すれば OK。

SSH の接続確認

Windows なら Tera term とか、Mac ならターミナルとかから接続できるか確認します。
デフォルトのユーザ名は pi、パスワードは raspberry です。

$ ssh pi@192.168.0.1

これでディスプレイとか USB 類を外し、SSH 経由で操作できるようになりました。

emacs のインストー

ここからは、いろいろとテキストをいじっていくことになります。
nano は使い慣れていないので、大好きな emacs をインストールしておきます。

$ sudo apt-get install emacs

Caps lock を Ctrl にする

以下のサイトのまんまです。

l-w-i.net

/etc/default/keyboard というファイルを書き換えます。

$ sudo emacs /etc/default/keyboard

途中に XKBOPTIONS という部分があるので、そこを以下のように書き換えます。

XKBOPTIONS="ctrl:nocaps"

このあと再起動すれば OK。

DHCP + DNS サーバーの導入

大部分は以下のリンクを参考にしました。
一部、DNS の設定や運用部分を追記しています。

gazee.net

Dnsmasq のインストー

apt-get でインストールします。

$ sudo apt-get install dnsmasq

設定

設定ファイルを編集します。

$ sudo emacs /etc/dnsmasq.conf

/etc/dnsmasq.conf の下のほうにある
#conf-file=/etc/dnsmasq.more.conf
の#を削除します。

# Log lots of extra information about DHCP transactions.
#log-dhcp

# Include another lot of configuration options.
conf-file=/etc/dnsmasq.more.conf
#conf-dir=/etc/dnsmasq.d

# Include all the files in a directory except those ending in .bak
#conf-dir=/etc/dnsmasq.d,.bak

この /etc/dnsmasq.more.conf を作成して編集します。

$ sudo emacs /etc/dnsmasq.more.conf
# ローカルホスト名を上位DNSに転送しない
domain-needed
# ローカルIPアドレスの逆引きを上位DNSに転送しない
bogus-priv
# ショートドメイン名を補完するドメイン名(hoge -> hoge.local)
local = /local/
# ローカルドメイン名
domain = local
# ショートホスト名を補完する
expand-hosts

# DHCPで割り当てするアドレスの範囲とリース時間
dhcp-range = 192.168.0.50, 192.168.0.200, 12h

# DHCPクライアントに通知するルータのIPアドレス
dhcp-option = option:router, 192.168.0.1
# DHCPクライアントに通知するDNSサーバのIPアドレス
dhcp-option = option:dns-server, 192.168.0.1

# 固定アドレスをふって、名前をつける
dhcp-host = aa:aa:aa:aa:aa:aa, pc1, 192.168.0.10, infinite

# 名前だけつける
dhcp-host = bb:bb:bb:bb:bb:bb, pc2

これでクライアントに関しては DNS/DHCP ともに動作します。
ただこの方法だと、dnsmasq が DHCP を発行した場合のみ名前解決がされるようで、固定アドレス (Raspberry pi 自身) を最初から持っているものは名前解決できませんでした。

これを解決するには、/etc/hosts に名前を書けば良いようです。

$ sudo emacs /etc/hosts

Raspberry pi 自身のアドレスを書きます。

127.0.0.1	localhost
::1		localhost ip6-localhost ip6-loopback
ff02::1		ip6-allnodes
ff02::2		ip6-allrouters

127.0.1.1	raspberrypi

192.168.0.1   raspi.local raspi

設定ができたので dnsmasq を再起動します。

$ sudo service dnsmasq restart

運用

DHCP リース記録の確認

どのように DHCP がリースされたかが気になったので調べてみました。
/var/lib/misc/dnsmasq.leases を見ることで実現できます。

$ cat /var/lib/misc/dnsmasq.leases 
0 aa:aa:aa:aa:aa:aa 192.168.0.10 pc1 01:aa:aa:aa:aa:aa:aa:aa
1506215087 bb:bb:bb:bb:bb:bb 192.168.0.50 pc2 01:bb:bb:bb:bb:bb:bb

一番左がリース時間のようです。

不要な DHCP リースの削除

dnsmasq の設定をいろいろ試していると、リース時間無限大でいらないエントリが残ってしまいました。
そういうものを消すには、単に上記の /var/lib/misc/dnsmasq.leases の不要な行を消せば良いようです。
その場合は、いったん dnsmasq のサービスを止めたほうが良さそうです。

$ sudo service dnsmasq stop
$ sudo emacs /var/lib/misc/dnsmasq.leases
0 aa:aa:aa:aa:aa:aa 192.168.0.10 pc1 01:aa:aa:aa:aa:aa:aa:aa
$ sudo service dnsmasq start

これでローカルネットワークの使い勝手が向上しました。

続編

今回はクローズネットワークを Raspberry pi で作りました。
ただこのあと、結局このネットワークを外につなぎたくなりました。
そのときの続編はこちらからどうぞ。

edosha.hatenablog.jp