通过nginx的upstream配置域名进行http/htts的访问最佳实践方案(406/404问题解决)

一 背景

​ 最近,开发部门有一个访问需求,被访问方给了我们两个https的域名访问接口,这里假设为:

https://aaa.target.com/my_target/login/
https://bbb.target.com/my_target/login/

​ 这两个域名解析出来的地址和接口信息都是一样的,但是根据要求,需要将两个域名访问接口作为主备的方式进行配置,在https://aaa.target.com/mytarget/login/出现异常不能使用的时候,能够动态切换到https://bbb.target.com/mytarget/login/访问域名接口。

​ 那么通过nginx来进行代理配置,首先想到的就是使用其负载均衡均衡的功能(upstream)对两个域名进行主备配置:

upstream mytarget
{
    server aaa.target.com:443 max_fails=30 fail_timeout=300s;
    server bbb.target.com:443 backup;
}

​ 以上配置,通过upstream创建了一个mytarget的访问池,访问该池的时候,首先会访问aaa.target.com:443,当访问30次失败后,该服务会停用300s,在300s之后重新尝试访问该地址;而当aaa.target.com:443访问失败到达30次后,服务停用,则会启用bbb.target.com地址用于访问。

​ 在server中,最初的配置如下:

server {
        listen       8901;
        server_name  target.server;
        
        location /login/ {
            proxy_pass https://mytarget/my_target/login/;
        }
   }

​ proxy_pass中只需要访问upstream访问池即可。

​ 但是通过实际情况对该网址进行访问(curl http://localhost:8901/login/),却返回了406 Not Acceptable错误。

​ 而当我们不使用upstream的方式进行请求的时候:

server {
        listen       8901;
        server_name  target.server;
        
        location /login/ {
            proxy_pass https://aaa.target.com/my_target/login/;
            #proxy_pass https://bbb.target.com/my_target/login/;
        }
   }

​ 请求(curl http://localhost:8901/login/))则可以顺利完成,HTTP1.1返回200代码。

​ 经过很长时间的分析和测试,最终还是无法解决该问题,故想到了通过“二级跳”的方式变向实现(经过测试,server里面如果是ip,也可以访问的通):

    upstream target {
        server 127.0.0.1:18901;
        server 127.0.0.1:18902 backup;
    }
    server {
        listen       8900;
        server_name  mytarget.server;

        location /login/ {
            proxy_pass   http://target/;
            proxy_next_upstream error timeout http_404 http_403;
        }

    }
    server {
        listen       18901;
        location / {
            proxy_pass   https://aaa.target.com/my_target/login/;
        }

    }
    server {
        listen       18902;
        location / {
            proxy_pass   https://bbb.target.com/my_target/login/;
        }

    }

​ 以上过程也很好理解,即分别对我们需要的https://aaa.target.com/my_target/login/和https://bbb.target.com/my_target/login/地址进行代理,通过本机的18901和18902端口提供服务;而upstream再对本机的18901端口和18902端口进行负载均衡(主备配置),然后再通过本机的8900端口代理访问127.0.0.1的18901和18902端口,最终实现访问https://aaa.target.com/my_target/login/或https://bbb.target.com/my_target/login/

​ 但二级跳的访问方式也具有一些缺陷,这个缺陷主要反映在我的日志可读性上,我们的当前http访问的日志格式如下:

log_format  main  '$remote_addr - $http_referer - $upstream_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$http_cookie" "$upstream_connect_time" "$upstream_response_time" "$request_time"';
    access_log    logs/access.log    main;

​ 也就是说我们在日志中会显示出 u p s t r e a m a d d r ,即被访问端解析出的地址,从而方便我们做一些问题排查依据和手段,但采取“二级跳”的方式,则导致我的 upstream_addr,即被访问端解析出的地址,从而方便我们做一些问题排查依据和手段,但采取“二级跳”的方式,则导致我的 upstreamaddr,即被访问端解析出的地址,从而方便我们做一些问题排查依据和手段,但采取二级跳的方式,则导致我的upstrema_addr显示的均为127.0.0.1:18901/18902,反而给我们的日志观察造成不便。

因此,最好的方式还是解决直接通过域名做负载均衡的访问问题,才能最好的达到我们的要求。但是看似合理的操作,为什么会产生406的问题?406的问题又该如何解决?

二 分析思路

​ 对于406这个问题的分析,我们首先要知道HTTP1.1返回406 Not Acceptable代码代表什么意思?根据资料解释:

http返回406错误的时候,往往说明是客户端错误 , 即客户端无法解析服务端返回的内容,一般是说在客户端发送的accept头里 , 设置了允许接受的类型 , 但是服务端没有按该格式返回,如果accept指定的类型和response返回的content-type类型不一致,会出现406 not acceptable错误。

​ 而针对该问题的解决方式有两种:

1.修改服务端按指定格式返回
2.修改客户端接受服务端的格式

​ 此时,我们可以通过curl -vvv的方式详细显示访问请求及返回代码:

​ 返回成功时侯的content-type:

在这里插入图片描述

​ 返回失败时候的content-type:
在这里插入图片描述

​ 此时可以看出,当返回406的时候,content-type返回的是text/html格式,而不是正确的application/json格式(其实,该格式是指返回后的内容的格式)。

​ 那么此时其实可以理解为当访问返回406的时候,我们在客户端想向对方的服务器端请求包头中的content-type为json格式,但是最终服务器端并没有找到我们想要的请求内容所以反馈了一个406 Not Acceptable的html。

起初,认为是由于客户端向服务器发送的包头请求类型不对,所以导致服务器返回的content-type也不对,最终产生406错误。因此,着重研究了如何让nginx强制向服务端请求我们想要的content-type,详细配置如下:

upstream mytarget
{
    server aaa.target.com:443 max_fails=30 fail_timeout=300s;
    server bbb.target.com:443 backup;
}


server {
  listen       8901;
  server_name  mytarget.server;
  location /login/ {
      type {} default_type application/json;
      add_header Content-Type 'application/json; charset=utf-8';
      proxy_pass https://mytarget/my_target/login/;
   }

​ 该配置中,实现强制指定nginx请求content-type的部分主要为:

type {} default_type application/json;
add_header Content-Type 'application/json; charset=utf-8';

​ 在nginx中,http层面的配置默认的content-type是application/octet-stream,charset=utf-8,所以在location中type{ }表示会先将默认的content-type置空,然后通过defalut_type将content-type改为application/json,而add_header Content-Type则表示直接在请求头中直接指定Content-type为 ‘application/json; charset=utf-8’;(这里还需要注意,想要强制生效,我们必须还要修改http引入的mime.types文件,需要在mime.type文件中加入application/json json;的配置,否则可能不生效,经过检查在我们配置中,之前就已经加入了)

​ 在这样设置后,再次进行模拟访问尝试(curl -vvv http://localhost:8901/login/), 发现http返回依然是406,而客户端返回的content-type还是text/html。最开始我一直认为是配置未生效,直到我在请求中加入了一段配置:

server {
  listen       8901;
  server_name  mytarget.server;
  location /login/ {
      type {} default_type application/json;
      add_header Content-Type 'application/json; charset=utf-8';
      return 200 '{"status":"success","result":"nginx json"}'
      proxy_pass https://mytarget/my_target/login/;
   }

​ 再次模拟访问尝试(curl -vvv http://localhost:8901/login/),发现nginx返回了200,且返回的内容也是我们定义的’{“status”:“success”,“result”:“nginx json”}',而这也说明我们的配置生效了。但是为什么直接访问地址还是不行呢?
在这里插入图片描述

​ 通过资料查询,我知道了,nginx在做代理转发的时候,会自行将前段请求请求头进行处理,并根据我们处理结果,向服务端发送请求。那么当我们请求头处理异常时,则会导致nginx转发到后段的请求由于服务器无法正确相应,返回406、400、404等错误。

​ 所以,我认为处理该问题最好的办法就是让nginx按照我们想要的方式处理前段发来的请求头,那么处理请求头该如何设置呢?在nginx中有一个参数,即proxy_set_header。

​ 该参数可以根据我们的需求设置请求头,而这里最终要的一个即为proxy_set_header HOST。在nginx官方指导文档中,proxy_set_header HOST有几种写法:

proxy_set_header HOST $host
proxy_set_header HOST $proxy_host
proxy_set_header HOST $host:$proxy_port
proxy_set_header HOST $http_host

这里,对几种方法的解释如下:

1.不设置proxy_set_header Host时,浏览器直接访问nginx,获取到的Host是proxy_pass后面的值,即 $proxy_host的值;
2.设置proxy_set_header Host $host时,浏览器直接访问nginx,获取到的Host是$host的值,没有端口信息,此时代码中如果有重定向路由,那么重定向时就会丢失端口信息,导致 404;
3.设置proxy_set_header Host $host:$proxy_port时,浏览器直接访问nginx,获取到的Host是 $host:$proxy_port的值;
4.proxy_set_header Host $http_host时,浏览器直接访问nginx,获取到的Host包含浏览器请求的IP和端口;

​ 此时,则可以知道,我们在对HOST设置不同变量的时候,则会封装不同的请求头,当请求头在远端服务器找不到的时候,则无法访问。

​ 由于在不设置proxy_set_header HOST的时候,默认时取proxy_pass后面的值,那此时其实向服务器端请求的时候,我们认为是向mystarget的upstream池中的地址发出请求,实际上则是向服务器真实请求了mytarget,而在服务器端根本不存在mytarget这个请求内容,所以会返回406这种无法处理客户端请求的消息。

​ 搞清楚原理后,我们则只需设置对应的HOST到请求头中就可以解决该问题了,但是发现我们设置了官方给的方式,都不能达到效果。

那么,这里就需要介绍nginx中proxy_set_header的隐藏用法(这个经过网上鲜有的资料和多次尝试以后,得出的结论):

​ 1.在proxy_set_header HOST后,我们可以直接加upstream中明确的域名地址:aaa.target.com,即

upstream mytarget
{
    server aaa.target.com:443 max_fails=30 fail_timeout=300s;
    server bbb.target.com:443 backup;
}
server {
        listen       8901;
        server_name  mytarget.server;
location /login/ {
            proxy_set_header Host aaa.target.com;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_pass https://mytarget/my_target/login/;

​ 该方式找了很久,找到两篇帖子这样设置,跟着设置后,发现返回成功。

​ 继续查阅资料发现,另外一篇帖子上虽然也是用上述方法,但是进行了更加详细的解释。大概意思是,proxy_set_header Host就是向服务器请求vhost的server_name,我们不能将该参数写成$http_host,否则请求的则是我们代理的server_name,即mytarget.server。同样,在服务器端并没有mytarget.server的vhost,对方服务器的vhost是aaa.target.com/bbb.target.com。

根据上述资料理解,其实可以理解:

1.当我们不配置HOST,则客户端会向服务端请求mytarget;

2.当我们配置HOST为$host的时候,则会向对方请求本机的ip或者域名,且不带端口;

3.当我们配置为 h o s t : host: host:proxy_prot的时候,则会向对方请求本机的ip或者域名,且带端口;

4.当我们配置为$http_host的时候,则会向对方请求我们设置的server_name,即target.server;

5.当我们配置为指定的aaa.target.com/bbb.target.com时,则会向对方请求响应的vhost;

​ 综上,只有第五种的配置可以实现我们向服务器发出正确的请求,而其他四种配置都无法在服务端找到正确的vhost,从而导致返回出现406或者404错误。

​ 可是,这种方式也存在问题。在我们的场景下,需要有两个域名,而这里我们只能使用一个域名,那么当aaa.target.com不可用的时候,需要请求bbb.target.com,但是proxy_set_header HOST依然回去请求aaa.target.com,两者不一样,则无法返回正确的值,则依然返回406,此时我们则需要手动进行调整,非常麻烦。

​ 于是我想通过第四种方式,将我们的server_name设置为我需要的aaa.target.com/bbb.target.com,然后使用$http_host,但发现还是不行(这里与查询到的帖子写的有所不符,不知道为什么)。

​ 最后,经过多次尝试,发现了proxy_set_header的隐藏用法:

​ 2.在proxy_set_header HOST后,我们可以直接加upstream中将server_name以变量的形式带入,而在server_name中,我们则可以写入我们需要请求的vhost(而且可以写多个),即server_name aaa.target.com bbb.target.com。具体写法如下:

upstream mytarget
{
    server aaa.target.com:443 max_fails=30 fail_timeout=300s;
    server bbb.target.com:443 backup;
}
server {
        listen       8901;
        server_name  aaa.target.com bbb.target.com;
location /login/ {
            proxy_set_header Host $server_name;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_pass https://mytarget/my_target/login/;

​ 经过模拟访问尝试,发现这种写法可以成功通过upstream的方式访问http/https。

三 问题解决方案

​ 在经过长时间的查阅资料、学习、理解和测试中,最终我们找到了nginx主备方式访问域名解决方案,为了更好的配合nginx代理作用,避免域名对应的ip改变,由于nginx解析缓存,导致nginx无法访问,我又加入了动态解析dns的相关配置,最终形成完整的最佳解决方案,具体完整访问配置最佳解决方案如下:

    upstream mytarget {
        server aaa.target.com:443 max_fails=10 fail_timeout=300s;
        server bbb.target.com:443 backup;
    }


    server {
        listen       8901;
        server_name  aaa.target.com bbb.target.com;
        resolver 61.139.2.69 valid=10s ipv6=off;

        location /login/ {
            #default_type application/json;
            #add_header Content-Type 'application/json; charset=utf-8';
            proxy_set_header Host $server_name;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
            set $cmpassport_addr https://mytarget/my_target/login/;
            proxy_pass $cmpassport_addr;
        }
   }

​ 想要动态解析,我们必须要将proxy_pass访问地址设置为变量,在变量中指定我们的具体访问地址,然后再加上resolver配置即可。这里的resolver表示通过61.139.2.69(四川电信)的dns,每10s中动态解析一次server中的域名,且关闭ipv6的解析。

  • 35
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值