以前用Docker Compose做開發環境,MySQL也在Docker裡執行,一切使用正常。後來開發環境遷移到K3s(輕量級的K8s),但是MySQL一啟動就被OOM Killer幹掉,所以一直沒遷移MySQL。
重現
使用Kubectl直接執行一個MySQL即可重現:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_ROOT_PASSWORD
value: root
resources:
limits:
memory: 4G
cpu: 500m
dmesg
可以看mysqld分配超過了3.7G記憶體然後被殺掉:
[ 839.399262] Tasks state (memory values in pages):
[ 839.399263] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
...
[ 839.399278] [ 34888] 0 34888 4208240 974177 7962624 0 -998 mysqld
[ 839.399280] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=...,mems_allowed=0,oom_memcg=/kubepods/pod..,task_memcg=/kubepods/pod../56..,task=mysqld,pid=34888,uid=0
[ 839.399294] Memory cgroup out of memory: Killed process 34888 (mysqld) total-vm:16832960kB, anon-rss:3895388kB, file-rss:1320kB, shmem-rss:0kB
[ 839.496988] oom_reaper: reaped process 34888 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
排查
優化MySQL配置
第一反應是MySQL配置有問題,於是修改配置減少各種buffer的大小:
[mysqld]
innodb_buffer_pool_size = 32M
innodb_buffer_pool_instances=1
innodb_log_file_size = 64M
innodb_log_buffer_size = 8M
key_buffer_size = 16k
myisam_sort_buffer_size = 16k
max_connections = 50
open_files_limit = 4096
max_allowed_packet = 1M
table_open_cache = 16
sort_buffer_size = 512k
net_buffer_length = 8K
read_buffer_size = 256K
read_rnd_buffer_size = 256K
thread_cache_size = 64
query_cache_size = 0
tmp_table_size = 12M
thread_stack=256K
然而問題依舊,改了不少配置都沒有效果,看來需要跟蹤或除錯。
用bpftrace定位記憶體分配
這裡沒有使用strace主要是因為啟動就被殺掉,需要修改映象啟動命令然後在容器內安裝和執行strace ,而bpftrace可以系統全域性跟蹤,在容器外就可以完成跟蹤,更加方便。
記憶體分配主要通過skb和mmap系統呼叫,用bpftrace跟蹤這兩個系統呼叫:
#!/usr/bin/bpftrace
tracepoint:syscalls:sys_enter_mmap / comm == "mysqld" /
{
printf("%d %s addr=%ld len=%ld flags=%ld\n", pid, probe, args->addr, args->len, args->flags);
/* printf("%s\n", ustack(perf, 10)); */
}
tracepoint:syscalls:sys_enter_brk / comm == "mysqld" /
{
printf("%d %s brk %d\n", pid, probe, args->brk);
}
sudo ./mysql-oom.bt
Attaching 2 probes...
57950 tracepoint:syscalls:sys_enter_brk brk 0
57950 tracepoint:syscalls:sys_enter_mmap addr=0 len=8740 flags=2
...
...
57950 tracepoint:syscalls:sys_enter_brk brk 1699086336
57950 tracepoint:syscalls:sys_enter_mmap addr=0 len=17179869184 flags=34
可以看到最後用 mmap
一次分配了 16G 記憶體,然後就被殺了。
嘗試獲取呼叫堆疊(ustack),但是都拿不到有用的資訊,可能mysql沒有編譯時沒有開啟frame pointer:
97694 tracepoint:syscalls:sys_enter_mmap addr=0 len=12288 flags=34
7f84c662730a 0x7f84c662730a ([unknown])
97694 tracepoint:syscalls:sys_enter_mmap addr=0 len=17179869184 flags=34
7f84c4c4064a 0x7f84c4c4064a ([unknown])
拿不到堆疊,再嘗試跟蹤所有系統系統呼叫:
#!/usr/bin/bpftrace
tracepoint:syscalls:sys_enter_* / comm == "mysqld" /
{
printf("%d %s\n", pid, probe);
}
...
輸出:
Attaching 331 probes...
...
115490 tracepoint:syscalls:sys_enter_close
115490 tracepoint:syscalls:sys_enter_brk
115490 tracepoint:syscalls:sys_enter_newstat
115490 tracepoint:syscalls:sys_enter_getrlimit
115490 tracepoint:syscalls:sys_enter_mmap addr=0 len=17179869184 flags=34
可以看到在最後一次 mmap
前呼叫了 getrlimit
, 猜測是mysql會根據系統資源限制來分配記憶體。
MySQL原始碼分析
MySQL原始碼中直接呼叫 getrlimit
的地方不多,排除了ndb、innodb_memcached、libevent之後,只有一處直接呼叫:
static uint set_max_open_files(uint max_file_limit)
{
struct rlimit rlimit;
uint old_cur;
DBUG_ENTER("set_max_open_files");
DBUG_PRINT("enter",("files: %u", max_file_limit));
if (!getrlimit(RLIMIT_NOFILE,&rlimit))
{
old_cur= (uint) rlimit.rlim_cur;
DBUG_PRINT("info", ("rlim_cur: %u rlim_max: %u",
(uint) rlimit.rlim_cur,
(uint) rlimit.rlim_max));
if (rlimit.rlim_cur == (rlim_t) RLIM_INFINITY)
rlimit.rlim_cur = max_file_limit;
if (rlimit.rlim_cur >= max_file_limit)
DBUG_RETURN(rlimit.rlim_cur); /* purecov: inspected */
rlimit.rlim_cur= rlimit.rlim_max= max_file_limit;
if (setrlimit(RLIMIT_NOFILE, &rlimit))
max_file_limit= old_cur; /* Use original value */
else
{
rlimit.rlim_cur= 0; /* Safety if next call fails */
(void) getrlimit(RLIMIT_NOFILE,&rlimit);
DBUG_PRINT("info", ("rlim_cur: %u", (uint) rlimit.rlim_cur));
if (rlimit.rlim_cur) /* If call didn't fail */
max_file_limit= (uint) rlimit.rlim_cur;
}
}
DBUG_PRINT("exit",("max_file_limit: %u", max_file_limit));
DBUG_RETURN(max_file_limit);
其中邏輯是:如果系統的檔案開啟限制是 RLIM_INFINITY
或者比要設定的 max_file_limit
大,都返回系統的限制。
這個函式也只被直接呼叫一次:
uint my_set_max_open_files(uint files)
{
struct st_my_file_info *tmp;
DBUG_ENTER("my_set_max_open_files");
DBUG_PRINT("enter",("files: %u my_file_limit: %u", files, my_file_limit));
files+= MY_FILE_MIN;
files= set_max_open_files(MY_MIN(files, OS_FILE_LIMIT)); // 獲取最大開啟檔案數
if (files <= MY_NFILE)
DBUG_RETURN(files);
// 分配記憶體
if (!(tmp= (struct st_my_file_info*) my_malloc(key_memory_my_file_info,
sizeof(*tmp) * files,
MYF(MY_WME))))
DBUG_RETURN(MY_NFILE);
// 初始化
/* Copy any initialized files */
memcpy((char*) tmp, (char*) my_file_info,
sizeof(*tmp) * MY_MIN(my_file_limit, files));
memset((tmp + my_file_limit), 0,
MY_MAX((int) (files - my_file_limit), 0) * sizeof(*tmp));
my_free_open_file_info(); /* Free if already allocated */
my_file_info= tmp;
my_file_limit= files;
DBUG_PRINT("exit",("files: %u", files));
DBUG_RETURN(files);
}
原來MySQL會根據最大可開啟檔案數,提前為每個檔案分配和初始化記憶體,在這個時候就可能分配過多記憶體,導致OOM。
解決
啟動前設定ulimit
因為K8s目前不支援設定ulimit,不少Helm Chart都在啟動前設定ulimit。
command: ["sh","-c", "ulimit -n 4096 && exec /usr/local/bin/docker-entrypoint.sh mysqld"]
修改K3s設定
K3s通過systemd啟動,可以修改k3s.service,限制K3s開啟檔案數,這個限制會傳給containerd:
LimitNOFILE=1048576