一台机器上要同时连三个 frp 服务端,网上大部分教程的做法是复制三份 frpc.service,改三次路径。改一次参数要同步改三个文件,迟早出错。

systemd 自带的**模板单元(template unit)**就是为这种场景准备的:写一份 frpc@.service,后面想开几个实例就开几个。这篇先用 frp 走一遍完整流程,再讲怎么把同一套写法套到 xray、socat、Python 脚本等任何需要多开的程序上。

模板单元是什么

普通单元文件叫 frpc.service,模板单元文件名多一个 @,叫 frpc@.service。

它本身不会运行,只有在你启动 frpc@azure_hk.service 的时候,systemd 才会拿这个模板生成一个真正的实例,并且把 @ 后面的那串字符串(这里是 azure_hk)作为实例名,替换掉单元文件里的 %i。

所以「一份模板 + N 个实例名 = N 个互相独立的服务」,每个实例有独立的进程、独立的日志、独立的启停状态,但共用同一份单元文件。

frp 实战

1. 写模板单元

新建 /etc/systemd/system/frpc@.service:

[Unit]
Description=Frp Client Service - %i
After=network.target syslog.target
Wants=network.target

[Service]
Type=simple
Restart=on-failure
RestartSec=5s
# %i 会被替换为 @ 后面的实例名
ExecStart=/usr/local/frp/frpc -c /usr/local/frp/frpc-%i.toml

[Install]
WantedBy=multi-user.target

2. 按命名约定放配置文件

模板能生效的前提是配置文件名和实例名一一对应。这里约定为 /usr/local/frp/frpc-<实例名>.toml:

  • frpc-azure_hk.toml → 实例 frpc@azure_hk
  • frpc-aliyun.toml → 实例 frpc@aliyun
  • frpc-vps2.toml → 实例 frpc@vps2

以后想新增一条隧道,只要丢一个配置文件进去,不用再碰 service 文件。

3. 启动

sudo systemctl daemon-reload
sudo systemctl enable --now frpc@azure_hk
sudo systemctl enable --now frpc@aliyun

enable --now 等于 enable(设置开机自启)加 start(立刻启动),一步到位。

4. 批量管理

实例名支持通配符,可以一条命令管住所有实例。引号一定要加,否则 * 会先被 shell 展开成当前目录下的文件名:

systemctl status "frpc@*"
sudo systemctl restart "frpc@*"
sudo systemctl stop "frpc@*"

看日志同理,-f 持续跟踪:

journalctl -u "frpc@*" -f

只看某一个实例:

journalctl -u frpc@azure_hk -n 100 --no-pager

推广到任何程序

上面这套写法跟 frp 没有任何绑定关系。只要一个程序满足「同一个二进制,靠不同参数/配置跑多份」,就能用模板单元。

可用的说明符

模板里能用的占位符不止 %i,常用的这几个:

说明符含义frpc@azure_hk.service 里的值
%i实例名(转义后的原始形式)azure_hk
%I实例名(反转义,- 还原成 /)azure_hk
%p前缀,即 @ 之前的部分frpc
%n完整单元名frpc@azure_hk.service
%N单元名去掉后缀frpc@azure_hk
%h运行用户的家目录/root
%u运行用户名root

%i 和 %I 的区别只在实例名里带特殊字符时才体现出来。因为实例名不能直接包含 /,需要用 - 代替,%I 会把它还原回去。如果你的实例名要从路径生成,用 systemd-escape 转一下:

systemd-escape -p /srv/app/site-a

例子一:多配置的 xray / v2ray

[Service]
ExecStart=/usr/local/bin/xray run -c /usr/local/etc/xray/%i.json

配置放 /usr/local/etc/xray/hk.json、tokyo.json,然后 systemctl enable --now xray@hk xray@tokyo。

例子二:多条 socat 端口转发

这类程序参数不在配置文件里,而是直接写在命令行上。做法是给每个实例配一个环境变量文件:

/etc/systemd/system/socat-fwd@.service

[Unit]
Description=socat forward - %i
After=network.target

[Service]
Type=simple
Restart=on-failure
RestartSec=5s
EnvironmentFile=/etc/socat-fwd/%i.conf
ExecStart=/usr/bin/socat TCP4-LISTEN:${LPORT},fork,reuseaddr TCP4:${RHOST}:${RPORT}

[Install]
WantedBy=multi-user.target

/etc/socat-fwd/nas.conf

LPORT=8080
RHOST=192.168.1.50
RPORT=5000

然后 sudo systemctl enable --now socat-fwd@nas。这一段可以和端口转发那篇配合起来看,那篇讲的是单条转发怎么写,这里解决的是转发条目一多就管不过来的问题。

小技巧:EnvironmentFile=-/etc/socat-fwd/%i.conf,路径前面加一个 -,表示文件不存在时不报错、继续启动。适合把环境变量文件当成可选覆盖用。

例子三:多开的 Python 脚本 / 机器人

[Service]
Type=simple
User=vinkey
WorkingDirectory=/opt/bots/%i
ExecStart=/opt/bots/%i/.venv/bin/python main.py
Restart=on-failure
RestartSec=10s

每个机器人一个目录、一个虚拟环境,bot@alpha、bot@beta 各跑各的。

几个容易踩的坑

改完模板一定要 daemon-reload。 模板文件改动不会自动生效,而且已经在跑的实例需要 restart 才会用上新配置:

sudo systemctl daemon-reload && sudo systemctl restart "frpc@*"

不能直接 enable 模板本身。 systemctl enable frpc@.service 会报错,因为 systemd 不知道该启哪个实例。如果确实想要一个「不写实例名时的默认值」,在 [Install] 段加一行:

[Install]
WantedBy=multi-user.target
DefaultInstance=azure_hk

给单个实例开小灶。 某个实例需要跟别人不一样的参数,不用为它单独复制一份单元文件,用 systemctl edit 生成 drop-in 覆盖即可:

sudo systemctl edit frpc@aliyun

它会生成 /etc/systemd/system/frpc@aliyun.service.d/override.conf,只写你要改的那几行,其余仍然继承模板。

通配符不要漏引号。 systemctl stop frpc@* 在当前目录恰好有同名文件时会被 shell 展开成别的东西,systemctl stop "frpc@*" 才是稳的。

查当前开了哪些实例:

systemctl list-units "frpc@*" --all