CKAD(Certified Kubernetes Application Developer)を取得した

CKAとCKAD両方取得するつもりだったので、受験して取得した。今回も自宅。UdemyにあるCKADコースのPracticeTestを一通りこなして、知らないことがないかざっと洗った後に受験という流れ。

そういうわけでCKA/CKAD両方持っておりますので、Kubernetes関係の困りごとなどあれば相談に乗れるかと思います。

Certified Kubernetes Application Developer (CKAD

inotifyインスタンスが監視している数がmax_user_watchesを越えた時に`no space left on device`エラーとなる様子を観察したり原因を調べたりする

前回、inotifyで監視しているオブジェクトの数を調べることができそうだ、と言うことがわかった。次は、そもそもなぜno space left on deviceになるのかを調べたい。なお、調査に使ったカーネルはv4.15である。

まずは監視オブジェクトの数がmax_user_watchesを超えるとno space left on deviceになることを確認したい。max_user_watchesの数を小さくして、前回使った検証用プログラムを実行すれば確認できそうである。

echo 16 >  /proc/sys/fs/inotify/max_user_watches

max_user_watchesを16にセットした。17個目のファイルを監視しようとするとエラーとなる、と仮説を立てたのだが、そうはならなかった。

ubuntu@alpaca-dev:~$ cat  /proc/sys/fs/inotify/max_user_watches
16
ubuntu@alpaca-dev:~$ ./main
start watching /tmp/foo_1.txt
start watching /tmp/foo_2.txt
start watching /tmp/foo_3.txt
start watching /tmp/foo_4.txt
start watching /tmp/foo_5.txt
start watching /tmp/foo_6.txt
start watching /tmp/foo_7.txt
start watching /tmp/foo_8.txt
start watching /tmp/foo_9.txt
start watching /tmp/foo_10.txt
start watching /tmp/foo_11.txt
start watching /tmp/foo_12.txt
2019/11/25 14:26:18 no space left on device

max_user_watchesの数を何回か変えてみたのだが、max_user_watches - 4 個までしか監視できず、その後エラーとなっている。

ちょっと謎なので、コードを追いかけていく。no space left on deviceとなるのは inotify_add_watchを実行したタイミングなので、この関数を読んでいく(link)。返り値の取得はinotify_update_watchで行なっているので、そこをみる。

	/* create/update an inode mark */
	ret = inotify_update_watch(group, inode, mask);
	path_put(&path);
fput_and_out:
	fdput(f);
	return ret;
}

inotify_update_watch(link)の返り値の取得はinotify_new_watchで行なっている(既に監視中でない場合)。

	/* try to update and existing watch with the new arg */
	ret = inotify_update_existing_watch(group, inode, arg);
	/* no mark present, try to add a new one */
	if (ret == -ENOENT)
		ret = inotify_new_watch(group, inode, arg);
	mutex_unlock(&group->mark_mutex);

	return ret;
}

inotify_new_watchを読むと、ENOSPCを返り値にセットしている箇所がある。これにより、no space left on deviceとなるケースがあることがわかる。

	/* increment the number of watches the user has */
	if (!inc_inotify_watches(group->inotify_data.ucounts)) {
		inotify_remove_from_idr(group, tmp_i_mark);
		ret = -ENOSPC;
		goto out_err;
	}

inc_inotify_watchesはinc_ucountを呼んでいる。

static inline struct ucounts *inc_inotify_watches(struct ucounts *ucounts)
{
	return inc_ucount(ucounts->ns, ucounts->uid, UCOUNT_INOTIFY_WATCHES);
}

ucountってなんだ…と思いつつ、さらに追いかける。ENOSPCを返すのはinc_ucountがNULLを返している時で、それはatomic_inc_belowの結果による。

struct ucounts *inc_ucount(struct user_namespace *ns, kuid_t uid,
			   enum ucount_type type)
{
	struct ucounts *ucounts, *iter, *bad;
	struct user_namespace *tns;
	ucounts = get_ucounts(ns, uid);
	for (iter = ucounts; iter; iter = tns->ucounts) {
		int max;
		tns = iter->ns;
		max = READ_ONCE(tns->ucount_max[type]);
		if (!atomic_inc_below(&iter->ucount[type], max))
			goto fail;
	}
	return ucounts;
fail:
	bad = iter;
	for (iter = ucounts; iter != bad; iter = iter->ns->ucounts)
		atomic_dec(&iter->ucount[type]);

	put_ucounts(ucounts);
	return NULL;
}

この関数内で使われているucountはuser_namespaceとkuid_tから取得(ない場合は作成)したもの。inotify_new_groupをみるとcurrent_user_ns()current_euidで作成していることがわかる。つまり、名前空間とEUID毎に監視可能なオブジェクトのサイズがmax_user_watchesで決められているのではないか、と考えられる。

inotify_watchers.shを使うとプロセスとそれが監視しているオブジェクト数をみることができる。リンク先のスクリプトはrootユーザの物をみているので以下のように変更し、実行してみると検証用プログラムの他にもファイル監視をしているプログラムがあり(VSCodeなど)、その監視対象オブジェクトの数が4個だった。

set -o errexit
set -o pipefail
lsof +c 0 -n -P -u ubuntu \
        | awk '/inotify$/ { gsub(/[urw]$/,"",$4); print $1" "$2" "$4 }' \
        | while read name pid fd; do \
                exe="$(readlink -f /proc/$pid/exe || echo n/a)"; \
                fdinfo="/proc/$pid/fdinfo/$fd" ; \
                count="$(grep -c inotify "$fdinfo" || true)"; \
                echo "$name $exe $pid $fdinfo $count"; \
        done

さらに、ファイル監視をしているプロセスを止めたところ、検証用プログラムが監視しているオブジェクト数と他のプロセスが監視しているプロセスの合計がmax_user_watchesを越えた瞬間no space left on deviceとなったため、少なくとも同じユーザで実行していればinotifyの監視カウントは共有されているようだ、ということがわかる。

名前空間云々はまだわかっていないが、ある程度このエラーになる理屈が理解でき、inotifyと仲良くなれた。

プロセスがinotifyで監視しているオブジェクトの数を取得する

inotifyを使っていると、プロセスが監視しているファイルの数がfs.inotify.max_user_watchesの値を上回ってno space left on deviceのエラーとなる…ということはよくあるのだけど、じゃあ今いくつ監視しているのか?を知りたくなるので調べた。結論としては、プロセスがオープンしいるinotifyインスタンスに監視中のオブジェクト一覧が保存されており、数えると知ることができる。

まずは調査用のプロセスが欲しいので、無限にファイルを作りそれを監視するプログラムを用意する。

package main

import (
	"fmt"
	"github.com/fsnotify/fsnotify"
	"log"
	"os"
	"time"
)

func NewWatcher() {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	done := make(chan bool)
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				log.Println("event:", event)
				if event.Op&fsnotify.Write == fsnotify.Write {
					log.Println("modified file:", event.Name)
				}
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("error:", err)
			}
		}
	}()

	i := 1
	for {
		path := fmt.Sprintf("/tmp/foo_%d.txt", i)
		_, err := os.Create(path)
		if err != nil {
			log.Fatal(err)
		}
		err = watcher.Add(path)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("start watching %s\n", path)
		i += 1
		time.Sleep(1 * time.Second)
	}
	<-done
}

func main() {
	NewWatcher()
}

このプログラムを実行したあと(プロセスIDは4244)、/proc/32531/fdを見てみる。anon_inode:inotifyへのリンクになっている3が調査対象となるfd。

ubuntu@alpaca-dev:~$ ls -ahltr /proc/4244/fd
total 0
dr-xr-xr-x 9 ubuntu ubuntu  0 Nov 25 13:28 ..
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 9 -> /tmp/foo_2.txt
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 8 -> 'anon_inode:[eventpoll]'
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 7 -> /tmp/foo_1.txt
l-wx------ 1 ubuntu ubuntu 64 Nov 25 13:28 6 -> 'pipe:[548152]'
lr-x------ 1 ubuntu ubuntu 64 Nov 25 13:28 5 -> 'pipe:[548152]'
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 4 -> 'anon_inode:[eventpoll]'
lr-x------ 1 ubuntu ubuntu 64 Nov 25 13:28 3 -> anon_inode:inotify
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 2 -> /dev/pts/2
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 14 -> /tmp/foo_7.txt
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 13 -> /tmp/foo_6.txt
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 12 -> /tmp/foo_5.txt
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 11 -> /tmp/foo_4.txt
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 10 -> /tmp/foo_3.txt
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 1 -> /dev/pts/2
lrwx------ 1 ubuntu ubuntu 64 Nov 25 13:28 0 -> /dev/pts/2
dr-x------ 2 ubuntu ubuntu  0 Nov 25 13:28 .

次に、/proc/4244/fdinfo/3の中を見てみると、監視しているディスクリプタ一覧がある。inotifyから始まる行を数えると、そのinotifyインスタンスで監視しているファイル数がわかる。

ubuntu@alpaca-dev:~$ cat /proc/4244/fdinfo/3
pos:    0
flags:  02000000
mnt_id: 13
inotify wd:10 ino:1a9d sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:9d1a00007e8b11e5
inotify wd:f ino:1a9c sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:9c1a0000a2cbafb8
inotify wd:e ino:1a9a sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:9a1a0000d962ed0a
inotify wd:d ino:1a99 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:991a00005d758c3d
inotify wd:c ino:1a73 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:731a0000aa47aa90
inotify wd:b ino:1a72 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:721a0000d00cc24f
inotify wd:a ino:1a6e sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:6e1a0000c3e218c9
inotify wd:9 ino:1a4d sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:4d1a00007a377089
inotify wd:8 ino:1a3e sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:3e1a0000cdc73ba9
inotify wd:7 ino:1a26 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:261a00008ce7205f
inotify wd:6 ino:1a23 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:231a0000f8d65bf0
inotify wd:5 ino:1a22 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:221a00003cc01fa5
inotify wd:4 ino:18f9 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:f9180000870fe1f0
inotify wd:3 ino:18e5 sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:e5180000d052e2f0
inotify wd:2 ino:9e sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:9e0000002427522c
inotify wd:1 ino:6e sdev:fc00001 mask:fc6 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:6e00000094549bc1

これをメトリクス化すれば、no space left on deviceエラーが発生した時に監視対象がmax_user_watchesの値を越えそうになっているのだな、ということが確認できるのではないか?

参考文献

CKA(Certified Kubernetes Administrator)を取得した

認定された。普段からKubernetesを触ってきて、それなりに知識と経験は備えているとは思っているが、念の為Udemyの Certified Kubernetes Administrator (CKA) with Practice Tests コースをざっくりこなしてから受けた。結構知らないこともあったので、知識の補完ができてよかった。

パスポートはもともと持っていたし、受験は自宅で受けたのでそこまで大変ではなかった。部屋から本を出して物がない状態にするのが手間だったくらい。なんか、試験を受けるのが久しぶりで合格したのが結構嬉しい。

docker/distributionで使われていないレイヤーやタグがついていないマニフェストをGCする

docker/distoributionを長く使っていると、どのイメージからも参照されていないレイヤーが出てきたり、タグがついていなくてほぼ使われないイメージが増えたりする。バックエンドにクラウドストレージを使っている場合は不要なデータを消せると嬉しいのだが、どうやって消せばいいのか?という疑問がある。

docker/distributionには garbage-collect コマンドがあり、これを使うことで不要なデータをGCすることができる。

`garbage-collect` deletes layers not referenced by any manifests

Usage:
  registry garbage-collect <config> [flags]
Flags:
  -m, --delete-untagged=false: delete manifests that are not currently referenced via tag
  -d, --dry-run=false: do everything except remove the blobs
  -h, --help=false: help for garbage-collect


Additional help topics:

デフォルトではマニフェストから参照されていないレイヤーを削除する。また、フラグを指定することでタグがついていないマニフェストも消すことができる。