NGINX Unit 是 NGINX 官方推出的多语言应用服务器,它把「进程管理」和「请求路由」两件事从应用框架里抽离出来,交给常驻进程统一编排。对已经用 NGINX 做反向代理的团队而言,Unit 提供了一条不必重写业务代码就能获得动态重载、多语言混部与细粒度路由的路径。本文从 Unit 的进程模型讲起,展开应用与路由配置、动态重载的零停机机制,以及它与 NGINX 反代配合时的边界与坑点。
1. Unit 的定位与进程模型
Unit 要解决的问题很具体:传统应用服务器把路由、静态文件、进程守护、热重载全部塞进框架内部,语言一旦确定就很难更换;而反向代理层又拿不到应用级的语义,只能按 URL 前缀转发。Unit 把中间这层独立出来,形成一条清晰的分工链。
Unit 启动后主要有三类进程:
- controller 进程:唯一的配置管理者,读取控制套接字上的 JSON 配置,负责解析、校验、落盘与下发。它本身不处理任何业务流量。
- router 进程:负责监听端口、解析 HTTP 请求、执行路由匹配,然后把请求通过内部套接字转发给具体的应用进程。
- 应用进程:由 router 按需拉起,每种语言对应一个运行时适配层(Python、Node.js、PHP、Ruby、Java、Go、Perl 等)。
这个模型的关键收益是:配置变更不需要重启监听端口。controller 把新配置编译成内部结构后通知 router,router 用新的路由表处理后续请求,老的应用进程处理完在途请求后自然退出。这与 nginx -s reload 的「新老 worker 交替」在思想上一致,但粒度更细——Unit 可以只重启某个应用,而不影响同一监听器上的其他应用。
一个容易被忽略的细节:Unit 的 router 和应用进程之间走的是 Unix 域套接字或抽象命名空间套接字,不走 TCP 回环。这减少了协议栈开销,也让应用进程可以完全隐藏在网络命名空间之外,只暴露 router。
1.1 与 NGINX 反代的职责划分
很多人会问:既然 Unit 自己就能监听 443、做 TLS、做路由,那 NGINX 还留在前面做什么?答案是两者的强项不同。
| 能力 | NGINX 反代 | Unit |
|---|---|---|
| 缓存与压缩 | 完整(proxy_cache、gzip/brotli) | 基础(静态文件、少量响应头) |
| 限流与 WAF | 完整(limit_req、ModSecurity) | 无 |
| 应用级路由 | 需手工写 location | 原生支持(routes 匹配) |
| 多语言进程管理 | 无 | 原生支持 |
| 动态重载 | 配置级 | 应用级 |
结论是常见的双层架构:NGINX 处理边缘关注点(TLS 终结、缓存、限流、日志),Unit 处理应用关注点(进程、路由、语言运行时)。这样 NGINX 侧的配置非常稳定,几乎只在证书轮换时变动;而应用的增删改由 Unit 的控制 API 完成,可以交给 CI 流水线自动化。
2. 安装与最小可用配置
Unit 的安装包由官方仓库提供,核心是 unit 主程序和每种语言的模块包(如 unit-python3.11、unit-node20)。只有装了对应模块,才能在配置里声明那种语言的应用。
# Debian/Ubuntu
sudo apt install unit unit-python3.11 unit-node20
# 确认控制套接字存在
ls -l /var/run/unit/control.sock
# 查看当前完整配置
sudo curl --unix-socket /var/run/unit/control.sock \
http://localhost/config
所有配置都通过一个 HTTP-over-Unix-socket 的接口读写。GET /config 读取全量配置,PUT /config/<path> 更新某个子树。这种「JSON 即配置」的设计让配置本身可以被程序生成,也天然适合做版本管理。
最小可用配置只需要三块:listeners、applications、routes。先看一个能跑起来的版本。
{
"listeners": {
"*:8080": {
"pass": "applications/demo"
}
},
"applications": {
"demo": {
"type": "python",
"path": "/srv/app/",
"module": "wsgi",
"processes": 4
}
}
}
pass 指向 applications/demo,意思是监听器把请求直接交给这个应用。processes 是每个应用进程内部的线程数上限(Unit 用多进程 + 每进程多线程模型),而不是进程总数;进程总数由 router 根据负载动态调整。这一点经常被误解,配成 "processes": 64 后反而因为内存膨胀导致 OOM。
2.1 配置的原子性与落盘
Unit 在 PUT 时会先做完整校验:类型是否存在、路径是否可读、语言模块是否加载。校验失败返回 400 并附带具体错误,旧配置保持不变。校验通过后新配置写入磁盘(/var/lib/unit/conf.json),再通知 router 生效。因此不存在「配置写了一半服务挂掉」的中间态。
# 只更新 demo 应用的 processes 字段
sudo curl -X PUT --unix-socket /var/run/unit/control.sock \
-d '8' \
http://localhost/config/applications/demo/processes
# 删除某个应用
sudo curl -X DELETE --unix-socket /var/run/unit/control.sock \
http://localhost/config/applications/demo
3. 应用定义:多语言运行时
每种语言的 applications 定义大同小异,差异集中在入口点声明上。
3.1 Python 与 WSGI/ASGI
Python 应用需要声明 module(模块名)和 callable(可调用对象名)。WSGI 与 ASGI 都用同一个 type: python,区别在 module 指向的入口。
{
"applications": {
"api": {
"type": "python",
"path": "/srv/api/",
"module": "asgi",
"callable": "app",
"processes": 4,
"environment": {
"PYTHONPATH": "/srv/api",
"DJANGO_SETTINGS_MODULE": "config.settings.prod"
}
}
}
}
path 会被加入模块搜索路径,environment 里的变量在应用进程启动时注入。注意 processes 语义是线程数,Python 受 GIL 限制,线程数对 CPU 密集任务收益有限,更适合用多进程部署(多个 Unit 实例或用 limits 分片)。
3.2 Node.js 与 PHP
Node.js 应用的入口是一个 CommonJS 模块,导出 { fetch } 或 { request, response } 形式的处理函数。Unit 会为每个 worker 线程加载一次模块,模块顶层代码因此会被执行多次,不要在顶层做全局状态初始化。
{
"applications": {
"web": {
"type": "node",
"path": "/srv/web/",
"module": "app.js",
"processes": 8
}
}
}
PHP 应用则是按 script 指定入口脚本,每个请求一个独立的 PHP 进程(或复用进程池),语义更接近传统 FPM。
{
"applications": {
"phpapp": {
"type": "php",
"root": "/srv/php/",
"script": "index.php",
"index": "index.php",
"processes": 4
}
}
}
3.3 应用级隔离与资源限制
limits 子对象可以限制请求体大小、超时与并发:
{
"applications": {
"api": {
"type": "python",
"path": "/srv/api/",
"module": "asgi",
"limits": {
"timeout": 30,
"requests": 5000,
"request_body_size": 10485760
}
}
}
}
timeout 是应用处理单请求的硬上限,超时后 Unit 直接断开并记录日志;requests 限制单进程处理多少请求后主动重启,用来规避内存泄漏累积。这两个参数是 Unit 相对传统框架的独特价值——语言无关的资源护栏。
4. 路由与监听器
当同一个监听器后面挂着多个应用时,需要 routes 做匹配分发。
4.1 匹配规则
routes 是一个有序数组,逐条匹配,命中即停。每条规则可以有 match(匹配条件)和 action(动作)。
{
"listeners": {
"*:8080": {
"pass": "routes"
}
},
"routes": [
{
"match": {
"uri": "/api/*"
},
"action": {
"pass": "applications/api"
}
},
{
"match": {
"host": "admin.example.com",
"uri": ["/", "/dashboard/*"]
},
"action": {
"pass": "applications/admin"
}
},
{
"action": {
"pass": "applications/fallback"
}
}
]
}
match 支持 uri、host、method、scheme、headers、arguments、cookies、source(客户端 IP 段)等条件,多个条件之间是 AND 关系,同一个条件的数组值是 OR 关系。uri 支持 * 通配与 ! 取反(如 "uri": ["!*.php"]),这让迁移 PHP 应用时排除敏感路径变得简单。
最后一条不带 match 的规则是兜底,等价于 NGINX 里的 location /。漏写兜底会导致未匹配请求返回 404,这是新手最常见的排错点。
4.2 监听器与 TLS
Unit 原生支持 TLS 终结,证书以 PEM 形式通过 API 上传,之后用名字引用:
sudo curl -X PUT --unix-socket /var/run/unit/control.sock \
--data-binary @fullchain.pem \
http://localhost/config/certificates/example/bundle
sudo curl -X PUT --unix-socket /var/run/unit/control.sock \
--data-binary @privkey.pem \
http://localhost/config/certificates/example/key
{
"listeners": {
"*:443": {
"pass": "routes",
"tls": {
"certificate": "example",
"session": {
"cache_size": 8000,
"timeout": 300
}
}
}
}
}
证书热更新只需要重新 PUT bundle 内容,不需要改配置也不需要重载,这对证书自动化非常友好。不过要注意:Unit 的 TLS 能力比 NGINX 弱,不支持 OCSP stapling 的细粒度配置,也不支持 SNI 动态证书的多级回退,因此生产环境更推荐在 NGINX 层终结 TLS,Unit 只监听内部明文端口。
5. 动态重载与零停机
Unit 的配置变更天然是热生效的,但「热生效」不等于「零停机」,理解差异很重要。
5.1 重载时到底发生了什么
当 PUT 更新 applications/api 时,router 的行为是:
- 按新配置预启动一批新应用进程(预热);
- 新请求路由到新进程;
- 老进程停止接收新请求,处理完在途请求后退出;
- 超过
limits.timeout仍未完成的老进程被强制回收。
因此零停机的条件是:新进程能在老进程被回收前完成预热。如果应用启动需要加载大模型或建立大量连接,就会出现请求打到未就绪进程上的情况。缓解手段是配置 ready_timeout 并配合外部探针。
5.2 滚动与回滚
由于配置就是 JSON,回滚等价于把旧 JSON 再 PUT 回去:
# 发布前先备份
sudo curl --unix-socket /var/run/unit/control.sock \
http://localhost/config > /etc/unit/backup-$(date +%s).json
# 回滚
sudo curl -X PUT --unix-socket /var/run/unit/control.sock \
-d @/etc/unit/backup-1700000000.json \
http://localhost/config
需要注意 PUT /config 是全量替换,会删掉不在 JSON 里的所有对象(包括证书)。更安全的做法是只 PUT 变化的子树,例如 PUT /config/applications/api,这样证书、其他应用都不受影响。
6. 与 NGINX 反向代理配合
生产推荐拓扑是 NGINX 在前、Unit 在后。NGINX 侧配置非常薄:
upstream unit_backend {
server 127.0.0.1:8080;
keepalive 64;
}
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
client_max_body_size 10m;
location / {
proxy_pass http://unit_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
}
}
几个要点:
- 开启 upstream keepalive 并设置
proxy_http_version 1.1+Connection "",否则每条请求都新建连接,Unit 侧会看到大量短连接。连接复用与健康检查的细节可参考 Nginx upstream keepalive 与健康检查 。 - 透传真实 IP:Unit 侧的
source匹配依赖X-Forwarded-For或 PROXY protocol。若在 Unit 里配了"proxy_protocol": true,则监听器必须开启 PROXY protocol,NGINX 侧要相应设置proxy_protocol on。 - 不要在两层都做压缩:Unit 的静态文件响应若已带
Content-Encoding,NGINX 侧再 gzip 会重复压缩,需用gzip_proxied配合判断。 - 路由下沉:把「按路径分发」交给 Unit 的 routes,把「按域名/TLS 分发」留给 NGINX,避免两边规则打架。微服务场景下的动态路由可参考 Nginx 动态路由与微服务 。
6.1 常见坑
- Unit 不识别
X-Forwarded-For里的多级代理,默认取最后一跳。若中间有 CDN,需要显式配置信任链。 - 请求体大小两层都要设:NGINX 的
client_max_body_size拦不住直接访问 Unit 端口的流量,Unit 侧也要设limits.request_body_size。 - Unix socket 权限:控制套接字默认只允许 root 访问,CI 里以普通用户调用会得到 403。
7. 可观测性与排错
Unit 的日志默认走 syslog 或文件,可通过配置调整:
{
"settings": {
"http": {
"log_route": true,
"server_version": false
}
}
}
log_route: true会在访问日志中记录命中的路由,排查「为什么请求走到了错误应用」时非常有用。server_version: false关闭Server响应头中的版本号,减少指纹泄露。
应用侧的 stdout/stderr 会被 Unit 捕获并写入配置的日志文件,路径在 applications.<name>.stderr 之类的字段里指定。若应用崩溃,Unit 会在日志里打印退出码与重启次数,配合 processes 与 requests 限制可以定位是 OOM 还是代码异常。
调试时最常用的三条命令:
# 1. 看当前生效配置
sudo curl --unix-socket /var/run/unit/control.sock http://localhost/config
# 2. 看监听器状态
sudo curl --unix-socket /var/run/unit/control.sock http://localhost/status
# 3. 直接对应用进程发请求(绕过 router),确认应用本身是否正常
sudo curl --unix-socket /var/run/unit/control.sock http://localhost/config/applications/api
/status 会返回各监听器的连接数、请求数与应用进程状态,是判断「是 Unit 的问题还是应用的问题」的第一手依据。若 status 显示应用进程数为 0 而请求 502,基本可以确定是应用启动失败——去日志里找 traceback。
8. 总结
NGINX Unit 的价值在于把应用侧的进程与路由管理标准化,让多语言混部和动态重载不再依赖框架各自的热重载实现。它最适合的场景是:已经有 NGINX 边缘层、应用以 WSGI/ASGI 或 Node.js 为主、希望把部署配置收敛成可程序化生成的 JSON。
它不适合的场景同样明确:需要复杂缓存与 WAF 的边缘服务、需要精细 TLS 控制的合规环境、以及重度依赖框架中间件生态的项目。把这些留在 NGINX 或应用框架里,Unit 只做它擅长的那一层,整体架构反而更简单。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。