在Docker容器中使用FUSE文件系统
容器使用 FUSE 的问题
我们一般使用的 Docker 容器都是非特权容器,也就是说容器内的 root 用户并不拥有真正的 root 权限,这就导致很多属于系统管理员的操作都被禁用了。
最近有个在 IBM Bluemix 容器内部挂载 FUSE 文件系统的需求,例如我使用 davfs2 挂载 WebDAV 服务器不出意外地会报错:
root@instance-007a20ff:~# mount.davfs https://dav.jianguoyun.com/dav/ /mnt/ Please enter the username to authenticate with server https://dav.jianguoyun.com/dav/ or hit enter for none. Username: xxx@xx.com Please enter the password to authenticate user fcoe@qq.com with server https://dav.jianguoyun.com/dav/ or hit enter for none. Password: mount.davfs: can't open fuse device mount.davfs: trying coda kernel file system mount.davfs: no free coda device to mount
mount.davfs 命令报错表示无法打开 fuse 设备,而 fuse 设备实际上是存在的(说明 fuse 模块也已经加载了):
root@instance-007a20ff:~# cat /sys/devices/virtual/misc/fuse/dev 10:229
从容器内部可以查看到 cgroup 实际允许访问的设备,并没有包含 fuse 设备:
root@instance-007a20ff:~# cat /sys/fs/cgroup/devices/devices.list c 1:5 rwm c 1:3 rwm c 1:9 rwm c 1:8 rwm c 5:0 rwm c 5:1 rwm c *:* m b *:* m c 1:7 rwm c 136:* rwm c 5:2 rwm c 10:200 rwm
手工允许 fuse 设备自然也是不可行的:
root@instance-007a20ff:~# echo "c 10:229 rwm" > /sys/fs/cgroup/devices/devices.allow -bash: /sys/fs/cgroup/devices/devices.allow: Permission denied
另外由于 Bluemix 提供的是非特权容器,即使允许访问 /dev/fuse 设备也会因为没有 mount 权限而挂载失败。
UML 系统修改
虽然 Docker 容器内部不能直接挂载使用 FUSE 文件系统,但我想到如果用 User-mode Linux(以下简称 UML) 来实现在应用层再运行一个 Linux kernel,就可以在 UML guest 系统中挂载 FUSE 文件系统了,而且 UML 系统中也可以通过 hostfs 直接访问容器本身的文件系统。
有关 UML 的介绍和编译使用可以参考我之前写的 小内存OpenVZ VPS使用UML开启BBR 文章。
首先我们需要修改 UML kernel 配置:
- 增加对 FUSE 的支持,这个是必须的了,否则无法在 UML guest 系统中使用 FUSE 文件系统;
- 增加了对 xfs、btrfs、ext4、squashfs、iso9660 等常见文件系统的支持(方便在 UML 系统中挂载各种文件系统镜像);
- 另外为了支持将 UML guest 的文件系统导出,启用了 sunrpc 和 NFS 服务器支持;
- UML 网络配置中增加了对 Slirp 和 VDE 网卡的支持。
UML kernel 目前支持常见的几种网络模式:
- TUN/TAP
最简单和常用的模式,不过 host kernel 需要支持 tun 或者 tap 设备,这个在 Docker 容器中一般都不可用的; - SLIP
SLIP 串行线路 IP 支持,现在一般很少用到了,同样 host kernel 需要支持 slip 设备; - Slirp
通过用户层的 slirp 程序实现 SLIP 连接,好处是不依赖任何内核层的设备,而且 slirp 可以支持非 root 用户使用,不过 Slirp 只支持模拟 IP 协议的数据包,详细可以参考 Slirp 开源项目的官方网站。 - VDE
VDE(Virtual Distributed Ethernet)可以在不同的计算机间实现软件定义的以太网络,同样支持在用户层以非 root 用户身份来运行,目前 Linux 上的 QEMU 和 KVM 虚拟机都支持 VDE 虚拟网络。
鉴于我们需要在 Docker 容器中运行 UML 系统,目前只能使用 Slirp 和 VDE 模式的网卡,另外单独的 slirp 程序使用起来相对 VDE 也更简单(支持 VDE 的 UML kernel 可以直接调用 slirp 程序,不像 VDE 还需要预先使用 vde_switch
等命令配置软件交换机),因此这里的 UML 系统就使用 Slirp 网卡了。
当然原来基于 busybox 的 UML 系统用户层也做了些修改:
- 增加 libfuse 支持挂载 FUSE 文件系统;
- 增加 rpcbind、nfs-common、nfs-kernel-server 等软件包,支持在 UML 系统中运行 NFS 服务器,导出 UML guest 的文件系统;
- 增加 dropbear,支持 SSH 和 SFTP 服务器和客户端了;
- 增加 httpfs2 FUSE 文件系统支持,来自 GitHub 上的 httpfs2-enhanced 项目,方便挂载访问 HTTP 和 HTTPS 远程文件;
- 增加 archivemount FUSE 文件系统支持,方便直接挂载 zip、rar、tar.gz 等各种格式的压缩包以支持直接访问压缩包中的文件;
- 增加 CurlFtpFs FUSE 文件系统支持,支持挂载 FTP 远程文件;
- 增加 davfs2 FUSE 文件系统支持,支持挂载远程 WebDAV 目录;
- 增加 sshfs FUSE 文件系统支持,支持通过 SFTP 方式挂载远程主机目录。
提示
- httpfs2-enhanced 最好使用支持 SSL 和多线程的版本,可以加快响应速度并能挂载 HTTPS 的远程文件;
- 我在测试中发现 httpfs2-enhanced 工具在国内的网络环境下挂载某些 HTTP 远程文件存在一些问题,因此做了简单的修改,并 fork 到我自己的 GitHub 仓库里了。有需要的朋友可以参考我修改过的 httpfs2-enhanced,检出其中的 http-fix 分支即可,我也已经针对原项目提交了 Pull request。
为了方便使用,我给原来的 uml-linux.sh
执行脚本增加了新的 uml.conf
配置文件:
UML_ID="umlvm" UML_MEM="64M" UML_NETMODE="slirp" #HOST_ADDR="192.168.0.3" #UML_ADDR="192.168.0.4" #UML_DNS="" TAP_DEV="tap0" ROUTER_DEV="eth0" VDE_SWITCH="" REV_TCP_PORTS="" REV_UDP_PORTS="" #FWD_TCP_PORTS="111 892 2049 32803 662" #FWD_UDP_PORTS="111 892 2049 947 32769 662 660"
简单说明如下:
UML_MEM
指定为 UML 系统分配多少内存,默认 64 MB;UML_NETMODE
比较重要,指定 UML 系统的网卡模式,目前支持tuntap
、slirp
、vde
这三个选项;- 如果是
tuntap
网卡模式,TAP_DEV
指定 host 主机上的 TUN 网卡设备名称,可以使用HOST_ADDR
配置 host 主机 TUN 网卡的 IP 地址,UML_ADDR
配置 UML guest 主机的 IP 地址(最好和HOST_ADDR
在同一网段),如果需要端口转发,那么还需要修改ROUTER_DEV
指定 host 主机用于转发的物理网卡设备名称; - 如果是
slirp
网卡模式,那么会直接使用 Slirp 默认固定的专用地址:10.0.2.2
为 host 主机地址,10.0.2.15
为 UML guest 主机的 IP 地址,并自动将 UML guest 系统内的 DNS 服务器地址设置为10.0.2.3
通过 host 主机进行域名解析; - 如果是
vde
网卡模式,那么必须修改VDE_SWITCH
指定 VDE 软件交换机的路径,VDE 软件交换机需要通过vde_switch
等命令预先配置,详细使用说明可以参考 Virtualsquare VDE Wiki 页面; FWD_TCP_PORTS
和FWD_UDP_PORTS
指定进行转发的 TCP 和 UDP 端口列表(多个转发端口以空格隔开),转发端口支持10080-80
这种形式(表示将 host 主机的 10080 端口转到 UML guest 的 80 端口),上面uml.conf
中的注释列出来的是 UML 系统中对外的 NFS 服务器所需要的端口(根据实际情况也可以只允许 111、892、2049 这几个端口)。
为了能够根据 uml.conf
文件配置 Slirp 的端口转发功能,我还为 slirp 程序增加了一个 slirp.sh
wrapper 脚本:
#!/bin/sh DIR="$( cd "$( dirname "$0" )" && pwd )" [ -f $DIR/uml.conf ] && . $DIR/uml.conf CMD=`which slirp-fullbolt 2>/dev/null || which slirp` for i in $FWD_TCP_PORTS; do if [ "x$i" = "x*" ]; then continue fi CMD="$CMD \"redir ${i%-*} ${i##*-}\"" done for i in $FWD_UDP_PORTS; do if [ "x$i" = "x*" ]; then continue fi CMD="$CMD \"redir udp ${i%-*} ${i##*-}\"" done eval "exec $CMD"
由于 UML kernel 调用 slirp 程序时不支持附加参数,这里才通过 slirp.sh
脚本来实现,功能也非常简单,通过 slirp 程序的 redir 选项配置端口转发。
提示
为了让 UML guest 系统的 Slirp 网络真正达到接近 host 主机的网络性能,host 主机上编译 slirp 程序时必须打开
FULL_BOLT
开关,并为 slirp 源代码打上 real full bolt 的 patch,否则 UML guest 系统内通过 Slirp 访问外网的速度会很慢。值得庆幸的是 Debian、Ubuntu 系统自带的 slirp 软件包一般都打上了这个 patch,而且提供了
slirp-fullbolt
和slirp
这两个程序分别对应FULL_BOLT
开启和关闭的 Slirp,我的slirp.sh
脚本也对此做了判断,优先使用速度更快的slirp-fullbolt
程序。
至于新的启动 UML 系统的脚本 uml-linux.sh
稍微有点长,这里就不贴出来了,和原来相比的改动就是增加对 Slirp 和 VDE 网络的支持。
另外新的 uml-linux.sh
脚本改为默认前台方式启动 UML 系统;如果需要以后台方式启动 UML 系统,则可以用 uml-linux.sh -D
的方式来运行。
使用 UML 挂载 FUSE 文件系统
修改之后支持 FUSE 和 NFS 服务器的 UML 系统可以从这里下载(百度网盘备用):
https://zohead.com/downloads/uml-fuse-nfsd-x64.tar.xz
https://pan.baidu.com/s/1bp6l7B5
解压缩下载下来的 uml-fuse-nfsd-x64.tar.xz
文件,运行其中的 uml-linux
脚本就可以启动 UML 系统了,此 UML 系统的 root 用户默认密码为 uml
。
下面我以挂载 HTTP 远程 iso 文件中的安装包为例,介绍如何在 UML 系统中使用 FUSE。
首先使用 httpfs2 挂载阿里云上的 CentOS 6.9 iso 文件(这里为 httpfs2 指定 -c /dev/null
参数是为了去掉 httpfs2 的网络访问输出日志),挂载成功之后可以查看挂载点中的文件:
~ # httpfs2 -c /dev/null http://mirrors.aliyun.com/centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso /tmp file name: CentOS-6.9-x86_64-minimal.iso host name: mirrors.aliyun.com port number: 80 protocol: http request path: /centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso auth data: (null) httpfs2: main: connecting to mirrors.aliyun.com port 80. No SSL session data. httpfs2: main: closing socket. httpfs2: main: connecting to mirrors.aliyun.com port 80. httpfs2: main: keeping socket open. file size: 427819008 httpfs2: main: closing socket. ~ # ls -l /tmp total 0 -r--r--r-- 1 root root 427819008 Mar 29 2017 CentOS-6.9-x86_64-minimal.iso
我们可以发现 httpfs2 的挂载点中实际上就只有一个远程文件名,我们可以直接挂载这个 iso 文件:
~ # mount -t iso9660 -o ro,loop /tmp/CentOS-6.9-x86_64-minimal.iso /media ~ # ls /media CentOS_BuildTag GPL RPM-GPG-KEY-CentOS-6 RPM-GPG-KEY-CentOS-Testing-6 isolinux EFI Packages RPM-GPG-KEY-CentOS-Debug-6 TRANS.TBL repodata EULA RELEASE-NOTES-en-US.html RPM-GPG-KEY-CentOS-Security-6 images
到这里就可以直接查看远程 iso 文件中的软件包了,你可以很方便地将远程 iso 中的软件包拷贝到 host 主机的文件系统中。
如果你想直接拷贝软件包中的特定文件,也可以通过 archivemount 来实现哦:
~ # archivemount /media/Packages/sed-4.2.1-10.el6.x86_64.rpm /mnt fuse: missing mountpoint parameter ~ # ls /mnt bin usr
这样直接复制软件包中的文件就实在太方便了(请忽略 archivemount 时的 fuse 警告,实际不影响使用)。
当然如果你想挂载 FTP 远程文件就可以通过 curlftpfs 命令来实现,也可以使用 mount.davfs 命令挂载 WebDAV 服务器上的文件(例如直接访问坚果云中的文件)。
导出 UML FUSE 文件系统
有些情况下我们需要将 UML 系统中的 FUSE 挂载点导出给 host 主机或者网络中的其它主机使用,这时可以通过 hostfs、SFTP、NFS 等方式实现,分别简单说明一下。
hostfs 导出 FUSE
这是最简单的方式,由于 UML 直接使用 hostfs 访问 host 主机的文件系统,因此 UML 系统内可以直接将 FUSE 中的文件拷贝到 hostfs 文件系统,只是这种方式 host 主机并不能直接访问 UML guest 主机的 FUSE 文件系统。
SFTP 导出 FUSE
这种方式也很简单,由于 UML 系统启动时自动运行了 dropbear 服务,我们可以先修改 uml.conf
配置文件设置 SSH 端口转发,将 host 主机的 2222 端口通过 Slirp 转发到 UML guest 系统内的 22 端口(如果 host 主机本身并没有运行 SSH 服务器,那甚至可以配置为 FWD_TCP_PORTS="22"
直接转发 22 端口):
FWD_TCP_PORTS="2222-22"
此时 host 主机就可以使用 scp 命令直接从 UML 的 FUSE 文件系统中拷贝文件了:
root@instance-007a20ff:~# scp -P 2222 root@192.168.1.52:/mnt/bin/sed /home
注意
上面命令中的
192.168.1.52
应根据实际情况替换为 host 主机上实际访问网络的网卡 IP 地址,不能使用 localhost 或者 127.0.0.1,因为 slirp 默认会自动选择访问网络的网卡,并不会进行本地转发。
NFS 导出 FUSE
首先需要修改 uml.conf
配置文件中的 FWD_TCP_PORTS
和 FWD_UDP_PORTS
值,将默认的 NFS 服务需要的 TCP 和 UDP 端口注释去掉,表示将这些端口转发到 UML 系统内。
提示
如果你需要在 host 主机上直接 NFS 挂载 UML 系统里的 FUSE 文件系统,由于 sunrpc 的 111 端口无法修改而且需要转发到 UML 系统内,这种情况下 host 主机的 portmap 或者 rpcbind 服务需要保持关闭状态,防止 111 端口被占用。
使用 uml-linux.sh
脚本启动 UML 系统,启动完成之后通过集成的 FUSE 相关工具挂载需要访问的 FUSE 文件系统。
假设需要导出 UML 系统中的 /mnt
FUSE 文件系统,UML 中默认的 NFS 导出目录配置文件 /etc/exports
如下:
/mnt 0.0.0.0/0.0.0.0(async,insecure,no_subtree_check,rw,no_root_squash,fsid=0)
上面的配置文件表示将 /mnt 目录通过 NFS 导出,默认允许所有主机访问,你可以根据需要修改导出目录路径和允许访问的主机地址。
接着在 UML 系统中通过集成的服务脚本启动 rpcbind 和 nfs 服务:
~ # /etc/init.d/rpcbind start Starting up rpcbind... ~ # /etc/init.d/nfs start Starting nfs service: fs.nfs.nlm_tcpport = 32803 fs.nfs.nlm_udpport = 32769 Exporting directories for NFS... Starting NFS daemon: rpc.nfsd: address family inet6 not supported by protocol TCP NFSD: the nfsdcld client tracking upcall will be removed in 3.10. Please transition to using nfsdcltrack. NFSD: starting 90-second grace period (net 000000006035a880) Starting NFS mountd:
如果一切正常的话,此时就可以在外部通过 NFS 挂载 UML 系统导出的 FUSE 文件系统进行访问了:
root@instance-007a20ff:~# mount -t nfs -o nolock,proto=tcp,mountproto=tcp 192.168.1.52:/mnt /media
提示
nolock
参数是为了防止如果挂载的主机上没有运行 portmap 或者 rpcbind 服务导致挂载失败的问题(如果直接在 host 主机上挂载,host 的对应服务需要关闭);
proto=tcp
和mountproto=tcp
参数指定 NFS 数据和挂载请求都使用 TCP 协议,防止使用的随机 UDP 端口无法被 slirp 转发的问题。
这里需要说明的是如果直接在 Docker 容器(UML 的 host 主机)上挂载 NFS,虽然绝大多数 Docker 容器平台都默认支持 NFS 文件系统,但在非特权容器内部由于没有权限 mount 还是会失败的。
总结
本文介绍的在容器中使用 UML 挂载 FUSE 文件系统并通过 NFS 导出 UML 文件系统的方法是一个比较小众的需求,不过也算达到我的目的了。
对于非特权 Docker 容器来说,虽然还不能直接挂载 UML guest 的文件系统,但初步看起来还是可以通过 LKL(Linux Kernel Library)在应用层访问 UML 网络并实现 NFS 挂载的,只是 LKL 库的 NFS 挂载目录并不能直接给 Docker 容器中的普通应用程序使用。
最后祝大家在即将到来的 2018 年能继续玩地开心 ^_^。