1 协议与报文格式
项目 |
说明 |
连接类型 |
长连接,单条 TCP/UnixSocket 可传多请求;由前端(Nginx 等)维护 keep-alive。 |
报文分段 |
"<len>:" + <header netstring> + "," + <body> |
<len> 计数 |
10 进制 ASCII,只计算 header 区(不含冒号和逗号)。 |
Header 编码 |
NUL (0x00 ) 分隔 key/value 对;结尾需有额外的 0x00 作为终止。 |
必备键 |
SCGI=1 , CONTENT_LENGTH , REQUEST_METHOD , REQUEST_URI , SERVER_PROTOCOL , SERVER_NAME , SERVER_PORT , REMOTE_ADDR … (完全沿袭 CGI 环境变量命名) |
正文编码 |
不做转换,按 CONTENT_TYPE 由后端自行解析。 |
响应格式 |
纯 HTTP:Status: 、Content-Type: 、自定义 Header… 连同空行和 body 一并返回。 |
错误闭包 |
后端若直接 close() 连接,Nginx 会向客户端返回 502 Bad Gateway ;若想返回 5xx,可主动输出 Status: 500\r\n… 。 |
Netstring 解析示例
Header 若为
CONTENT_LENGTH\x0013\x00SCGI\x001\x00REQUEST_METHOD\x00POST\x00
则总字节为 53,首部写成 53:
,之后接逗号分隔正文。
2 前端服务器支持矩阵 & 重要指令
前端 |
支持方式 |
关键指令/模块 |
特殊注意 |
|
Nginx |
原生 |
scgi_pass , scgi_param , scgi_buffers , scgi_cache 等(ngx_http_scgi_module ) |
一定 include scgi_params ;可用 unix:/run/app.sock 提升性能 |
|
Apache |
mod_proxy_scgi |
`ProxyPass "unix:/tmp/app.sock |
scgi://localhost/"` |
需启用 mod_proxy & mod_proxy_scgi |
Caddy 2 |
核心 |
reverse_proxy unix//run/app.sock scgi |
Caddy 会自动转换头部 |
|
lighttpd |
mod_scgi |
scgi.server = ("/" => ( "socket" => "/tmp/app.sock" )) |
早期版本默认一次连接只跑一次请求 |
|
HAProxy |
TCP 直透 |
mode tcp + 适当的 timeout 配置 |
仅做四层负载,不解析 SCGI 头 |
|
Nginx 常用 scgi_*
指令表
指令 |
作用 |
默认 |
建议值 / 场景 |
scgi_buffers |
设置几个缓存区缓存上游响应 |
8 4k |
大文件下载改成 16 16k |
scgi_busy_buffers_size |
响应过大时一次性写磁盘的阈值 |
无 |
与上行 buffer 总和相同 |
scgi_read_timeout |
等后端响应最大时间 |
60s |
SSE/长轮询调到 1h |
scgi_next_upstream |
发生哪些错误自动重试 |
error timeout |
加入 invalid_header http_500 http_502 等 |
scgi_cache |
启动 Nginx 层缓存 |
off |
静态或幂等 GET 场景可提升 QPS |
3 多语言实现参考
3.1 Go(标准库版本,可平滑复用 net/http
Handler)
package main
import (
"bufio"
"io"
"log"
"net"
"net/http"
"strconv"
"strings"
)
func main() {
ln, err := net.Listen("unix", "/run/app.sock")
if err != nil { log.Fatal(err) }
for {
conn, _ := ln.Accept()
go serve(conn)
}
}
func serve(c net.Conn) {
defer c.Close()
br := bufio.NewReader(c)
for {
size, err := readSize(br)
if err != nil { return }
hdr := make([]byte, size)
if _, err = io.ReadFull(br, hdr); err != nil { return }
if b, _ := br.ReadByte(); b != ',' { return }
env := parse(hdr)
contentLen, _ := strconv.Atoi(env["CONTENT_LENGTH"])
body := io.LimitReader(br, int64(contentLen))
req, _ := http.NewRequest(env["REQUEST_METHOD"], env["REQUEST_URI"], body)
for k, v := range env {
if strings.HasPrefix(k, "HTTP_") {
req.Header.Set(cgiToHeader(k), v)
}
}
resp := newBufferedResponse()
http.DefaultServeMux.ServeHTTP(resp, req)
resp.WriteTo(c)
}
}
优势:直接挂任何 net/http
生态(Gin、Echo、Chi…)。
劣势:缺少连接池、超时控制需自行包装(可借助 context
)。
3.2 Python
pip install flup6
python -m flup.server.scgi --bind=/run/app.sock myapp:application
flup6
兼容 WSGI;部署 Gunicorn 时加 -k scgi
.
- 性能高于 mod_wsgi/FastCGI(少一层帧解析)。
3.3 Rust
use scgi::Server;
fn main() {
Server::bind("/run/app.sock").serve(|req| {
format!("Status: 200 OK\r\nContent-Type: text/plain\r\n\r\nHi {}", req.path)
}).unwrap();
}
实测 TPS:使用 Tokio + scgi-async
crate,Ryzen 7 5800X 单核可跑 ≈130 k req/s(8 KiB 响应)。
4 性能与容量规划
层面 |
建议 |
前端连接数 |
worker_connections × scgi_buffers 大小 ≈ 峰值并发 × 平均响应体积 / 0.75 |
后端进程模型 |
CPU 密集:每核 1 进程;IO 密集:每核 ≥2 进程或异步模型 |
TCP 优化 |
后端监听 SO_REUSEPORT ;内核调大 somaxconn=65535 、tcp_tw_reuse=1 |
Unix Socket VS TCP |
本机建议 UDS(平均省 10–15 μs RTT、无需检查端口防火墙) |
Bench 工具 |
wrk -t4 -c200 -d30s --script=bench.lua http://127.0.0.1/ ;或直接 ab 、hey |
5 监控、日志与诊断
监控项 |
采集方式 |
|
|
QPS / 延迟 |
Nginx $request_time 、$upstream_response_time ;Prometheus-Exporter |
|
|
后端饱和 |
`netstat -an |
grep ESTAB |
wc -l`;应用内暴露 Goroutine/Thread 数 |
5xx 比例 |
Nginx log_format 打标签,Grafana 走日志聚合 |
|
|
连接泄漏 |
长连接但无请求时,应定期 PING 或使用 scgi_connect_timeout |
|
|
抓包调试 |
socat -x -v TCP-LISTEN:9999,fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:/run/app.sock |
|
|
故障定位思路
- 502 → 检查后端监听 / 权限 / SELinux。
- 长轮询超时 → 调大
scgi_read_timeout
并确认后端 flusher.Flush()
。
- POST 大文件断流 →
client_max_body_size
+ 后端 CONTENT_LENGTH
正确解析。
6 安全 & 多租户
- 最小暴露面:Nginx ↔ 后端使用 UDS + chmod 660;TCP 时仅监听
127.0.0.1
/ ::1
。
- 资源隔离:Docker / systemd-slice;限制 fd/CPU/Memory。
- 请求体大小:双端限制:
client_max_body_size 10m;
+ 业务校验 CONTENT_LENGTH
。
- Header 注入:Nginx 默认会把非法字符剔除;后端仍需白名单校验。
- DoS 防护:
limit_req_zone $binary_remote_addr zone=rl:10m rate=20r/s;
。
- 日志脱敏:对
QUERY_STRING
/ Body 按 key 进行“星号掩码”或哈希。
7 容器化 & CI/CD
主题 |
方案 |
|
|
镜像层次 |
FROM golang:1.22-alpine AS build → go build → FROM alpine 仅拷贝二进制 |
|
|
健康检查 |
`HEALTHCHECK CMD wget -qO- --post-data=‘’ http://localhost/health |
|
exit 1` |
热更新 |
Kubernetes RollingUpdate ;或 systemd socket-activation 配合二进制重载 |
|
|
配置注入 |
Nginx include /etc/nginx/conf.d/*.conf; ;后端读取环境变量 |
|
|
多实例调度 |
使用 Sidecar 模式共享 /run/app.sock ;或前端跑 DaemonSet ,Pod 内本地调用 |
|
|
8 常见坑 & FAQ
症状 |
根因 |
处理 |
connect() failed (111: Connection refused) |
后端未监听 / 路径错 |
检查 scgi_pass unix:/run/app.sock 路径 & 权限 |
请求巨慢但 CPU 空闲 |
Nginx → 后端 socket backlog 满 / 上游慢 |
调 net.core.somaxconn 、worker_connections ;排查 DB |
上传文件损坏 |
二进制 body 被当作文本处理 |
io.Copy 不做编码转换;禁用 CRLF 变换 |
Header 丢失 |
漏配 scgi_param HTTP_... $http_... |
自定义 Header 需手动声明 |
403 Forbidden (static path) |
Nginx location 重叠 |
location /static/ { root /var/www; } 优先级高于 / |
9 延伸阅读与工具
- 官方协议:
scgi.org
(原草案存档)。
- ngx_http_scgi_module 文档:详列全部 30+ 指令。
- scgi-curl:命令行 SCGI 客户端,可直接
scgi-curl -d 'k=v' /run/app.sock /hello
.
- uWSGI docs “SCGI gateway”:展示 SCGI 与其他协议桥接方式。
- Paper:“Netstrings: A formal network encoding”——理论来源。
结语
- SCGI 的定位是 “足够轻 + 简单 + 长连接”;
- 在 本地 IPC / 同机多语言微服务 场景下,往往优于 FastCGI/HTTP;
- 若需要更丰富管理功能(进程池、平滑 reload、指标),可套 supervisor / systemd / uwsgi;
- 按本文路线完成 PoC → 接入监控 → 优化缓冲区 → 上线即可达到 10^5 RPS 级别。
如需 代码审阅、Nginx 高级调优、K8s YAML 示例或性能基准脚本,告诉我具体环境,我再细化。