一 背景
最近,开发部门有一个访问需求,被访问方给了我们两个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的相关配置,最终形成完整的最佳解决方案,具体完整访问配置最佳解决方案如下:文章来源:https://www.toymoban.com/news/detail-806023.html
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的解析。文章来源地址https://www.toymoban.com/news/detail-806023.html
到了这里,关于通过nginx的upstream配置域名进行http/htts的访问最佳实践方案(406/404问题解决)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!