From d8ac807cb78d4f2181b14f820acbfe4997b4240e Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 7 Apr 2026 17:06:38 +0800 Subject: [PATCH] =?UTF-8?q?docs(nginx):=20=E6=9B=B4=E6=96=B0=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E4=B8=8E=20stream=20=E6=96=87=E6=A1=A3=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20njs/=E5=8F=AF=E8=A7=82=E6=B5=8B=E6=80=A7/A?= =?UTF-8?q?CME=20=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 04: 新增 random 负载均衡、upstream 响应时间变量详解 - 10: 新增访问控制、连接限制、地理/真实IP模块、高级日志配置 - 24: 新增 worker_aio_requests、EPOLLEXCLUSIVE 详解 - 30: njs JavaScript 模块完整指南 - 31: OpenTelemetry 可观测性集成指南 - 32: ACME 自动证书管理指南 Co-Authored-By: Claude --- docs/04-nginx-proxy-loadbalancing.md | 110 ++ docs/10-nginx-stream-tcp-udp.md | 372 ++++++- docs/24-nginx-core-events.md | 125 +++ docs/30-nginx-njs-guide.md | 1232 +++++++++++++++++++++ docs/31-nginx-observability.md | 1508 ++++++++++++++++++++++++++ docs/32-nginx-acme-ssl.md | 1236 +++++++++++++++++++++ docs/README.md | 9 + 7 files changed, 4591 insertions(+), 1 deletion(-) create mode 100644 docs/30-nginx-njs-guide.md create mode 100644 docs/31-nginx-observability.md create mode 100644 docs/32-nginx-acme-ssl.md diff --git a/docs/04-nginx-proxy-loadbalancing.md b/docs/04-nginx-proxy-loadbalancing.md index f011478..9bef752 100644 --- a/docs/04-nginx-proxy-loadbalancing.md +++ b/docs/04-nginx-proxy-loadbalancing.md @@ -190,6 +190,45 @@ upstream backend { } ``` +**随机负载均衡(1.15.1+)**: +```nginx +upstream backend { + random; # 纯随机选择 + server srv1.example.com; + server srv2.example.com; + server srv3.example.com; +} + +# Power of Two Choices 算法(更智能) +upstream backend { + random two; # 随机选两台,按权重择优 + server srv1.example.com; + server srv2.example.com; + server srv3.example.com; +} + +# 结合最少连接策略 +upstream backend { + random two least_conn; # 随机选两台,选连接数少的 + server srv1.example.com; + server srv2.example.com; +} +``` + +**random 算法参数说明**: + +| 参数 | 说明 | +|------|------| +| `two` | 随机选择两台服务器,再根据策略择优 | +| `least_conn` | 与 `two` 配合,选择连接数较少的服务器 | +| `least_time=header` | 与 `two` 配合,选择响应头时间最短的服务器(NGINX Plus)| +| `least_time=last_byte` | 与 `two` 配合,选择完整响应时间最短的服务器(NGINX Plus)| + +**适用场景**: +- 多个负载均衡器共享后端时避免锁竞争 +- 对一致性要求不高但需要低延迟的场景 +- 配合 `zone` 实现无锁负载均衡 + ### server 指令参数 | 参数 | 说明 | 默认值 | @@ -757,12 +796,83 @@ http { ## 16. 内置变量 +### 代理相关变量 + | 变量 | 说明 | |------|------| | `$proxy_host` | proxy_pass 中的服务器名称和端口 | | `$proxy_port` | proxy_pass 中的端口 | | `$proxy_add_x_forwarded_for` | X-Forwarded-For 头 + 客户端 IP | +### Upstream 响应时间变量(用于性能监控) + +| 变量 | 说明 | 单位 | +|------|------|------| +| `$upstream_addr` | 上游服务器地址(IP:端口)| - | +| `$upstream_connect_time` | 与上游建立连接的时间(含 SSL 握手)| 秒 | +| `$upstream_header_time` | 接收到上游响应头的时间 | 秒 | +| `$upstream_response_time` | 完整响应时间(从建立连接到接收完成)| 秒 | +| `$upstream_response_length` | 上游响应体长度 | 字节 | +| `$upstream_bytes_received` | 从上游接收的总字节数 | 字节 | +| `$upstream_bytes_sent` | 发送到上游的总字节数 | 字节 | +| `$upstream_status` | 上游返回的 HTTP 状态码 | - | +| `$upstream_cache_status` | 缓存命中状态(HIT/MISS/EXPIRED 等)| - | +| `$upstream_queue_time` | 请求在队列中等待的时间(NGINX Plus)| 秒 | + +**日志格式中使用响应时间变量**: + +```nginx +log_format detailed '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + 'rt=$request_time ' + 'uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" ' + 'urt="$upstream_response_time" ' + 'upstream=$upstream_addr ' + 'upstream_status=$upstream_status ' + 'upstream_bytes=$upstream_response_length'; + +access_log /var/log/nginx/access.log detailed; +``` + +**响应时间变量解读**: + +``` +请求时间线: +客户端 ──▶ NGINX ──▶ 连接上游 ──▶ 发送请求 ──▶ 接收响应头 ──▶ 接收响应体 ──▶ 客户端 + │ │ │ │ │ + │ │ │ │ │ + └───────────┴──────────────┴────────────────┴────────────────┘ + │ │ │ + $upstream_ $upstream_ $upstream_ + connect_time header_time response_time +``` + +- `$upstream_connect_time`:TCP 连接 + SSL 握手时间 +- `$upstream_header_time`:从开始到收到响应头 +- `$upstream_response_time`:完整请求处理时间 +- `$request_time`:从客户端发起请求到响应完成(包含所有上游) + +**基于响应时间的告警配置示例**: + +```nginx +# 慢请求日志 +map $upstream_response_time $slow_log { + default 0; + "~^[2-9]\." 1; # 2秒以上 + "~^[0-9]{2,}" 1; # 10秒以上 +} + +server { + location /api/ { + # 记录慢请求 + access_log /var/log/nginx/slow.log detailed if=$slow_log; + proxy_pass http://backend; + } +} +``` + --- ## 17. 综合配置示例 diff --git a/docs/10-nginx-stream-tcp-udp.md b/docs/10-nginx-stream-tcp-udp.md index dcced8e..1ed31ea 100644 --- a/docs/10-nginx-stream-tcp-udp.md +++ b/docs/10-nginx-stream-tcp-udp.md @@ -689,4 +689,374 @@ upstream backend { health_check interval=5s passes=2 fails=3; health_check_timeout 5s; } -``` \ No newline at end of file +``` + +--- + +## 15. 访问控制模块 + +### ngx_stream_access_module + +基于 IP 地址的访问控制,允许或拒绝特定客户端连接。 + +**指令**: + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `allow` | `allow address \| CIDR \| unix: \| all;` | — | stream, server | +| `deny` | `deny address \| CIDR \| unix: \| all;` | — | stream, server | + +**配置示例**: + +```nginx +stream { + # 数据库访问控制 + server { + listen 3306; + + # 允许内网访问 + allow 10.0.0.0/8; + allow 192.168.0.0/16; + allow 172.16.0.0/12; + + # 拒绝其他所有 + deny all; + + proxy_pass mysql_backend; + } + + # Redis 访问控制 + server { + listen 6379; + + # 仅允许特定 IP + allow 192.168.1.100; + allow 192.168.1.101; + deny all; + + proxy_pass redis_backend; + } + + # 管理端口(仅本地) + server { + listen 9000; + + allow 127.0.0.1; + deny all; + + proxy_pass admin_backend; + } +} +``` + +**规则匹配顺序**: +- 按配置顺序依次检查 +- 首个匹配的规则决定结果 +- 未匹配任何规则时默认允许 + +--- + +## 16. 连接限制模块 + +### ngx_stream_limit_conn_module + +限制并发连接数,防止资源耗尽。 + +**指令**: + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `limit_conn_zone` | `limit_conn_zone key zone=name:size;` | — | stream | +| `limit_conn` | `limit_conn zone number;` | — | stream, server | +| `limit_conn_log_level` | `limit_conn_log_level info \| notice \| warn \| error;` | error | stream, server | + +**配置示例**: + +```nginx +stream { + # 按客户端 IP 限制连接数 + limit_conn_zone $binary_remote_addr zone=addr:10m; + + # 按上游服务器限制连接数 + limit_conn_zone $server_addr zone=server:10m; + + # MySQL 代理 - 每 IP 最多 10 个连接 + server { + listen 3306; + limit_conn addr 10; + proxy_pass mysql_backend; + } + + # Redis 代理 - 每 IP 最多 5 个连接 + server { + listen 6379; + limit_conn addr 5; + limit_conn_log_level warn; + proxy_pass redis_backend; + } + + # 全局连接限制 + server { + listen 8080; + limit_conn addr 50; # 每 IP 最多 50 + limit_conn server 1000; # 服务总连接上限 + proxy_pass backend; + } +} +``` + +**内存计算**: +- 1MB 共享内存可存储约 16,000 个 32 字节 key($binary_remote_addr) +- 或约 8,000 个 IPv6 地址(16 字节) + +--- + +## 17. 地理位置模块 + +### ngx_stream_geo_module + +根据客户端 IP 地址创建变量值,用于地理路由或访问控制。 + +**指令**: + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `geo` | `geo [$address] $variable { ... }` | — | stream | + +**配置示例**: + +```nginx +stream { + # 基础地理映射 + geo $remote_addr $region { + default other; + 10.0.0.0/8 internal; + 192.168.0.0/16 internal; + 172.16.0.0/12 internal; + + # 中国大陆 IP 段(示例) + 1.0.1.0/24 china; + 1.0.2.0/23 china; + # ... 更多 IP 段 + } + + # 使用变量进行路由 + map $region $backend_pool { + internal internal_backend; + china china_backend; + other global_backend; + } + + upstream internal_backend { + server 192.168.1.1:3306; + } + + upstream china_backend { + server 10.0.1.1:3306; + } + + upstream global_backend { + server 10.0.2.1:3306; + } + + server { + listen 3306; + proxy_pass $backend_pool; + } +} +``` + +**高级用法**: + +```nginx +stream { + # 使用变量作为地址源 + geo $realip_remote_addr $region { + default other; + # ... 配置 + } + + # 带 CIDR 包含 + geo $country { + default XX; + include /etc/nginx/geo/countries.conf; + } + + # countries.conf 内容示例: + # 1.0.1.0/24 CN; + # 1.0.2.0/23 CN; + # 1.1.1.0/24 AU; +} + + # 使用 GeoIP 数据库(需 ngx_stream_geoip_module) + geoip_country /usr/share/GeoIP/GeoIP.dat; + + map $geoip_country_code $backend { + default global_backend; + CN china_backend; + US us_backend; + EU eu_backend; + } +} +``` + +--- + +## 18. 真实 IP 模块 + +### ngx_stream_realip_module + +处理 PROXY 协议头,获取客户端真实 IP 地址。 + +**指令**: + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `set_real_ip_from` | `set_real_ip_from address \| CIDR;` | — | stream, server | +| `real_ip_header` | `real_ip_header field;` | proxy_protocol | stream, server | + +**配置示例**: + +```nginx +stream { + server { + listen 3306 proxy_protocol; # 接收 PROXY 协议 + + # 信任的代理服务器地址 + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 192.168.0.0/16; + set_real_ip_from 172.16.0.0/12; + + proxy_pass mysql_backend; + } +} +``` + +**可用变量**: + +| 变量 | 说明 | +|------|------| +| `$realip_remote_addr` | 原始客户端地址(PROXY 协议中的地址)| +| `$realip_remote_port` | 原始客户端端口 | +| `$proxy_protocol_addr` | PROXY 协议中的客户端地址 | +| `$proxy_protocol_port` | PROXY 协议中的客户端端口 | +| `$proxy_protocol_server_addr` | PROXY 协议中的目标服务器地址 | +| `$proxy_protocol_server_port` | PROXY 协议中的目标服务器端口 | + +**典型场景**: + +```nginx +stream { + # 场景:负载均衡器 → NGINX → 后端 + server { + listen 3306 proxy_protocol; + + # 负载均衡器的 IP + set_real_ip_from 10.0.0.1; + set_real_ip_from 10.0.0.2; + + # 使用真实 IP 进行限流 + limit_conn_zone $realip_remote_addr zone=conn_limit:10m; + limit_conn conn_limit 10; + + # 日志记录真实 IP + log_format main '$realip_remote_addr [$time_local] ' + '$protocol $status $bytes_sent $bytes_received'; + access_log /var/log/nginx/stream.log main; + + proxy_pass mysql_backend; + } +} +``` + +--- + +## 19. 高级日志配置 + +### 日志格式详解 + +```nginx +stream { + # JSON 格式日志 + log_format json_combined escape=json + '{' + '"time_local":"$time_local",' + '"remote_addr":"$remote_addr",' + '"server_addr":"$server_addr",' + '"server_port":"$server_port",' + '"protocol":"$protocol",' + '"status":"$status",' + '"bytes_sent":"$bytes_sent",' + '"bytes_received":"$bytes_received",' + '"session_time":"$session_time",' + '"upstream_addr":"$upstream_addr",' + '"upstream_bytes_sent":"$upstream_bytes_sent",' + '"upstream_bytes_received":"$upstream_bytes_received",' + '"upstream_connect_time":"$upstream_connect_time"' + '}'; + + # 详细格式日志 + log_format detailed '$remote_addr - [$time_local] ' + '$protocol/$status ' + 'sent:$bytes_sent recv:$bytes_received ' + 'time:$session_time ' + 'upstream:$upstream_addr ' + 'upstream_time:$upstream_connect_time'; + + # 条件日志(仅记录错误) + map $status $loggable { + ~^[23] 0; + default 1; + } + + server { + listen 3306; + access_log /var/log/nginx/stream.json json_combined; + access_log /var/log/nginx/stream_errors.log detailed if=$loggable; + proxy_pass backend; + } +} +``` + +### 日志缓冲与压缩 + +```nginx +stream { + server { + listen 3306; + + # 缓冲写入(提升性能) + access_log /var/log/nginx/stream.log main buffer=32k flush=5s; + + # gzip 压缩日志 + access_log /var/log/nginx/stream.log.gz main gzip buffer=32k; + + proxy_pass backend; + } +} +``` + +### open_log_file_cache + +缓存日志文件描述符,减少文件打开操作: + +```nginx +stream { + open_log_file_cache max=1000 inactive=20s valid=1m min_uses=2; + + server { + listen 3306; + access_log /var/log/nginx/stream.log main; + proxy_pass backend; + } +} +``` + +**参数说明**: + +| 参数 | 说明 | +|------|------| +| `max` | 缓存的最大文件描述符数 | +| `inactive` | 非活动文件保留时间 | +| `valid` | 检查文件是否有效的时间间隔 | +| `min_uses` | 最小使用次数才缓存 | \ No newline at end of file diff --git a/docs/24-nginx-core-events.md b/docs/24-nginx-core-events.md index 351d001..250aade 100644 --- a/docs/24-nginx-core-events.md +++ b/docs/24-nginx-core-events.md @@ -1069,9 +1069,134 @@ events { # 互斥锁(现代 Linux 不需要) accept_mutex off; + + # 异步 I/O 请求数(epoll + aio 场景) + worker_aio_requests 64; # 默认 32 } ``` +### 6.3 新增指令详解 + +#### worker_aio_requests + +设置使用 `epoll` 和 `aio` 时,单个 worker 进程的最大未完成异步 I/O 操作数。 + +**语法**:`worker_aio_requests number;` + +**默认值**:`worker_aio_requests 32;` + +**上下文**:events + +**版本**:1.1.4+ + +```nginx +events { + worker_connections 10240; + use epoll; + multi_accept on; + worker_aio_requests 64; # 提高异步 I/O 并发 +} + +http { + # 配合 aio 使用 + location /videos/ { + aio threads; + sendfile on; + } +} +``` + +**适用场景**: +- 高并发文件传输(视频、图片服务) +- 使用 `aio threads` 异步 I/O +- 大文件下载服务 + +#### ssl_object_cache_inheritable + +控制 SSL 对象(证书、密钥等)在配置重载时是否继承。 + +**语法**:`ssl_object_cache_inheritable on | off;` + +**默认值**:`ssl_object_cache_inheritable on;` + +**上下文**:main + +**版本**:1.27.4+ + +```nginx +# 默认情况下,SSL 对象在 reload 时会继承 +ssl_object_cache_inheritable on; + +http { + server { + listen 443 ssl; + # 静态证书路径 - 支持继承 + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + } +} +``` + +**说明**: +- 开启时,未修改的 SSL 证书/密钥在 reload 时复用 +- 变量形式加载的 SSL 对象无法继承 +- 关闭时,每次 reload 都重新加载所有 SSL 对象 + +### 6.4 EPOLLEXCLUSIVE 标志详解 + +**版本支持**:Linux 2.6.39+,nginx 1.11.3+ + +`EPOLLEXCLUSIVE` 是 Linux 内核提供的标志,用于解决多 worker 进程的惊群问题。 + +**传统惊群问题**: +``` + 新连接到达 + │ + ┌──────────┬─────────┴─────────┬──────────┐ + ▼ ▼ ▼ ▼ + Worker 1 Worker 2 Worker 3 Worker 4 + (唤醒) (唤醒) (唤醒) (唤醒) + │ │ │ │ + └──────────┴─────────┬─────────┴──────────┘ + ▼ + 只有一个 accept 成功 + 其他唤醒浪费 CPU +``` + +**使用 EPOLLEXCLUSIVE 后**: +``` + 新连接到达 + │ + ┌──────────┬─────────┴─────────┬──────────┐ + ▼ ▼ ▼ ▼ + Worker 1 Worker 2 Worker 3 Worker 4 + (唤醒) (休眠) (休眠) (休眠) + │ + ▼ + accept 成功,处理连接 +``` + +**nginx 配置建议**: + +```nginx +# Linux 2.6.39+ 不需要 accept_mutex +events { + use epoll; + accept_mutex off; # 关闭互斥锁,使用 EPOLLEXCLUSIVE + multi_accept on; # 可以开启,EPOLLEXCLUSIVE 已解决惊群 +} +``` + +**优势**: +- 减少不必要的进程唤醒 +- 降低 CPU 使用率 +- 提高高并发场景性能 + +**注意事项**: +- 仅对 `epoll` 有效 +- 需要内核 2.6.39+ +- nginx 1.11.3+ 自动启用 + ### 6.3 内核参数优化 ```bash diff --git a/docs/30-nginx-njs-guide.md b/docs/30-nginx-njs-guide.md new file mode 100644 index 0000000..d251fd5 --- /dev/null +++ b/docs/30-nginx-njs-guide.md @@ -0,0 +1,1232 @@ +# Nginx njs JavaScript 模块指南 + +## 目录 + +1. [njs 概述与特性](#1-njs-概述与特性) +2. [安装与启用](#2-安装与启用) +3. [核心指令](#3-核心指令) +4. [njs 语法基础](#4-njs-语法基础) +5. [常见应用场景与配置示例](#5-常见应用场景与配置示例) +6. [njs vs Lua 对比](#6-njs-vs-lua-对比) +7. [性能优化建议](#7-性能优化建议) + +--- + +## 1. njs 概述与特性 + +### 什么是 njs + +njs(nginx JavaScript)是 nginx 的一个模块,它使用 JavaScript 语言扩展 nginx 的服务器功能。njs 提供了一个嵌入式的 JavaScript 引擎,以及一个独立的命令行工具用于开发和调试。 + +### 主要特性 + +| 特性 | 说明 | +|------|------| +| **ECMAScript 5.1+ 兼容** | 遵循 ES5.1 严格模式,支持部分 ES6+ 特性 | +| **双引擎支持** | 支持原生 njs 引擎和 QuickJS 引擎 (v0.8.6+) | +| **异步支持** | 完整的 Promise 和 async/await 支持 (v0.7.0+) | +| **Fetch API** | 内置 HTTP 客户端功能 (v0.7.0+) | +| **Crypto API** | WebCrypto 和 Node.js 风格的加密支持 | +| **共享字典** | 跨 worker 进程的内存键值存储 (v0.8.0+) | +| **HTTP 和 Stream** | 同时支持 HTTP 和 TCP/UDP Stream 模块 | + +### 适用场景 + +- **访问控制**: 复杂的安全检查逻辑 +- **请求/响应处理**: 动态修改头部、响应体 +- **内容生成**: 灵活的内容处理程序 +- **API 网关**: JWT 验证、动态路由 +- **数据处理**: 过滤和转换响应体 + +--- + +## 2. 安装与启用 + +### 从源码编译安装 + +#### 编译参数 + +```bash +# 下载 nginx 和 njs 源码 +cd /usr/local/src +wget http://nginx.org/download/nginx-1.25.3.tar.gz +tar -xzf nginx-1.25.3.tar.gz + +git clone https://github.com/nginx/njs.git +cd nginx-1.25.3 + +# 编译安装(动态模块方式推荐) +./configure \ + --prefix=/etc/nginx \ + --sbin-path=/usr/sbin/nginx \ + --modules-path=/usr/lib/nginx/modules \ + --with-compat \ + --add-dynamic-module=../njs/nginx + +make && make install +``` + +#### 启用模块 + +在 nginx.conf 顶部添加: + +```nginx +# 动态加载 njs 模块 +load_module modules/ngx_http_js_module.so; + +# 如果使用 Stream 模块 +load_module modules/ngx_stream_js_module.so; + +user nginx; +worker_processes auto; +... +``` + +### 使用包管理器安装 + +#### CentOS/RHEL + +```bash +# 添加 nginx 官方仓库 +sudo tee /etc/yum.repos.d/nginx.repo << 'EOF' +[nginx-stable] +name=nginx stable repo +baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ +gpgcheck=1 +enabled=1 +gpgkey=https://nginx.org/keys/nginx_signing.key +EOF + +# 安装 nginx 和 njs 模块 +sudo yum install nginx nginx-module-njs +``` + +#### Ubuntu/Debian + +```bash +# 添加 nginx 官方仓库 +sudo apt-get update +sudo apt-get install curl gnupg2 ca-certificates lsb-release + +curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo apt-key add - +sudo tee /etc/apt/sources.list.d/nginx.list << 'EOF' +deb http://nginx.org/packages/ubuntu `lsb_release -cs` nginx +deb-src http://nginx.org/packages/ubuntu `lsb_release -cs` nginx +EOF + +# 安装 nginx 和 njs 模块 +sudo apt-get update +sudo apt-get install nginx nginx-module-njs +``` + +#### Alpine Linux + +```bash +apk add nginx nginx-mod-http-njs +``` + +### 验证安装 + +```bash +# 检查模块是否加载 +nginx -V 2>&1 | grep njs + +# 使用 njs CLI 工具验证 +njs -v + +# 测试 JavaScript 语法 +njs -c "console.log('Hello from njs');" +``` + +--- + +## 3. 核心指令 + +### 指令汇总表 + +| 指令 | 语法 | 上下文 | 说明 | +|------|------|--------|------| +| `js_import` | `js_import module.js \| export_name from module.js;` | http, server, location | 导入 njs 模块 | +| `js_set` | `js_set $variable module.function [nocache];` | http, server, location | 设置变量处理器 | +| `js_content` | `js_content module.function;` | location, if, limit_except | 设置内容处理器 | +| `js_body_filter` | `js_body_filter module.function [buffer_type=string \| buffer];` | location, if, limit_except | 响应体过滤器 | +| `js_header_filter` | `js_header_filter module.function;` | location, if, limit_except | 响应头过滤器 | +| `js_var` | `js_var $variable [value];` | http, server, location | 声明可写变量 | +| `js_engine` | `js_engine njs \| qjs;` | http, server, location | 设置 JavaScript 引擎 | +| `js_path` | `js_path path;` | http, server, location | 设置模块搜索路径 | +| `js_shared_dict_zone` | `js_shared_dict_zone zone=name:size [timeout=time] [type=string\|number] [evict];` | http | 共享内存字典 | +| `js_periodic` | `js_periodic module.function [interval=time] [jitter=number] [worker_affinity=mask];` | location | 周期性任务 | +| `js_preload_object` | `js_preload_object name.json \| name from file.json;` | http, server, location | 预加载配置对象 | + +### 指令详细说明 + +#### js_import + +导入 JavaScript 模块文件。 + +```nginx +# 基本用法 +js_import /etc/nginx/njs/http.js; + +# 使用别名 +js_import main from /etc/nginx/njs/http.js; + +# 导入特定导出 +js_import {hello, api} from /etc/nginx/njs/utils.js; +``` + +#### js_set + +使用 JavaScript 函数设置 nginx 变量。 + +```nginx +# 基本用法 +js_set $foo http.foo; + +# 不缓存模式(每次引用都执行) +js_set $dynamic http.dynamic_handler nocache; +``` + +#### js_content + +将 JavaScript 函数设置为 location 的内容处理器。 + +```nginx +location /api { + js_content api.handleRequest; +} +``` + +#### js_body_filter + +设置响应体过滤器函数,用于修改响应内容。 + +```nginx +location / { + proxy_pass http://backend; + js_body_filter http.modifyBody; +} +``` + +#### js_engine + +选择 JavaScript 引擎(njs 或 QuickJS)。 + +```nginx +http { + # 使用 QuickJS 引擎(ES2023 支持) + js_engine qjs; + + # 或使用原生 njs 引擎 + js_engine njs; +} +``` + +--- + +## 4. njs 语法基础 + +### ECMAScript 兼容性 + +njs 遵循 **ECMAScript 5.1 (严格模式)**,并支持部分 ES6+ 扩展。 + +### 支持的 ES6+ 特性 + +| 特性 | 版本 | 示例 | +|------|------|------| +| `let` / `const` | 0.6.0+ | `const x = 10; let y = 20;` | +| 箭头函数 | 0.3.1+ | `(a, b) => a + b` | +| 模板字符串 | 0.3.2+ | `` `Hello ${name}` `` | +| async/await | 0.7.0+ | `async function() { await ... }` | +| Promise | 0.3.8+ | `Promise.all()`, `.then()` | +| ES6 模块 | 0.3.0+ | `export default {...}` | +| 可选链操作符 | 0.9.6+ | `obj?.property` | +| 逻辑赋值 | 0.9.6+ | `a ||= b`, `a &&= b` | + +### 不支持的特性 + +| 特性 | 说明 | +|------|------| +| 类(Classes) | 不支持 `class` 关键字 | +| 解构赋值 | `const {a, b} = obj` 不支持 | +| 展开运算符 | `...` 展开语法不支持 | +| 默认参数 | `function(a=1)` 不支持 | +| 生成器函数 | `function*`, `yield` 不支持 | +| Proxy/Reflect | 不支持 | +| Map/Set | 不支持原生 Map/Set | + +### 请求对象(r)API + +HTTP 处理函数接收请求对象 `r`,包含以下属性和方法: + +#### 属性 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `r.method` | string | HTTP 方法 (GET, POST 等) | +| `r.uri` | string | 请求 URI | +| `r.httpVersion` | string | HTTP 版本 | +| `r.remoteAddress` | string | 客户端 IP 地址 | +| `r.headersIn` | object | 请求头(只读) | +| `r.headersOut` | object | 响应头(可写) | +| `r.args` | object | URL 查询参数 | +| `r.variables` | object | nginx 变量 | +| `r.requestText` | string | 请求体文本 | +| `r.requestBuffer` | Buffer | 请求体 Buffer | +| `r.status` | number | 响应状态码 | + +#### 方法 + +| 方法 | 说明 | +|------|------| +| `r.return(status[, body])` | 返回响应 | +| `r.send(data)` | 发送响应体片段 | +| `r.sendHeader()` | 发送响应头 | +| `r.finish()` | 完成响应 | +| `r.log(msg)` | 记录信息日志 | +| `r.error(msg)` | 记录错误日志 | +| `r.subrequest(uri[, opts[, cb]])` | 发起子请求 | +| `r.internalRedirect(uri)` | 内部重定向 | + +### ngx 全局对象 + +| 属性/方法 | 说明 | +|-----------|------| +| `ngx.fetch(url[, opts])` | Fetch API 请求 | +| `ngx.log(level, msg)` | 写入错误日志 | +| `ngx.version` | nginx 版本 | +| `ngx.worker_id` | Worker 进程 ID | +| `ngx.shared.` | 共享字典访问 | + +### 内置模块 + +```javascript +// 文件系统模块 (v0.8.9+) +import fs from 'fs'; +const data = fs.readFileSync('/path/to/file'); + +// 加密模块 (v0.7.0+) +import crypto from 'crypto'; +const hash = crypto.createHash('sha256'); + +// Buffer 模块 +import { Buffer } from 'buffer'; +const buf = Buffer.from('hello'); + +// 查询字符串 +import qs from 'querystring'; +const obj = qs.parse('a=1&b=2'); +``` + +--- + +## 5. 常见应用场景与配置示例 + +### 示例 1: 动态响应生成 + +创建一个简单的 "Hello World" 端点。 + +**JavaScript 文件 (`/etc/nginx/njs/hello.js`):** + +```javascript +function hello(r) { + r.return(200, "Hello world!\n"); +} + +function personalizedHello(r) { + const name = r.args.name || "Guest"; + r.return(200, `Hello, ${name}!\n`); +} + +function jsonResponse(r) { + const data = { + method: r.method, + uri: r.uri, + headers: r.headersIn, + remoteAddress: r.remoteAddress + }; + r.headersOut['Content-Type'] = 'application/json'; + r.return(200, JSON.stringify(data, null, 2)); +} + +export default { hello, personalizedHello, jsonResponse }; +``` + +**nginx 配置:** + +```nginx +load_module modules/ngx_http_js_module.so; + +events {} + +http { + js_import /etc/nginx/njs/hello.js; + + server { + listen 80; + server_name example.com; + + # 简单的 Hello World + location /hello { + js_content hello.hello; + } + + # 个性化问候 + location /greet { + js_content hello.personalizedHello; + } + + # JSON 响应 + location /info { + js_content hello.jsonResponse; + } + } +} +``` + +### 示例 2: 请求头处理 + +修改请求头和响应头。 + +**JavaScript 文件 (`/etc/nginx/njs/headers.js`):** + +```javascript +// 添加自定义响应头 +function addCustomHeaders(r) { + r.headersOut['X-Powered-By'] = 'njs'; + r.headersOut['X-Request-ID'] = r.variables.request_id; + r.headersOut['X-Processed-At'] = new Date().toISOString(); + + // 继续处理到上游 + r.internalRedirect('@proxy'); +} + +// 响应头过滤器 +function modifyResponseHeaders(r) { + // 移除敏感头 + delete r.headersOut['Server']; + delete r.headersOut['X-Powered-By']; + + // 添加安全头 + r.headersOut['X-Frame-Options'] = 'SAMEORIGIN'; + r.headersOut['X-Content-Type-Options'] = 'nosniff'; + r.headersOut['Referrer-Policy'] = 'strict-origin-when-cross-origin'; +} + +// 基于请求头的路由 +function routeByHeader(r) { + const apiVersion = r.headersIn['X-API-Version']; + + if (apiVersion === 'v2') { + r.internalRedirect('@api_v2'); + } else if (apiVersion === 'v1') { + r.internalRedirect('@api_v1'); + } else { + r.return(400, JSON.stringify({ error: 'Invalid API version' })); + } +} + +// 设置变量用于日志 +function getClientInfo(r) { + const userAgent = r.headersIn['User-Agent'] || 'unknown'; + const device = userAgent.match(/Mobile|Android|iPhone/i) ? 'mobile' : 'desktop'; + return device; +} + +export default { + addCustomHeaders, + modifyResponseHeaders, + routeByHeader, + getClientInfo +}; +``` + +**nginx 配置:** + +```nginx +http { + js_import /etc/nginx/njs/headers.js; + + # 使用 js_set 设置变量 + js_set $device_type headers.getClientInfo; + + log_format custom '$remote_addr - $device_type - "$request" ' + '$status $body_bytes_sent'; + + server { + listen 80; + + # 添加自定义头并代理 + location /api { + js_content headers.addCustomHeaders; + } + + location @proxy { + proxy_pass http://backend; + js_header_filter headers.modifyResponseHeaders; + } + + # 基于 API 版本路由 + location / { + js_content headers.routeByHeader; + } + + location @api_v1 { + proxy_pass http://api-v1-backend; + } + + location @api_v2 { + proxy_pass http://api-v2-backend; + } + } +} +``` + +### 示例 3: JWT 验证(简化版) + +实现简单的 JWT token 验证。 + +**JavaScript 文件 (`/etc/nginx/njs/jwt.js`):** + +```javascript +import crypto from 'crypto'; + +// Base64URL 解码 +function base64UrlDecode(str) { + // 添加标准 Base64 填充 + const padding = '='.repeat((4 - str.length % 4) % 4); + const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding; + return Buffer.from(base64, 'base64').toString('utf8'); +} + +// 简单的 JWT 验证(仅验证签名格式,生产环境请使用完整实现) +function verifyJwt(r) { + const authHeader = r.headersIn['Authorization']; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + r.return(401, JSON.stringify({ error: 'Missing or invalid Authorization header' })); + return; + } + + const token = authHeader.substring(7); + const parts = token.split('.'); + + if (parts.length !== 3) { + r.return(401, JSON.stringify({ error: 'Invalid JWT format' })); + return; + } + + try { + const payload = JSON.parse(base64UrlDecode(parts[1])); + + // 检查过期时间 + if (payload.exp && payload.exp < Date.now() / 1000) { + r.return(401, JSON.stringify({ error: 'Token expired' })); + return; + } + + // 在变量中存储用户信息 + r.variables.jwt_sub = payload.sub || ''; + r.variables.jwt_role = payload.role || 'user'; + + // 继续处理 + r.internalRedirect('@protected'); + + } catch (e) { + r.return(401, JSON.stringify({ error: 'Invalid token payload' })); + } +} + +// 检查权限 +function checkRole(r, requiredRole) { + const userRole = r.variables.jwt_role || ''; + return userRole === requiredRole; +} + +export default { verifyJwt, checkRole }; +``` + +**nginx 配置:** + +```nginx +http { + js_import /etc/nginx/njs/jwt.js; + + # 声明变量 + js_var $jwt_sub; + js_var $jwt_role; + + server { + listen 80; + + # 公开端点 + location /public { + proxy_pass http://backend; + } + + # 需要 JWT 验证的端点 + location /protected { + js_content jwt.verifyJwt; + } + + location @protected { + proxy_pass http://backend; + proxy_set_header X-User-ID $jwt_sub; + proxy_set_header X-User-Role $jwt_role; + } + + # 管理员端点 + location /admin { + js_content jwt.verifyJwt; + } + + location @admin { + # 检查角色 + if ($jwt_role != 'admin') { + return 403 "Forbidden"; + } + proxy_pass http://admin-backend; + } + } +} +``` + +### 示例 4: 动态路由 + +根据请求参数动态选择上游。 + +**JavaScript 文件 (`/etc/nginx/njs/router.js`):** + +```javascript +// 基于地理位置的路由 +function routeByGeo(r) { + const country = r.variables.geoip_country_code || 'US'; + + const regionMap = { + 'CN': '@asia_backend', + 'JP': '@asia_backend', + 'KR': '@asia_backend', + 'DE': '@eu_backend', + 'FR': '@eu_backend', + 'UK': '@eu_backend', + 'US': '@us_backend', + 'CA': '@us_backend' + }; + + const target = regionMap[country] || '@default_backend'; + r.internalRedirect(target); +} + +// 基于请求体的路由(用于 webhooks) +function routeByPayload(r) { + try { + const body = JSON.parse(r.requestText || '{}'); + const eventType = body.event || 'unknown'; + + const routeMap = { + 'payment.success': '@payment_success', + 'payment.failed': '@payment_failed', + 'user.created': '@user_created', + 'user.deleted': '@user_deleted' + }; + + const target = routeMap[eventType] || '@default_webhook'; + r.internalRedirect(target); + + } catch (e) { + r.return(400, JSON.stringify({ error: 'Invalid JSON' })); + } +} + +// 基于负载的路由(选择最空闲的上游) +function routeByLoad(r) { + const upstreams = ['backend1', 'backend2', 'backend3']; + let selected = upstreams[0]; + let minConnections = Number.MAX_SAFE_INTEGER; + + for (const upstream of upstreams) { + // 使用共享字典存储连接数 + const connections = ngx.shared.load_stats.get(upstream) || 0; + if (connections < minConnections) { + minConnections = connections; + selected = upstream; + } + } + + // 增加计数 + ngx.shared.load_stats.incr(selected, 1, 0, 60); + + r.variables.target_upstream = selected; + r.internalRedirect('@dynamic_proxy'); +} + +// A/B 测试路由 +function routeABTest(r) { + const cookie = r.headersIn['Cookie'] || ''; + const variantMatch = cookie.match(/ab_variant=(\w+)/); + let variant = variantMatch ? variantMatch[1] : null; + + if (!variant) { + // 50/50 分流 + variant = Math.random() < 0.5 ? 'a' : 'b'; + // 设置 cookie + r.headersOut['Set-Cookie'] = `ab_variant=${variant}; Path=/; Max-Age=86400`; + } + + if (variant === 'a') { + r.internalRedirect('@variant_a'); + } else { + r.internalRedirect('@variant_b'); + } +} + +export default { + routeByGeo, + routeByPayload, + routeByLoad, + routeABTest +}; +``` + +**nginx 配置:** + +```nginx +http { + js_import /etc/nginx/njs/router.js; + + # 配置共享字典 + js_shared_dict_zone zone=load_stats:1M type=number; + + # GeoIP 模块(可选) + geoip_country /usr/share/GeoIP/GeoIP.dat; + + upstream backend1 { + server 10.0.1.10:8080; + } + + upstream backend2 { + server 10.0.1.11:8080; + } + + upstream backend3 { + server 10.0.1.12:8080; + } + + server { + listen 80; + + # 地理位置路由 + location / { + js_content router.routeByGeo; + } + + # Webhook 路由 + location /webhooks { + js_content router.routeByPayload; + } + + # A/B 测试 + location /experiment { + js_content router.routeABTest; + } + + # 后端定义 + location @asia_backend { + proxy_pass http://asia-cluster; + } + + location @eu_backend { + proxy_pass http://eu-cluster; + } + + location @us_backend { + proxy_pass http://us-cluster; + } + + location @default_backend { + proxy_pass http://default-cluster; + } + } +} +``` + +### 示例 5: 响应体修改 + +使用 body filter 修改响应内容。 + +**JavaScript 文件 (`/etc/nginx/njs/body_filter.js`):** + +```javascript +// 简单的文本替换过滤器 +function replaceText(r, data, flags) { + if (data.length > 0) { + // 替换敏感信息 + let modified = data.toString().replace( + /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, + '****-****-****-****' + ); + + // 替换邮箱 + modified = modified.replace( + /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, + '***@***.***' + ); + + r.sendBuffer(modified, flags); + } else { + r.sendBuffer(data, flags); + } +} + +// 添加内容到响应体 +function appendContent(r, data, flags) { + if (flags.last) { + // 在最后添加内容 + const append = '\n'; + r.sendBuffer(data + append, flags); + } else { + r.sendBuffer(data, flags); + } +} + +// JSON 数据修改 +function modifyJson(r, data, flags) { + if (data.length > 0) { + try { + const obj = JSON.parse(data.toString()); + + // 添加服务器信息 + obj._meta = { + processed_by: 'nginx-njs', + timestamp: Date.now() + }; + + // 移除敏感字段 + delete obj.password; + delete obj.secret_key; + delete obj.internal_notes; + + r.sendBuffer(JSON.stringify(obj), flags); + } catch (e) { + // JSON 解析失败,原样返回 + r.sendBuffer(data, flags); + } + } else { + r.sendBuffer(data, flags); + } +} + +// HTML 注入(添加分析脚本) +function injectAnalytics(r, data, flags) { + if (data.length > 0) { + let html = data.toString(); + + if (flags.last && html.includes('')) { + const analytics = ` + +`; + html = html.replace('', analytics + ''); + } + + r.sendBuffer(html, flags); + } else { + r.sendBuffer(data, flags); + } +} + +export default { + replaceText, + appendContent, + modifyJson, + injectAnalytics +}; +``` + +**nginx 配置:** + +```nginx +http { + js_import /etc/nginx/njs/body_filter.js; + + server { + listen 80; + + # 敏感信息脱敏 + location /api/users { + proxy_pass http://backend; + js_body_filter body_filter.replaceText; + } + + # JSON API 修改 + location /api/data { + proxy_pass http://backend; + js_body_filter body_filter.modifyJson; + } + + # HTML 页面注入 + location / { + proxy_pass http://backend; + js_body_filter body_filter.injectAnalytics; + } + } +} +``` + +### 示例 6: 使用 Fetch API 的请求聚合 + +将多个后端请求合并为一个响应。 + +**JavaScript 文件 (`/etc/nginx/njs/aggregate.js`):** + +```javascript +async function aggregateUserData(r) { + const userId = r.args.userId; + + if (!userId) { + r.return(400, JSON.stringify({ error: 'Missing userId' })); + return; + } + + try { + // 并行发起多个请求 + const [profileRes, ordersRes, preferencesRes] = await Promise.all([ + ngx.fetch(`http://user-service/users/${userId}`), + ngx.fetch(`http://order-service/orders?userId=${userId}`), + ngx.fetch(`http://preference-service/preferences/${userId}`) + ]); + + // 解析所有响应 + const [profile, orders, preferences] = await Promise.all([ + profileRes.json(), + ordersRes.json(), + preferencesRes.json() + ]); + + // 合并数据 + const result = { + user: profile, + orders: orders, + preferences: preferences, + aggregatedAt: new Date().toISOString() + }; + + r.headersOut['Content-Type'] = 'application/json'; + r.return(200, JSON.stringify(result, null, 2)); + + } catch (e) { + r.return(500, JSON.stringify({ error: 'Failed to aggregate data', message: e.message })); + } +} + +// 带缓存的请求 +async function cachedFetch(r) { + const cacheKey = 'api:' + r.uri; + const cached = ngx.shared.api_cache.get(cacheKey); + + if (cached) { + r.headersOut['X-Cache'] = 'HIT'; + r.return(200, cached); + return; + } + + try { + const response = await ngx.fetch('http://backend' + r.uri); + const body = await response.text(); + + // 缓存 60 秒 + ngx.shared.api_cache.set(cacheKey, body, 60000); + + r.headersOut['X-Cache'] = 'MISS'; + r.return(response.status, body); + + } catch (e) { + r.return(502, JSON.stringify({ error: 'Backend unavailable' })); + } +} + +export default { aggregateUserData, cachedFetch }; +``` + +**nginx 配置:** + +```nginx +http { + js_import /etc/nginx/njs/aggregate.js; + + # 配置共享字典用于缓存 + js_shared_dict_zone zone=api_cache:10M type=string evict; + + server { + listen 80; + + # 数据聚合端点 + location /api/aggregate { + js_content aggregate.aggregateUserData; + } + + # 带缓存的代理 + location /api/ { + js_content aggregate.cachedFetch; + } + } +} +``` + +--- + +## 6. njs vs Lua 对比 + +| 特性 | njs (JavaScript) | Lua (ngx_lua) | +|------|------------------|---------------| +| **语言流行度** | 广泛,前后端通用 | 游戏/嵌入式领域为主 | +| **学习曲线** | 低(开发者熟悉) | 中等(需学习新语言) | +| **异步支持** | 原生 Promise/async-await | 协程 (coroutine) | +| **模块生态** | Node.js 部分兼容 | LuaRocks 生态 | +| **JSON 处理** | 原生支持 | 需 cjson 库 | +| **正则表达式** | 原生支持 | 模式匹配(不同语法) | +| **调试工具** | njs CLI 工具 | 有限 | +| **内存管理** | 自动垃圾回收 | 自动垃圾回收 | +| **执行引擎** | njs/QuickJS | LuaJIT/PUC-Rio | +| **HTTP 客户端** | 内置 Fetch API | 需 cosocket | +| **共享内存** | js_shared_dict_zone | ngx.shared.DICT | +| **子请求** | r.subrequest() | ngx.location.capture | + +### 选择建议 + +| 场景 | 推荐方案 | +|------|----------| +| 团队熟悉 JavaScript | njs | +| 需要复杂异步逻辑 | njs(async/await 更清晰) | +| 已有 Lua 代码库 | 继续使用 Lua | +| 极致性能要求 | LuaJIT(可能更快) | +| 前后端代码共享 | njs | +| OpenResty 生态依赖 | Lua | + +--- + +## 7. 性能优化建议 + +### 1. 使用 QuickJS 引擎 + +QuickJS 引擎在某些场景下性能更好,支持 ES2023。 + +```nginx +http { + js_engine qjs; +} +``` + +### 2. 合理配置 Context 重用 + +```nginx +http { + # 调整 QuickJS context 池大小(默认 128) + js_context_reuse 256; +} +``` + +### 3. 使用缓存 + +```javascript +// 在 njs 中缓存计算结果 +const cache = {}; + +function cachedOperation(r) { + const key = r.args.key; + + if (cache[key]) { + return cache[key]; + } + + const result = expensiveComputation(key); + cache[key] = result; + return result; +} +``` + +### 4. 使用共享字典 + +```nginx +# 配置足够大的共享内存 +js_shared_dict_zone zone=my_cache:100M type=string timeout=300s evict; +``` + +### 5. 避免同步阻塞 + +```javascript +// 使用异步操作 +async function good(r) { + const result = await ngx.fetch('http://backend'); + r.return(200, await result.text()); +} + +// 避免同步文件操作 +import fs from 'fs'; +// 仅使用同步方法,因为 njs 在 nginx 中不支持异步文件操作 +``` + +### 6. 过滤器性能注意 + +```javascript +// js_body_filter 和 js_header_filter 只支持同步操作 +function filter(r, data, flags) { + // 只能使用同步操作 + r.sendBuffer(data.toLowerCase(), flags); + + // 以下操作不支持: + // await r.subrequest(...) + // setTimeout(...) +} +``` + +### 7. 预加载配置对象 + +```nginx +# 预加载配置到内存 +js_preload_object config.json; +js_preload_object api_keys from /etc/nginx/secrets/keys.json; +``` + +```javascript +// 在 JavaScript 中访问 +function handler(r) { + const config = ngx.conf.config; + const keys = ngx.conf.api_keys; +} +``` + +### 8. 调整 Fetch API 缓冲区 + +```nginx +http { + # 增加 Fetch API 缓冲区 + js_fetch_buffer_size 32k; + js_fetch_max_response_buffer_size 10m; + js_fetch_timeout 30s; + + # 启用连接池 + js_fetch_keepalive 32; + js_fetch_keepalive_timeout 60s; +} +``` + +### 9. 监控和调试 + +```javascript +// 使用定时器监控性能 +function monitoredHandler(r) { + const start = Date.now(); + + // 处理逻辑... + + const duration = Date.now() - start; + r.headersOut['X-Response-Time'] = duration + 'ms'; + ngx.log(ngx.INFO, `Request processed in ${duration}ms`); +} + +// 内存统计(仅限 CLI) +console.log(njs.memoryStats); +``` + +### 10. 代码组织最佳实践 + +```javascript +// 模块化组织代码 +// utils.js +function logRequest(r) { + ngx.log(ngx.INFO, `${r.method} ${r.uri}`); +} + +function sanitizeInput(input) { + return input.replace(/[<>]/g, ''); +} + +export default { logRequest, sanitizeInput }; + +// handlers.js +import utils from 'utils.js'; + +function handleApiRequest(r) { + utils.logRequest(r); + // ... +} + +export default { handleApiRequest }; +``` + +--- + +## 附录:快速参考 + +### 常用代码片段 + +```javascript +// 1. 读取请求体 +const body = r.requestText; +const json = JSON.parse(body); + +// 2. 设置响应头 +r.headersOut['X-Custom'] = 'value'; +r.headersOut['Content-Type'] = 'application/json'; + +// 3. 获取查询参数 +const param = r.args.name; +const allParams = r.args; // 对象 + +// 4. 获取请求头 +const auth = r.headersIn['Authorization']; + +// 5. 子请求 +const reply = await r.subrequest('/internal/api'); +r.return(200, reply.responseText); + +// 6. 外部 HTTP 请求 +const response = await ngx.fetch('https://api.example.com/data'); +const data = await response.json(); + +// 7. 日志记录 +r.log('Info message'); +r.warn('Warning message'); +r.error('Error message'); +ngx.log(ngx.INFO, 'Nginx log'); + +// 8. 共享字典操作 +const dict = ngx.shared.myDict; +dict.set('key', 'value', 60000); // 60秒 TTL +const val = dict.get('key'); +dict.incr('counter', 1, 0); + +// 9. Base64 编码/解码 +const encoded = btoa('hello'); +const decoded = atob(encoded); + +// 10. 哈希计算 +import crypto from 'crypto'; +const hash = crypto.createHash('sha256').update('data').digest('hex'); +``` + +### 版本要求 + +| 功能 | 最低版本 | +|------|----------| +| 基本 HTTP 模块 | 0.4.0 | +| Stream 模块 | 0.4.4 | +| Fetch API | 0.7.0 | +| WebCrypto | 0.7.0 | +| async/await | 0.7.0 | +| 共享字典 | 0.8.0 | +| QuickJS 引擎 | 0.8.6 | +| fs 模块 | 0.8.9 | +| Fetch keepalive | 0.9.2 | + +### 官方资源 + +- [njs 官方文档](https://nginx.org/en/docs/njs/) +- [ngx_http_js_module](https://nginx.org/en/docs/http/ngx_http_js_module.html) +- [GitHub 仓库](https://github.com/nginx/njs) +- [示例代码](https://github.com/nginx/njs-examples/) diff --git a/docs/31-nginx-observability.md b/docs/31-nginx-observability.md new file mode 100644 index 0000000..fc22ecd --- /dev/null +++ b/docs/31-nginx-observability.md @@ -0,0 +1,1508 @@ +# NGINX OpenTelemetry 可观测性指南 + +本文档介绍如何在 NGINX 中使用 OpenTelemetry 模块实现分布式追踪和可观测性。 + +## 目录 + +1. [OpenTelemetry 概述](#opentelemetry-概述) +2. [模块指令参考](#模块指令参考) +3. [分布式追踪配置](#分布式追踪配置) +4. [与 Jaeger/Zipkin 集成](#与-jaegerzipkin-集成) +5. [自定义属性和事件](#自定义属性和事件) +6. [完整配置示例](#完整配置示例) +7. [最佳实践](#最佳实践) + +--- + +## OpenTelemetry 概述 + +### 什么是 OpenTelemetry + +OpenTelemetry 是一个开源的可观测性框架,提供标准化的 API、库和工具来收集分布式追踪、指标和日志数据。它由 Cloud Native Computing Foundation (CNCF) 托管,是 Prometheus、Jaeger 和 OpenCensus 等项目合并后的统一解决方案。 + +### 核心概念 + +| 概念 | 描述 | +|------|------| +| **Trace** | 分布式追踪,表示请求在系统中的完整调用链路 | +| **Span** | 追踪中的基本工作单元,包含操作名称、起止时间、属性等 | +| **Context** | 追踪上下文,用于在服务间传播追踪信息(traceparent/tracestate) | +| **Resource** | 描述产生遥测数据的实体(如服务名称、版本、主机) | +| **Exporter** | 将遥测数据发送到后端存储(如 OTLP、gRPC) | + +### 架构流程 + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Client │───▶│ NGINX │───▶│ Backend │───▶│Database │ +└─────────┘ └────┬────┘ └─────────┘ └─────────┘ + │ + ▼ + ┌────────────────┐ + │ ngx_otel_module │ + └───────┬────────┘ + │ + ▼ + ┌───────────────┐ ┌───────────┐ ┌──────────┐ + │OTEL Collector │───▶│ Jaeger │ │ Zipkin │ + └───────────────┘ └───────────┘ └──────────┘ +``` + +### 模块版本要求 + +- NGINX Plus R28 或更高版本 +- `ngx_otel_module` 动态模块(从源码编译或 NGINX Plus 包含) + +--- + +## 模块指令参考 + +### otel_exporter + +配置 OpenTelemetry 数据导出参数。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_exporter` | `{ ... }` | — | `http` | + +**子指令:** + +| 子指令 | 语法 | 默认值 | 描述 | +|--------|------|--------|------| +| `endpoint` | `[(http\|https)://]host:port;` | — | OTLP/gRPC 端点地址 | +| `trusted_certificate` | `path;` | 系统 CA | PEM 格式 CA 证书文件(v0.1.2+) | +| `header` | `name value;` | — | 自定义 HTTP 请求头 | +| `interval` | `time;` | `5s` | 导出最大间隔时间 | +| `batch_size` | `number;` | `512` | 每批次最大 Span 数量 | +| `batch_count` | `number;` | `4` | 每个 worker 的待处理批次数 | + +**示例:** + +```nginx +http { + otel_exporter { + endpoint otel-collector:4317; + interval 5s; + batch_size 512; + batch_count 4; + trusted_certificate /etc/nginx/certs/ca.pem; + header X-API-Key secret_key; + } +} +``` + +### otel_service_name + +设置 OTel Resource 的 `service.name` 属性。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_service_name` | `name;` | `unknown_service:nginx` | `http` | + +**示例:** + +```nginx +http { + otel_service_name nginx-gateway; +} +``` + +### otel_resource_attr + +设置自定义 OTel Resource 属性(v0.1.2+)。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_resource_attr` | `name value;` | — | `http` | + +**示例:** + +```nginx +http { + otel_resource_attr deployment.environment production; + otel_resource_attr service.version 1.2.3; + otel_resource_attr host.name $hostname; +} +``` + +### otel_trace + +启用或禁用 OpenTelemetry 追踪。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_trace` | `on \| off \| $variable;` | `off` | `http`, `server`, `location` | + +**示例:** + +```nginx +http { + otel_trace off; + + server { + listen 80; + otel_trace on; + + location /api { + otel_trace on; + } + + location /health { + otel_trace off; # 健康检查不记录 + } + } +} +``` + +### otel_trace_context + +配置 traceparent/tracestate 头的传播方式。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_trace_context` | `extract \| inject \| propagate \| ignore;` | `ignore` | `http`, `server`, `location` | + +**选项说明:** + +| 值 | 描述 | +|----|------| +| `extract` | 从入站请求中提取追踪上下文,继承上游标识符 | +| `inject` | 向出站请求注入新的追踪上下文,覆盖现有上下文 | +| `propagate` | 更新现有上下文(先 extract 再 inject),保持追踪链完整 | +| `ignore` | 忽略上下文头处理 | + +**示例:** + +```nginx +server { + location / { + # 作为入口网关,注入新追踪上下文 + otel_trace_context inject; + proxy_pass http://backend; + } + + location /api/ { + # 作为中间代理,传播上游追踪上下文 + otel_trace_context propagate; + proxy_pass http://api_backend; + } +} +``` + +### otel_span_name + +定义 OTel Span 的名称。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_span_name` | `name;` | location 名称 | `http`, `server`, `location` | + +**示例:** + +```nginx +server { + location /api/users { + otel_span_name "GET /api/users"; + # 或使用变量 + otel_span_name "$request_method $uri"; + } +} +``` + +### otel_span_attr + +添加自定义 OTel Span 属性。 + +| 指令 | 语法 | 默认值 | 上下文 | +|------|------|--------|--------| +| `otel_span_attr` | `name value;` | — | `http`, `server`, `location` | + +**示例:** + +```nginx +server { + location /api/ { + otel_span_attr http.route "/api/*"; + otel_span_attr user.id $remote_user; + otel_span_attr client.ip $remote_addr; + } +} +``` + +### 嵌入式变量 + +| 变量 | 描述 | +|------|------| +| `$otel_trace_id` | 追踪标识符 | +| `$otel_span_id` | 当前 Span 标识符 | +| `$otel_parent_id` | 父 Span 标识符 | +| `$otel_parent_sampled` | 父 Span 的采样标志(`1` 或 `0`) | + +--- + +## 分布式追踪配置 + +### Trace 上下文传播 + +追踪上下文传播是分布式追踪的核心,确保请求在多个服务间保持相同的追踪标识。 + +#### W3C Trace Context 标准 + +NGINX 使用 W3C Trace Context 标准: +- **traceparent**: `00-{trace-id}-{parent-id}-{flags}` +- **tracestate**: 厂商特定的上下文信息 + +``` +Traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 + │ │ │ │ │ + │ │ │ │ └── 标志位( sampled: 01) + │ │ │ └── 父 Span ID + │ │ └── Trace ID + │ └── 版本 + └── 固定前缀 +``` + +#### 传播模式配置 + +**场景 1: 边缘网关(追踪入口)** + +```nginx +http { + otel_service_name nginx-edge-gateway; + otel_trace on; + + server { + listen 80; + server_name api.example.com; + + location / { + # 注入新的追踪上下文 + otel_trace_context inject; + + # 将追踪 ID 传递给后端 + proxy_set_header X-Trace-ID $otel_trace_id; + proxy_set_header X-Span-ID $otel_span_id; + + proxy_pass http://backend_cluster; + } + } +} +``` + +**场景 2: 中间代理(追踪传播)** + +```nginx +server { + listen 8080; + + location / { + # 传播上游追踪上下文 + otel_trace_context propagate; + + # 将追踪头传递给下游服务 + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + + proxy_pass http://internal_services; + } +} +``` + +**场景 3: 混合模式** + +```nginx +server { + location /public/ { + # 公共 API: 创建新追踪 + otel_trace_context inject; + proxy_pass http://public_backend; + } + + location /internal/ { + # 内部服务: 传播已有追踪 + otel_trace_context propagate; + proxy_pass http://internal_backend; + } + + location /health { + # 健康检查: 忽略追踪 + otel_trace off; + return 200 "healthy\n"; + } +} +``` + +### Span 配置 + +#### 标准 Span 属性 + +NGINX 自动记录的 Span 属性: + +| 属性 | 描述 | 示例值 | +|------|------|--------| +| `http.method` | HTTP 方法 | GET, POST, PUT | +| `http.url` | 请求 URL | `https://api.example.com/users` | +| `http.scheme` | 协议 | http, https | +| `http.host` | 主机名 | `api.example.com` | +| `http.status_code` | 响应状态码 | 200, 404, 500 | +| `http.user_agent` | 用户代理 | Mozilla/5.0... | +| `http.request_content_length` | 请求体大小 | 1024 | +| `http.response_content_length` | 响应体大小 | 2048 | +| `net.peer.ip` | 客户端 IP | 192.168.1.100 | +| `net.peer.port` | 客户端端口 | 54321 | + +#### 自定义 Span 名称 + +```nginx +map $request_method $span_name { + default "$request_method $uri"; + GET "get_request"; + POST "create_resource"; +} + +server { + location /api/ { + otel_span_name $span_name; + proxy_pass http://backend; + } +} +``` + +#### 条件性 Span 属性 + +```nginx +map $status $error_type { + ~^[45] "client_or_server_error"; + default ""; +} + +server { + location / { + otel_span_attr error.class $error_type; + otel_span_attr request.id $request_id; + otel_span_attr tenant.id $http_x_tenant_id; + + proxy_pass http://backend; + } +} +``` + +### 采样策略 + +采样控制追踪数据的收集量,平衡可观测性和性能开销。 + +#### 采样类型 + +| 采样类型 | 描述 | 使用场景 | +|----------|------|----------| +| **Head-Based** | 在追踪开始时决定采样 | 低延迟、低资源开销 | +| **Tail-Based** | 基于完整追踪数据决定 | 捕获错误/慢请求 | +| **Parent-Based** | 继承父 Span 的采样决定 | 保持追踪完整性 | + +#### 配置示例 + +**1. 始终采样(开发/测试环境)** + +```nginx +http { + otel_trace on; + # 所有请求都记录 +} +``` + +**2. 比例采样(基于变量)** + +```nginx +# 使用 Lua 或外部模块实现比例采样 +# 这里展示基于 Nginx 变量的实现 + +split_clients "$remote_addr$request_id" $trace_sampled { + 10% "1"; # 10% 采样率 + * "0"; # 90% 不采样 +} + +server { + location / { + otel_trace $trace_sampled; + proxy_pass http://backend; + } +} +``` + +**3. 基于请求特征采样** + +```nginx +map $uri $should_trace { + default "0"; + ~*\.html$ "1"; # 采样 HTML 页面 + /api/critical/ "1"; # 采样关键 API + /api/payment/ "1"; # 采样支付相关 +} + +map $http_x_debug $force_trace { + default ""; + true "1"; +} + +server { + location / { + # 优先使用 debug header,其次基于 URI + otel_trace $force_trace$should_trace; + proxy_pass http://backend; + } +} +``` + +**4. 错误/慢请求采样(结合 OpenTelemetry Collector)** + +```yaml +# otel-collector-config.yaml +processors: + tail_sampling: + policies: + - name: slow_requests + type: latency + latency: {threshold_ms: 500} + - name: errors + type: status_code + status_code: {status_codes: [500, 502, 503, 504]} + - name: probabilistic + type: probabilistic + probabilistic: {sampling_percentage: 10} +``` + +--- + +## 与 Jaeger/Zipkin 集成 + +### Jaeger 集成 + +#### 方法 1: Jaeger 原生 OTLP(推荐) + +Jaeger 1.35+ 原生支持 OTLP 协议。 + +**docker-compose.yaml:** + +```yaml +version: "3.8" + +services: + jaeger: + image: jaegertracing/all-in-one:1.60.0 + container_name: jaeger + ports: + - "16686:16686" # Jaeger UI + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + environment: + - COLLECTOR_OTLP_ENABLED=true + networks: + - observability + + nginx: + image: nginx:alpine + container_name: nginx + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "80:80" + depends_on: + - jaeger + networks: + - observability + +networks: + observability: + driver: bridge +``` + +**nginx.conf:** + +```nginx +load_module modules/ngx_otel_module.so; + +events { + worker_connections 1024; +} + +http { + # OTLP 导出器配置 + otel_exporter { + endpoint jaeger:4317; + interval 5s; + batch_size 512; + } + + # 服务标识 + otel_service_name nginx-gateway; + otel_resource_attr deployment.environment production; + otel_resource_attr host.name $hostname; + + # 启用追踪 + otel_trace on; + + server { + listen 80; + server_name localhost; + + location / { + otel_trace_context inject; + otel_span_name "$request_method $uri"; + + # 传递追踪上下文给后端 + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + + proxy_pass http://backend; + } + + location /jaeger { + # 返回当前追踪信息(调试用途) + default_type application/json; + return 200 '{"trace_id":"$otel_trace_id","span_id":"$otel_span_id"}'; + } + } +} +``` + +#### 方法 2: 通过 OpenTelemetry Collector + +用于需要额外处理的场景(过滤、转换、批处理)。 + +**docker-compose.yaml:** + +```yaml +version: "3.8" + +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:0.117.0 + container_name: otel-collector + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro + ports: + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + - "9464:9464" # Prometheus metrics + networks: + - observability + + jaeger: + image: jaegertracing/all-in-one:1.60.0 + container_name: jaeger + ports: + - "16686:16686" + environment: + - COLLECTOR_OTLP_ENABLED=true + networks: + - observability + + nginx: + image: nginx:alpine + container_name: nginx + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "80:80" + depends_on: + - otel-collector + networks: + - observability + +networks: + observability: + driver: bridge +``` + +**otel-collector-config.yaml:** + +```yaml +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + + resource: + attributes: + - key: environment + value: production + action: upsert + + tail_sampling: + policies: + - name: slow_requests + type: latency + latency: {threshold_ms: 500} + - name: errors + type: status_code + status_code: {status_codes: [500, 502, 503, 504]} + +exporters: + otlp/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + + debug: + verbosity: detailed + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource, tail_sampling] + exporters: [otlp/jaeger, debug] +``` + +### Zipkin 集成 + +#### 方法 1: 通过 OpenTelemetry Collector + +**docker-compose.yaml:** + +```yaml +version: "3.8" + +services: + zipkin: + image: openzipkin/zipkin:3 + container_name: zipkin + ports: + - "9411:9411" + environment: + - STORAGE_TYPE=mem + networks: + - observability + + otel-collector: + image: otel/opentelemetry-collector-contrib:0.117.0 + container_name: otel-collector + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro + ports: + - "4317:4317" + - "4318:4318" + depends_on: + - zipkin + networks: + - observability + + nginx: + image: nginx:alpine + container_name: nginx + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "80:80" + depends_on: + - otel-collector + networks: + - observability + +networks: + observability: + driver: bridge +``` + +**otel-collector-config.yaml:** + +```yaml +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + zipkin: + endpoint: http://zipkin:9411/api/v2/spans + format: json + + debug: + verbosity: detailed + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [zipkin, debug] +``` + +#### 方法 2: Zipkin 直接接收 + +如果您的系统已使用 Zipkin,可以让 Collector 同时接收 OTLP 和 Zipkin 格式。 + +```yaml +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + + zipkin: + endpoint: 0.0.0.0:9411 + +processors: + batch: + +exporters: + zipkin: + endpoint: http://zipkin:9411/api/v2/spans + +service: + pipelines: + traces: + receivers: [otlp, zipkin] + processors: [batch] + exporters: [zipkin] +``` + +--- + +## 自定义属性和事件 + +### 自定义 Span 属性 + +#### 静态属性 + +```nginx +http { + otel_resource_attr service.namespace ecommerce; + otel_resource_attr service.version 2.1.0; + + server { + location /api/ { + otel_span_attr api.version v1; + otel_span_attr team backend; + } + } +} +``` + +#### 动态属性(使用变量) + +```nginx +map $request_time $latency_bucket { + ~^0\.[0-4] "fast"; + ~^0\.[5-9] "medium"; + default "slow"; +} + +server { + location / { + otel_span_attr http.latency_bucket $latency_bucket; + otel_span_attr request.size $request_length; + otel_span_attr response.size $bytes_sent; + otel_span_attr upstream.addr $upstream_addr; + otel_span_attr upstream.response_time $upstream_response_time; + + proxy_pass http://backend; + } +} +``` + +#### 条件属性 + +```nginx +map $upstream_status $upstream_error { + ~^[45] "true"; + default "false"; +} + +map $upstream_cache_status $cache_hit { + HIT "true"; + default "false"; +} + +server { + location / { + otel_span_attr upstream.error $upstream_error; + otel_span_attr cache.hit $cache_hit; + otel_span_attr cache.status $upstream_cache_status; + + proxy_pass http://backend; + proxy_cache my_cache; + } +} +``` + +### 业务属性 + +```nginx +server { + location /api/orders { + # 业务相关属性 + otel_span_attr business.domain orders; + otel_span_attr business.criticality high; + otel_span_attr business.region $geoip_country_code; + + # 用户相关属性(注意:避免 PII) + otel_span_attr user.type $http_x_user_type; + otel_span_attr user.tier $http_x_user_tier; + + proxy_pass http://order_service; + } +} +``` + +### 使用 Lua 扩展(需要 lua-nginx-module) + +```nginx +server { + location / { + access_by_lua_block { + local otel = require("opentelemetry") + local span = otel.get_current_span() + + -- 添加自定义属性 + span:set_attribute("custom.timestamp", ngx.now()) + span:set_attribute("custom.request_hash", ngx.md5(ngx.var.request_uri)) + + -- 添加事件 + span:add_event("request_processing_started", { + ["http.method"] = ngx.var.request_method, + ["client.ip"] = ngx.var.remote_addr + }) + } + + proxy_pass http://backend; + } +} +``` + +--- + +## 完整配置示例 + +### 示例 1: 基础配置 + +```nginx +# 加载动态模块 +load_module modules/ngx_otel_module.so; + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # OpenTelemetry 导出器配置 + otel_exporter { + endpoint otel-collector:4317; + interval 5s; + batch_size 512; + batch_count 4; + } + + # 服务标识 + otel_service_name nginx-proxy; + otel_resource_attr deployment.environment production; + otel_resource_attr host.name $hostname; + + # 全局启用追踪 + otel_trace on; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'trace_id=$otel_trace_id span_id=$otel_span_id'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + upstream backend { + server backend1:8080 weight=5; + server backend2:8080 weight=5; + keepalive 32; + } + + server { + listen 80; + server_name localhost; + + # 健康检查:禁用追踪 + location /health { + otel_trace off; + access_log off; + return 200 "healthy\n"; + } + + # 静态资源:采样 + location /static/ { + otel_trace $http_x_trace_sampled; + alias /var/www/static/; + expires 1d; + } + + # API 请求:完整追踪 + location /api/ { + otel_trace on; + otel_trace_context propagate; + otel_span_name "$request_method $uri"; + + otel_span_attr http.route /api/*; + otel_span_attr api.version v1; + otel_span_attr request.id $request_id; + + 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-Request-ID $request_id; + + # 传递追踪上下文 + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + + proxy_pass http://backend; + } + + # 默认位置 + location / { + otel_trace_context inject; + proxy_pass http://backend; + } + } +} +``` + +### 示例 2: 多环境配置 + +```nginx +load_module modules/ngx_otel_module.so; + +events { + worker_connections 1024; +} + +http { + # 根据环境变量配置 + env NGINX_ENV; + env OTEL_ENDPOINT; + + # 动态采样率配置 + split_clients "$remote_addr$request_id" $trace_sampled { + 10% "1"; + * "0"; + } + + map $http_x_b3_sampled $b3_sampled { + default ""; + "1" "1"; + "0" ""; + "true" "1"; + "false" ""; + "d" "1"; + } + + map $b3_sampled$trace_sampled $final_trace { + default "0"; + ~.*1.* "1"; + } + + # OTLP 导出器 + otel_exporter { + endpoint ${OTEL_ENDPOINT}; + interval 5s; + batch_size 512; + } + + otel_service_name nginx-${NGINX_ENV}; + otel_resource_attr deployment.environment ${NGINX_ENV}; + + # 生产环境:按比例采样 + # 测试环境:全量采样 + otel_trace ${NGINX_ENV} == "prod" ? $final_trace : on; + + # 上游配置 + upstream api_backend { + server api1.internal:8080; + server api2.internal:8080; + } + + upstream web_backend { + server web1.internal:8080; + server web2.internal:8080; + } + + # API 网关 + server { + listen 8080; + server_name api.example.com; + + location / { + otel_trace_context propagate; + otel_span_name "api:$request_method $uri"; + + otel_span_attr upstream.service api; + otel_span_attr rate.limit.bucket $limit_req_status; + + proxy_pass http://api_backend; + } + } + + # Web 网关 + server { + listen 80; + server_name www.example.com; + + location / { + otel_trace_context inject; + otel_span_name "web:$request_method $uri"; + + otel_span_attr upstream.service web; + otel_span_attr cache.status $upstream_cache_status; + + proxy_pass http://web_backend; + proxy_cache web_cache; + } + } +} +``` + +### 示例 3: 微服务网关配置 + +```nginx +load_module modules/ngx_otel_module.so; + +events { + worker_connections 4096; +} + +http { + # OpenTelemetry 配置 + otel_exporter { + endpoint otel-collector:4317; + interval 3s; + batch_size 256; + header X-Scope-OrgID tenant-1; + } + + otel_service_name nginx-microgateway; + otel_resource_attr service.namespace platform; + otel_resource_attr service.version 1.0.0; + otel_resource_attr deployment.environment production; + + # 追踪配置 + otel_trace on; + + # 日志格式包含追踪信息 + log_format trace '$remote_addr - $remote_user [$time_iso8601] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + '"trace_id":"$otel_trace_id",' + '"span_id":"$otel_span_id",' + '"parent_id":"$otel_parent_id"'; + + access_log /var/log/nginx/access.log trace; + + # 服务发现(使用 resolver) + resolver 127.0.0.11 valid=30s; + + # 服务定义 + upstream user_service { + server user-service:8080 resolve; + keepalive 64; + } + + upstream order_service { + server order-service:8080 resolve; + keepalive 64; + } + + upstream inventory_service { + server inventory-service:8080 resolve; + keepalive 64; + } + + # 通用追踪配置 + map $request_method $trace_operation { + GET "read"; + POST "create"; + PUT "update"; + DELETE "delete"; + PATCH "patch"; + default "unknown"; + } + + server { + listen 80; + server_name gateway.internal; + + # 追踪上下文传播 + otel_trace_context propagate; + + # User Service + location /api/users/ { + otel_span_name "users:$trace_operation"; + otel_span_attr service.name user-service; + otel_span_attr service.operation $trace_operation; + otel_span_attr service.resource users; + + proxy_pass http://user_service/; + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + } + + # Order Service + location /api/orders/ { + otel_span_name "orders:$trace_operation"; + otel_span_attr service.name order-service; + otel_span_attr service.operation $trace_operation; + otel_span_attr service.resource orders; + + proxy_pass http://order_service/; + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + } + + # Inventory Service + location /api/inventory/ { + otel_span_name "inventory:$trace_operation"; + otel_span_attr service.name inventory-service; + otel_span_attr service.operation $trace_operation; + otel_span_attr service.resource inventory; + + proxy_pass http://inventory_service/; + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + } + + # 健康检查(无追踪) + location /health { + otel_trace off; + access_log off; + return 200 '{"status":"healthy","service":"nginx"}'; + } + + # 追踪信息端点(调试) + location /debug/trace { + otel_trace on; + default_type application/json; + return 200 '{ + "trace_id": "$otel_trace_id", + "span_id": "$otel_span_id", + "parent_id": "$otel_parent_id", + "sampled": "$otel_parent_sampled" + }'; + } + } +} +``` + +### 示例 4: Kubernetes 环境配置 + +```nginx +load_module modules/ngx_otel_module.so; + +events { + worker_connections 1024; +} + +http { + # 从环境变量读取 K8s 信息 + env KUBERNETES_NAMESPACE; + env KUBERNETES_POD_NAME; + env KUBERNETES_NODE_NAME; + env OTEL_COLLECTOR_SERVICE; + + # OTLP 导出器 + otel_exporter { + endpoint ${OTEL_COLLECTOR_SERVICE}:4317; + interval 5s; + batch_size 512; + } + + # 丰富的资源属性 + otel_service_name nginx-ingress; + otel_resource_attr k8s.namespace.name ${KUBERNETES_NAMESPACE}; + otel_resource_attr k8s.pod.name ${KUBERNETES_POD_NAME}; + otel_resource_attr k8s.node.name ${KUBERNETES_NODE_NAME}; + otel_resource_attr host.name ${KUBERNETES_POD_NAME}; + + # 启用追踪 + otel_trace on; + + # 上游配置(K8s Service) + resolver kube-dns.kube-system.svc.cluster.local valid=10s; + + server { + listen 80; + + location / { + otel_trace_context propagate; + otel_span_name "$request_method $uri"; + + otel_span_attr k8s.destination.service $proxy_host; + otel_span_attr k8s.destination.namespace ${KUBERNETES_NAMESPACE}; + + # 传递 K8s 相关的追踪头 + proxy_set_header X-Request-ID $request_id; + proxy_set_header traceparent $http_traceparent; + proxy_set_header tracestate $http_tracestate; + + proxy_pass http://backend-service; + } + } +} +``` + +--- + +## 最佳实践 + +### 1. 采样策略 + +**生产环境建议:** + +```nginx +# 使用 Head-Based 采样降低开销 +split_clients "$request_id" $trace_decision { + 5% "1"; # 5% 基础采样 + * ""; +} + +# 关键路径始终采样 +map $uri $is_critical { + default ""; + ~*payment "1"; + ~*order "1"; + ~*auth "1"; +} + +map $trace_decision$is_critical $should_trace { + default "0"; + ~.*1.* "1"; +} + +otel_trace $should_trace; +``` + +**关键原则:** +- 错误率高的服务:提高采样率 +- 高流量服务:降低采样率(0.1% - 1%) +- 关键业务路径:全量采样 +- 使用 Parent-Based 采样保持追踪链完整 + +### 2. 敏感数据处理 + +**禁止在 Span 属性中包含:** +- 密码、API Key +- 信用卡号、身份证号 +- 个人身份信息 (PII) +- 会话令牌 + +**安全实践:** + +```nginx +# 正确:使用安全的标识符 +otel_span_attr user.id $http_x_user_id; # 用户 ID +otel_span_attr session.hash $cookie_session_hash; # 会话哈希 + +# 错误:不要记录敏感信息 +# otel_span_attr user.email $http_x_user_email; # 禁止! +# otel_span_attr auth.token $http_authorization; # 禁止! + +# 敏感路径禁用追踪 +location /auth/login { + otel_span_attr auth.endpoint login; + # 不记录请求体 + proxy_pass http://auth_service; +} +``` + +### 3. Span 命名规范 + +使用清晰、一致的命名: + +```nginx +# 推荐:包含 HTTP 方法和路径 +otel_span_name "$request_method $uri"; + +# 或按服务分类 +otel_span_name "nginx:$request_method $uri"; + +# 避免:过于笼统或过于详细 +# otel_span_name "request"; # 太笼统 +# otel_span_name "GET /api/v1/users/12345"; # 包含动态 ID +``` + +### 4. 上下文传播 + +**服务边界处理:** + +```nginx +# 入口服务:注入新上下文 +server { + location /api/ { + otel_trace_context inject; + # 向后传递 + proxy_set_header traceparent $http_traceparent; + proxy_pass http://backend; + } +} + +# 中间服务:传播上下文 +server { + location / { + otel_trace_context propagate; + # 既提取上游上下文,又注入到下游 + proxy_set_header traceparent $http_traceparent; + proxy_pass http://next_service; + } +} + +# 出口服务:提取上下文 +server { + location / { + otel_trace_context extract; + # 只使用上游传入的上下文,不向后传播 + proxy_pass http://final_backend; + } +} +``` + +### 5. 性能优化 + +**减少开销的配置:** + +```nginx +http { + # 增大批处理大小减少网络开销 + otel_exporter { + endpoint otel-collector:4317; + interval 10s; # 增大导出间隔 + batch_size 1024; # 增大批大小 + batch_count 8; # 增加队列深度 + } + + # 选择性启用追踪 + map $request_uri $trace_enabled { + ~*\.(css|js|png|jpg|gif|ico)$ ""; # 静态资源不追踪 + /health ""; # 健康检查不追踪 + /metrics ""; # 指标端点不追踪 + default "1"; # 其他请求追踪 + } + + otel_trace $trace_enabled; +} +``` + +### 6. 监控 Collector 健康 + +```nginx +# 监控 OTLP 导出器状态 +server { + location /nginx_status { + stub_status on; + allow 10.0.0.0/8; + deny all; + } + + location /otel_status { + default_type application/json; + return 200 '{ + "module": "ngx_otel_module", + "service_name": "${otel_service_name}", + "trace_enabled": "${otel_trace}" + }'; + } +} +``` + +### 7. 故障排查 + +**常见问题及解决方案:** + +| 问题 | 可能原因 | 解决方案 | +|------|----------|----------| +| 没有追踪数据 | Collector 不可达 | 检查网络连通性和端口 | +| 追踪链断裂 | 上下文传播配置错误 | 检查 `otel_trace_context` 设置 | +| Span 名称重复 | 未使用变量 | 使用 `$uri` 或 `$request_uri` | +| 采样率异常 | 变量配置错误 | 检查 `split_clients` 或 map | +| 属性缺失 | 变量未定义 | 使用 `map` 提供默认值 | + +**调试配置:** + +```nginx +# 临时开启详细日志 +error_log /var/log/nginx/error.log debug; + +# 添加调试端点 +server { + location /debug/otel { + default_type application/json; + return 200 '{ + "trace_id": "$otel_trace_id", + "span_id": "$otel_span_id", + "parent_id": "$otel_parent_id", + "parent_sampled": "$otel_parent_sampled", + "request_id": "$request_id", + "http_traceparent": "$http_traceparent", + "http_tracestate": "$http_tracestate" + }'; + } +} +``` + +### 8. 多协议支持 + +如果后端服务使用不同协议: + +```nginx +# W3C Trace Context (标准) +proxy_set_header traceparent $http_traceparent; +proxy_set_header tracestate $http_tracestate; + +# B3 Propagation (Zipkin) +proxy_set_header X-B3-TraceId $otel_trace_id; +proxy_set_header X-B3-SpanId $otel_span_id; +proxy_set_header X-B3-ParentSpanId $otel_parent_id; +proxy_set_header X-B3-Sampled $otel_parent_sampled; + +# Jaeger Propagation +proxy_set_header uber-trace-id "$otel_trace_id:$otel_span_id:$otel_parent_id:$otel_parent_sampled"; +``` + +--- + +## 参考资源 + +- [NGINX OpenTelemetry Module 官方文档](https://nginx.org/en/docs/ngx_otel_module.html) +- [OpenTelemetry 官方文档](https://opentelemetry.io/docs/) +- [W3C Trace Context 规范](https://www.w3.org/TR/trace-context/) +- [Jaeger 文档](https://www.jaegertracing.io/docs/) +- [Zipkin 文档](https://zipkin.io/) + +--- + +*文档版本: 1.0 | 最后更新: 2025-01* diff --git a/docs/32-nginx-acme-ssl.md b/docs/32-nginx-acme-ssl.md new file mode 100644 index 0000000..dd9ca7b --- /dev/null +++ b/docs/32-nginx-acme-ssl.md @@ -0,0 +1,1236 @@ +# ACME 自动证书管理指南 + +本文档详细介绍如何在 Nginx 中使用 `ngx_http_acme_module` 模块实现 ACME 协议自动证书管理,包括 Let's Encrypt 的配置、自动续期、多域名和通配符证书等高级用法。 + +--- + +## 1. ACME 协议概述 + +### 1.1 什么是 ACME + +ACME(Automatic Certificate Management Environment)协议是由 Let's Encrypt 开发的标准协议,用于自动化域名验证和 SSL/TLS 证书颁发。它使服务器能够自动获取和续期证书,无需人工干预。 + +### 1.2 工作原理 + +ACME 工作流程分为两个阶段: + +``` +┌─────────────┐ ┌─────────────┐ +│ ACME 客户端 │ ←──────────────→ │ CA 服务器 │ +│ (Nginx) │ 1. 账户注册 │ (Let's │ +│ │ 2. 订单创建 │ Encrypt) │ +│ │ 3. 挑战验证 │ │ +│ │ 4. 证书颁发 │ │ +└─────────────┘ └─────────────┘ +``` + +**完整流程:** + +1. **账户注册**:ACME 客户端生成密钥对并向 CA 注册账户 +2. **订单创建**:客户端请求为指定域名颁发证书 +3. **挑战验证**:CA 要求客户端证明对域名的控制权(通过 HTTP-01、DNS-01 或 TLS-ALPN-01) +4. **证书颁发**:验证通过后,CA 签发证书并发布到 Certificate Transparency (CT) 日志 + +### 1.3 挑战类型对比 + +| 挑战类型 | 验证方式 | 通配符支持 | 端口 80 要求 | 适用场景 | +|:--------:|:--------:|:----------:|:------------:|:---------| +| HTTP-01 | 在 `/.well-known/acme-challenge/` 放置文件 | ❌ | ✅ 必须可用 | 标准 Web 服务器,最简单 | +| DNS-01 | 添加 `_acme-challenge` TXT 记录 | ✅ | ❌ 不需要 | 通配符证书、内部服务器 | +| TLS-ALPN-01 | 通过 TLS ALPN 扩展验证 | ❌ | ❌ 不需要 | 仅支持 443 端口的环境 | + +--- + +## 2. ngx_http_acme_module 指令详解 + +### 2.1 指令汇总表 + +| 指令 | 语法 | 默认值 | 上下文 | 描述 | +|:-----|:-----|:------:|:-------|:-----| +| `acme_issuer` | `acme_issuer name { ... }` | — | http | 定义 ACME 证书颁发机构对象 | +| `uri` | `uri uri;` | — | acme_issuer | ACME 服务器目录 URL(必填) | +| `account_key` | `account_key alg[:size] \| file;` | — | acme_issuer | 账户私钥(支持 ecdsa/rsa 或文件路径) | +| `challenge` | `challenge type;` | `http-01` | acme_issuer | 挑战类型:`http-01` 或 `tls-alpn-01` | +| `contact` | `contact URL;` | — | acme_issuer | 联系邮箱(建议 `mailto:` 格式) | +| `external_account_key` | `external_account_key kid file;` | — | acme_issuer | 外部账户授权密钥(EAB) | +| `preferred_chain` | `preferred_chain name;` | — | acme_issuer | 指定首选证书链 | +| `profile` | `profile name [require];` | — | acme_issuer | 请求特定证书配置文件 | +| `ssl_trusted_certificate` | `ssl_trusted_certificate file;` | — | acme_issuer | 验证 ACME 服务器证书的 CA 证书 | +| `ssl_verify` | `ssl_verify on \| off;` | `on` | acme_issuer | 是否验证 ACME 服务器证书 | +| `state_path` | `state_path path \| off;` | `acme_` | acme_issuer | 持久化存储路径(`off` 禁用) | +| `accept_terms_of_service` | `accept_terms_of_service;` | — | acme_issuer | 同意服务条款(部分服务器必需) | +| `acme_shared_zone` | `acme_shared_zone zone=name:size;` | `zone=ngx_acme_shared:256k` | http | 共享内存区大小 | +| `acme_certificate` | `acme_certificate issuer [identifier ...] [key=alg[:size]];` | — | server | 定义要请求的证书 | + +### 2.2 嵌入式变量 + +在配置了 `acme_certificate` 的 `server` 块中可用: + +| 变量 | 说明 | 用途 | +|:-----|:-----|:-----| +| `$acme_certificate` | SSL 证书路径 | `ssl_certificate` 指令 | +| `$acme_certificate_key` | SSL 证书私钥路径 | `ssl_certificate_key` 指令 | + +--- + +## 3. Let's Encrypt 配置步骤 + +### 3.1 基础配置(HTTP-01 挑战) + +**步骤 1:编译或启用模块** + +确保 Nginx 包含 `ngx_http_acme_module` 模块: + +```bash +nginx -V 2>&1 | grep -o 'http_acme_module' +``` + +**步骤 2:配置 DNS 解析器** + +ACME 模块需要解析 Let's Encrypt 服务器域名: + +```nginx +# nginx.conf + +# 配置 DNS 解析器(根据你的网络环境调整) +resolver 8.8.8.8 8.8.4.4 valid=300s; +resolver_timeout 10s; +``` + +**步骤 3:定义 ACME 颁发机构** + +```nginx +# Let's Encrypt 生产环境 +acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-letsencrypt; + accept_terms_of_service; +} + +# Let's Encrypt 测试环境(开发调试时使用) +acme_issuer letsencrypt_staging { + uri https://acme-staging-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-staging; + accept_terms_of_service; +} +``` + +**步骤 4:配置共享内存** + +```nginx +# 增大共享内存以支持多个证书 +acme_shared_zone zone=ngx_acme_shared:1M; +``` + +**步骤 5:配置 HTTPS 服务器** + +```nginx +server { + listen 443 ssl; + server_name www.example.com example.com; + + # 启用 ACME 自动证书 + acme_certificate letsencrypt; + + # 使用 ACME 变量 + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + + # 证书缓存优化(避免每次请求都解析) + ssl_certificate_cache max=2; + + # SSL 优化配置 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + location / { + root /var/www/html; + index index.html; + } +} +``` + +**步骤 6:配置 HTTP 服务器(用于挑战)** + +```nginx +server { + listen 80; + server_name www.example.com example.com; + + # ACME HTTP-01 挑战需要访问 80 端口 + # Nginx ACME 模块会自动处理 /.well-known/acme-challenge/ 路径 + + location / { + return 301 https://$server_name$request_uri; + } +} +``` + +### 3.2 首次启动和验证 + +```bash +# 测试配置语法 +nginx -t + +# 重载配置 +nginx -s reload + +# 查看日志确认证书申请状态 +tail -f /var/log/nginx/error.log +``` + +### 3.3 Let's Encrypt 速率限制 + +| 限制类型 | 限制值 | 时间窗口 | +|:---------|:-------|:---------| +| 账户注册 | 10 个 | 每 IP 每 3 小时 | +| 新订单 | 300 个 | 每账户每 3 小时 | +| 域名证书 | 50 个 | 每域名每 7 天 | +| 相同标识符集 | 5 个 | 每 7 天 | +| 验证失败 | 5 次 | 每标识符每小时 | + +**重要提示:** +- 使用 Staging 环境进行开发和测试 +- 续期操作通常不会触发速率限制 +- 使用 ARI(ACME Renewal Information)可免除速率限制 + +--- + +## 4. 自动续期配置 + +### 4.1 自动续期原理 + +`ngx_http_acme_module` 模块会自动处理证书续期: + +1. **监控证书有效期**:模块持续监控已颁发证书的过期时间 +2. **自动续期触发**:在证书过期前自动发起续期请求 +3. **无缝替换**:新证书获取后自动替换,无需重启 Nginx +4. **持久化存储**:证书和密钥存储在 `state_path` 指定的目录 + +### 4.2 状态目录结构 + +``` +/var/cache/nginx/acme-letsencrypt/ +├── account/ # 账户密钥和配置 +│ ├── private.key # 账户私钥 +│ └── registration.json # 账户注册信息 +├── orders/ # 订单状态 +│ └── *.json +└── certs/ # 颁发的证书 + ├── example.com/ # 按域名组织 + │ ├── cert.pem # 证书 + │ ├── chain.pem # 证书链 + │ ├── fullchain.pem # 完整证书链 + │ └── privkey.pem # 私钥 + └── www.example.com/ +``` + +### 4.3 配置证书续期监控 + +```nginx +# 可选:配置日志监控续期情况 +error_log /var/log/nginx/acme.log info; + +acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-letsencrypt; + accept_terms_of_service; + + # 可选:指定首选证书链 + preferred_chain "ISRG Root X1"; +} +``` + +### 4.4 备份和恢复 + +**备份脚本:** + +```bash +#!/bin/bash +# backup-acme.sh + +BACKUP_DIR="/backup/nginx-acme/$(date +%Y%m%d)" +mkdir -p "$BACKUP_DIR" + +# 备份 ACME 状态目录 +cp -r /var/cache/nginx/acme-letsencrypt "$BACKUP_DIR/" + +# 备份 Nginx 配置 +cp -r /etc/nginx "$BACKUP_DIR/nginx-config" + +echo "ACME backup completed: $BACKUP_DIR" +``` + +**恢复脚本:** + +```bash +#!/bin/bash +# restore-acme.sh + +BACKUP_DIR="$1" + +if [ -z "$BACKUP_DIR" ]; then + echo "Usage: $0 " + exit 1 +fi + +# 恢复 ACME 状态 +systemctl stop nginx +cp -r "$BACKUP_DIR/acme-letsencrypt" /var/cache/nginx/ +chown -R nginx:nginx /var/cache/nginx/acme-letsencrypt +systemctl start nginx + +echo "ACME restore completed from: $BACKUP_DIR" +``` + +### 4.5 监控和告警 + +**检查证书过期时间脚本:** + +```bash +#!/bin/bash +# check-cert-expiry.sh + +CERT_DIR="/var/cache/nginx/acme-letsencrypt/certs" +WARNING_DAYS=7 + +for cert_path in $CERT_DIR/*/cert.pem; do + if [ -f "$cert_path" ]; then + domain=$(basename $(dirname "$cert_path")) + expiry=$(openssl x509 -enddate -noout -in "$cert_path" | cut -d= -f2) + expiry_epoch=$(date -d "$expiry" +%s) + now_epoch=$(date +%s) + days_left=$(( ($expiry_epoch - $now_epoch) / 86400 )) + + if [ $days_left -lt $WARNING_DAYS ]; then + echo "WARNING: Certificate for $domain expires in $days_left days" + else + echo "OK: Certificate for $domain expires in $days_left days" + fi + fi +done +``` + +--- + +## 5. 多域名证书管理 + +### 5.1 多域名证书(SAN 证书) + +单个证书可以包含多个域名(Subject Alternative Names): + +```nginx +acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-letsencrypt; + accept_terms_of_service; +} + +server { + listen 443 ssl; + # 主域名和多个别名 + server_name example.com www.example.com api.example.com; + + # 为所有 server_name 申请单个证书 + acme_certificate letsencrypt; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + location / { + proxy_pass http://backend; + } +} +``` + +### 5.2 独立域名证书 + +为不同域名申请独立证书: + +```nginx +acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-letsencrypt; + accept_terms_of_service; +} + +# 主站点 +server { + listen 443 ssl; + server_name example.com www.example.com; + + acme_certificate letsencrypt example.com www.example.com; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + location / { + root /var/www/example; + } +} + +# API 站点 +server { + listen 443 ssl; + server_name api.example.com; + + acme_certificate letsencrypt api.example.com; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + location / { + proxy_pass http://api-backend; + } +} + +# 博客站点 +server { + listen 443 ssl; + server_name blog.example.com; + + acme_certificate letsencrypt blog.example.com; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + location / { + proxy_pass http://blog-backend; + } +} +``` + +### 5.3 多 ACME 账户配置 + +不同域名使用不同的 ACME 账户: + +```nginx +# 主账户 +acme_issuer letsencrypt_main { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-main; + accept_terms_of_service; + account_key ecdsa:384; +} + +# 客户项目账户 +acme_issuer letsencrypt_client { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:client-projects@example.com; + state_path /var/cache/nginx/acme-client; + accept_terms_of_service; + account_key ecdsa:256; +} + +server { + listen 443 ssl; + server_name project1.example.com; + + acme_certificate letsencrypt_client project1.example.com; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; +} +``` + +--- + +## 6. 通配符证书配置(DNS 验证) + +### 6.1 DNS-01 挑战说明 + +通配符证书(如 `*.example.com`)必须使用 DNS-01 挑战类型验证。这要求: + +1. 能够自动修改 DNS 记录(通过 DNS 提供商 API) +2. 在 `_acme-challenge.example.com` 添加 TXT 记录 +3. 等待 DNS 传播后验证 + +**注意:** `ngx_http_acme_module` 本身不直接支持 DNS-01 挑战(需要外部 DNS 管理工具配合),可以使用 `certbot` 等工具获取通配符证书后由 Nginx 使用。 + +### 6.2 使用 Certbot 获取通配符证书 + +```bash +# 安装 certbot 和 DNS 插件(以 Cloudflare 为例) +# Ubuntu/Debian +sudo apt install certbot python3-certbot-dns-cloudflare + +# CentOS/RHEL +sudo yum install certbot python3-certbot-dns-cloudflare +``` + +**配置 DNS API 凭证:** + +```bash +# 创建 Cloudflare 凭证文件 +sudo mkdir -p /etc/letsencrypt +sudo cat > /etc/letsencrypt/dnscloudflare.ini << 'EOF' +dns_cloudflare_api_token = your-api-token-here +EOF +sudo chmod 600 /etc/letsencrypt/dnscloudflare.ini +``` + +**申请通配符证书:** + +```bash +sudo certbot certonly \ + --dns-cloudflare \ + --dns-cloudflare-credentials /etc/letsencrypt/dnscloudflare.ini \ + -d "example.com" \ + -d "*.example.com" \ + --preferred-challenges dns-01 +``` + +**自动续期配置:** + +```bash +# 测试续期 +certbot renew --dry-run + +# 添加定时任务 +echo "0 3 * * * root certbot renew --quiet --deploy-hook 'nginx -s reload'" | sudo tee -a /etc/crontab +``` + +### 6.3 Nginx 使用通配符证书 + +```nginx +server { + listen 443 ssl; + # 使用通配符匹配所有子域名 + server_name *.example.com; + + # 使用 certbot 获取的证书 + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + + ssl_certificate_cache max=2; + + # 子域名路由 + location / { + # 提取子域名 + set $subdomain ""; + if ($host ~* ^([^.]+)\.example\.com$) { + set $subdomain $1; + } + + # 根据子域名代理到不同后端 + proxy_pass http://$subdomain-backend; + } +} + +# 主域名单独配置 +server { + listen 443 ssl; + server_name example.com; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + + location / { + root /var/www/main; + } +} +``` + +### 6.4 主流 DNS 提供商 Certbot 插件 + +| 提供商 | 安装命令 | 配置方式 | +|:-------|:---------|:---------| +| Cloudflare | `python3-certbot-dns-cloudflare` | API Token | +| Route53 | `python3-certbot-dns-route53` | AWS IAM 凭证 | +| Alibaba Cloud | `certbot-dns-aliyun` | Access Key | +| Tencent Cloud | `certbot-dns-tencentcloud` | Secret ID/Key | +| GoDaddy | `certbot-dns-godaddy` | API Key/Secret | + +--- + +## 7. 与 Certbot 方案对比 + +### 7.1 方案对比表 + +| 特性 | ngx_http_acme_module | Certbot | +|:-----|:--------------------:|:-------:| +| **集成度** | 内置于 Nginx,无需外部工具 | 独立程序,需单独安装 | +| **配置复杂度** | 纯 Nginx 配置 | 需要额外配置和定时任务 | +| **HTTP-01 支持** | ✅ 原生支持 | ✅ 支持 | +| **DNS-01 支持** | ❌ 不支持 | ✅ 支持 | +| **通配符证书** | ❌ 不支持 | ✅ 支持 | +| **TLS-ALPN-01** | ✅ 支持 | ❌ 不支持 | +| **自动续期** | ✅ 自动,无需外部任务 | ✅ 需配置 cron/systemd timer | +| **证书热重载** | ✅ 无缝更新 | ⚠️ 需 reload/restart Nginx | +| **多 Web 服务器** | ❌ 仅 Nginx | ✅ Apache, Nginx 等 | +| **外部账户绑定** | ✅ 支持 EAB | ✅ 支持 | + +### 7.2 选择建议 + +**选择 ngx_http_acme_module:** +- 纯 Nginx 环境,追求配置简洁 +- 使用 HTTP-01 或 TLS-ALPN-01 挑战 +- 不需要通配符证书 +- 希望证书续期完全自动化 + +**选择 Certbot:** +- 需要通配符证书 +- 使用 DNS-01 挑战 +- 多 Web 服务器环境 +- 需要与外部系统集成 + +### 7.3 混合方案 + +结合两者优势:使用 `ngx_http_acme_module` 处理常规证书,使用 `certbot` 处理通配符证书: + +```nginx +# 常规域名使用内置 ACME +server { + listen 443 ssl; + server_name www.example.com api.example.com; + + acme_certificate letsencrypt; + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; +} + +# 通配符使用 certbot 证书 +server { + listen 443 ssl; + server_name *.example.com; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; +} +``` + +--- + +## 8. 完整配置示例 + +### 8.1 单站点基础配置 + +```nginx +# /etc/nginx/nginx.conf + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # DNS 解析器配置 + resolver 8.8.8.8 8.8.4.4 valid=300s; + resolver_timeout 10s; + + # ACME 共享内存 + acme_shared_zone zone=ngx_acme_shared:1M; + + # Let's Encrypt 配置 + acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-letsencrypt; + accept_terms_of_service; + + # 使用 ECDSA 账户密钥(更高效) + account_key ecdsa:384; + + # 启用证书验证 + ssl_verify on; + } + + # HTTP 服务器 - 处理 ACME 挑战和重定向 + server { + listen 80; + server_name example.com www.example.com; + + # ACME 挑战自动处理 + # 其他请求重定向到 HTTPS + location / { + return 301 https://$server_name$request_uri; + } + } + + # HTTPS 服务器 + server { + listen 443 ssl http2; + server_name example.com www.example.com; + + # 启用 ACME 自动证书 + acme_certificate letsencrypt; + + # 使用 ACME 变量 + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + # SSL 安全配置 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # OCSP Stapling + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate $acme_certificate; + + # 安全响应头 + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + root /var/www/example; + index index.html index.htm; + + location / { + try_files $uri $uri/ =404; + } + + # 静态文件缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 6M; + access_log off; + } + } +} +``` + +### 8.2 多站点生产配置 + +```nginx +# /etc/nginx/nginx.conf + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 2048; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + + # DNS 配置 + resolver 8.8.8.8 1.1.1.1 valid=300s; + resolver_timeout 10s; + + # ACME 配置 + acme_shared_zone zone=ngx_acme_shared:2M; + + # Let's Encrypt 生产环境 + acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:ssl@example.com; + state_path /var/cache/nginx/acme-letsencrypt; + accept_terms_of_service; + account_key ecdsa:384; + preferred_chain "ISRG Root X1"; + } + + # Let's Encrypt 测试环境 + acme_issuer letsencrypt_staging { + uri https://acme-staging-v02.api.letsencrypt.org/directory; + contact mailto:ssl@example.com; + state_path /var/cache/nginx/acme-staging; + accept_terms_of_service; + } + + # SSL 优化配置(共享) + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 1d; + ssl_session_tickets off; + ssl_stapling on; + ssl_stapling_verify on; + + # 通用安全响应头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # 站点配置目录 + include /etc/nginx/conf.d/*.conf; +} +``` + +```nginx +# /etc/nginx/conf.d/01-http-default.conf + +# HTTP 默认服务器 - 处理所有 80 端口请求 +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + # ACME 挑战处理 + location /.well-known/acme-challenge/ { + # 由 ngx_http_acme_module 自动处理 + root /var/cache/nginx/acme-challenges; + } + + # 所有其他请求重定向到 HTTPS + location / { + return 301 https://$host$request_uri; + } +} +``` + +```nginx +# /etc/nginx/conf.d/10-example.com.conf + +# 主站点 +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name example.com www.example.com; + + acme_certificate letsencrypt; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + + root /var/www/example; + index index.html index.php; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP 处理 + location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + location ~ /\.ht { + deny all; + } +} +``` + +```nginx +# /etc/nginx/conf.d/20-api.example.com.conf + +# API 站点 +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name api.example.com; + + acme_certificate letsencrypt api.example.com; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + ssl_certificate_cache max=2; + + add_header Strict-Transport-Security "max-age=63072000" always; + + # API 限流 + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req zone=api burst=20 nodelay; + + location / { + proxy_pass http://api_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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_cache_bypass $http_upgrade; + } +} + +upstream api_backend { + server 10.0.1.10:8080; + server 10.0.1.11:8080; + keepalive 32; +} +``` + +### 8.3 测试环境配置 + +```nginx +# 测试/开发环境使用 Staging + +acme_issuer letsencrypt_staging { + uri https://acme-staging-v02.api.letsencrypt.org/directory; + contact mailto:dev@example.com; + state_path /var/cache/nginx/acme-staging; + accept_terms_of_service; +} + +server { + listen 443 ssl; + server_name staging.example.com; + + # 开发环境使用 Staging + acme_certificate letsencrypt_staging staging.example.com; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + + # 注意:Staging 证书不会被浏览器信任 + # 仅用于测试续期流程 + + location / { + root /var/www/staging; + } +} +``` + +--- + +## 9. 故障排查指南 + +### 9.1 常见问题及解决方案 + +#### 问题 1:证书申请失败(验证失败) + +**症状:** +``` +[error] acme: challenge failed for example.com +``` + +**排查步骤:** + +1. **检查 DNS 解析** + ```bash + nslookup example.com + dig example.com A + ``` + +2. **检查 80 端口可访问性** + ```bash + # 从外部测试 + curl -I http://example.com/ + + # 检查防火墙 + sudo iptables -L -n | grep 80 + ``` + +3. **检查 Nginx 错误日志** + ```bash + sudo tail -f /var/log/nginx/error.log + ``` + +4. **验证挑战响应** + ```bash + # 手动测试挑战 URL + curl http://example.com/.well-known/acme-challenge/test + ``` + +**解决方案:** +- 确保域名 DNS 解析正确且已生效 +- 确保防火墙允许 80 端口入站连接 +- 确保 `server_name` 包含申请证书的域名 + +#### 问题 2:DNS 解析失败 + +**症状:** +``` +[error] could not resolve acme-v02.api.letsencrypt.org +``` + +**解决方案:** + +```nginx +# 检查 resolver 配置 +http { + # 使用可靠的 DNS 服务器 + resolver 8.8.8.8 8.8.4.4 1.1.1.1 valid=300s; + resolver_timeout 30s; + ... +} +``` + +**测试 DNS 解析:** +```bash +# 测试系统 DNS +nslookup acme-v02.api.letsencrypt.org + +# 测试 Nginx 配置语法 +nginx -t +``` + +#### 问题 3:速率限制错误 + +**症状:** +``` +[error] acme: 429 Too Many Requests +``` + +**排查:** +```bash +# 检查最近申请记录 +grep "acme" /var/log/nginx/error.log | tail -20 +``` + +**解决方案:** +- 切换到 Staging 环境进行测试 +- 等待当前速率限制窗口重置 +- 检查是否有重复申请配置 +- 使用现有证书而不是重新申请 + +#### 问题 4:证书不自动续期 + +**症状:** +证书过期但未自动续期 + +**排查:** +```bash +# 检查证书状态 +openssl x509 -in /var/cache/nginx/acme-letsencrypt/certs/example.com/cert.pem -noout -dates + +# 检查 Nginx 错误日志 +grep -i "acme\|certificate" /var/log/nginx/error.log +``` + +**解决方案:** + +```nginx +# 确保配置正确 +acme_issuer letsencrypt { + uri https://acme-v02.api.letsencrypt.org/directory; + contact mailto:admin@example.com; + state_path /var/cache/nginx/acme-letsencrypt; # 确保有写入权限 + accept_terms_of_service; +} +``` + +**检查权限:** +```bash +# 确保 Nginx 用户可以写入 state_path +sudo chown -R nginx:nginx /var/cache/nginx/acme-letsencrypt +sudo chmod 700 /var/cache/nginx/acme-letsencrypt +``` + +#### 问题 5:$acme_certificate 变量为空 + +**症状:** +``` +[emerg] BIO_new_file("$acme_certificate") failed +``` + +**解决方案:** + +```nginx +# 确保 acme_certificate 指令在 server 块中 +server { + listen 443 ssl; + server_name example.com; + + # 必须先声明 acme_certificate + acme_certificate letsencrypt; + + # 然后才能使用变量 + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; +} +``` + +### 9.2 调试配置 + +**启用详细日志:** + +```nginx +# 开发调试时使用 +error_log /var/log/nginx/acme-debug.log debug; + +acme_issuer letsencrypt_staging { + uri https://acme-staging-v02.api.letsencrypt.org/directory; + contact mailto:debug@example.com; + state_path /var/cache/nginx/acme-staging; + accept_terms_of_service; + + # 禁用服务器证书验证(仅测试时使用) + # ssl_verify off; +} +``` + +### 9.3 诊断脚本 + +```bash +#!/bin/bash +# diagnose-acme.sh - ACME 诊断脚本 + +echo "=== Nginx ACME 诊断 ===" +echo + +# 检查 Nginx 版本和模块 +echo "1. Nginx 版本和模块:" +nginx -V 2>&1 | grep -E "(nginx version|http_acme_module)" +echo + +# 检查配置语法 +echo "2. 配置语法检查:" +nginx -t +echo + +# 检查 DNS 解析 +echo "3. DNS 解析测试:" +nslookup acme-v02.api.letsencrypt.org 2>/dev/null || echo "DNS 解析失败" +echo + +# 检查证书目录 +echo "4. ACME 状态目录:" +for dir in /var/cache/nginx/acme-*; do + if [ -d "$dir" ]; then + echo " 目录: $dir" + echo " 大小: $(du -sh "$dir" 2>/dev/null | cut -f1)" + echo " 权限: $(stat -c %a "$dir" 2>/dev/null || stat -f %A "$dir" 2>/dev/null)" + echo " 所有者: $(stat -c %U:%G "$dir" 2>/dev/null || stat -f %Su:%Sg "$dir" 2>/dev/null)" + fi +done +echo + +# 检查现有证书 +echo "5. 现有证书:" +for cert in /var/cache/nginx/acme-*/certs/*/cert.pem; do + if [ -f "$cert" ]; then + domain=$(basename $(dirname "$cert")) + echo " 域名: $domain" + openssl x509 -in "$cert" -noout -dates -subject 2>/dev/null | sed 's/^/ /' + echo + fi +done + +# 检查端口监听 +echo "6. 端口监听:" +ss -tlnp | grep -E ":80|:443" | sed 's/^/ /' +echo + +# 检查防火墙 +echo "7. 防火墙状态:" +if command -v iptables &> /dev/null; then + iptables -L -n | grep -E "80|443" | sed 's/^/ /' +else + echo " iptables 不可用" +fi +echo + +# 检查错误日志 +echo "8. 最近的 ACME 相关错误:" +grep -i "acme" /var/log/nginx/error.log 2>/dev/null | tail -10 | sed 's/^/ /' +echo + +echo "=== 诊断完成 ===" +``` + +### 9.4 获取帮助 + +**官方资源:** +- Nginx ACME 模块文档:https://nginx.org/en/docs/http/ngx_http_acme_module.html +- Let's Encrypt 文档:https://letsencrypt.org/docs/ +- Let's Encrypt 社区:https://community.letsencrypt.org/ + +**测试工具:** +- SSL Labs 测试:https://www.ssllabs.com/ssltest/ +- Let's Debug:https://letsdebug.net/(诊断域名验证问题) + +--- + +## 附录:快速参考 + +### 配置检查清单 + +- [ ] DNS 解析器已配置(`resolver`) +- [ ] `acme_shared_zone` 已定义 +- [ ] `acme_issuer` 块配置正确 +- [ ] `acme_certificate` 指令在 `server` 块中 +- [ ] `ssl_certificate` 使用 `$acme_certificate` 变量 +- [ ] 80 端口服务器配置正确 +- [ ] state_path 目录可写 +- [ ] 防火墙允许 80/443 端口 +- [ ] 域名 DNS 已生效 + +### 常用命令 + +```bash +# 测试配置 +nginx -t + +# 重载配置 +nginx -s reload + +# 查看证书信息 +openssl x509 -in cert.pem -noout -text + +# 查看证书过期时间 +openssl x509 -in cert.pem -noout -dates + +# 手动清理状态(谨慎使用) +rm -rf /var/cache/nginx/acme-letsencrypt/certs/example.com +``` + +--- + +*文档版本:1.0* +*最后更新:2025年4月* diff --git a/docs/README.md b/docs/README.md index 1c694df..0d20d85 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,9 @@ | 27 | [安全深度指南](./27-nginx-security-deep-dive.md) | WAF/ModSecurity、DDoS 防护、OWASP Top 10、安全头部 | | 28 | [API 网关配置](./28-nginx-api-gateway.md) | API 路由设计、JWT 验证、限流配额、版本控制 | | 29 | [动态配置与服务发现](./29-nginx-dynamic-config.md) | 动态 upstream、etcd/Consul、dyups、nginx-unit | +| 30 | [njs JavaScript 模块](./30-nginx-njs-guide.md) | njs 引擎、js_import/js_set/js_content、Fetch API、共享字典 | +| 31 | [OpenTelemetry 可观测性](./31-nginx-observability.md) | ngx_otel_module、分布式追踪、Jaeger/Zipkin 集成、采样策略 | +| 32 | [ACME 自动证书管理](./32-nginx-acme-ssl.md) | ngx_http_acme_module、Let's Encrypt、自动续期、HTTP-01/DNS-01 挑战 | --- @@ -68,10 +71,16 @@ ### 扩展与第三方 - [第三方扩展模块](./22-nginx-third-party-modules.md) - NJS, Lua, Brotli, RTMP 等 - [Lua 模块深度指南](./26-nginx-lua-guide.md) - OpenResty、ngx_lua、cosocket +- [njs JavaScript 模块](./30-nginx-njs-guide.md) - njs 引擎、JavaScript 扩展 ### 安全深度 - [安全与访问控制](./09-nginx-security.md) - 综合安全配置 - [安全深度指南](./27-nginx-security-deep-dive.md) - WAF、DDoS、OWASP +- [ACME 自动证书管理](./32-nginx-acme-ssl.md) - Let's Encrypt、自动 SSL 证书 + +### 可观测性 +- [日志与监控](./08-nginx-logging-monitoring.md) - 访问日志、错误日志、stub_status +- [OpenTelemetry 可观测性](./31-nginx-observability.md) - 分布式追踪、Jaeger/Zipkin ### API 与动态配置 - [API 网关配置](./28-nginx-api-gateway.md) - API 路由、JWT、限流配额