Rest Term

Blog

Linuxカーネル Docker関連 cgroupsのメモ

Linux_Containers_logo_150

前回はLinuxの namespace(名前空間) についてCプログラムやツールを使っていろいろと確認できましたので、今回は cgroups についても調べます。

namespaceは生成したプロセスに対してリソース体系を割り当てる(隔離空間を作る)のに対して、cgroupsは指定したプロセスのグループに対してリソース制限をかけます。似ているようで全然別の機能ですね。

環境

* CentOS 7.2 (kernel-3.10.0-327.4.5.el7.x86_64)
* Ubuntu 14.04 (3.13.0-77-generic)
* Docker 1.9.1

cgroups

Control Groups provide a mechanism for aggregating/partitioning sets of
tasks, and all their future children, into hierarchical groups with
specialized behaviour.

cgroupsは control groups の略でタスクをグループ化したり、そのグループ内のタスクに対して様々なリソース制御を行うための仕組みです。namespaceではホスト名やPID空間などのカーネル/OSが扱うリソースを制御(隔離)しますが、cgroupsで制御するのはCPUやメモリといった物理的なリソースです。

/sys/fs/cgroup 以下に仮想的なファイルシステムとしてのインタフェースが提供されています。今回の作業環境も上述の通りCentOSとUbuntuの両方で確認していますが、CentOSの場合はsystemd経由でcgroupsを操作するのに対して、Ubuntuの場合は直接 /sys/fs/cgroup を書き換えています。Ubuntu 15.04からはUpstartからsystemdに置き換わるらしいですが、ディレクトリ以下の構造は基本的には変わりません。

実際に /sys/fs/cgroup の中身を見てみるとcpuやmemoryといったわかりやすい名前のサブディレクトリが見えます。

仮想的なファイルシステムと前述した通り、ここでファイル/ディレクトリ操作をすることで様々なリソース制御を行います。これらはサブシステムと呼ばれています。

サブシステム 概要
blkio ブロックデバイスの入出力
cpu CPUリソースの割り当て・制限
cpuacct タスクが消費するCPU時間をレポート
cpuset グループへのCPU,メモリノードの割り当て
devices デバイスへのアクセス制限
freezer グループに属するプロセスの一時停止/再開
hugetlb cgroupからのhugetlbの使用
memory タスクが消費するメモリリソースのレポートと制限
perf_event cgroup単位でのperfツールの使用

cgroupsによってタスクをグループ化しますが、そのグループは各々ヒエラルキー(hierarchy:階層)構造を持ち、上記のサブシステムによるリソース制限を受けます。そのヒエラルキー構造は仮想ファイルシステム上で表現されます。

ここではいくつかのサブシステムをピックアップしていろいろ調べてみます。

cpuset/cpu/memoryサブシステム

まずは挙動がわかりやすいcpuset/cpu/memoryサブシステムについて。dockerコンテナを作成する際の以下のオプションに関係してきます。オプション名はDockerのバージョンによって変わることがあるので注意してください(その際はdeprecatedメッセージが出力されます)。

--cpuset-cpus 使用するCPUコアを指定 (cpusetサブシステム)
--cpu-shares CPU時間の割り当てを相対比率で指定、デフォルト 1024 (cpuサブシステム)
-m | --memory 使用メモリの上限 (memoryサブシステム)

上記dockerオプションはcpuset,cpu,memoryサブシステムの以下のファイルを操作してリソース制限を行います。ここではUbuntu上で動作確認していますが、/sys/fs/cgourps/cpuset,cpu,memoryディレクトリに以下のファイルがあります。

  • cpuset.cpus
  • cpu.shares
  • memory.limit_in_bytes

まずはcpusetサブシステムの動作確認のために適当なdockerコンテナを作って起動し、CPU1のみを利用するように --cpuset-cpus オプションで指定してddコマンドを実行し続けます。

dockerではコンテナを起動すると各サブシステムに docker/{コンテナID} 名のディレクトリを作成します。上記のコンテナで指定した --cpuset-cpus の値を確認してみます。

次はcpuサブシステムが関係する --cpu-shares の挙動を確認してみます。2つめのコンテナを以下のオプションで起動します。--cpu-shares 2048 と指定していますが、これは同じCPUコアで複数のコンテナのプロセスが実行される場合に、どちらのコンテナを優先的に実行するかを相対的な重みで指定するオプションです。デフォルト値が1024なので、ここではその2倍の値を指定しています。その結果、2つめのコンテナは2倍の優先度でCPU時間が割り当てられることになります。

2つのコンテナでのddコマンドのCPU使用率が綺麗に1対2に分かれています。確認のため cpu.shares ファイルの中身も見てみます。

次はmemoryサブシステムが関係する --memory オプションの挙動を確認します。コンテナから使用できるメモリの上限値を指定するオプションで、数値の後ろにmを付ければMB(メガバイト)指定ができます。

--memory オプションを指定しない場合(デフォルト)だと、memory.limit_in_bytesファイルには 18446744073709551615 (bytes)と書かれていました。スワップ領域の容量も同じサイズに制限されるため、128mと指定した場合はプロセスは物理メモリ128MB + スワップ128MBまで使用できます。そのサイズを超えるとOOM Killerが発動してコンテナ内のプロセスが強制終了させられます。

また、以下のファイルの中身を見るとコンテナ内でのメモリ使用状況を確認することができます。

ここまで紹介した各ファイル内の値を直接書き換えることでもコンテナ内のリソースを任意に制限をすることができます。まぁdockerを使っているなら素直にdockerのコマンドから指定した方が良いですね。

devicesサブシステム

次にdevicesサブシステムについていろいろ確認してみます。devicesはグループに所属するプロセスがアクセス可能なデバイスを制限するサブシステムです。例えばコンテナ内から直接ネットワークやディスクのデバイスにアクセスさせないように設定できます。

cgroupから始まっているファイルは他のサブシステムと共通のファイル、devicesから始まっているファイルはdevicesサブシステム専用のファイルとなっています。それ以外の tasks、notify_on_release、release_agent の3つはどうやら歴史的な理由でプレフィックスが付いていないようです。これらに頼るのなら焼かれる覚悟をしておけって書いてありますけど、怖すぎるんですが。

前述のdevicesプレフィックスが付いた3つのファイルを使って各種アクセス制御を行います。

devices.allow アクセスを許可するデバイスを追加
devices.deny アクセスを禁止するデバイスを追加
devices.list 現在のアクセス許可されているデバイスの状態を表示

Linux Kernel Updates Vol 2014.08の内容に沿ってこちらの環境でも動作確認してみます。まずはdevices.listの中身を見て現在のデバイスの状態を確認しておきます。

dockerコンテナの内部ではどうなっているでしょう。

なにやらいろいろと制限されているようです。以下に読み方を整理します。

  • 先頭のアルファベット a: 全てのデバイス、b: ブロックデバイス、c: キャラクタデバイス
  • n:m (n:mはメジャー番号:マイナー番号 or ワイルドカード)は/devのノード番号
  • 末尾のアルファベット3つ r: 読み込み可能、w: 書き込み可能、m: 新規作成可能

となっています。つまり a *:* rwm は全てのデバイスへの操作が可能であることを示しています。そして上記の内容を読み解くと、まずは全てのキャラクタデバイスとブロックデバイスへの読み書きを禁止した後に、個々のキャラクタデバイスに必要に応じて権限を与えていることが読み取れます。/devのノード番号だけみてもわかりにくいですが、例えば 1:3 は /dev/null 、1:8 は /dev/random を差しています。このように各デバイスへの操作を制限することによってdockerコンテナ内のセキュリティを担保しているのですが、ここでは試しにその制限を解除してみます。

また、devices.allowに明示的に/dev/kmsgへの読み書きを許可することもできます。

devicesサブシステムの動作確認ができました。他にもサブシステムはありますが動作確認はこれくらいにして、カーネルのソースコードも少しだけ見ておきたいと思います。

カーネルの実装

task_struct 構造体(Linuxプロセスを表現するデータ構造)のメンバに css_set というcgroupを管理するための構造体があります。

また、list_head 構造体はわかりやすいのでカーネルを読むときはこの辺りから慣れていくとと良いかもしれません。Doubly linked listの実装でカーネル内のあちこちに登場してきますので。css_set の中身は以下のようになっており、全てのタスクはこの css_set に対する参照カウント(のポインタ)を保持しています。

css_setcgroup_subsys_stateという構造体の集合を保持していることがわかります。cssというのはcgroup_subysys_stateを略なんでしょう。tasks は前述の task_struct 構造体メンバの cg_list と(連結リストとして)繋がっており、これを辿ればcgroupsに属する全てタスクにアクセスできるようになっています。

cgroup_subsys_state 単体だけを見てもよくわかりませんが、css_set からコメント文も読みつつ cgroups 構造体まで辿ってくるとなんとなく構造が見えてきます。ここまでに前述した list_head がたくさん登場していますが、これを使ってcgroupsのヒエラルキー構造を表現していることが読めます。ちなみにRCUというのは排他制御機構 Read-Copy-Update の実装ですが、cgroupsでも多くの場所で活用されているようです。詳細はWikipediaが詳しいです。

ここまではcgroups自体の実装に関する部分で、サブシステムの実装はどうなっているのかわかりません。ここでは例としてdevicesサブシステムを少し覗いてみます。

現在のタスク(プロセス)が所属するcgroupを取得(task_devcgroup関数)、前述したデバイスタイプと番号(c 5:1 とか)を指定してパーミッションのチェックを行っています(may_access関数)。

cgroup.h/cにはサブシステム自体の実装は含まれておらず、cpuサブシステムならkernel/sched/ 、memoryサブシステムならmm/以下にあり、タスクスケジューラやメモリ管理などカーネルの主機能の中にサブシステムの実装も含まれている形です。

サブシステムの実装詳細については今後も興味のあるものから少しずつ読み進めていくつもりです。今回は前回調べたnamespaceに続いて、Dockerコンテナを作成するために必要な技術要素であるcgroupsについて概要を整理しました。

参考

おまけ: Cscopeについて

効率良くLinuxカーネルのコードリーディングするためにCscopeというツールを利用しています。

Cscope is a developer's tool for browsing source code. It has an impeccable Unix pedigree, having been originally developed at Bell Labs back in the days of the PDP-11. Cscope was part of the official AT&T Unix distribution for many years, and has been used to manage projects involving 20 million lines of code!

使い方は以下のエントリーを参考にさせてもらいました。Linuxカーネルのソースコードは規模が大きいので、cscopeのデータベースは必ず転置インデックス付きで作成しておきましょう。また、Emacs使いであればhelmインタフェースからも利用できます。

 

Tags: ,

TypeScript入門 – 機械学習の実装 2 Logistic Regression

TypeScript_Logo
前回はTypeScript入門ということで、TypeScriptで Denoising Autoencoders という種類のニューラルネットワークを作ったのと、AngularJSやAngular Materialの使い方を少し学ぶことができました。

このDenoising Autoencoderを構成要素として何層も積み重ねるとStacked Denoising Autoencoderとなり、Deep Learning(深層学習)とも呼ばれます。Denoising Autoencoderを実装してあれば、残りは出力層での教師有り学習で用いるロジスティック回帰やソフトマックス関数など小さな部品を作るだけです。実はそれとは別に、前回の記事の後に小規模なConvolutional Neural Network(CNN)もTypeScriptで書いてみたのですが、MNISTの学習すら一向に終わらないので少し実装を見直しているところです。漢なら手書きasm.jsに挑むべきって言われたんですけど、なかなかロマンを感じますね。

とりあえず今回は追加部品として作ったロジスティック回帰(Logistic Regression)を動かして正しく学習できているか見てみます。

ソースコードは今回もGitHubにアップしていますので興味があれば。

ロジスティック回帰なので分類タスクとなります。まずは簡単な二値分類から。

* 出力結果

データ作成時にランダム要素が入っているので実行の度に結果は多少変わりますが、予測結果を見るにきちんと分類できていますし、クロスエントロピー損失も徐々に減っているのを確認できました。次は多クラス分類、iris(アヤメ)とdigits(手書き数字)データセットを使います。どちらもscikit-learn(sklearn.datasets)に実装されているので、TypeScriptから読み込めるオブジェクトに変換しておきました。

Large53digits_data

* 実行結果

irisデータセットは綺麗なデータなので良い分類結果が出ていますが、データ数が少ないので動作確認結果として妥当かどうか心配です。次にdigitsデータセットも同様に学習、テストを行ってみます。こちらはわかりやすい手書き数字認識のタスクとなります。

* 実行結果

約92%の正答率です。学習率のスケジューリングを丁寧にすればもっと精度は上がると思います。このくらいのデータ規模になると、学習には100イテレーションでも数秒かかりました。MNISTやCIFAR-10だとどれくらいの時間がかかるのでしょうか。

また、ユーティリティ系モジュールとして scikit-learn でいうところの LabelBinalier や metrics モジュールの機能を一部TypeScriptでも作りました。機械学習のアルゴリズムだけでなく、データ整形で必須となる機能が揃っているのも scikit-learn の便利なところ。ただ、重要な機能ではあるんですが移植作業をしていても全然楽しくないのが辛いです。

TypeScript 所感

このエントリーの主題はTypeScriptの勉強なので、この言語を使っていての個人的な所感を。

型のある安心感

機械学習で中心となる行列演算周りのコードを書いていると、メソッドの引数や戻り値がベクトルなのか行列なのかをきちんと検査してくれるのは安心感があります。Typed Arrayを使うプログラムだとありがたみがより大きいかもしれません。また、型推論のおかげで型指定をある程度省略できるのも助かります。

今回 interface は活用できていませんが、もし他の機械学習用のモジュールを追加する場合は使ってみようと思います。scikit-learnでも学習は fit、予測は predict のようにメソッド名が統一されているので真似するのが良さそうです。

コンパイルはそんなに遅くない

昔はTypeScriptのコンパイル(JavaScriptへのトランスパイル)がとても遅かったらしいですが、現在の僕の開発環境だと遅いという感覚はありません。また、Visual Studio Codeのエディタは軽快だし、インテリセンスの反応も良くサクサクと実装を進めることができます。
vscode_cap1

言語仕様面での課題

課題と書いてしまうと語弊がありますが、例えば機械学習だと行列の要素積などのelement-wiseな計算を頻繁に行うのですが、演算子をオーバーロードできる言語だと綺麗に(数式に近い感じで)実装しやすいです。現在のTypeScriptの仕様だと演算子のオーバーロードはできないので、メソッドチェーン方式でそれっぽい感じで書くしかないのでしょうか。醜いですけど、。Pythonでライブラリを使わずに一から実装しました系のエントリーはたくさん見かけますけど、思いっきりNumPy使ってるじゃないですか。羨ましい。

同じような感想を持っている方もいるようです。

アプリケーション開発者側ではオーバーロードは避けるべきなケースが多いかもしれませんが、アプリケーション開発者が使うライブラリの作成者側にとっては必要な機能だったりすることが多いのではないでしょうか。

おわりに

機械学習のアルゴリズムを実際に書いてみると、例えば損失の値がNaNになったり、ソフトマックス関数の計算中にexpの結果がオーバーフローしてInfに落ちたり、コンピュータで数値計算する際のハマり所にたくさん出会えるのでなかなか楽しいです。これは書籍やWebサイト上で数式を眺めているだけだと経験できないこと。数式と実装のギャップを埋めるための数値計算上のテクニックをいろいろ学ぶことが出来ます。

TypeScriptでの実装はPythonやC++で実装するより何倍も面倒なんですけど、結果としてたくさんコードが書けるので良い暇つぶしになるかなと思いました。あと、2011年にJavaScriptで決定木というアルゴリズムを書いたのですが、

これを発展させて勾配ブースティング(Gradient Boosting Decision Tree)を今度はTypeScriptで書いてみたいなと思っています。いきなり書くのは難しそうなので、Boostingに必要な部品から作ったらいいかなと考えています。

体系的な知識だけが増えて技術力が伴わない頭でっかちなエンジニアにはなりたくないので、ひたすら手を動かしてコードを書く習慣を付けたいと思います。さすがに2,3年ほど技術的なことから離れていたのでリハビリはもうしばらく続きそうです。

 

Tags: , , ,

Linuxカーネル Docker関連 namespaceのメモ

Linux_Containers_logo_150

最近はやっとまともにDockerを使い始めたということもあり、基盤技術を追う必要性も強く感じてきました。Linuxカーネルのコンテナ技術周りのコードリーディングを再開しているのですが楽しいです。

環境

* CentOS 7.2 (kernel-3.10.0-327.4.5.el7.x86_64)
* Ubuntu 14.04 (3.13.0-77-generic)

読んでいるのはほとんどnamespaceとcgroup周りですけれど、この2つは違う機能なのでごっちゃにして覚えないようにしたいところです。このエントリーではnamespaceについて、利用方法やカーネルの実装なども含めてメモしておきます。

namespace (名前空間)

Linuxにおける namespace(名前空間) はプロセスに対して以下の6種類のリソースを分離するための機能として提供されています。

名前空間 定数 概要
IPC名前空間 CLONE_NEWIPC IPC(Inter-Process Communication:プロセス間通信)リソースであるSystem V IPCオブジェクト、POSIXメッセージキューを分離する。異なる名前空間の共有メモリやセマフォにアクセスできないようにする。
マウント名前空間 CLONE_NEWNS ファイルシステムツリーを分離する。異なる名前空間のファイルシステムにアクセスできないようにする。全てのユーザースペースはDockerイメージからマウントされる。chroot は使用しない。
ネットワーク名前空間 CLONE_NEWNET ネットワークデバイスやIPアドレス、ルーティングテーブルなどのネットワークインタフェースを分離する。異なる名前空間でそれぞれ仮想ネットワークを構築することができる。
PID名前空間 CLONE_NEWPID PID(プロセスID)空間を分離する。異なる名前空間で同じPIDのプロセスを作ることができる。
ユーザー名前空間 CLONE_NEWUSER UID/GIDを分離する。異なる名前空間で同じUIDのユーザーを作ることができ、root(UID=0)を名前空間外で操作の特権(root権限)を持たないようにセキュリティを設定する。
UTS名前空間 CLONE_NEWUTS uname() システムコールから返される2つのシステム識別子(nodename および domainname)を分離する。これにより各コンテナはそれぞれ独自のホスト名とNISドメイン名を持つことができる。

これらのリソースを分離することで、Dockerはコンテナ内外で別々の権限、リソース体系を構築することができるようになっています。ちなみにUTS名前空間のUTSという名前、元々は Unix Time-sharing System の略とのことで、現在は既にその意味は失われているようです。

/proc/{pid}/ns/ 以下で名前空間の一覧が確認できます。これらは特別なシンボリックリンクになっていて直接操作することはできません。

関連するシステムコール

名前空間をプロセス(プログラム)から利用する方法を整理しておきます。関連するシステムコールは以下の3つ。

clone(2) 新しいプロセスを生成する。 プロセス生成と同時に子プロセスを異なる名前空間に所属ことができる。新しく名前空間を作成するには clone(2) 呼び出し時に flags 引数で CLONE_NEW* のフラグを一つ以上指定する(上記の名前空間の表を参照)。例えばUTF名前空間を分離する際は CLONE_NEWUTS 、マウント名前空間を分離するには CLONE_NEWNS のように指定する。
unshare(2) 新しい名前空間を作成する。flags引数はclone(2)と同様だが新しいプロセスは作成しない。
setns(2) 既存の名前空間に呼び出したプロセスをアタッチする(参加させる)。clone(2)やunshare(2)のように新しい名前空間を作るわけではない。

clone(2)は名前空間を扱う以外にもスレッドの実装など様々な場所で使われていますね。他の2つのシステムコールは今回初めて知りました。

上記のシステムコール clone(2) を使ってPID名前空間の確認をしてみます。

通常、clone(2)のchild_stack引数は子プロセスのために用意したスタック(メモリ空間)の一番大きいアドレスを指定します(スタックはアドレスが小さい方向へと伸びるため)。

確認のため、PID名前空間を作成せずに(CLONE_NEWPIDフラグを指定せずに)実行してみます。

なるほど、PID名前空間はわかりやすいですね。次はネットワーク名前空間も加えて試してみます(※動作確認を簡単に行うためにsystem()関数を使っています)。

上述のようにclone(2)のflags引数は複数指定することができます。

新しく作成されるネットワーク名前空間はloだけ設定されるようです。ついでにUTS名前空間も確認。

ホスト名を変更しています。UTS名前空間もわかりやすいですね。

前述のCプログラムからのネットワーク名前空間の確認だけではわかりにくいので、実際に2つのネットワーク名前空間を作って相互に通信できるか試してみたいと思います。ip コマンドを使うと便利です。

仮想ネットワークを作ってネットワーク名前空間を跨いだ通信を確認できました。上記のように毎回 sudo ip netns exec netns1 ~ と打つのが面倒な場合、作成した名前空間で最初にシェルを立ち上げておけば、そのシェル上でいろいろ操作できるので便利です(sudo ip netns exec netns1 /bin/bash)。

次にDockerコンテナに対して名前空間の切り替えを確認したいと思います。事前にDockerでコンテナを作っておきます。

各名前空間(ユーザー名前空間以外)の番号が異なっていますので、異なる名前空間上でDockerコンテナが動作していることを確認できます。このコンテナに対してsetns(2)を使って名前空間の切り替えを行います。
* ns_exec.c

実行中のDockerコンテナ内のファイルシステムでBashが起動していることがわかります。

カーネルの実装

名前空間関連のカーネルの実装を一部紹介します。
* include/kernel/nsproxy.h

名前空間は nsproxy 構造体で管理されています。ここでは5つの名前空間が定義されています。ユーザー名前空間は他の名前空間と異なり独立していません。

sethostname(2) の実装を見てみます。
* kernel/sys.c

ns_capable関数でケーパビリティ(capability)のチェック、ここではCAP_SYS_ADMIN(root)権限で実行されているか確認しています。引数のcurrentは現在のプロセスのtask_struct構造体で、前述のnsproxy構造体をメンバに持っています。ここではUTS名前空間内のユーザー名前空間を参照してケーパビリティのチェックしています。また、utsname()関数でホスト名を管理するnew_utsname構造体へのポインタを返しています。task_struct構造体というのはLinuxのプロセスを表現する大きな構造体で、プロセスの実行状態やメモリ、ソケット、ファイル情報、他プロセス(親や子)との関係など多くの情報を管理しています。定義は include/linux/sched.h にありますが長いためここでは割愛します。

* include/uapi/linux/utsname.h, include/linux/utsname.h

このように名前空間の利用を前提とした処理になっています。UTS名前空間はシンプルなため比較的読みやすいですが、他の名前空間の実装も確認したら後で追記します。コンテナ技術の支えるもうひとつの重要機能である cgroup についても整理・メモしておこうと思います。

ここ1, 2年くらいは技術の現場から離れていたこともあり、Linuxカーネル読書会・勉強会などにも参加していなかったので効率良くコードリーディングできていません、。やはりもうしばらくリハビリ期間が必要です。

参考

 

Tags: , ,