技術部門のBlog

Code & Daily thoughts.

Dockerで実現するリアルタイムシステムのためのプロセスパーティショニング

「パーティショニング」はリアルタイムシステムや高信頼システムを構築する上で、あると便利なプロセスを隔離する機能です。

今回は「Docker」の内部で使われているLinux軽量コンテナ技術を使って簡易的なパーティショニングを実現します。

DockerはWebの世界で極めて高い注目を集めている強力なコンテナ構築環境です。知能ロボットなど複雑なソフトウェアの組み合わせからなるリアルタイムシステム構築でもその強力な機能が使える可能性を示すことができればと思います。

リアルタイムというと「とにかく処理を速くする」というイメージがあるかもしれませんが、ロボットにおけるリアルタイムとは「処理を決められた時間に始めて決められた時間までに確実に終わらせること」を指します。例えば、自動走行車における操舵やブレーキは時間に遅れてしまうと事故を招きます、これはリアルタイムです。一方で、ビットコインのマイニングのような処理は非常に高速な計算ではありますが、時間についての要求は甘いのでリアルタイムではありません(同じコインを争って見つけようとしている場合はリアルタイムになりえますが)。

リアルタイムシステムを考える場合、ソフトウェアをその処理に必要なCPUパワーこみで考える必要があります。ソフトウェア開発においては

モジュール=アルゴリズム+データ構造(=プログラム)

ですが、リアルタイムシステムにおいては

モジュール=プログラム+プロセッシングパワー

と考えてシステムを設計していく必要があるのです。

知能ロボットを開発していると、開発の過程で機能を高度化させようとしてソフトウェアモジュールを足したところ、リアルタイム性が崩れてしまい、システム全体としてうまく動作しなくなってしまうということが良く起ります。

そのような時に浮かぶのが「プログラム+それを実行するのに必要なプロセッシングパワー」を組でシステムに追加できるようにしてしまえば理想的にシステム拡張できるのではないか、というアイディアです。

安西先生、山崎先生のASPIRE http://ci.nii.ac.jp/naid/110002873424

油田先生らの機能分散アーキテクチャ https://www.jstage.jst.go.jp/article/jrsj1983/14/2/14_2_238/_article/-char/ja/

などは、この理想を追い求めた代表的な研究です。

このブログの筆者もこの理想を追い求めた過去があります(3番目の著者)。 http://ci.nii.ac.jp/naid/110002775061

とはいえ、現実的に考えると複数独立したプロセッサを同一システムに組み込むのであれば、最初からマルチコアプロセッサを使ってしまった方が周辺回路を単一化できるぶん有利です。また、ロボットの電気回路はモータ制御の大電流のスイッチングからくるノイズで不安定になりがちなので、弱電系を分散させて通信でつなぐより制御系から離れた場所で一ヶ所にまとめてしまった方がより安定して動作させることができるという利点もあります。

この背景から同じプロセッサ上で複数ソフトウェアを仲良くリアルタイム動作させようと皆試みるわけなのですが、UNIX系OSに元々備わっているnice値の調整だけではなかなか思うように調整できず苦労するわけです。

このような状況で便利に使えるのがパーティショニング機能です。パーティショニングではマルチコアプロセッサを相互に独立したプロセッサとして扱い、各プロセッサ上で動くプロセスの分離や保護を行ってくれます。

組み込み系の本格的なパーティショニング機能付きOSにはTOPPERS/PARKがあるのですが、無料版では残念ながら動作する環境が極めて限られています。 https://www.toppers.jp/park.html

今回Linuxで汎用的に動作するDockerを使ってパーティショニングを実現してみます。 Dockerを使ったパーティショニングについて説明する前にDockerの元の機能について紹介します。

Dockerは分類から言うと仮想化ソフトウェアではあるのですが、とても「薄い」仮想化を行うのが特徴です。DockerはホストOSとしてLinuxを使うのですが、ゲストOSでもそのLinuxをそのまま使います。Linuxをそのまま使うためゲスト側ではLinux系のOSしか動作しないのですが、Ubuntu, Debian, CentOS, Redhatの各バージョンを仮想化させて同時に動作させることができるのです。

digraph g { Ubuntuイメージ -> Linuxカーネル CentOSイメージ -> Linuxカーネル Redhatイメージ -> Linuxカーネル } g Ubuntuイメージ Ubuntuイメージ Linuxカーネル Linuxカーネル Ubuntuイメージ->Linuxカーネル CentOSイメージ CentOSイメージ CentOSイメージ->Linuxカーネル Redhatイメージ Redhatイメージ Redhatイメージ->Linuxカーネル

上記の仮想化はLinuxのファイル入出力システムコールの設定を少し変更することで実現されています。各プロセスは同じLinuxカーネル上で動作するのですが、プロセスごとにファイルアクセスAPIを通して見えるファイルツリーが違うようにカーネル側から制御するのです。Linuxでセキュリティに使われるchrootという仕組みを知っている方は、その応用として理解するとわかりやすいでしょう。

この仕組を使うと以下のようなことができます。RTMの公開されているコンポーネントの中にOpenHRIというコンポーネント集があります。このブログの著者が前職で開発していたソフトウェアなのですが、バイナリパッケージがUbuntu 10.04以降更新されておらず、最近のUbuntuでは動作させることができません。

ここで、以下のようなDockerfile(Dockerの設定ファイル)を書きます。

Dockerfile
1
2
3
4
5
6
7
8
9
FROM ubuntu:10.04

MAINTAINER Yosuke Matsusaka <yosuke.matsusaka@gmail.com>

RUN apt-get update
RUN apt-get install -y python-software-properties
RUN apt-add-repository ppa:openhri/ppa
RUN apt-get update
RUN apt-get install -y openhriaudio

その後、同じフォルダで以下のコマンドを入れると、Ubuntu 10.04 (Lucid)のイメージがDockerのインデックスサイトからダウンロードされ仮想イメージの構築が行われ、最新の14.04上であってもファイルツリーとしては10.04の環境上でOpenHRIのプロセスを立ち上げることができます。

1
2
$ docker build -t openhri-lucid .
$ docker run openhri-lucid [イメージ上で起動したいコマンド]

Dockerを使って起動されたプロセスにはファイルツリーだけでなく、Linux標準の仮想ネットワークブリッジ作成機能を使ってホストとは独立したネットワークが作成され接続されます。

各プロセスは厳密に言うとLinux上で動作するいちプロセスにしか過ぎないのですが、仮想ファイルツリーと仮想ネットワークインタフェースを持つため、仮想マシンを起動しているのと同等にホストから分離できるわけです。とは言え、実態はいちプロセスにすぎないのでコンピュータのリソース消費は通常のプロセスと同様に少なく、また通常のプロセスを立ち上げるのと同様に瞬時に起動します。

このような仮想化の方法は「軽量コンテナ」と呼ばれ、Web開発の分野では急速に広まりつつあります。軽くて使い回しが良い上にソフトの実行に必要な環境ごとサーバに送り込んで動作させることができるという一度使いはじめると手放せなくなる強力な環境です。

さて、いよいよ今回のメインのパーティショニングの説明に入ります。Dockerにはファイルやネットワークの仮想化を行うだけでなくLinuxのcgroups機能を使った強力なプロセス制御機能が備わっています。

まずは、以下のようなPythonスクリプトを通常の環境で実行してみます。

heavy-load.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python

import os

pid = os.fork()

if pid == 0:
    pid2 = os.fork()
    if pid2 == 0:
        pid3 = os.fork()
        if pid3 == 0:
            pid4 = os.fork()
            if pid4 == 0:
                print 'giving you heavy load'
            else:
                while True:
                    1
        else:
            while True:
                1
    else:
        while True:
            1
else:
    while True:
        1

このスクリプトはforkシステムコールを使って4つの重いプロセスを起動します。スクリプト起動後にhtopコマンドを使ってシステムの負荷を見てみると当然のように以下のような天井に張り付いた状態になります。

Heavy Load

このスクリプトをDockerイメージの中に押し込むために以下のようなDockerfileを書きます。

Dockerfile
1
2
3
4
5
6
7
8
9
FROM ubuntu:14.04

MAINTAINER Yosuke Matsusaka <yosuke.matsusaka@gmail.com>

RUN apt-get update
RUN apt-get install -y python
ADD heavy-load.py /heavy-load.py

CMD /heavy-load.py

Ubuntu 14.04では標準でDockerが用意されていますが、cgroups機能を使うためには最新のDockerが必要なため、以下のようにしてDocker社が配布している最新のバイナリをインストールします。

1
2
3
4
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9
$ sudo sh -c "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
$ sudo apt-get update
$ sudo apt-get install lxc-docker

インストールができたら、まずはイメージをビルドします。

1
$ docker build -t heavy-load-partitioned .

では、最初にプロセスをCPU 0に閉じ込めてみましょう。

1
$ docker run --cpuset=0 heavy-load-partitioned

htopの出力から閉じ込めに成功していることが確認できます。空いているCPUはリアルタイム処理を行うための余力として残しておくことができるので先ほどに比べて安心です。

Heavy Load

CPU 0と2を使って他を空けておきたい、というようなことも柔軟にできます。

1
$ docker run --cpuset=0,2 heavy-load-partitioned

Heavy Load

使用CPUだけでなく使用メモリの制限を与えることもできます。タスク1をCPU 0を使って起動、タスク2をCPU 1を使って起動し、それぞれ200MBのメモリ制限を加えるには以下のようにします。

1
2
$ docker run --cpuset=0 -m 200m task1
$ docker run --cpuset=1 -m 200m task2

以上、Dockerで遊んでみましたが、パーティショニングの環境としてもかなり使いやすいことがわかったと思います。一般向けでも8コアなどというCPUが販売されるこの時代、マルチコアプロセッサを使った高度なロボット開発への応用の可能性を感じていただけたとすると幸いです。

おまけ

Raspberry Piファン向けにARMプロセッサで動作するDockerもあります。Raspberry PiはシングルコアCPUを使っているのでパーティショニングの恩恵が薄いのが残念なのですが、次のバージョンあたりでマルチコア化を期待できるかもしれません。 https://github.com/resin-io/lxc-docker-PKGBUILD

おまけ2

リアルタイム性の確保の観点からすると今回紹介したパーティショニングによるプロセスの負荷の制御だけでは不十分で、ディスクアクセスなどのリアルタイム性を阻害する(プリエンプションを妨害する)APIへの対処が行われた特別なLinuxカーネルを使う必要がありました。以前はlinux-rtというパッケージを使うのが定石だったのですが、標準カーネルに機能が取り込まれてしまったようです。ある水準のリアルタイム性確保が標準カーネルでできてしまうことになります。良い時代になったものです。 https://wiki.ubuntulinux.jp/UbuntuStudioTips/Setup/Kernels

Comments