Skip to content

Nginx 错误页

Nginx 的 error_page 指令允许你自定义错误响应页面,提升用户体验。

基本语法

nginx
error_page code ... [=[response]] uri;

常见配置示例

基本错误页面配置

nginx
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

指定完整路径

nginx
error_page 404 = /error_pages/404.html;

更改响应状态码

nginx
error_page 404 =200 /not_found.html;  # 返回404内容但状态码为200
error_page 404 = /not_found.html;     # 保持原始状态码404

使用命名位置

nginx
error_page 404 @not_found;

location @not_found {
    return 301 /new-location;
}

直接返回文本

nginx
error_page 403 /403.html;
location = /403.html {
    internal;
    return 403 'Forbidden: You don\'t have permission to access this page';
}

多错误码指向同一页面

nginx
error_page 400 401 402 403 404 /4xx.html;
error_page 500 501 502 503 504 /5xx.html;

变量动态生成错误页

nginx
error_page 404 @custom_error;

location @custom_error {
    add_header Content-Type text/html;
    return 404 "<html><body><h1>404 Not Found</h1><p>Requested URI: $uri</p></body></html>";
}

完整配置模板

nginx
# 全局块
user www;
worker_processes auto;           			# 工作进程数(通常设为CPU核心数)
error_log /var/log/nginx/error.log warn;  	# 错误日志路径及级别
pid /var/run/nginx.pid;          			# PID文件路径
worker_rlimit_nofile 65535;      			# 单个进程最大文件打开数

# 事件块
events {
  worker_connections 2048;      # 每个Worker的最大连接数
  multi_accept on;              # 是否一次性接受所有新连接
  use epoll;                    # 事件驱动模型(Linux推荐epoll)
}

http {
    # 隐藏版本信息
    server_tokens off;
    # 共享内存区域,用于多工作进程间状态同步
    upstream backend {
        zone backend_zone 64k;
        least_conn;              	# 最少连接算法
        ip_hash;					# 会话保持
        server 192.168.1.101:8080 weight=3 max_fails=3 fail_timeout=30s;
        server 192.168.1.102:8080 weight=2 max_fails=3 fail_timeout=30s;
        server backup.example.com:8080 backup;  # 备份节点
        
        keepalive 32;            # 连接池大小
    }
    
    # 增强日志格式
    log_format main '$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"';
    
    server {
        listen 80;
        server_name example.com;
        
        access_log /var/log/nginx/access.log main;
        
        # 静态资源缓存
        location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
            expires 3h;
            add_header Cache-Control "public";
        }
        
        # API速率限制
        location /api/ {
            limit_req zone=api_limit burst=200 nodelay;
            proxy_pass http://backend;
            
            proxy_connect_timeout 5s;     # 连接超时
            proxy_read_timeout 30s;      # 响应读取超时
            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_next_upstream error timeout http_500 http_502 http_504;
        }
        
        # 状态监控
        location /nginx_status {
            stub_status;
            allow 192.168.1.0/24;
            deny all;
        }
    }
    
    # 请求限制区域定义
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
}

Nginx 统一配置文件

在Nginx安装目录的conf中创建conf.d目录,所有的server块配置都放在conf.d目录下,文件以.conf结尾

nginx
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    include conf.d/www.conf; #虚拟网站配置信息统一放在了当前的conf.d目录下
}

Nginx 状态信息

确认安装模块

状态模块--with-http_stub_status_module在编译时指定。

bash
[root@localhost ]# nginx -V
nginx version: nginx/1.26.3
built by gcc 12.3.1 (openEuler 12.3.1-30.oe2403) (GCC) 
built with OpenSSL 3.0.12 24 Oct 2023
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx --user=www --group=www --with-http_stub_status_module --with-http_ssl_module

状态模块配置

nginx
server{
    listen 80;
    server_name status.com;
    location / {
    	stub_status on; #开启状态信息功能
    	access_log off; #不记录访问日志
    }
}

结果解析

bash
Active connections: 1 
server accepts handled requests
 47 47 45 
Reading: 0 Writing: 1 Waiting: 0
  • 活动连接(Active connections):表示当前 Nginx 正在处理的 活动连接数 为 1 个(包括正在读取、写入和等待的连接)
  • 连接统计 (server accepts handled requests)
    • accepts: 47 - 表示 Nginx 自启动以来 接受的客户端连接总数(TCP 连接)。
    • handled: 47 - 表示 Nginx 成功处理的连接数(通常等于 accepts,除非达到 worker_connections 限制)。
    • requests: 45 - 表示 Nginx 处理的 HTTP 请求总数(一个连接可以发送多个请求,如 HTTP Keep-Alive)。
  • 当前连接状态 (Reading, Writing, Waiting)
    • Reading (0): 当前有 0 个连接 正在读取客户端请求(Nginx 正在接收请求头或请求体)。
    • Writing (1): 当前有 1 个连接 正在向客户端发送响应(Nginx 正在返回数据)。
    • Waiting (0): 当前有 0 个连接 处于空闲状态(Keep-Alive 连接,等待客户端发送新请求)

数据库读写分离

将读请求分发到多个从库,写请求定向到主库

nginx
stream {
    upstream mysql_read {
        server read1.example.com:3306;
        server read2.example.com:3306;
    }
    
    upstream mysql_write {
        server write.example.com:3306;
    }
    
    server {
        listen 3306;
        proxy_pass mysql_read;
    }
}
  • 使用Nginx的stream模块实现TCP层负载均衡
  • 结合中间件(如MySQL Router)实现更智能的路由

Nginx 域名配置

本机业务

nginx
server {
    listen 8080;
    server_name www.example.com;
    location / {
        root html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
}

反向代理

nginx
server {
    listen 80;
    server_name www.example.com;
    location / {
        proxy_pass http://192.168.1.10;
    }
}

负载均衡

nginx
upstream example.com {
    server 192.168.10.2:8080;
    ...
}
server {
    listen 80;
    server_name www.example.com;
    location / {
        proxy_pass http://192.168.1.10;
        proxy_set_header host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

二级目录

服务端

nginx
server {
    listen 80;
    server_name www.example.com;
    location /article {
        root /data/wwwroot/www;
        index index.html;
    }
}

负载均衡配置

nginx
server {
    listen 80;
    server_name www.example.com;
    location /article {
        proxy_pass http://192.168.1.10;
        proxy_set_header host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

静态资源

nginx
server {
  location /page/ {
    alias  /home/user/web/page/dist/;
    try_files $uri $uri/ /page/index.html;
  }
}

当浏览器访问 https://example.com/page 时,nginx 会代理到 /home/user/web/page/dist/目录,此时,$uri 就表示 /page

try_files 会先检查 alias 目录下 page文件是否存在,存在则返回该文件;

然后会检查 page/目录是否存在,存在则返回 page/ 目录下的 index.xml(page 不带斜杠表示文件,page/ 带斜杠表示目录);

最后如果都找不到,则返回 /page/index.html ,此时 /page/表示相对目录,和上面的 location /page/表示一个意思,即 alias 表示的路径,所以 /page/index.html 完整的路径表示 /home/user/web/page/dist/index.html

alias 和 root 的区别?

  • alias 是一个目录别名的定义,可以直接替换,比如 /page/ 就表示 /home/user/web/page/dist/
  • root 是最上层目录的定义,访问 /page/ 则表示 访问 root 后面的目录 + /page/,即 /home/user/web/page/dist/page/

反向代理

单节点的反向代理配置,将 HTTP 请求转发的后端服务器,但是只能代理到单节点,无法在集群中使用

nginx
server {
  # 二级目录
  location /testapi/ {
    proxy_pass     http://178.168.1.10:9120/;
    index   /;      # index.html index.htm;
    proxy_set_header Host $host;
    # 表示与这个nginx 直接通信的IP, 如果这个是第一层代理,这将是最真实的客户端IP
    proxy_set_header X-Real-IP $remote_addr;
    # 表示与这个nginx 直接通信的Port, 如果这个是第一层代理,这将是最真实的客户Port
    proxy_set_header X-Real-Port $remote_port;
    # NGINX 在转发请求时会将客户端的 IP 地址添加到 X-Forwarded-For 头部
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    client_max_body_size 8M;
    client_body_buffer_size 128k;
    proxy_connect_timeout       10s; 
    proxy_send_timeout          60s;
    proxy_read_timeout          60s;
  }
}

负载均衡

负载均衡配置 用于将 HTTP 请求分布到多个节点的 后端服务器;负载均衡功能支持自动下线,当某一台节点服务端返回 500,则会自动下线该节点不再访问。负载均衡的配置可以单独写成 conf 文件。

nginx
upstream openserver-api {
	# ip_hash; 根据访问ip的hash结果分配 
	# least_conn;请求分配到连接数最少的服务
	# fair; 请求分配到后端响应的时间最短
	server 127.0.0.1:8900 weight=100 max_fails=10 fail_timeout=15s;
	server 127.0.0.1:8901 weight=100 max_fails=10 fail_timeout=15s;
}

server {
	location /test-api/{
		proxy_pass     http://openserver-api/;
		index   /;      # index.html index.htm;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Real-Port $remote_port;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		client_max_body_size 8M;
		client_body_buffer_size 128k;
	}
}
  • max_fails:出现N次失败,负载均衡停止转发流量
  • fail_timeout:在N秒内失败

Nginx 代理MySQL

确保 Nginx 编译时包含了 --with-stream 模块

nginx
stream {
    server {
        listen 3306;
        proxy_pass mysql_backend;
        proxy_connect_timeout 1s;
        proxy_timeout 3s;
    }

    upstream mysql_backend {
        server mysql_server1:3306;
        server mysql_server2:3306 backup;
    }
}

Nginx 代理Redis

nginx
stream {
    server {
        listen 6379;
        proxy_pass redis_backend;
    }

    upstream redis_backend {
        server redis1:6379;
        server redis2:6379;
    }
}

Nginx CORS 跨域

基础 CORS

nginx
location /api/ {
    # 允许所有域名跨域访问(生产环境不推荐)
    add_header 'Access-Control-Allow-Origin' '*';
    
    # 允许的请求方法
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    
    # 允许的请求头
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,Content-Type,Accept,Origin';
    
    # 预检请求缓存时间
    add_header 'Access-Control-Max-Age' 1728000;
    
    # 允许携带凭据(如cookies)
    add_header 'Access-Control-Allow-Credentials' 'true';
}

预检请求(OPTIONS)处理

nginx
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://client.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
    }
    
    # 正常请求处理...
}

多域名动态允许

nginx
# 使用map定义允许的域名列表
map $http_origin $allowed_origin {
    default "";
    "~^https://(www\.)?example\.com$" $http_origin;
    "~^https://app\.example\.org$" $http_origin;
    "~^https://staging\.example\.net$" $http_origin;
}


server {
    location /api/ {
        if ($cors_origin) {
            add_header 'Access-Control-Allow-Origin' $allowed_origin;
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
        
        # 处理OPTIONS请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
            add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,X-Requested-With';
            add_header 'Access-Control-Max-Age' 1728000;
            return 204;
        }
    }
}

安全增强

nginx
server {
    location /api/ {
        ...
        # 安全增强
        server {
            # 基础安全头
            add_header X-Content-Type-Options    "nosniff";
            add_header X-Frame-Options           "DENY";
            add_header X-XSS-Protection          "1; mode=block";

            # 增强安全头
            add_header Content-Security-Policy   "default-src 'self' https:; frame-ancestors 'none'";
            add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
            add_header Referrer-Policy           "strict-origin-when-cross-origin";

            # 隐私保护
            add_header Permissions-Policy        "geolocation=(), microphone=()";
            add_header Clear-Site-Data           "\"cache\", \"cookies\", \"storage\"";
        }
        ...
    }
}
HTTP 头作用
X-Content-Type-Optionsnosniff禁止浏览器自动推断 MIME 类型,防止 MIME 混淆攻击
X-Frame-OptionsDENY禁止页面被嵌入 <iframe>,防止点击劫持
X-XSS-Protection1; mode=block启用 XSS 过滤,检测到攻击时阻止页面加载
Content-Security-Policydefault-src 'self'限制资源加载来源,防止 XSS/数据注入
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload强制 HTTPS,防止 SSL 剥离攻击
X-Permitted-Cross-Domain-Policiesnone禁止跨域策略文件(如 Flash/PDF 的 crossdomain.xml
Referrer-Policyno-referrer-when-downgrade控制 Referer 头,防止 URL 泄露
Feature-Policymicrophone 'none'; camera 'none'禁用浏览器敏感 API(如摄像头、麦克风)
X-DNS-Prefetch-Controloff禁止 DNS 预解析,防止隐私泄露
Cross-Origin-Resource-Policysame-origin限制跨域资源加载
Cross-Origin-Opener-Policysame-origin防止跨窗口攻击(如 Spectre)
Cross-Origin-Embedder-Policyrequire-corp强制跨域资源使用 CORS 或 CORP
Permissions-Policygeolocation=(), microphone=()替代 Feature-Policy,控制浏览器 API 权限
Clear-Site-Data"cache", "cookies", "storage"强制清除客户端缓存、Cookie 和存储数据

隐藏版本号

在浏览器响应头里有 Server 项,这里返回的是当前服务 web 服务器的名称,如 Nginx、Kong 等。

但是这里不应该把服务器的版本号返回出来,万一版本有漏洞,会被针对攻击。

bash
# 1、关闭 http{} 的 server_tokens
server_tokens off;

#2、修改 ../nginx/conf/fastcgi_params 
fastcgi_params SERVER_SOFTWARE 
nginx/$nginx_version
# 改为 
fastcgi_params SERVER_SOFTWARE nginx

fastcgi_params 这个文件里的存放的都是 Nginx 环境变量。

日志 JSON 格式化

将 Nginx 的日志格式化为 JSON,可以修改 log_format 指令,将日志字段转化为 JSON 格式。

JSON 格式的日志,更适合日志聚合和后续处理,比如 ELK, Flume 等工具。

JSON 格式化配置:

nginx
http{
    log_format json '{
      "remote_addr": "$remote_addr",
      "connection": "$connection",
      "connection_requests": "$connection_requests",
      "remote_user": "$remote_user",
      "time_local": "$time_local",
      "request_length": "$request_length",
      "request": "$request",
      "status": "$status",
      "request_time": "$request_time",
      "upstream_response_time": "$upstream_response_time",
      "body_bytes_sent": "$body_bytes_sent",
      "content_length": "$content_length",
      "http_x_forwarded_for": "$http_x_forwarded_for",
      "upstream_addr": "$upstream_addr",
      "http_referer": "$http_referer",
      "http_user_agent": "$http_user_agent"
    }';

  # 日志格式可以引用main ,也可以引用 json
  access_log /var/log/nginx/access.log json;
    
}

确保将此配置添加到 http 块中,并设置 access_log 指令以使用这个 JSON 格式:

配置各种响应头

配置响应头是为了告诉浏览器某些操作,**比如禁止缓存、禁止iframe嵌套等,**Nginx 作为 Web 服务器,支持设置响应头。加在 server 里可以全局统一

nginx
location / {
  # 禁止浏览器缓存
  add_header Cache-Control "no-cache, no-store, must-revalidate";
  add_header Pragma "no-cache";
  add_header Expires 0;

  #该页面不允许在frame中展示,另外还有 SAMEORIGIN(表示该页面可以在相同域名页面的frame中展示),
  # ALLOW-FROM url(页面可以在指定来源的frame中展示)
  # 这个头加之前需要和前端同步
  add_header X-Frame-Options:DENY

  #启用XSS保护;
  add_header X-Xss-Protection: 1;
  add_header X-Xss-Protection: mod=block;

  # 严格按照 Content-Type指定的类型加载,直接猜测
  add_header X-Content-Type-Options "nosniff";

  #指定哪些域名可以访问当前资源
  add_header Access-Control-Allow-Origin "*";

  #防止浏览器在下载文件时自动执行内容(如 JS 文件)
  add_header X-Download-Options "noopen";

  # 默认只允许加载与页面相同源的资源,帮助防止跨站脚本攻击(XSS)
  add_header Content-Security-Policy "default-src 'self';" always;

  # 启用 HSTS,强制所有连接使用 HTTPS,禁止304 跳转
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

IP限流

我们系统上线后可能会遭遇外部刷接口、暴力测试等行为,尤其是权益类的服务,羊毛党的黄牛特别多。我之前有做过权益兑换系统,这个服务经常会被暴力刷接口,我们可以通过 Nginx 限流机制,熔断刷子流量,从而提升服务可用性。

在 Nginx 中,可以使用 限流(Rate Limiting)功能来控制进入服务器的请求数量。Nginx 提供了两个主要的限流模块:

  • limit_req:限制请求的速率(单位时间内请求的次数)。
  • limit_conn:限制并发连接数(每个 IP 地址允许的最大并发连接数)。

limit_req - 限制请求速率

limit_req 模块可以限制特定 IP 地址的请求频率。例如,限制每秒钟一个 IP 地址只能发出一定数量的请求。

nginx
http {
  # 定义一个限流区域
  limit_req_zone$binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;

  server {
    listen 80;
    server_name example.com;

    location / {
      # 使用上面定义的限流区域,限制请求频率
      limit_req zone=req_limit_per_ip burst=20 nodelay;
      # 设置请求超限时返回的页面
      error_page 503 /custom_503.html;
            
      # 处理正常请求
      root /usr/share/nginx/html;
      index index.html;
    }
  }
}

解释

  • limit_req_zone:定义一个名为 req_limit_per_ip 的限流区域。$binary_remote_addr 是客户端的 IP 地址,以二进制格式存储,可以减少内存消耗。rate=10r/s 限制每秒最多 10 次请求。zone=req_limit_per_ip:10m 表示存储在内存中,并且该区域的大小是 10MB,可以存储多个 IP 地址的限流信息。
  • limit_req:在具体的 location 配置中启用限流,使用定义好的 req_limit_per_ip 区域。burst=20 允许一定的请求突发,即在瞬间请求量超过正常限速时,最多可以允许 20 个请求超出限速。nodelay 表示一旦超出请求限制,就立即拒绝请求,而不是延迟处理。

limit_conn - 限制连接数

limit_conn 模块限制每个客户端(每个客户端都有 IP 地址)的并发连接数。这对于防止单个客户端占用过多服务器资源非常有效。

nginx
http {
  # 定义每个客户端(IP 地址)的最大连接数为 1
  limit_conn_zone$binary_remote_addr zone=conn_limit_per_ip:10m;

  server {
    listen80;
    server_name example.com;

    location / {
      # 限制每个 IP 的最大连接数
      limit_conn conn_limit_per_ip 1;

      # 处理正常请求
      root /usr/share/nginx/html;
      index index.html;
    }
  }
}

解释

  • limit_conn_zone:定义了一个名为 conn_limit_per_ip 的区域,用于存储每个 IP 地址的连接信息,zone=conn_limit_per_ip:10m 指定了 10MB 的存储空间。
  • limit_conn:在 location 配置中启用连接限制,限制每个 IP 地址只能有一个并发连接。

灰度发布

在软件部署高可用架构体系中,灰度发布是逐步将新版本的应用程序或功能推送到一部分用户,而不是一次性让所有用户都使用新的版本。Nginx 作为主流的高性能反向代理服务器,凭借其强大的流量控制和路由能力,成为灰度发布设计中不可或缺的核心组件。在 Nginx 中配置灰度发布,通常是通过基于 IP请求头cookieURL 参数 等的负载均衡策略来实现的。这可以确保新的版本只对一部分用户可见,直到测试完成并验证没有问题后,才会将所有流量切换到新版本。本文将讲解如何基于 Nginx 实现的几个灰度发布方案。

假设我们有两个 Tomcat 节点,一个是生产环境,一个是部署了新代码但还不能公开的环境。可以通过 Nginx 配置负载均衡和访问控制来实现灰度发布。

灰度发布方案

方案 1:基于请求头进行灰度发布

可以使用自定义的请求头(例如 X-GrayRelease)来控制流量的分配。这个请求头可以在测试人员访问时加上(如通过浏览器插件或代码),来决定请求发送到哪个节点。

nginx
http {
  upstream tomcat_prod {
    server tomcat-prod1.example.com:8080;
  }

  upstream tomcat_canary {
    server tomcat-canary1.example.com:8080;
  }

  server {
    listen80;
    server_name example.com;

    location / {
      # 检查是否存在 X-GrayRelease 请求头
      if ($http_x_grayrelease = "true") {
          # 如果请求头为 "true",则流量路由到测试环境
          proxy_pass http://tomcat_canary;
      }

      # 否则,流量路由到生产环境
      proxy_pass http://tomcat_prod;
    }
  }
}

说明

  • 当请求中带有 X-GrayRelease: true 请求头时,流量会被转发到 tomcat_canary(修改后的版本)。
  • 没有请求头或者请求头为其它值时,流量会默认转发到生产环境 tomcat_prod

另一种常见的方式是通过 Cookie 来控制访问。测试人员访问时,Nginx 会检查用户的 Cookie 是否有特定值,从而决定流量的路由。

nginx
http {
  upstream tomcat_prod {
    server tomcat-prod1.example.com:8080;
    server tomcat-prod2.example.com:8080;
  }

  upstream tomcat_canary {
    server tomcat-canary1.example.com:8080;
    server tomcat-canary2.example.com:8080;
  }

  server {
    listen80;
    server_name example.com;

    location / {
      # 检查请求中是否有灰度发布的 Cookie
      if ($cookie_grayrelease = "true") {
        # 如果 Cookie 为 "true",路由到测试环境
        proxy_pass http://tomcat_canary;
      }

      # 默认路由到生产环境
      proxy_pass http://tomcat_prod;
    }
  }
}

说明

  • 如果请求中带有 grayrelease=true 的 Cookie,流量将路由到 tomcat_canary(灰度版本)。
  • 没有该 Cookie 或 Cookie 值不为 true 时,流量会默认路由到生产环境 tomcat_prod

方案 3:基于 IP 地址进行灰度发布

如果我们想为特定 IP 地址的用户提供灰度版本(例如,测试人员的 IP 地址,最好是固定IP,通过 VPN 技术实现),可以基于 IP 地址进行路由。

nginx
http {
  upstream tomcat_prod {
    server tomcat-prod1.example.com:8080;
  }

  upstream tomcat_canary {
    server tomcat-canary1.example.com:8080;
  }

  server {
    listen80;
    server_name example.com;

    location / {
      # 基于 IP 地址路由
      if ($remote_addr = "192.168.1.100") {
        # 只有测试人员的 IP 地址才能访问测试环境
        proxy_pass http://tomcat_canary;
      }

      # 默认路由到生产环境
      proxy_pass http://tomcat_prod;
    }
  }
}

说明

  • 如果请求来源于 IP 地址 192.168.1.100(测试人员的 IP),流量将被路由到 tomcat_canary(灰度版本)。
  • 其他所有用户的流量都将路由到生产环境 tomcat_prod

部署与流程

在实际部署过程中,我们可以按照以下步骤进行灰度发布:

  1. 部署新版本到测试环境

    首先,将修改后的代码部署到 tomcat-canary 节点(灰度环境),确保其可以访问但不对所有用户开放。

  2. Nginx 配置路由策略

    根据你的灰度发布策略(请求头、Cookie、IP 等),在 Nginx 中配置流量的分配。你可以选择逐步增加灰度发布的比例,例如先让 10% 的流量访问 tomcat-canary,然后逐步增加,直到 100% 流量都切换到新版本。

  3. 测试和验证

    测试人员或 QA 团队可以通过特定的请求头、Cookie 或 IP 地址访问灰度版本(tomcat-canary),进行验证和测试。

  4. 正式发布

    一旦灰度版本通过测试,可以逐步将所有流量切换到新的版本。可以直接修改 Nginx 配置,将所有流量都路由到 tomcat-canarytomcat-prod,并最终清除灰度发布的控制条件。

nginx
http {
  upstream tomcat_prod {
    server tomcat-prod1.example.com:8080 weight=90;  # 生产环境,权重为 90
    server tomcat-canary1.example.com:8080 weight=10;  # 灰度版本,权重为 10
  }

  server {
    listen80;
    server_name example.com;

    location / {
      # 根据权重分配流量
      proxy_pass http://tomcat_prod;  # 默认路由到生产环境
    }
  }
}