コンテナに使用されているLinuxカーネル技術 & 簡易コンテナ実装
コンテナとは
コンテナとは、仮想化の一種で、複数のアプリケーションを単一のホストOS上で互いに干渉することなく実行できるようにするものです。コンテナは、アプリケーションとその依存関係を1つのイメージにパッケージ化し、異なる環境でも一貫してアプリケーションを展開・実行することを容易にします。コンテナはVMとよく比較されますが、どちらも1つのホスト上で複数の分離された環境を実行できる技術だからです。しかし、コンテナは、ホストのOSを共有するため、より軽量化されます。その結果、起動時間が短縮され、リソースの使用量が減り、パフォーマンスが向上が見込めます。
Linuxカーネル技術
コンテナは、Linuxカーネルが提供するいくつかの機能とサブシステムを基盤としています。これらの機能により、コンテナ技術に不可欠なプロセス分離、リソース管理、およびセキュリティが可能になります。Linuxでコンテナを実装するために使用される主要な機能について調査していきます。
namespace
Linuxにおけるnamespaceはプロセスの見ることができるリソースに制限をかけることができます。例えばプロセスID、ネットワークインターフェイス、マウントポイント、ユーザーIDなど、さまざまなシステムリソースの個別のインスタンスを作成することにより、プロセスの分離を実現することができます。各コンテナは独自のネームスペース内で実行され、コンテナ内のプロセスはそのネームスペース内のリソースのみを参照および操作できるようになります。この分離により、コンテナ間の干渉を防ぎ、セキュリティを向上させることができます。
名前空間 | 定数 | 概要 |
---|---|---|
UTS(Unix Time Sharing) | CLONE_NEWUTS | ホスト名とドメイン名を分離 |
PID(Process ID) | CLONE_NEWPID | プロセスIDの番号空間を分離 |
Network | CLONE_NEWNET | ネットワークインターフェイス、ルーティングテーブルを分離 |
Mount | CLONE_NEWNS | ファイルシステムのマウントポイントを分離 |
IPC(Interprocess Communication) | CLONE_NEWIPC | メッセージキュー、共有メモリー、セマフォなどのIPCリソースを分離 |
User | CLONE_NEWUSER | ユーザーID、グループIDを分離 |
namespace API
namespaceのAPIとして以下のシステムコールがあります。
syscall | 説明 |
---|---|
clone(2) | clone(2) システムコールは新しいプロセスを作成する |
setns(2) | setns(2) システムコールを使うと、呼び出したプロセスを既存の名前空間に参加させることができる |
unshare(2) | unshare(2) システムコールは、 呼び出したプロセスを新しい名前空間に移動する |
この説明がイラスト付きで理解しやすいです。
Linuxコマンドで確認してみる
ここでは例としてPID namespaceを使用して、PIDが分離されているかを確認したいと思います。unshareというコマンドを使うことで新規のnamespaceを作成することが可能です。ここでは--pidオプションを使用し、新規PID namespaceを作成します。まず$ps ax
でroot pid namespaceを確認します(一部省略)。
$ ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:16 /sbin/init
2 ? S 0:00 [kthreadd]
3 ? I< 0:00 [rcu_gp]
~~~
74062 pts/0 R+ 0:00 ps -ax
このようにたくさんのプロセスを確認できました。次にunshareコマンドを使用し、
PID namespaceを分離します。
$ sudo unshare --pid --mount-proc --fork /bin/bash
# ps ax
PID TTY STAT TIME COMMAND
1 pts/1 S 0:00 /bin/bash
8 pts/1 R+ 0:00 ps -ax
-
--pid
pid namespaceを分離する -
--mount-proc
プログラムを実行する直前に、procファイルシステムをマウントポイントにマウントする(デフォルトは/proc) -
--fork
unshareプロセスの子プロセスとして実行する
ご覧の通りPIDを分離することができました。最後に分離したPID namespaceがroot pid namespaceからどのように見えているか確認してみましょう。
$ pstree -p | grep unshare
| `-sshd(90416)-(省略)-sudo(94574)---unshare(94575)---bash(94576)
root pid namespaceではbashはpid=94576で動いていることが確認できました。
chroot
前節ではunshareを用いてPID namespaceの分離を行ってみました。しかしコンテナ内からホストのファイルシステムが見えてしまっています。
$ sudo unshare --pid --mount-proc --fork /bin/bash
# ls /
bin dev home lib32 libx32 media opt root sbin srv tmp var
boot etc lib lib64 lost+found mnt proc run snap sys usr
linuxにはrootディレクトリを変更するためのコマンド$chroot
が存在しています。rootディレクトリを変更することで、そのディレクトリより上の階層にはアクセス出来ない状態となります。では早速chrootを使用しrootディレクトリを変更してみましょう。
$ mkdir new_root_dir
$ sudo chroot new_root_dir
chroot: failed to run command ‘/bin/bash’: No such file or directory
エラーが発生してしましました。これは新しいrootディレクトリに/bin/bashファイルが存在していないからです。それどころかchroot実行後のrootディレクトリは空となっています。そのため自分の実行したいコマンドが使えるように実行ファイルを配置する必要があります。ホストのbashシェルとその依存関係をコピーするなどの方法があると思いますが、ここでは$docker export
を使用してalpine linuxを入れます。$docker export
はコンテナのファイルシステムを tarアーカイブとして出力(export)します。
$ docker pull alpine
$ docker create [イメージのID]
$ docker export [コンテナのID] > alpine.tar
$ mv alpine.tar new_root_dir/alpine.tar
$ tar xvf new_root_dir/alpine.tar -C new_root_dir
$ rm new_root_dir/alpine.tar
$ sudo chroot new_root_dir sh
# ls
bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
Cgroup(コントロールグループ)
コントロールグループ(cgroup)は、Linuxカーネルの機能で、CPU時間、システムメモリ、ネットワーク帯域幅、またはこれらのリソースの組み合わせなどを、ユーザー定義のプロセスグループに割り当てることを可能とします。また、プロセスグループを監視し、特定のリソースへのアクセスを拒否したり、グループの凍結や再起動も可能です。これは、リソースの分離と制限を可能にするため、Linuxコンテナの背後にある重要な技術です。Linuxカーネルは/sys/fs/cgroupディレクトリ以下にcgroupファイルシステムをマウントしています。ここでは例としてcpuの使用量に上限を設けてみましょう。
Linuxコマンドで確認してみる
まず、cgroup-toolsパッケージをインストールする必要があります。まだインストールしていない場合は、以下のコマンドでインストールすることができます(Ubuntu、Debianの場合)
$ sudo apt-get install cgroup-tools
パッケージがインストールされたら、新たにcgropuを作成することが可能です。ここでは例としてcpuの使用量に上限を設けてみましょう。cpuサブシステムにmy_cgroupという名前のcgroupを作成します。
$ sudo cgcreate -g cpu:/my_cgroup
cgroupを作成したので、パラメータを変更することができます。cpu.cfs_quota_usパラメータにマイクロ秒単位の値を設定することでcpuの使用率に制限をかけることが可能です。デフォルトでは、cpu.cfs_period_usパラメータは1000000(1秒)に設定されているので、cpu.cfs_quota_usを200000に設定すると、cgroupのプロセスがCPU時間の20%に制限されます。
$ echo 200000 | sudo tee /sys/fs/cgroup/cpu/my_cgroup/cpu.cfs_quota_us
作成したcgroupに制限したいプロセスを追加するには、追加したいプロセスのPIDを書き込む(ここではcgroup.procsファイル)だけです。
$ echo [プロセスのPID] | sudo tee /sys/fs/cgroup/cpu/my_cgroup/cgroup.procs
無限ループのプログラムなどを動かすことで、実際にCPU使用率が制限されている事を確認することができます。
Go言語を用いて簡易コンテナをつくってみる
ここまでLinuxカーネルを用いてコンテナがどのように実現されているか確認してきました。
ここではGo言語のsyscallを用いて簡易コンテナを実装してみます。
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
)
func main() {
switch os.Args[1] {
case "run":
run()
case "child":
child()
default:
panic("help")
}
}
func run() {
if len(os.Args) < 4 {
fmt.Printf("Usage: %s <rootfs path> <cgroup name> <command>\n", os.Args[0])
os.Exit(1)
}
fmt.Printf("Running %v \n", os.Args[4])
//go run main.go child [command...]
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
//新たなnamespaceを作成
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
if err := cmd.Run(); err != nil {
fmt.Printf("Error starting the command - %s\n", err)
os.Exit(1)
}
}
func child() {
fmt.Printf("Child Running %v \n", os.Args[4])
rootfs := os.Args[2]
cgroupName := os.Args[3]
cg(cgroupName)
//rootディレクトリとカレントディレクトリをrootfsに設定
syscall.Chroot(rootfs)
syscall.Chdir("/")
//新たなホストネームを設定
syscall.Sethostname([]byte("container"))
//procをマウント
syscall.Mount("proc", "proc", "proc", 0, "")
cmd := exec.Command(os.Args[4], os.Args[5:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
func cg(cgroupName string) {
pid := os.Getpid()
cgroupPath := filepath.Join("/sys/fs/cgroup/cpu", cgroupName)
if err := os.Mkdir(cgroupPath, 0755); err != nil && !os.IsExist(err) {
fmt.Printf("Error creating cgroup - %s\n", err)
os.Exit(1)
}
if err := ioutil.WriteFile(filepath.Join(cgroupPath, "tasks"), []byte(strconv.Itoa(pid)), 0644); err != nil {
fmt.Printf("Error adding the process to the cgroup - %s\n", err)
os.Exit(1)
}
if err := ioutil.WriteFile(filepath.Join(cgroupPath, "cpu.cfs_quota_us"), []byte("200000"), 0644); err != nil {
fmt.Printf("Error setting the CPU quota - %s\n", err)
os.Exit(1)
}
}
Discussion