跳到主要内容

MySQL Docker挂载数据至SMB-权限问题排查

· 阅读需 16 分钟

使用Docker搭建MySQL服务时,考虑到MySQL存储的数据较大,希望存放在空间较大的nas上。于是通过容器目录映射的方式将数据通过smb的方式储存,但是报错 Errcode: 13 - Permission denied

下面记录下排查过程和解决方案

问题描述

My Yaml

version: "2"
services:
mysql:
container_name: mysql
image: mysql:5.7.23
privileged: true
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8 --collation-server=utf8_general_ci
volumes:
- cifs_mount:/var/lib/mysql
ports:
- 3306:3306
environment:
- MYSQL_USER=root
- MYSQL_ROOT_PASSWORD=123
- MYSQL_DATABASE=db

volumes:
cifs_mount:
driver: local
driver_opts:
type: cifs
device: //${SMB_HOST}/nas/home/mysql
o: "username=${SMB_USER},password=${SMB_PWD}"

Errcode: 13 - Permission denied

$ docker-compose up 
mysql_future | Initializing database
mysql_future | mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (Errcode: 13 - Permission denied)
mysql_future | 2024-05-26T16:25:35.482190Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
mysql_future | 2024-05-26T16:25:35.491790Z 0 [ERROR] --initialize specified but the data directory exists and is not writable. Aborting.
mysql_future | 2024-05-26T16:25:35.491839Z 0 [ERROR] Aborting

由于出现了 Errcode: 13 - Permission denied 的报错,于是认为一定是权限给少了,那么如何添加权限,最简单的方式是直接映射一个 root 进容器。

排查过程

Try: 设置进程ID为root

    environment:
- MYSQL_USER=root
- MYSQL_ROOT_PASSWORD=123
- MYSQL_DATABASE=db
+ - UID=0
+ - PID=0

在Docker容器中,通过设置环境变量可以控制容器中进程的运行环境。在这个例子中,设置了环境变量:

  1. PUID=0:指定了容器中进程的用户ID(PUID)。在这里设置为0,表示将容器中的进程以root用户的身份运行。这样设置可以让容器内的进程具有root用户的权限,可以执行一些需要特权的操作。
  2. PGID=0:指定了容器中进程的组ID(PGID)。同样设置为0,表示将容器中的进程所属的组设置为root组。

结果是报错依旧同上,那么我们就会很疑惑,为什么还会出现权限问题。猜测一是文件夹不允许写操作,猜测二是文件夹的用户和进程的用户不一致。

文件夹的用户和权限是什么

登录nas查看文件夹权限,结果发现文件夹的用户为 ppsteven ,用户组为 user 。文件夹 mysql 是我在mac上使用用户ppsteven 连接smb手动创建的,故该文件夹下会存在 .DS_Store 。用户为我smb的账号ppsteven

root@ppnas:/srv/mergerfs/nas/nas/home/mysql# ls -al
总用量 12
drwxrwsr-x 3 ppsteven users 76 5月 27 00:25 .
drwxrwsr-x 6 ppsteven users 128 5月 26 02:40 ..
-rw-rw-r-- 1 ppsteven users 4096 5月 26 02:41 ._.DS_Store
-rw-rw-r-- 1 ppsteven users 6148 5月 27 00:25 .DS_Store
drwxrwsr-x 2 ppsteven users 10 10月 16 2018 mysql_data

根据提供的输出,可以看到mysql_data文件夹的权限为drwxrwsr-x,所有者为ppsteven,所属组为users。这里有一个特殊的权限位S,它表示设置了粘滞位(Sticky Bit)。

根据权限表示法:

  • d 表示这是一个目录
  • rwx 表示所有者(ppsteven)具有读取、写入和执行权限
  • rws 表示所属组(users)具有读取、写入和执行权限,并且设置了粘滞位
  • r-x 表示其他用户具有读取和执行权限

粘滞位在目录上的作用是,只有目录的所有者、文件的所有者或者root用户才能删除目录中的文件,即使其他用户有写权限。这有助于防止普通用户误删其他用户的文件。因此,根据权限设置,ppsteven用户可以读取、写入和执行mysql_data目录,users组的用户也可以读取、写入和执行该目录,并且设置了粘滞位。

结论:文件夹用户为 ppsteven,用户组为user,权限为读写

操作文件夹的用户是谁?

目前我们可以知道文件夹有读写权限,那么 Errcode: 13 - Permission denied 的问题大概率是由于用户组的不匹配造成的了,下面我们看下 目前 大概有哪些用户。

文件夹权限:

  • ppsteven:smb 注册账户
  • root:nas root 账户

进程:

  • ppsteven:Ubuntu 账户
  • root:Ubuntu账户

现在走进了一个死胡同,按一般来说,当进程为root时,拥有全部文件夹的访问权限。而出现上述权限不足的问题时,只有可能出现以下情形:

  1. 用户为ppsteven,文件夹权限为root
  2. 用户为 X,文件夹权限为 ppsteven 或 root

自然就容易产生的疑惑是,明明已经在 Try中显式指定了用户为root,以上两种问题都不应该发生。

最直接的方式就是启动容器,查看用户

docker run --rm -t mysql:5.7.23 sh -c 'id'
uid=0(root) gid=0(root) groups=0(root)

结论:操作文件夹的用户就是root,而且无需显式指定 UID 和 PID。

这个结论和上面的两个推测是相冲突的,只能说明一点,容器当前用户确实是root,但是执行mysql进程的另有其人。具体是谁,需要进入容器排查。

容器中排查

1. 容器持久运行不退出

version: "2"
services:
mysql:
container_name: mysql
image: mysql:5.7.23
privileged: true
- command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8 --collation-server=utf8_general_ci
+ command: tail -f /dev/null

2. 找到容器默认执行的命令

dockerhub-mysql:5.7.23 中找到了Dockerfile 的命令

16	ENTRYPOINT ["docker-entrypoint.sh"]
17 EXPOSE 3306/tcp 33060/tcp
18 CMD ["mysqld"]

docker-entrypoint.sh mysqld

3. 执行mysqld

果然出现了之前看到的错误

root@ba9456632723:/# docker-entrypoint.sh mysqld
Initializing database
mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (Errcode: 13 - Permission denied)
2024-05-26T17:24:44.679361Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2024-05-26T17:24:44.687552Z 0 [ERROR] --initialize specified but the data directory exists and is not writable. Aborting.
2024-05-26T17:24:44.687578Z 0 [ERROR] Aborting

4. 确认文件夹的用户和权限

刚刚在 nas 上看到的文件夹的用户是 ppsteven,进入容器后再次查看,用户变为了 root

root@ba9456632723:/# ls -al /var/lib/
total 36
drwxr-xr-x 1 root root 4096 Oct 16 2018 .
drwxr-xr-x 1 root root 4096 Oct 11 2018 ..
drwxr-xr-x 1 root root 4096 Oct 16 2018 apt
drwxr-xr-x 1 root root 4096 Oct 16 2018 dpkg
drwxr-xr-x 2 root root 4096 Jun 26 2018 misc
drwxr-xr-x 2 root root 0 Oct 16 2018 mysql
drwxrwx--- 2 mysql mysql 4096 Oct 16 2018 mysql-files
drwxr-x--- 2 mysql mysql 4096 Oct 16 2018 mysql-keyring
drwxr-xr-x 2 root root 4096 Oct 11 2018 pam
drwxr-xr-x 1 root root 4096 Oct 11 2018 systemd

结论:容器卷挂载的用户和文件系统中的并不一样,查询资料:

默认情况下,容器卷的device的默认用户组是root。这是因为在Docker中,容器卷的权限是由Docker引擎所控制的。当容器卷被挂载到容器中时,Docker引擎会将该卷所属的用户和用户组设置为root。这是为了确保容器在访问卷中的文件时具有足够的权限。如果需要更改默认的用户组,可以在容器启动命令中使用--user选项来指定用户和用户组。

5. 确认真实执行的用户

root@ba9456632723:/# whereis docker-entrypoint.sh
docker-entrypoint: /usr/local/bin/docker-entrypoint.sh
root@ba9456632723:/# cat /usr/local/bin/docker-entrypoint.sh

发现脚本对root有特殊处理

# allow the container to be started with `--user`
if [ "$1" = 'mysqld' -a -z "$wantHelp" -a "$(id -u)" = '0' ]; then
_check_config "$@"
DATADIR="$(_get_config 'datadir' "$@")"
mkdir -p "$DATADIR"
chown -R mysql:mysql "$DATADIR"
exec gosu mysql "$BASH_SOURCE" "$@"
fi

这部分代码主要是用于允许容器以--user参数启动。如果脚本的第一个参数是mysqld,且$wantHelp为空,并且当前用户的id为0(即root用户),则执行以下操作:

  1. 调用_check_config函数检查配置。
  2. 获取MySQL的数据目录路径,并创建该目录。
  3. 使用chown命令将数据目录的所有权设置为mysql:mysql
  4. 使用gosu命令以mysql用户身份重新执行脚本($BASH_SOURCE表示当前脚本文件),并传递之前的参数。

这段代码的作用是在容器启动时,如果以root用户身份启动,并且参数是mysqld,则将执行权限切换为mysql用户,并重新执行脚本以启动MySQL服务。这样可以避免以root用户身份运行MySQL服务,提高安全性。

结论:真实执行的用户是 mysql。在 dockerhub-mysql:5.7.23 中存在一个命令 /bin/sh -c groupadd -r mysql && useradd -r -g mysql mysql 创建了mysql 的用户。

6. chown -R mysql:mysql "$DATADIR" 无效

可以看到 bash 脚本中当发现当前用户为root时,会将文件的用户和用户组改为 mysql 并使用 mysql 去执行。但是从最终的效果中发现并没有成功更改,我后续多次执行此命令,发现是无效,但是没有报错。如下

root@ba9456632723:/# id
uid=0(root) gid=0(root) groups=0(root)
root@ba9456632723:/# chown -R mysql:mysql /var/lib/mysql
root@ba9456632723:/# ls -al /var/lib/
total 36
drwxr-xr-x 1 root root 4096 Oct 16 2018 .
drwxr-xr-x 1 root root 4096 Oct 11 2018 ..
drwxr-xr-x 1 root root 4096 Oct 16 2018 apt
drwxr-xr-x 1 root root 4096 Oct 16 2018 dpkg
drwxr-xr-x 2 root root 4096 Jun 26 2018 misc
drwxr-xr-x 2 root root 0 Oct 16 2018 mysql
drwxrwx--- 2 mysql mysql 4096 Oct 16 2018 mysql-files
drwxr-x--- 2 mysql mysql 4096 Oct 16 2018 mysql-keyring
drwxr-xr-x 2 root root 4096 Oct 11 2018 pam
drwxr-xr-x 1 root root 4096 Oct 11 2018 systemd

查阅资料可知:chown 对于挂载的磁盘可能不起作用

chown will have no effect on mounted disks/partitions

通过查阅 mount.cifs 的文档可知,如果要设置挂载的文件系统的用户,需要指定用户名或者用户数字ID,默认用户为0(root)

uid=arg

sets the uid that will own all files or directories on the mounted filesystem when the server does not provide ownership information. It may be specified as either a username or a numeric uid. When not specified, the default is uid 0.

mount -t cifs \
-o username=****,password="****",uid=www-data \
//192.168.1.10/Public/Documents/Docs \
/mnt/docs

参考

解决方案

我们可以看到本质上是由于数据目录 mysql 归属用户为 root,但是执行为 mysql 报的权限不足的错误。

那么我们可以直接将容器映射时直接指定归属即可。

1. 查看mysql用户id

root@ba9456632723:/# cat /etc/passwd | grep mysql
mysql:x:999:999::/home/mysql:

2. 设置目录id

volumes:
cifs_mount:
driver: local
driver_opts:
type: cifs
device: //${SMB_HOST}/nas/home/mysql
- o: "username=${SMB_USER},password=${SMB_PWD}"
+ o: "username=${SMB_USER},password=${SMB_PWD},uid=999,gid=999"

成功运行

root@fc502a823b1e:/# ls -al /var/lib/
total 36
drwxr-xr-x 1 root root 4096 Oct 16 2018 .
drwxr-xr-x 1 root root 4096 Oct 11 2018 ..
drwxr-xr-x 1 root root 4096 Oct 16 2018 apt
drwxr-xr-x 1 root root 4096 Oct 16 2018 dpkg
drwxr-xr-x 2 root root 4096 Jun 26 2018 misc
drwxr-xr-x 2 mysql mysql 0 Oct 16 2018 mysql
drwxrwx--- 2 mysql mysql 4096 Oct 16 2018 mysql-files
drwxr-x--- 2 mysql mysql 4096 Oct 16 2018 mysql-keyring
drwxr-xr-x 2 root root 4096 Oct 11 2018 pam
drwxr-xr-x 1 root root 4096 Oct 11 2018 systemd
root@fc502a823b1e:/# docker-entrypoint.sh mysqld
Initializing database
2024-05-26T17:51:53.023965Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2024-05-26T17:51:55.708097Z 0 [Warning] InnoDB: New log files created, LSN=45790
2024-05-26T17:51:56.325563Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2024-05-26T17:51:56.495066Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: a4ba4e3a-1b88-11ef-b584-0242ac1e0002.
2024-05-26T17:51:56.555978Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2024-05-26T17:51:56.564034Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

结论

本次排查 MySQL 容器化部署时,遇到将mysql数据挂载至SMB上遇到的权限不足的问题。

通过排查问题发现,容器运行用户为root,文件夹权限为读写,所属用户组为root。但是依旧出现权限不足的问题时,mysql 容器在运行时会针对当前用户为root的情形进行处理,会将文件夹用户和用户组更改为mysql,并使用mysql 用户执行命令。

在此过程中,chown 更改用户组的行为不知什么原因没有成功改动,导致了权限不足的情况。

解决方案是,在挂载时指定目录权限为 mysql后可成功运行。

参考

拓展知识

1. 如果不显式指定 PID=0,UID=0,那么容器中真实运行的用户是谁?

在Dockerfile中未指定用户时,默认情况下容器中的进程会以root用户身份运行。如果指定了USER,则以该用户执行。查看当前用户的方式如下:

  1. 首先进入容器内部:
docker exec -it [容器名称] /bin/bash
  1. 然后使用以下命令查看当前用户的 UID 和 GID:
id

2.SMB 是什么

Mount an SMB Share in Linux