Nginx流量镜像

3,409 阅读5分钟

流量镜像

流量镜像 (Traffic Mirroring),也称为流量影子 (Traffic Shadowing),是一种强大的、无风险的测试应用版本的方法,它将实时流量的副本发送给被镜像的服务。采用这种方法,您可以搭建一个与原环境类似的环境以进行验收测试,从而提前发现问题。由于镜像流量存在于主服务关键请求路径带外,终端用户在测试全过程不会受到影响。

nginx_http_mirror_module模块特性

利用 mirror 模块,可以将线上实时流量拷贝至其他环境同时不影响源站请求的响应,因为 Nginx 会丢弃 mirror 的响应

mirror 模块可用于以下几个场景:

  • 通过预生产环境测试来观察新系统对生产环境流量的处理能力
  • 复制请求日志以进行安全分析
  • 复制请求用于数据科学研究

将生产环境的流量拷贝到预上线环境或测试环境的好处:

  • 可以验证功能是否正常,以及服务的性能;
  • 用真实有效的流量请求去验证,又不用造数据,不影响线上正常访问;
  • 这跟灰度发布还不太一样,镜像流量不会影响真实流量;
  • 可以用来排查线上问题;
  • 重构,假如服务做了重构,这也是一种测试方式;

Nginx的流量镜像是只复制镜像发送到配置好的后端,但是后端响应返回到nginx之后,nginx是自动丢弃掉的,这个特性就保证了镜像后端的不管任何处理不会影响到正常客户端的请求

复制的镜像请求和原始请求是相关联的,只要镜像请求没有处理完成,原始请求就会被阻塞

基础配置

配置文件由两部分组成:后端服务与代理

image-20211229105458455

安装hey

#下载hey
wget https://hey-release.s3.us-east-2.amazonaws.com/hey_linux_amd64
#如果下载速度较慢,可使用xiaoz软件库链接
wget http://soft.xiaoz.org/linux/hey_linux_amd64
#赋予执行权限
chmod +x hey_linux_amd64
#移动文件到sbin目录
mv hey_linux_amd64 /usr/sbin/hey

image-20211229105657495

hey -n 10000 -c 100 -m GET http://localhost:8000 #对`https://www.qq.com/`发起100个GET并发请求,请求总数为10000个,执行完毕后hey还会打印统计信息
# `-n`:请求总数
# `-c`:客户端连接数
# `-m`:请求方法,比如`GET/POST`
hey -z 10s -q 1000 -n 100000 -c 1 -t 1 http://localhost:8000

image-20211229142709507

基础流量镜像配置

mirror模块配置分为两部分,源地址(original)和镜像地址(mirror) 。配置位置能够为nginx配置文件的http, server, location上下文

# original配置
location / { # 指定源uri为/缓存
    mirror /mirror; # 指定镜像uri为/mirror
    # 与proxy_request_buffering、fastcgi_request_buffering、scgi_request_buffering和 uwsgi_request_buffering冲突,
    # 一旦开启mirror_request_body为on,则请求自动缓存;code
    mirror_request_body off; # off | on:指定是否镜像请求body部分
    proxy_pass http://127.0.0.1:9502; # 指定上游server的地址
}
​
# mirror配置
location = /mirror {
    internal; # 指定此location只能被“内部的”请求调用,外部的调用请求会返回”Not found” (404)xml
    proxy_pass http://127.0.0.1:8081$request_uri; # 指定上游server的地址it
    proxy_set_header X-Original-URI $request_uri; # 设置镜像流量的头部io
}
​
# nginx支持配置多个mirror uri
location / {
    mirror /mirror1;
    mirror /mirror2;
    mirror_request_body off;
    proxy_pass http://127.0.0.1:9502;
}
​
location = /mirror1 {
    internal;
    proxy_pass http://127.0.0.1:8081$request_uri;
}
​
location = /mirror2 {
    internal;
    proxy_pass http://127.0.0.1:9081$request_uri;
}

示例:

upstream backend {
    server backend.local:10000;
}
​
upstream test_backend {
    server test.local:20000;
}
​
server {
    server_name proxy.local;
    listen 8000;
​
    location / {
        mirror /mirror; # mirror 指令制定镜像 uri 为 /mirror
        proxy_pass http://backend;
    }
​
    location = /mirror {
        internal; # internal 指定此 location 只能被“内部的”请求调用,外部的调用请求会返回 ”Not found” (404)
        proxy_pass http://test_backend$request_uri;
    }
}

image-20211229143718628

再次进行压力测试:

hey -z 10s -q 1000 -n 100000 -c 1 -t 1 http://localhost:8000

image-20211229144041939

Nginx 如何实现流量镜像

当请求到达 Nginx 时,如果 Nginx 开启了流量镜像功能,就会将请求复制一份,并根据 mirror location 中的配置来处理这份复制的请求。

复制的镜像请求和原始请求是相关联的,按照我的理解,只要镜像请求没有处理完成,原始请求就会被阻塞。

如果镜像请求响应很缓慢,原始请求就会被阻塞。

只复制流量的一部分

如果你不确定镜像后端是否能够正确处理原始请求,你可以只复制一部分流量到镜像后端

mirror 指令没有更多的配置项,它只会将所有的请求复制一份,并根据 mirror location 中的配置来处理请求,所以在 mirror 指令中做文章是行不通的,我们只能修改 mirror location 中的配置。修改后的配置文件如下:

upstream backend {
    server backend.local:10000;
}  
upstream test_backend {
    server test.local:20000;
}
  
split_clients $remote_addr $mirror_backend {  # 实现了只复制部分流量到镜像后端
    # split_clients 会将左边的变量 $remote_addr(requests remote address)经过 MurmurHash2 算法进行哈希,
    # 得出的值如果在前 `50%`(从 0 到 2147483500),那么 $mirror_backend 的值为 test_backend
    # 如果不在前 50%,那么 $mirror_backend 的值为空字符 ""
    50% test_backend; # 将流量复制到镜像后端
    *   ""; # 如果 `$mirror_backend` 变量的值为空字符串,就不复制流量
    # 因为镜像请求的错误响应并不会影响原始请求,所以丢弃镜像请求并返回错误响应是很安全的
}
   
server {
    server_name proxy.local;
    listen 8000;
    location / {
        mirror /mirror;
        proxy_pass http://backend;
    }
    location = /mirror { #请求会转发到 $mirror_backend 变量定义的后端,$mirror_backend 变量由 split_clients 配置块定义
        internal;
        if ($mirror_backend = "") {
             return 400;
        } 
        proxy_pass http://$mirror_backend$request_uri;
    }
}

这个方法的优点在于可以根据任何变量或变量组合来拆分镜像流量。如果你想真正区分用户,那么 remote_address 可能不适合作为拆分镜像流量的依据,因为用户可能会更换 IP。这时你最好使用用户粘性密钥来拆分镜像流量,例如 API key

根据请求中的 api key 来拆分镜像流量,只需要split_client 配置块中的 $remote_addr 改为 $arg_apikey

split_clients $arg_apikey $mirror_backend {
    50% test_backend;
    *   "";
}

使用ngx_http_mirror_module模块进行流量拷贝的配置技巧

Nginx复制GET及POST请求流量

server {
    listen    80;
    server_name kevin.com;
    # 源站配置
    location / {
        access_log /usr/local/nginx/logs/access.log accesslog;
        mirror /mirror;
        mirror_request_body on;
        proxy_pass http://kevin.upstream.name;
    }
    # 镜像站点配置
    location /mirror{
        internal;
        # 内部配置
        proxy_pass http://mirror.kevin.upstream.name$request_uri;
        proxy_pass_request_body on;
        proxy_set_header X-Original-URI $request_uri; #使用真实的url重置url    
    }
}

Nginx不允许复制POST请求流量

默认是支持POST流量复制的,需要通过下面配置来禁止。

server {
    listen    80;
    server_name kevin.com;
    # 源站配置
    location / {
        access_log /usr/local/nginx/logs/access.log accesslog;
        mirror /mirror;
        mirror_request_body off;
        proxy_pass http://kevin.upstream.name;    
    }
    # 镜像站点配置
    location /mirror{
        # 判断请求方法,不是GET返回403
        if($request_method != GET) {
            return 403;
        }
        internal; #内部配置        
        proxy_pass http://mirror.kevin.upstream.name$request_uri;       
        proxy_pass_request_body off;        
        # mirror_request_body和proxy_pass_request_body都设置为off,则Conten-length需要设置为"",否则有坑!        
        proxy_set_header Content-Length "";        
        proxy_set_header X-Original-URI $request_uri; # 使用真实的url重置url    
    }
}

拷贝流量放大

配置多分mirror镜像点

server {
    listen    80; 
    server_name kevin.com;   
    # 源站配置  
    location / {     
        access_log /usr/local/nginx/logs/access.log accesslog;      
        mirror /mirror;     
        # 多加一份mirror,流量放大一倍     
        mirror /mirror;      
        mirror_request_body on;      
        proxy_pass http://kevin.upstream.name;  
    }   
    # 镜像站点配置  
    location /mirror{    
        internal; # 内部配置       
        proxy_pass http://mirror.kevin.upstream.name$request_uri;   
        proxy_pass_request_body on;   
        proxy_set_header X-Original-URI $request_uri; #使用真实的url重置url    
    }
}

配置mirror镜像日志

mirror中不支持配置access_log,解决方法:mirror-location跳转到server,在server中配置accesslog。

server { 
    listen    80;   
    server_name kevin.com;  
    # 源站配置 
    location / {    
        access_log /usr/local/nginx/logs/access.log accesslog;    
        mirror /mirror;    
        mirror_request_body on;   
        proxy_pass http://kevin.upstream.name;  
    } 
    # 镜像站点配置   
    location /mirror{    
        internal; # 内部配置     
        # 跳转到下面的内部server   
        proxy_pass http://127.0.0.1:10992$request_uri;    
        proxy_pass_request_body off;     
        proxy_set_header Content-Length "";   
        proxy_set_header X-Original-URI $request_uri; #使用真实的url重置url   
    }
    server { 
        # server没法设置为内部 
        listen 127.0.0.1:10992; 
        location / {  
            # 判断放在server,使得post请求日志可以记录   
            if($request_method != GET) {  
            return 403;
        }    
        access_log /usr/local/nginx/logs/access.log accesslog;  
        proxy_pass http://mirror.kevin.upstream.name; 
    }
}

Nginx流量拷贝的注意事项

mirror镜像配置日志

镜像配置不正确,导致流量复制操作没正常执行。如果mirror镜像配置缺少日志,会严重影响调试。所以强烈建议配置镜像日志,配置方法如如上"配置mirror镜像日志"。部分错误配置的错误信息在在error日志中。

mirror_request_body/proxy_pass_request_body与Content-Length需配置一致

如果mirror_request_body或者proxy_pass_request_body设置为 off,则Content-Length必须设置为"",因为nginx(mirror_request_body)tomcat(mirror_request_body)处理post请求时,会根据Content-Length获取请求体,如果Content-Length不为空,而由于mirror_request_body或者proxy_pass_request_body设置为off,处理方以为post有内容,当request_body中没有,处理方会一直等待至超时,则前者为offnginx会报upstream请求超时;后者为off,tomcat会报异常