为uWSGI的vassal可靠使用FUSE文件系统 (使用Linux)

要求:uWSGI 1.9.18, 带FUSE和名字空间支持的Linux内核。

FUSE是一门允许在用户空间实现文件系统的技术 (因此,名字是:Filesystem in Userspace,即用户空间中的文件系统)。 有很多高质量的FUSE文件系统,因此,让你的应用依赖于它们是一种普遍形势。

FUSE文件系统是正常的系统进程,所以就像系统中的任何进程一样,它们会崩溃 (或许也许你不自觉地杀死了它们)。除此之外,如果你托管多个应用,每个都要求一个FUSE挂载点,那么你也许想要避免污染主挂载点名字空间,并且更重要的是,避免系统中存在未使用的挂载点 (例如,一个实例已经被完全移除了,而你不需要它的FUSE挂载点在系统中仍然可用)。

这个教程的目的是配置一个Emperor和一系列的vassal,每个挂载一个FUSE文件系统。

Zip文件系统

fuse-zip 是一个FUSE进程,将一个zip文件作为文件系统公开。

我们的目标是把整个应用存储在一个zip归档文件中,并且指示uWSGI将其作为一个文件系统(通过FUSE)挂载在 /app 之下。

Emperor

[uwsgi]
emperor = /etc/uwsgi/vassals
emperor-use-clone = fs,pid

这里的技巧是使用Linux名字空间来在一个新的pid和文件名字空间中创建vassal。

第一个 (fs) 允许由vassal创建的挂载点只对这个vassal有用 (而不会混淆主要系统),而 pid 允许uWSGI master成为这个vassal的“初始”进程 (pid 1)。成为”pid 1”意味着,当你死掉了,你所有的孩子也会死掉。在我们的场景之下 (其中,我们的vassal在启动的时候加载了一个FUSE进程),它意味着当这个vassal被销毁的时候,FUSE进程也会被销毁,同时它的挂载点也会被销毁。

一个Vassal

[uwsgi]
uid = user001
gid = user001

; mount FUSE filesystem under /app (but only if it is not a reload)
if-not-reload =
  exec-as-user = fuse-zip -r /var/www/app001.zip /app
endif =

http-socket = :9090
psgi = /app/myapp.pl

这里,我们使用 fuse-zip 命令的 -r 选项来获得一个只读挂载。

监控挂载点

当前设置的问题是,如果 fuse-zip 进程死掉了,那么实例将不再能够访问 /app ,知道重新生成实例。

uWSGI 1.9.18添加了 --mountpoint-check 选项。它强制master不断地验证指定文件系统。如果它失败了,那么整个实例将会被粗鲁地销毁。由于我们处于Emperor之下,因此vassal被销毁后会立即以一种干净的状态重启 (允许再次启动FUSE挂载点)。

[uwsgi]
uid = user001
gid = user001

; mount FUSE filesystem under /app (but only if it is not a reload)
if-not-reload =
  exec-as-user = fuse-zip -r /var/www/app001.zip /app
endif =

http-socket = :9090
psgi = /app/myapp.pl

mountpoint-check = /app

来点重金属:一个CoW rootfs (unionfs-fuse)

unionfs-fuse 是一个联合文件系统(union filesystem)的用户空间实现。一个联合文件系统是多个文件系统的堆栈,因此,具有相同名字的目录被合并成单个视图。

联合文件系统不止这些,其中一个最有效的特性是写时拷贝 (COW或者CoW)。启用CoW意味着你将有一个不可变的/只读的挂载点基础,所有对其修改将会指向另一个挂载点。

我们的目标是拥有一个由我们所有客户共享的只读rootfs,以及对每个客户有一个可写挂载点 (配置为CoW),其中,将会存储每个修改。

Emperor

可以使用前面的Emperor配置,但是我们需要准备我们的文件系统。

层次将是:

/ufs (where we initially mount our unionfs for each vassal)
/ns
  /ns/precise (the shared rootfs, based on Ubuntu Precise Pangolin)
  /ns/lucid (an alternative rootfs for old-fashioned customers, based on Ubuntu Lucid Lynx)
  /ns/saucy (another shared rootfs, based on Ubuntu Saucy Salamander)

  /ns/cow (the customers' writable areas)
    /ns/cow/user001
    /ns/cow/user002
    /ns/cow/userXXX
    ...

创建我们的rootfs:

debootstrap precise /ns/precise
debootstrap lucid /ns/lucid
debootstrap saucy /ns/saucy

并且在每个中创建 .old_root 目录 (对 pivot_root 是必须的,见下):

mkdir /ns/precise/.old_root
mkdir /ns/lucid/.old_root
mkdir /ns/saucy/.old_root

确保安装所需的库到它们每一个中 (特别是你的语言所需的库)。

在这个rootfs中, uwsgi 二进制文件必须是可执行的,因此你必须花点时间在它上面 (一个好方法是对每个发行版编译一个语言插件,并且将其放公用目录中,例如,每个rootfs可以拥有一个 /opt/uwsgi/plugins/psgi_plugin.so 文件,以此类推)。

一个Vassal

这里,事情变得有点复杂了。我们需要加载unionfs进程 (以root用户,因为它必须是我们新的rootfs),然后调用 pivot_root (Linux上可以用的一个更高级的 chroot )。

钩子(hook) 是在各种uWSGI启动阶段运行自定义命令(或者函数)的最佳方式。

在我们的例子中,我们将在”pre-jail”阶段运行FUSE进程,然后在”as-root”阶段(在 pivot_root 之后)处理挂载点。

[uwsgi]
; choose the approach that suits you best (plugins loading)
; this will be used for the first run ...
plugins-dir = /ns/precise/opt/uwsgi/plugins
; and this after a reload (where our rootfs is already /ns/precise)
plugins-dir = /opt/uwsgi/plugins
plugin = psgi

; drop privileges
uid = user001
gid = user001

; chdir to / to avoid problems after pivot_root
hook-pre-jail = callret:chdir /
; run unionfs-fuse using chroot (it is required to avoid deadlocks) and cow (we mount it under /ufs)
hook-pre-jail = exec:unionfs-fuse -ocow,chroot=/ns,default_permissions,allow_other /precise=RO:/cow/%(uid)=RW /ufs

; change the rootfs to the unionfs one
; the .old_root directory is where the old rootfs is still available
pivot_root = /ufs /ufs/.old_root

; now we are in the new rootfs and in 'as-root' phase
; remount the /proc filesystem
hook-as-root = mount:proc none /proc
; bind mount the original /dev in the new rootfs (simplifies things a lot)
hook-as-root = mount:none /.old_root/dev /dev bind
; recursively un-mount the old rootfs
hook-as-root = umount:/.old_root rec,detach

; common bind
http-socket = :9090

; load the app (fix it according to your requirements)
psgi = /var/www/myapp.pl

; constantly check for the rootfs (seems odd but is is very useful)
mountpoint-check = /

如果你的应用会试着写入它的文件系统,那么你会看到,在它的 /cow 目录中,所有的已创建/已更新文件都能用。

注释

一些FUSE文件系统不会提交写入,直到它们取消挂载。在这样的情况下,在vassal关闭的时候取消挂载是个不错的技巧:

[uwsgi]
; vassal options ...
...
; umount on exit
exec-as-user-atexit = fusermount -u /app