こんにちは、技術部プラットフォームグループのそめやポチです。最近はpng形式の画像をjpeg形式に変換する仕事をしています。
この記事では、私が社内のKubernetesクラスタのお世話をしているときに出会ったトラブルとその解決方法、またトラブルが起こった原因について説明します。トラブルの原因についてはKubernetes, Nginx, Linuxの3つのプロダクトについて、コードリーディングをしながら解説します。
直面した事象
急にKubernetesクラスタ内のNginxコンテナが起動しなくなりました。
GMOペパボが提供しているサービスの一つであるminneでは、検証用のKubernetesクラスタを利用しています。本番環境で使用しているKubernetesクラスタを模倣した、開発・検証のための環境です。そのクラスタ内でトラブルが起きていました。
トラブルの説明をするために、まずこの検証用のKubernetesクラスタの構成を簡単に紹介します。
開発者はそれぞれ、自分が開発中のアプリケーションを検証用のKubernetesクラスタにPodとしてデプロイします。それぞれのアプリケーションPodにはインターネットを経由してアクセスできるため、開発者は自身が本番環境に加えようとしている変更をオンラインで検証できます。
minneのアプリケーションPodにはメインのアプリケーションコンテナの他に、いくつかのサイドカーコンテナが配置されています。その中の一つに、プロキシのためのNginxコンテナがあります。
あるとき、新規にデプロイされたPodが起動しなくなりました。調査してみると、Nginxコンテナが起動していない状態でした。
コンテナのログは次の文で終わっていました。
/docker-entrypoint.d/20-envsubst-on-templates.sh: 26: envsubst: Argument list too long
エラーログによると、どうやら引数のリストが長すぎるそうです。何度かデプロイを試すも、どのPodもNginxコンテナが同じログを出力して起動しません。いったい何が起きたのでしょうか。
解決方法
原因の解説に入る前に、まずは私が採用した解決策をご紹介します。
アプリケーションPodを管理するDeploymentリソースの定義に、spec.template.spec.enableServiceLinks: false
を追加するだけです。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: minne-app
name: minne-app
spec:
replicas: 1
selector:
matchLabels:
app: minne-app
strategy: {}
template:
metadata:
labels:
app: minne-app
spec:
enableServiceLinks: false # この行を追加
containers:
- args:
- nginx
- -g
- daemon off;
- -c
- /nginx/nginx.conf
# 以下省略
enableServiceLinks
のbool値は、次のように機能します。
ref: https://kubernetes.io/ja/docs/tutorials/services/connect-applications-service/
KubernetesはServiceを探す2つの主要なモードとして、環境変数とDNSをサポートしています。 前者はすぐに動かせるのに対し、後者はCoreDNSクラスターアドオンが必要です。 備考:もしServiceの環境変数が望ましくないなら(想定しているプログラムの環境変数と競合する可能性がある、処理する変数が多すぎる、DNSだけ使いたい、など)、pod specでenableServiceLinksのフラグをfalseにすることで、このモードを無効化できます。
つまり、Serviceリソースにまつわる環境変数の有無を切り替えるためのフラグです。
ちなみに、enableServiceLinks: false
を設定すると、副作用としてServiceについての環境変数を利用した通信ができなくなります。それらの環境変数そのものがコンテナからなくなるからです。お気をつけください。このパラメータを設定する以外の事象の解決方法としては、Namespaceを適切に分割する手法が考えられます。今回は、関連する環境変数を利用していなかったことから、1行のコードの追加で済む手法を採用しました。
さて、この1行の追加によってNginxコンテナは問題なく起動するようになりました。なぜでしょうか?
原因
Nginxコンテナが正常に起動しなくなった要因は、開発者の増加による影響でした。
Kubernetesはデフォルトの挙動として、同じNamespace内にあるServiceリソースの数だけ、コンテナに環境変数を設定します。また、Nginxコンテナは起動時に、テンプレートファイル内に記載されている環境変数の置換を実行します。envsubst: Argument list too long
は、置換する環境変数の量が閾値より多いときに出るエラーです。
開発者が増えた結果、それぞれのPodへルーティングをするためのServiceリソースも増え、一つのコンテナに設定される環境変数も増加したため、Nginxコンテナが起動する際にenvsubst: Argument list too long
のエラーが起きたのでした。
それでは、それぞれのソフトウェアの、今回の事象につながった部分のコードを見ていきましょう。
Kubernetes
kubernetes/kubernetesリポジトリで管理されているコードを探索します。
if service.Namespace == metav1.NamespaceDefault && masterServices.Has(serviceName) {
if _, exists := serviceMap[serviceName]; !exists {
serviceMap[serviceName] = service
}
} else if service.Namespace == ns && enableServiceLinks {
serviceMap[serviceName] = service
}
先ほど「Kubernetesはデフォルトの挙動として〜」と記述しましたが、正確にはKubeletの機能です。
Kubeletは、Kubernetesクラスタ内のPodのライフサイクルやヘルスチェックを管理するコンポーネントです。提示したコードの後半部分から、enableServiceLinks
がtrueのときは同じNamespaceにあるServiceがserviceMap
というハッシュに追加されることが分かります。前半部分からは、どうやらdefault
NamespaceにあるmasterServices
に属するServiceについてはenableServiceLinks
の値に関係なく追加されるらしいことが分かります。このserviceMap
がコンテナへの環境変数設定に使用されます。
Nginx
nginxinc/docker-nginxリポジトリで管理されているコードを探索します。
auto_envsubst() {
local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}"
local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}"
local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}"
local template defined_envs relative_path output_path subdir
defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
[ -d "$template_dir" ] || return 0
if [ ! -w "$output_dir" ]; then
echo >&3 "$ME: ERROR: $template_dir exists, but $output_dir is not writable"
return 0
fi
find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do
relative_path="${template#$template_dir/}"
output_path="$output_dir/${relative_path%$suffix}"
subdir=$(dirname "$relative_path")
# create a subdirectory where the template file exists
mkdir -p "$output_dir/$subdir"
echo >&3 "$ME: Running envsubst on $template to $output_path"
envsubst "$defined_envs" < "$template" > "$output_path"
done
}
この関数はentrypointで実行されます。終盤の行に次の記述があります。
envsubst "$defined_envs" < "$template" > "$output_path"
テンプレートファイル内の環境変数を実際の値に置換して出力しています。エラーログが示す行からも、この部分でエラーになっていることが分かります。つまり、コンテナ起動時のenvsubst(環境変数置換処理)でエラーが起きたのです。
Linux
せっかくなのでArgument list too long
のエラーについてもLinuxカーネルのコードから探していきましょう。
Argument list too long
の出所を探す
torvalds/linuxリポジトリで管理されているコードを探索します。ちなみにこのリポジトリはLinuxカーネルのミラーリポジトリです。
https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
...
どうやらE2BIG
というエラーコードがArgument list too long
に対応しているようです。
このエラーコードはどこで返されるのでしょうか。
https://github.com/torvalds/linux/blob/708283abf896dd4853e673cc8cba70acaf9bf4ea/fs/exec.c#L434-L458
static int count(struct user_arg_ptr argv, int max)
{
int i = 0;
if (argv.ptr.native != NULL) {
for (;;) {
const char __user *p = get_user_arg_ptr(argv, i);
if (!p)
break;
if (IS_ERR(p))
return -EFAULT;
if (i >= max)
return -E2BIG;
++i;
if (fatal_signal_pending(current))
return -ERESTARTNOHAND;
cond_resched();
}
}
return i;
}
調査の結果、E2BIGのエラーコードを返す可能性がある関数が複数ありました。このcount
関数は与えられた引数の数がmax
を超えた場合に-E2BIG
というエラーコードを返します。
実際に再現してみる
ここで、実際にenvsubst
コマンドからArgument list too long.
エラーが返る現象を簡単に再現してみましょう。次のようなシェルスクリプトを用意します。
$ cat test.sh
#!/bin/bash
envsubst "$(seq 2 100000)"
これをstrace
でトレースしてみます。strace
は、指定したプロセスが発行するシステムコールをトレースできるツールです。-f
オプションを使用すると、子プロセスのシステムコールもトレースできます。
$ strace -f ./test.sh
execve("./test.sh", ["./test.sh"], 0x7ffcc0fe61f8 /* 23 vars */) = 0
...
[pid 54917] execve("/usr/bin/envsubst", ["envsubst", "2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n1"...], 0x55e7d655ca10 /* 23 vars */) = -1 E2BIG (Argument list too long)
...
envsubst
がexecve
によって呼び出され、E2BIG
のエラーコードが返された様子が観察できました。
execve
からArgument list too long.
までの流れを把握する
再度Linuxカーネルのソースコード内のfs/exec.c
を探索します。
https://github.com/torvalds/linux/blob/3669558bdf354cd352be955ef2764cde6a9bf5ec/fs/exec.c#L1888
do_execveat_common
関数からcount
関数が返されているのを発見しました。
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
...
retval = count(argv, MAX_ARG_STRINGS);
...
do_execveat_common
関数はdo_execve
関数内で呼び出されます。
static int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}
https://github.com/torvalds/linux/blob/3669558bdf354cd352be955ef2764cde6a9bf5ec/fs/exec.c#L2109
do_execve
関数は、execve
という名前のシステムコールとして定義されています。
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
これでexecve
という名前のシステムコールからArgument list too long.
のエラーが出るまでの流れを掴むことができました。
おわり
以上、Kubernetesクラスタの運用中に出会ったトラブルからLinuxカーネルのコードまで読んでみました。なかなか疲れますがやってみると楽しいですね。みなさんもトラブルシュートにかこつけてOSSのコードリーディングをしてみてはいかがでしょうか。
余談
この記事の初稿ではLinuxカーネルの項目は該当のエラーコードがある場所だけを示して終わっていました。しかし記事のレビューを社内のエンジニアに求めたところ、社内の低レイヤーな人々がわらわらと集まってきたのでシステムコールについて更に詳しく調べる動機が生まれました。
ペパボのいるだけで成長できる環境を体感した事例でした。