1. 模块定位与工作流程
关键认识:SSI 过滤器属于 输出过滤链(header_filter + body_filter)。在 upstream 生成响应后、发送给客户端之前,SSI 对
Content-Type
符合ssi_types
的响应体进行 流式解析 → 指令执行 → 结果重写。
核心流程
-
头处理:
- 检查
Content-Type
是否匹配; - 判断
Content-Length
/Transfer-Encoding
,若有分块则在边解析边输出。
- 检查
-
body_filter 阶段:
- 按块读取上游数据;
- 识别 SSI 注释
<!--# … -->
; - 遇到
include virtual
时,创建 子请求(subrequest),并根据wait
参数决定阻塞或并行; - 解析完毕后把结果拼入输出链;
- 若启用了
ssi_last_modified on
且响应没被修改,则保留原Last-Modified
。
-
向下游输出(和 gzip、chunked/HTTP2 framing 等一起完成)。
等待 vs 并行
wait="no" (默认) | wait="yes" |
---|---|
所有 include 子请求并行 | 当前子请求完成后才继续解析后续指令 |
最大吞吐/最低 TTFB | 保证顺序、避免数据依赖冲突 |
会占用并发连接数 | 性能牺牲,可控制 |
2. 指令参考
指令 | 默认 | 说明与典型场景 | |
---|---|---|---|
`ssi on | off` | off | 开启 SSI。可按 if ($args ~ …) 条件化。 |
ssi_types mime … | text/html | 追加需要解析的 Content-Type ;* 为所有。多类型时要注意节点 CPU。 | |
`ssi_last_modified on | off` | off | 修改后报文通常不可靠,默认删除 Last-Modified ;静态 include 可开启。 |
`ssi_silent_errors on | off` | off | 打开后隐藏 [an error occurred…] ,实际错误仍写 error log。线上建议启用。 |
ssi_min_file_chunk size | 1k | 小于阈值的文件片段用常规 write(),大于则 sendfile();根据磁盘 I/O 调整。 | |
ssi_value_length n | 256 | SSI 参数最大字节数。变量或 URL 很长时需调大,否则截断导致 400/解析失败。 |
3. SSI 命令参考
通用语法
<!--# <command> [param1="value1"] [param2='value2'] ... -->
- 参数中可嵌入
$
变量;在运行时替换。 "
和'
均可;未引用的值会被当作变量名。
3.1 include
参数 | 必选 | 说明 | |
---|---|---|---|
file | 二选一 | 读取本地文件(受 root /alias 限制,不走 upstream) | |
virtual | 二选一 | 以子请求方式访问 URI,可被 proxy_pass /FastCGI 等处理 | |
stub | 可选 | 出错或空 body 时插入 <block> 占位 | |
`wait="yes | no"` | 可选 | 同步/异步 |
set="var" | 可选 | 把完整返回体写入变量(需 subrequest_output_buffer_size 设定上限) |
文件安全:
file
会进行根目录拼接并 去除..
;仍需谨慎配置root
防止泄露。
3.2 echo
参数 | 说明 |
---|---|
var | 变量名 |
encoding | none 、url 、entity (默认,HTML 实体编码) |
default | 变量不存在时输出 |
3.3 if / elif / else / endif
-
只能嵌套 1 层;
-
表达式语法(BNF):
expr := "$var" | "$var" ( "=" | "!=" ) ( text | /regexp/ ) text := 任意无空格字符串,可包含$var regexp:= POSIX 正则,可含 (?P<name>) 捕获
-
例子:
<!--# if expr="$http_user_agent = /Chrome/" --> <!--# include file="chrome.html" --> <!--# elif expr="$cookie_beta" --> ... <!--# else --> ... <!--# endif -->
3.4 block / endblock + stub
-
用于多处占位或 fallback:
<!--# block name="empty" --> <!--# endblock --> ... <!--# include virtual="/api/x" stub="empty" -->
3.5 set
<!--# set var="greet" value="Hello, $arg_name" -->
4. 内嵌变量与解析顺序
-
系统变量:
$date_local
、$date_gmt
-
HTTP 核心变量:
$remote_addr
、$args
、$http_*
等 -
子请求 set/echo 产生的变量
-
变量作用域:
- 同一响应体中,所有 SSI 指令共享【同一变量空间】;
- 子请求内部可覆盖父级变量,但仅在该子请求上下文生效。
5. 与其他过滤器的交互
过滤器 | 顺序(大→小) | 注意点 |
---|---|---|
gzip | 在 SSI 之后 | 否则 SSI 无法解析压缩体 |
sub_filter | 在 SSI 之前 | 先替换,后解析 include |
proxy_cache / fastcgi_cache | 缓存的是 SSI 处理后的最终页面 | 若要分块缓存请改用 slice / ESI |
headers_more | header_filter 里操作,与 SSI header 处理无冲突 |
6. 性能与资源占用
指标 | 影响因素 | 建议 |
---|---|---|
CPU | 解析正则、变量替换 | 避免复杂循环、嵌套 if |
内存 | 子请求 buffer、变量表 | subrequest_output_buffer_size 仅设实际最大片段 |
并发 | include virtual 并行连接 | 上游受压时配合 limit_conn 、proxy_cache_lock |
磁盘 | include file sendfile() 切片 | 调整 ssi_min_file_chunk 、禁用 sendfile 于低速盘 |
7. 安全注意事项
-
file 路径逃逸
- 坚守
root
边界;配合disable_symlinks if_not_owner;
。
- 坚守
-
Header 泄露
- 子请求可携带整套客户端头;敏感场景用
proxy_set_header
覆盖。
- 子请求可携带整套客户端头;敏感场景用
-
XSS
echo default=...
/include
输出未经转义;务必手动encoding="entity"
或后端 HTML Escape。
-
拒绝服务
- 无限递归 include 可能放大 CPU,设置
ssi_value_length
+proxy_read_timeout
。
- 无限递归 include 可能放大 CPU,设置
8. 调试与故障排查
现象 | 核心排查点 |
---|---|
页面原样输出 SSI 注释 | ① ssi on; 是否在最终匹配的 location② ssi_types 是否含该 MIME③ gzip 是否提前压缩 |
[an error occurred ...] | ① 文件 404 ② 子请求 5xx(看 error.log) |
子请求一直 Pending | ① 后端慢/堵塞 ② wait="yes" 阻塞③ proxy_read_timeout 太短被 kill |
变量为空 | ① 上下文作用域不同(父子请求) ② $var 拼写③ 长度超过 ssi_value_length 被截 |
调试利器
error_log /var/log/nginx/error.log notice;
ssi_silent_errors off; # 暂时关闭静默
log_format ssi '$time_local $status $uri $args "$ssi_last_command"';
$ssi_last_command
(1.25.3+)可打印最后执行的 SSI 指令,若升级后可用。
9. 生产最佳实践模板
9.1 CDN 边缘 + 动态碎片
map $cookie_ab $ab_ver { "A" "/frag/a.html"; default "/frag/b.html"; }
location /index.html {
ssi on;
ssi_types text/html;
ssi_silent_errors on;
# 缓存 1h
proxy_cache edge;
proxy_cache_valid 200 1h;
# 页面里写:
<!--# include file="$ab_ver" -->
}
9.2 自适应语言
set $lang $cookie_lang$arg_lang;
if ($lang = "") { set $lang "en"; }
location / {
ssi on;
ssi_types text/html;
# 在 HTML 中:
<!--# include virtual="/i18n/$lang/header.html" stub="empty" -->
}
9.3 界面灰度开关
<!--# if expr="$cookie_gray = 1" -->
<!--# include file="new_banner.html" -->
<!--# else -->
<!--# include file="old_banner.html" -->
<!--# endif -->
10. FAQ 速查
问题 | 快速答案 |
---|---|
SSI 能否递归 include? | 可以,但深度过大会耗资源;无循环检测。 |
能否在 proxy_cache 前执行 SSI? | 不行,SSI 位于输出链。若想分层缓存,请结合 slice 或 CDN 的 ESI。 |
set 写入的变量能跨请求? | 仅在同一主请求上下文;不会带到下一个 HTTP 请求。 |
如何限制 include 并发? | limit_conn_zone + limit_conn ;或全局 limit_req 。 |
如何在 JS 里动态执行 SSI? | 纯 SSI 在服务器完成,无法客户端动态触发;可用 ajax 走 JSON + DOM。 |
结语
- 轻量:与 CGI/PHP 相比,SSI 处理量小、并行强、可直接在 CDN 节点解析。
- 灵活:配合 NGINX 变量/正则,可做 A/B、i18n、灰度、SEO 静态化等。
- 谨慎:注意文件路径、变量注入、循环 include 风险,合理使用
ssi_silent_errors
与limit_*
。
掌握以上内容,你就拥有了 静态文件注入动态片段 的一把利器。在 CDN 加速、微前端、边缘渲染等场景,将大幅提升用户首屏速度、缓存命中率并简化后端渲染复杂度。祝你使用顺利,若还有细节想深入,随时再聊!