Nginx Unit 应用服务器实战:一份 JSON 同时跑 PHP/Python/Node,配置热加载不断线

PHP-FPM 之外的选择:Nginx Unit 是什么

个人站长部署 PHP 网站,默认路径几乎是固定的:Nginx 做前端,PHP-FPM 通过 FastCGI 处理后端。这套组合能跑,但它的配置是分裂的——Nginx 一个配置文件、PHP-FPM 一个进程池配置文件,两边要手动对齐 socket 路径、超时、buffer 大小,改一处常常要动两处,出错时排查还得来回看两套日志。

Nginx Unit 是 Nginx 团队后来推出的应用服务器,思路完全不同:它把「接收 HTTP 请求」和「运行应用代码」合并到一个进程里,用一套 JSON 配置同时描述路由和进程管理,而且配置可以热加载——改完立刻生效,不用 reload、不断连接。

它一开始只支持 PHP 和 Python,现在支持的语言已经很全:PHP、Python、Node.js、Java、Go、Ruby、Perl、WebAssembly。对个人站长最直接的价值是:

  • 把 Nginx + PHP-FPM 两套配置简化成 Unit 一套,配置更少、脑负担更轻。
  • 配置热加载,改 max_children、加路由不用重启服务、不断线。
  • 同一台机器上跑多种语言的应用(比如 PHP 主站 + Python 定时接口 + Node 的小工具),全由 Unit 统一管。
  • 内置静态文件服务、路由匹配、URL 重写、限速,很多原本要写 Nginx 规则的事直接用它搞定。

它不适合所有场景:Unit 的功能广度不如 Nginx(没有完整的反代生态、没有大量第三方模块),所以常见架构是「Nginx 在最前面做 TLS 和缓存,把动态请求转给 Unit」;也可以让 Unit 直接对外。下面从安装讲到配置、热加载、多语言,最后给几个必踩的坑。

安装 Unit:用官方源,别自己编译

Unit 官方提供了各发行版的仓库,装起来很干净。以 Debian 12 为例:

apt update
apt install -y curl gnupg2 ca-certificates lsb-release

# 导入官方 GPG key
curl -fsSL https://unit.nginx.org/keys/nginx-keyring.gpg \
  | gpg --dearmor -o /usr/share/keyrings/nginx-keyring.gpg

# 添加 Unit 官方源
echo "deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] \
https://packages.nginx.org/unit/debian/ $(lsb_release -cs) unit" \
  > /etc/apt/sources.list.d/unit.list

apt update

# 装核心 + PHP 语言的模块(模块版本要和 PHP 主版本对应)
apt install -y unit unit-php unit-python3

systemctl enable --now unit
systemctl status unit --no-pager

其他语言模块同理:unit-php、unit-python3、unit-nodejs20、unit-java、unit-go、unit-ruby。装哪个就支持哪个,不装的不占资源。

Unit 默认监听 127.0.0.1:8080(具体端口看配置),管理接口在 /var/run/unit/control.sock。它有个独立的控制 socket 用于热加载配置,默认只有 unit 用户能访问——这点很重要,后面讲安全时会用到。

核心概念:configuration 三段结构

Unit 的配置是 JSON,通过控制 API 提交。顶层只有三个键,记住它们就掌握了大半:

{
  "listeners": { ... },   // 监听哪个地址端口
  "routes":    [ ... ],   // 怎么路由请求(可嵌套,即路由树)
  "applications": { ... } // 应用怎么跑(语言、进程数、工作目录)
}

一个最小的 PHP 站点配置长这样:

{
  "listeners": {
    "127.0.0.1:8300": {
      "pass": "routes"
    }
  },
  "routes": [
    {
      "match": { "uri": "!*.php" },
      "action": {
        "share": "/var/www/site$uri",
        "fallback": { "pass": "applications/phpapp" }
      }
    },
    {
      "action": { "pass": "applications/phpapp" }
    }
  ],
  "applications": {
    "phpapp": {
      "type": "php",
      "root": "/var/www/site/",
      "script": "index.php",
      "processes": { "max": 10, "spare": 4 }
    }
  }
}

逐段解释:

  • listeners 里 127.0.0.1:8300 是监听地址,pass: "routes" 表示交给路由树处理。对外服务时如果前面有 Nginx,监听 127.0.0.1 就够;让 Unit 直接对外就写 *:80。
  • routes 是路由树。第一条:match uri !*.php(不是 PHP 结尾的请求)走 share——直接从磁盘发静态文件;若文件不存在(fallback)交给 PHP 处理,这是给「伪静态」用的:URL 没有对应文件时交给框架入口。
  • applications 定义应用。type: "php"、root 是站点根目录、script 是默认入口。
  • processes 就是 PHP-FPM 里 pm.max_children 的等价物。max: 10 表示最多 10 个进程,spare: 4 表示常备空闲进程。这里还支持 idle_timeout(空闲多久回收)等参数,比 PHP-FPM 的进程池配置更集中。

通过控制 API 热加载配置

Unit 的配置不直接编辑文件,而是通过 unitc 工具或控制 socket 的 API 提交。官方客户端 unitc 让这件事变得很简单:

# 把完整的 JSON 覆盖到 /config
unitc /config < myconfig.json

# 只改某一个子树(比如只改 applications/phpapp 的进程数)
unitc /config/applications/phpapp < phpapp.json

# 查看当前配置
unitc /config

# 查看某个部分
unitc /config/listeners

用 curl 直接调控制 API 也行(socket 路径视安装方式而定):

# 读取当前配置
curl --unix-socket /var/run/unit/control.sock http://localhost/config/

# 只更新 applications/phpapp 这个子树(PUT 是合并,不是覆盖整个 config)
curl -X PUT --data-binary @phpapp.json \
  --unix-socket /var/run/unit/control.sock \
  http://localhost/config/applications/phpapp

# 看某个 listener
curl --unix-socket /var/run/unit/control.sock \
  http://localhost/config/listeners/127.0.0.1:8300

热加载的价值在这里体现得淋漓尽致:你想把 PHP 进程从 10 调到 20,改一行 JSON、PUT 一下,几百毫秒内新进程就起来了,正在处理的请求不受影响,连接不断。对比 PHP-FPM——改 pm.max_children 要 service php-fpm reload,虽然也是平滑重载,但涉及发信号、旧进程优雅退出,配置分散在两个文件里还容易忘同步。Unit 把「配置」和「生效」之间的链路缩到最短。

更妙的是,PUT 支持「点路径」精确修改,不需要提交整份配置。比如只想改一个路由的限速值,就 PUT 那个路由节点,其余部分原样不动——这在自动化脚本里非常好用。

一台机器跑多种语言

个人站长常常一台机器上跑着杂七杂八的东西:主站是 PHP,某个接口用 Python 写的,还有个小工具是 Node.js。传统上要分别用 PHP-FPM、gunicorn/uWSGI、pm2 三套进程管理器,各配各的端口、各写各的 systemd unit,日志分成三处。

用 Unit,这些都能收进同一份配置:

{
  "listeners": {
    "127.0.0.1:8300": { "pass": "routes" }
  },
  "routes": [
    {
      "match": { "uri": "/api/*" },
      "action": { "pass": "applications/pyapp" }
    },
    {
      "match": { "uri": "/tools/*" },
      "action": { "pass": "applications/nodeapp" }
    },
    { "action": { "pass": "applications/phpapp" } }
  ],
  "applications": {
    "phpapp":  { "type": "php",    "root": "/var/www/site/", "script": "index.php" },
    "pyapp":   { "type": "python",
                 "path": "/opt/pyapp/",
                 "module": "wsgi",
                 "callable": "app" },
    "nodeapp": { "type": "external",
                 "executable": "/usr/bin/node",
                 "arguments": ["/opt/nodeapp/server.js"] }
  }
}

几点注意:

  • Python 用 WSGI 时,module 指向模块名、callable 指向 WSGI 应用对象;用 ASGI(FastAPI/Starlette)则 type 还是 python,通过 protocol: "asgi" 指定。
  • Node.js 常见写法是把 Node 应用仍作为「外部进程」由 Unit 管理(type: "external"),Unit 负责拉起和守护;也可以用 Unit 的 Node 模块直接跑(type: "nodejs",需要应用导出符合 Unit 接口的模块)。
  • 路由匹配是按顺序的,先写的先匹配。把具体的 /api/*、/tools/* 放前面,兜底的 PHP 放最后,逻辑才正确。

这样一来,语言各不相同但也无所谓了,Unit 里一份 JSON 就是全部真相,进程数、工作目录、入口全在一处。配上 unitc 热加载,加一个接口、改一个进程数都是秒级的操作。

静态文件、限速与 TLS

Unit 内置的能力,很多原本要靠 Nginx 规则实现:

{
  "settings": {
    "http": {
      "max_body_size": 10000000,     // 约 10MB,等价 client_max_body_size
      "discard_unsafe_fields": true
    }
  },
  "routes": [
    {
      "match": { "uri": "/static/*" },
      "action": {
        "share": "/var/www/site/static",
        "response_headers": {
          "Cache-Control": "public, max-age=2592000",
          "X-Content-Type-Options": "nosniff"
        }
      }
    },
    {
      "match": { "uri": "/api/*", "method": "POST" },
      "action": {
        "pass": "applications/phpapp",
        "limits": { "rate": 5, "burst": 10, "delay": 0 }
      }
    },
    { "action": { "pass": "applications/phpapp" } }
  ]
}
  • share 直接发静态文件,还能顺手压 Cache-Control、X-Content-Type-Options 这类响应头。
  • limits 就是请求限速,等价于 Nginx 的 limit_req:每秒 5 个请求、突发 10 个、不延迟。用来保护登录接口、评论接口很合适。
  • max_body_size 等价于 client_max_body_size,默认值偏小,上传文件要记得调大,否则会 413。

TLS 也可以在 Unit 里直接配(listeners 里加 tls 对象指向证书链和私钥),不过很多人还是喜欢让 Nginx/Caddy 在最前面处理证书自动续期,Unit 只管应用。两种都行,选你顺手的。

五个必踩的坑

坑一:静态文件路径没配对,全变成 404。 Unit 的 share 是「从指定根目录找文件」,路径拼接规则和 Nginx 的 root/alias 语义不完全一样。写 share: "/var/www/site/static" 配 uri: "/static/*" 时,要确认请求 /static/a.png 最终落到 /var/www/site/static/static/a.png 还是 /var/www/site/static/a.png——用 share: "/var/www/site" 配合完整 uri 更不容易错。上线前一定用一个真实静态文件测通。

坑二:忘了 fallback,伪静态 404。 使用框架(ThinkPHP、Laravel、Typecho 等)时,URL 往往不对应真实文件(比如 /post/123)。路由里必须给静态文件匹配加 fallback 指向应用,否则不存在的文件直接返回 404,永远到不了框架入口。这是从 Nginx try_files 迁移过来最容易漏的一步。

坑三:控制 socket 权限太松,等于给了 root。 能访问 Unit 控制 socket 的人可以任意改配置,包括把某个应用的可执行文件改成任意命令——等同于命令执行。所以 socket 必须只允许 unit 用户访问,绝不暴露到公网、不放进任何 Web 可读目录。如果你自己写了脚本调控制 API,脚本权限也要收紧。

坑四:用旧配置整体覆盖,把别人加的监听弄丢。 PUT /config 是替换整个配置。如果你手上有份过期的 JSON,一提交就把别人(或你自己另一个脚本)新加的应用、监听全冲掉。改配置优先用「点路径 PUT」只改需要改的子树;要整体替换时,先 GET /config 拿到当前完整配置,改完再 PUT,别用本地旧文件。

坑五:以为装了 Unit 就能替代 Nginx 的一切。 Unit 没有 Nginx 那种庞大的反代/模块生态,也缺少成熟的缓存、灰度、复杂重写能力。现实里最常见也最稳的架构是 Nginx 在前(TLS、proxy_cache、限流、WAF),Unit 在后跑应用。把 Unit 当「更简洁的 PHP-FPM + 内置路由」,而不是「Nginx 杀手」,预期才不会错。

要不要把现有站点迁到 Unit

给你一个务实的判断:

  • 如果现在网站跑得稳定、你对 Nginx + PHP-FPM 那套配置很熟,不必为了新而迁。稳定压倒一切。
  • 如果你正要新建站、或受够了 PHP-FPM 配置文件分散、或一台机器上跑着多种语言想统一管理,Unit 值得一试——它的配置集中、热加载、多语言统一,确实能省心。
  • 迁移时可以先并行:Nginx 加一个测试 location 把部分请求转给 Unit,验证没问题再逐步切全量。Unit 和 PHP-FPM 可以同时跑在同一台机器,互不干扰。

Unit 的意义不在于「又冒出一个新服务器」,而在于它把「配置应用怎么跑」这件事变得前所未有的直接:一份 JSON、一个控制接口、改完即生效。对一个人维护服务器、没时间在好几套配置文件之间来回对照的站长来说,这种「简单可维护」本身就是生产力。工具好不好,最后看的不是功能列表有多长,而是它让你少踩多少坑、少花多少时间。Unit 在这一点上,交出了让人愿意一试的答卷。

Last modification:October 5th, 2026 at 09:25 pm

Leave a Comment