记一次nginx使用map后的uri丢失

nginx的map函数定义变量时, 使用http_proxy代理时, 会导致nginx的$args参数丢失

post thumb
Ops
作者 Louis 发表于 2020年12月30日

[TOC]

背景

偶然有一次机会使用nginx做灰度发布. 想到了nginx的map的功能, 于是使用map进行设置变量. 由于设置变量匹配后, 在反向代理过程中, 会丢失部分的$args及重写的$1/$2等自定义的参数. 这篇博客便是记录一下自己的解决方法.

灰度发布(又名金丝雀发布,英文一般称为GrayReleaseDark launch)是为了能够让用户逐步过渡到新功能一种发布方式。 一般是产品上线一个功能,希望在线上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。

我这边采用基于HEADER分流策略

  • 获取灰度标识,使用的是$http_version(即$http_header名)的写法;

采用map来实现

    map $http_version $group_staging_be_gateway_service {
        ~^v2$ staging_be_gateway_service_svr_v2;
        default staging_be_gateway_service_svr;
    }

    upstream staging_be_gateway_service_svr {
        dynamic_resolve fallback=stale fail_timeout=30s;
        server be-gateway-service.staging.svc.cluster.local:8000;
        keepalive 512;
    }

    upstream staging_be_gateway_service_svr_v2 {
        dynamic_resolve fallback=stale fail_timeout=30s;
        server be-gateway-service-v2.staging.svc.cluster.local:8001;
        keepalive 512;
    }

    server {
        listen 80;
        server_name api.fenghong.tech;

        location ~ ^/v1/(.+)$ {
            # add_header Access-Control-Allow-Origin *;
            # add_header access-control-allow-methods "GET,PUT,POST,DELETE";
            proxy_set_header        Host $host;
           
            # 添加http1.1声明和header中connection重写
            proxy_http_version 1.1;
            proxy_set_header Connection "";
          
            proxy_pass http://$group_staging_be_gateway_service/__api/$1?$args;
        }
    }

问题描述

使用上述这套配置后. 进行api访问. 直接加入header version: v2 模拟灰度访问. 发现 $1和$args全部丢失.

# louis @ LAPTOP-I2BJS52K in ~ [15:22:48]
$ curl 'https://api.fenghong.tech/v1/crm/member/agent_area/2/mappedByDistrictCode?contractId=1039' -H 'version: v2' \
>   -H 'authority: api.fenghong.tech' \
>   -H 'accept: application/json, text/plain, */*' \
>   -H 'authorization: Bearer 3799fd1e-74e2-3b92-a109-52b32d3b8026' \
>   -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' \
>   -H 'content-type: application/json;charset=UTF-8' \
>   -H 'origin: https://www.fenghong.tech' \
>   -H 'sec-fetch-site: cross-site' \
>   -H 'sec-fetch-mode: cors' \
>   -H 'sec-fetch-dest: empty' \
>   -H 'referer: https://www.fenghong.tech/' \
>   -H 'accept-language: zh-CN,zh;q=0.9' \
>   --data-binary '[440203]' \
>   --compressed
{"timestamp":"2020-12-29T07:22:48.541+0000","status":404,"error":"Not Found","message":"No message available","path":"/__api/"}

模拟正常访问去掉version: v2 这个header, 即进入正常访问, 这个时候配置居然没有丢失 $1和$args. 很纳闷.

# louis @ LAPTOP-I2BJS52K in ~ [15:25:48]
$ curl 'https://api.fenghong.tech/v1/crm/member/agent_area/2/mappedByDistrictCode?contractId=1039'   \
  -H 'authority: api.fenghong.tech' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'authorization: Bearer 3799fd1e-74e2-3b92-a109-52b32d3b8026' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' \
  -H 'content-type: application/json;charset=UTF-8' \
  -H 'origin: https://www.fenghong.tech' \
  -H 'sec-fetch-site: cross-site' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-dest: empty' \
  -H 'referer: https://www.fenghong.tech/' \
  -H 'accept-language: zh-CN,zh;q=0.9' \
  --data-binary '[440203]' \
  --compressed
{"code":"0","message":"success","data":{"440203":null}}

在预发布环境测试出来后, 要解决这个问题, 得重新写策略来匹配.

方案一

使用rewrite last, 这个会导致多请求一次匹配. 性能原始的proxy_pass稍低. 关于nginx rewrite策略的last. 我以前写过一篇文章. 建议可以看看nginx rewrite的break和last


        location ~ ^/v1/(.+)$ {
            # add_header Access-Control-Allow-Origin *;
            # add_header access-control-allow-methods "GET,PUT,POST,DELETE";
            proxy_set_header        Host $host;

            # 添加http1.1声明和header中connection重写
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            # 重写后去匹配 location ~ ^/__api {} 相关策略.
            rewrite ^/v1/(.*)$ /__api/$1?$args last;

            #proxy_pass http://$group_staging_be_gateway_service/__api/$1?$args;
        }

使用这个配置之后. 灰度环境, 访问立即正常.

# louis @ LAPTOP-I2BJS52K in ~ [15:28:48]
$ curl 'https://api.fenghong.tech/v1/crm/member/agent_area/2/mappedByDistrictCode?contractId=1039' -H 'version: v2'  \  
  -H 'authority: api.fenghong.tech' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'authorization: Bearer 3799fd1e-74e2-3b92-a109-52b32d3b8026' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' \
  -H 'content-type: application/json;charset=UTF-8' \
  -H 'origin: https://www.fenghong.tech' \
  -H 'sec-fetch-site: cross-site' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-dest: empty' \
  -H 'referer: https://www.fenghong.tech/' \
  -H 'accept-language: zh-CN,zh;q=0.9' \
  --data-binary '[440203]' \
  --compressed
{"code":"0","message":"success","data":{"440203":null}}

方案二

使用rewrite break , 直接请求proxy_pass.

        location ~ ^/v1/(.+)$ {
            # add_header Access-Control-Allow-Origin *;
            # add_header access-control-allow-methods "GET,PUT,POST,DELETE";
            proxy_set_header        Host $host;

            # 添加http1.1声明和header中connection重写
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            
            #重写后, 立即break, 然后进入 proxy_pass
            rewrite ^/v1/(.*)$ /__api/$1?$args break;
            proxy_pass http://$group_staging_be_gateway_service;
        }

请求示例

# louis @ LAPTOP-I2BJS52K in ~ [15:31:40]
$ curl 'https://api.fenghong.tech/v1/crm/member/agent_area/2/mappedByDistrictCode?contractId=1039' -H 'version: v2'  \
  -H 'authority: api.fenghong.tech' \
  -H 'accept: application/json, text/plain, */*' \
  -H 'authorization: Bearer 3799fd1e-74e2-3b92-a109-52b32d3b8026' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' \
  -H 'content-type: application/json;charset=UTF-8' \
  -H 'origin: https://www.fenghong.tech' \
  -H 'sec-fetch-site: cross-site' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-dest: empty' \
  -H 'referer: https://www.fenghong.tech/' \
  -H 'accept-language: zh-CN,zh;q=0.9' \
  --data-binary '[440203]' \
  --compressed
{"code":"0","message":"success","data":{"440203":null}}

显然. 方案二比方案一更加优化. 因此, 我选择方案二作为线上版本.

总结

这三种配置, 在不加入header version: v2的情况下. 使用curl访问都不会丢失 $1和$args. 只有在加入header version: v2的情况下, 原始配置会丢 $1和$args. 方案一和方案二的配置不会丢失 $1和$args.

参考文档

上一篇
记一次kubernetes v1.18.7 ingress访问异常问题及解决