☰
runit实践:用run脚本和sv命令轻量管理常驻服务
2026/10/15 23:18:01 网站建设 项目流程

老实说,第一次把服务丢给runit管理的时候,我心里是有点嫌弃的——systemd不都够用了吗,为什么还要装一个第三方进程管理器?但后面在容器环境和几台配置很一般的老机器上折腾了几次,我彻底改变了看法。runit这套“run脚本 + sv命令”的思路,在某些场景下比写一长串systemd unit文件要顺手得多,轻量到几乎感觉不到它的存在。

runit本质上是一个进程监督程序,也叫service manager。它做的事情很朴素:帮你把一个程序拉起来,盯着它,挂了自动拉起,还能顺便管日志。这正好补上了systemd在某些环境里“杀鸡用牛刀”的尴尬。这篇文章不打算讲太深的理论,主要分享我在Ubuntu上安装runit、把一个常驻进程交给它管理、以及后续排坑的完整过程。内容对刚接触进程管理的人友好,也适合已经用惯systemd但想换个思路的老手。

1. 为什么需要runit:当systemd显得“太重”的时候

先搞清楚一个基本问题:系统里已经有systemd在管服务,为什么还要再引入一个runit?

我在实际项目里遇到的典型场景是这样的:某几个内部小工具,本质上就是一个脚本或者一个单进程二进制,需要一直跑着,不需要复杂依赖,不需要开机前的网络等待,甚至不需要开机自启,只要在特定时候有人把它拉起来、挂掉之后能自动恢复就行。这种“轻量常驻”需求用systemd去写unit文件,虽然也能做,但总觉得步骤偏重:要写[Unit]、[Service]、[Install]几段,要处理ExecStart、Restart、RestartSec这些字段,有时候还得处理After依赖。而且systemd作为系统的1号进程,覆盖的职责面太广,在很多精简环境里根本不是首选。

而runit的设计哲学就简单直接:每个服务就是一个目录,目录里放一个run脚本,这个脚本负责把服务进程跑起来;日志是另一个子目录里的log/run脚本,负责接住标准输出。整个模型用一堆文件和一个进程就能说明白,几乎零依赖,不用看dbus,不用理解target。这种直观性是runit最打动我的地方。

我把两个方案的关键差异做了个对比:

维度systemdrunit
服务定义unit文件,包含大量字段一个run脚本,按shell逻辑自由写
启动/停止命令systemctl start/stopsv up/down
服务状态查看systemctl statussv status
开机自启systemctl enable建立符号链接到服务目录
日志处理journald或重定向文件svlogd,每个服务自带
依赖管理完善,支持target/after弱,主要靠脚本自己处理
资源占用相对较重极轻,内存占用很低
适用场景常规系统服务、复杂依赖容器、嵌入式、精简环境

所以runit适合的群体很明确:想要“一段脚本就接管一个服务”的人,或者对资源占用敏感、追求极简的人。反过来,如果面对的是十几个互相有依赖、需要严格控制启停顺序的企业级服务,那还是老老实实用systemd,runit在这方面的能力确实偏弱。

2. 安装runit:一条命令和另一种选择

2.1 apt安装:一条命令搞定

在Ubuntu上装runit非常省事,软件源里就有现成包。我在机器上执行的是:

sudo apt update sudo apt install runit

装完之后先验证一下主要命令是否都在:

which sv which runsvdir which runsv which chpst which svlogd

如果这些命令都能找到,说明核心组件已经齐了。Ubuntu的runit包会把命令装到/usr/bin或者/usr/sbin,具体路径因发行版而异,用which查一下最稳。

有一个细节值得注意:在Ubuntu上用apt安装runit,并不会把系统默认的init替换掉,也不会破坏现有systemd。这一点对新手来说反而友好,runit装完之后更像一个“额外的进程管理补充工具”,你想用它管理某些服务就管理某些服务,不用动系统底层。

装完之后还要解决一个问题:runsvdir这个“大总管”谁来启动。runit的机制需要runsvdir先跑起来,它才会持续监视服务目录。Ubuntu下通常可以把它做成一个systemd服务来拉起,操作方式很直接:

sudo systemctl enable runit.service sudo systemctl start runit.service

有的发行版里服务名不叫runit.service,可能是runsvdir-start.service。不确定的时候可以先查一下实际有哪些相关服务单元:

systemctl list-unit-files | grep -i runit

看到真实名字后再去enable,别不管三七二十一照着网上文章硬抄。

2.2 源码编译:另一种选择

如果你用的是极简系统,软件源里没有runit,或者迫切需要最新版,那就走源码编译路线。runit官网能下载到稳定版源码包,我以常见的runit版本为例,解压后进入目录:

wget http://.../runit-2.1.2.tar.gz tar -xzf runit-2.1.2.tar.gz cd runit-2.1.2

从源码编译runit不复杂,官方提供了一套编译脚本:

./package/compile

编译完成后,可执行文件会生成在command/目录下,里面包含sv、runsv、runsvdir、chpst、setuidgid、svlogd、vlogger等工具。把它们装到系统路径下即可:

sudo cp command/* /usr/local/bin/

2.3 验证安装

不论用哪种方式,装完后跑一下版本或者帮助信息确认没问题:

sv -v runsvdir -v

看到版本号输出就算成功。我遇到的坑之一是,某些精简环境下PATH里没有包含/usr/local/bin,导致系统服务里调用sv时报“command not found”。所以装完源码版之后,要么把命令软链到/usr/bin,要么确保PATH环境变量覆盖到位。这个细节看着小,实际排查起来非常浪费时间。

3. 理解runit的服务模型:run脚本、日志和监督进程

3.1 最小的run脚本

runit管理服务的方式,和systemd有本质差别。每个被管的服务,在/etc/sv/下面有自己的目录,目录里必须有一个可执行的run脚本。

举个例子,如果我有一个很简单的服务,只是想让它每30秒往日志里写一行,我可以先建一个服务目录:

sudo mkdir -p /etc/sv/demo

然后写/etc/sv/demo/run:

#!/bin/sh exec /bin/sh -c 'while true; do date >> /tmp/demo.log; sleep 30; done'

再给它执行权限:

sudo chmod +x /etc/sv/demo/run

关键就在exec这个词。runit的监督逻辑是:它把run脚本直接当成“服务本体”来看待,也就是说runsv进程会去等待这个run脚本的进程退出。如果你在脚本末尾不加exec,shell会先启动一个子进程去跑命令,然后自己退出;一旦shell先退出了,runsv会认为服务已经结束,于是开始重启流程,进程状态来回横跳,造成“服务永远在重启”的假象。

所以一个基本铁律:run脚本最后一行必须用exec启动真正的服务进程,让这个服务进程取代shell,成为runsv直接盯着的进程。这个细节我在给不少人排错的时候反复强调过,新手特别容易踩。

3.2 权限控制:别用root直接跑应用

如果服务需要特定用户运行,run脚本里用chpst或者setuidgid来降权,而不建议直接用root启动应用进程。理由和日常安全习惯一样:应用一旦被攻破,root权限等于把整个机器拱手让人。

setuidgid用法很简单:

#!/bin/sh exec setuidgid appuser /usr/local/bin/app --config=/etc/app/config.toml

chpst功能更丰富一些,可以通过-u指定用户和组,用-d指定工作目录,还可以设置环境变量。常用的写法是:

#!/bin/sh exec chpst -u appuser:appgroup -d /opt/app /usr/local/bin/appserver

我的经验是:如果只是单纯切换用户,用setuidgid就够了;如果需要同时指定组、工作目录、环境变量,用chpst更方便。这里注意一点,chpst -u后面的用户组参数格式是user:group,不是简单的冒号可选,写错会导致启动失败。

3.3 日志服务:log/run与svlogd

runit和日志的关系特别融洽,这也是我比较偏爱它的原因之一。每个服务目录下可以再建一个log子目录,里面放一个log/run脚本,专门负责把服务的标准输出重定向到日志。

一个标准的服务目录结构长这样:

/etc/sv/demo/ ├── run └── log/ └── run

log/run脚本常写成:

#!/bin/sh exec svlogd -tt /var/log/demo

解释一下:svlogd是runit自带的日志守护进程,-tt表示给每条日志加完整时间戳。/var/log/demo是日志落地目录,svlogd会自动创建,并把标准输入中的内容写进去,还会自己做日志轮转。

这里要特别说明exec再次出现的原因:log/run脚本同样需要让svlogd进程取代shell,runsv才会把日志进程和主服务进程一样监督起来。日志进程挂了,runsv会同样拉起,保证日志链路不中断。这一套设计下来,等于“服务本体”和“日志收集”都处于被监督状态,省去了大量自己写维护脚本的功夫。

如果不想让日志落到本地文件,而是想按天切割,svlogd也能通过配置文件实现。不过这些属于进阶用法,日常用-tt加路径已经能满足一大半需求。

3.4 runsvdir与/etc/service的联动

服务目录建好了,run脚本也写好了,怎么让runit真正开始管它?这里需要理解runsvdir的作用。

runsvdir是一个“大总管”进程,它会持续监视某个目录。目录下出现的服务子目录或符号链接,都会被分配给一个对应的runsv进程去监督。在Ubuntu上,默认监视的目录通常是/etc/service。

所以把一个服务交给runit管理的动作其实非常简单:在/etc/service下建立一个指向/etc/sv/demo的符号链接:

sudo ln -s /etc/sv/demo /etc/service/demo

符号链接建立之后,runsvdir会立刻发现新成员,自动为它拉起runsv进程。反过来,想彻底停用某个服务,把符号链接删掉即可:

sudo rm /etc/service/demo

runsvdir发现链接消失,会通知runsv把服务进程停掉。这个机制用起来很直觉,不需要记一堆命令,本质上就是“文件系统操作即服务管理”。

4. 实操:把一个常驻服务完整接入runit

4.1 示例服务准备

为了演示完整流程,我准备一个模拟服务。实际工作中可能是一个API服务、一个消息队列worker,但核心思路完全一样。我在/opt/demo-server/下放了一个脚本,用来模拟一个持续运行的服务进程:

sudo mkdir -p /opt/demo-server sudo tee /opt/demo-server/server.sh >/dev/null <<'EOF' #!/bin/bash while true; do printf '%s say hello from demo server\n' "$(date '+%F %T')" sleep 30 done EOF sudo chmod +x /opt/demo-server/server.sh

注意这个脚本是故意“前台运行”的:它一直循环,不会自己退出。runit管的服务必须是这样常驻型的,才能体现出监督意义。如果脚本跑一下就退出了,runsv会反复把它拉起来,虽然在有些场景下这种“一次性任务反复执行”也有用,但我们先按常规常驻服务的逻辑来。

4.2 创建服务目录与脚本

接着创建runit服务目录:

sudo mkdir -p /etc/sv/demo-server/log

然后写主run脚本:

sudo tee /etc/sv/demo-server/run >/dev/null <<'EOF' #!/bin/sh exec /opt/demo-server/server.sh EOF

再写日志脚本:

sudo tee /etc/sv/demo-server/log/run >/dev/null <<'EOF' #!/bin/sh exec svlogd -tt /var/log/demo-server EOF

两个脚本都加上执行权限:

sudo chmod +x /etc/sv/demo-server/run sudo chmod +x /etc/sv/demo-server/log/run

另外把日志目录建出来,并改好归属。如果日志目录不存在,svlogd会自动创建,但为了避免权限问题,我习惯在启动前先手工建好:

sudo mkdir -p /var/log/demo-server sudo chown -R root:root /var/log/demo-server

4.3 挂接并启动服务

现在进入关键一步,把服务链接到/etc/service,触发runsvdir接管:

sudo ln -s /etc/sv/demo-server /etc/service/demo-server

等待一两秒,查看状态:

sv status /etc/service/demo-server

如果一切正常,会看到类似这样的输出:

run: /etc/service/demo-server: (pid 12345) 1s

看到run:和pid,说明服务已经在runsv的监督下正常运行。再检查一下日志:

tail -f /var/log/demo-server/current

由于svlogd默认把日志写到current文件里,所以tail这个文件就能看到服务循环输出的内容。看到带时间戳的日志行,说明整条生命周期已经跑通。

4.4 常用sv命令对照

服务挂接之后,日常管理都通过sv命令完成。这里列一下我最常用的几个操作:

命令作用
sv status /etc/service/demo-server查看服务状态
sv up /etc/service/demo-server启动服务(之前down掉的话)
sv down /etc/service/demo-server停止服务
sv restart /etc/service/demo-server重启服务
sv hup /etc/service/demo-server给服务进程发送HUP信号
sv term /etc/service/demo-server发送TERM信号
sv -w 300 up /etc/service/demo-server等待最多300秒直到启动完成
sv exit /etc/service/demo-server退出管理该服务的runsv进程

这里有个比较容易被误解的点:sv down和sv exit的区别。sv down是让这个服务停止运行,但runsv还在,之后可以用sv up再次拉起。sv exit则是把对应的runsv进程本身退掉,相当于彻底解除对这个服务的监督,服务也会随之停止。日常维护基本用sv down/up/restart就够了,sv exit通常在删除服务前用。

5. 常见问题与排查实录

5.1 服务状态显示up,但进程明明没跑起来

这是我遇到频率最高的问题。症状是sv status显示run:,但用ps看对应进程并不存在,或者进程反复出现又消失。

这种问题十有八九是run脚本里没有用exec,或者脚本里写了&把进程放后台了。runit的监督逻辑盯的是“run脚本进程本身”是否存活,如果你在脚本里用&启动子进程,run脚本会立刻退出,runsv重启的节奏可能快于子进程真正起来的速度,状态自然就飘了。

排查看起来很简单,实际排查时还要多看一层:如果确认用的是exec,那问题往往在服务进程本身,比如端口被占用、配置文件错误、依赖的目录没有权限。这时候不要只盯着runit,先把run脚本里的命令单独拿出来跑一遍,看具体报错。把“runit的问题”和“服务自身的问题”分开排查,效率会高很多。

5.2 日志文件一直不产生,或者日志不更新

日志不产生,通常可以从三个角度查:

一是看看log/run脚本有没有执行权限。runit对脚本权限要求很严格,缺少可执行权限,runsv会不断尝试拉起log/run但一直失败。可以ls -l查看权限,顺手用chmod +x补上。

二是目录归属问题。svlogd要以某个特定用户写入日志目录,如果目录权限是root且不允许其他人写,svlogd会一直报错。解决办法是把日志目录归属给运行log服务的用户,或者直接把目录权限放宽到可写。

三是服务本身有没有往标准输出写东西。svlogd收集的是服务的stdout/stderr,如果你的服务压根不打印日志,那/var/log/demo-server/current自然不会有内容。这不是runit的问题,而是服务端没有定义好日志输出策略。

还有一个隐藏极深的小坑:svlogd对当前日志文件有命名约定,默认是current,而且svlogd自己会锁定这个文件的写入,不允许其他进程同时以追加模式写它。如果你临时用echo或者别的工具去手动写current,大概率会遇到诡异的权限和写入失败,这不是bug,是保护机制。

5.3 run脚本报错:runsv反复重启

这种状况的表现是:服务的pid不断变化,日志里刷出启动报错。排查顺序建议是:

先单独执行run脚本里的命令,用sh -x或者其他方式看具体错误。比如:

sudo sh -x /etc/sv/demo-server/run

这样就能看到脚本到底走到哪一步挂的。常见原因无非是路径写错、环境变量缺失、依赖的二进制不在PATH里。这里要特别提一下:从systemd拉起的runsvdir环境,和交互式shell的环境差异很大,很多你希望在PATH里出现的目录可能压根不在。遇到诡异的“命令找不到”,可以直接在run脚本里显式设置PATH,比如:

#!/bin/sh export PATH=/usr/local/bin:/usr/bin:/bin exec /opt/demo-server/server.sh

这个常被我当作经验分享,因为同样一个脚本在交互终端能跑,在runit下就是启动失败,多数就是环境变量这条线出了问题。

5.4 服务想开机自启,但chkconfig不生效

如果你习惯用sysvinit那一套思路去处理runit服务,会绕弯子。runit的开机自启不依赖什么注册机制,它只认/etc/service下的链接。所以正确做法就是确保runsvdir这个进程是开机自启的,然后把这个服务链接建好就行。

在Ubuntu上,我上面提到过,一般把runit的systemd unit启用即可:

sudo systemctl enable runit.service

这样开机后runsvdir起来,扫描/etc/service,看到demo-server链接就会自动接管。不需要额外为每个服务做一次enable操作。理解了这一层,就觉得runit的“开机自启”比systemd容易太多,因为本质上就是一条符号链接的事。

5.5 服务flapping很快,runsv不断重启

如果服务启动即退出,runsv会按默认策略每隔几秒重启它,看起来像CPU被占满、日志刷屏。这通常是在run脚本的启动命令中记错了参数。排查时直接把run脚本里的命令粘贴到终端执行,立刻能看到错误信息。不要原地加日志反复重启,效率很低。

6. 一些我踩过坑之后的个人体会

runit这套体系上手之后,我觉得它最出色的地方是“心智负担小”:不用理解一堆抽象概念,只需要记住“目录 + run脚本 + 符号链接 + sv命令”这四件事,就能把服务生命周期管理得明明白白。尤其是容器场景,我用runsvdir /etc/service作为容器入口,一个容器里同时跑多个进程,每个进程独立监督、独立日志,非常顺手。

最后分享一个小技巧:如果你临时想给一个正在runit监督下的服务做热更新,不需要写复杂的reload逻辑,直接sv restart即可。也可以利用sv hup发送HUP信号给服务进程,很多自带reload能力的服务会自己优雅地重新加载配置。这个习惯我已经维持了很久,比手动kill再拉起要安全得多。实际用得多了,你会慢慢发现runit虽然古老,但它那种“监督一个前台进程”的模型,反而是最简单的正确。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询