问题背景
实现多活主备状态下,用户通过域名访问网站,避免用户访问到备站点;
总结一下:
- 同时存在多个nginx容器活跃,需要用户每次通过域名访问,都能访问到特定的nginx容器;
- 主备站点角色可能随着主备状态互换,即原来是主站点的,可能后来是备站点;
- DNS服务器为第三方提供,无法深度定制;
- 备机需要能够通过ip单独访问;

知识背景
DNS解析存在多级缓存(此处仅列举到DNS服务器层;后续流程不涉及)

DNS一对多域名的解析随机性
当一个域名对应多个IP时,DNS解析具有随机性;常用此做服务器的负载均衡;

DNS 域名解析负载均衡缺点:
- 目前的DNS是多级解析的,每一级DNS都可能缓存A记录,当某台服务器下线之后,即使修改了A记录,要使其生效也需要较长的时间,这段时间,DNS任然会将域名解析到已下线的服务器上,最终导致用户访问失败。
- 不能够按服务器的处理能力来分配负载。DNS负载均衡采用的是简单的轮询算法,不能区分服务器之间的差异,不能反映服务器当前运行状态,所以其的负载均衡效果并不是太好。
可能方案
为了实现问题,总结下来,只可能有2种解决方案:
- 在DNS解析层面,控制DNS解析结果
- 在DNS解析到备机后,提供备机跳转解决方案
方案一、dns解析层面控制DNS解析结果
1.1 通过内置DNS服务器控制IP解析(伪造DNS)
DNS服务器在实际使用场景中,均为客户提供的第三方DNS服务器,无法控制第三方DNS服务器解析的规则;且即使成功控制,由于存在浏览器缓存、本地缓存等多种不可控因素,不可避免还是会存在解析到备机IP,此方案不可行;
1.2、让备机nginx的状态能够影响DNS服务器解析
目前DNS服务器解析不会判断服务器状态。无法通过主备机的状态改变影响DNS服务器解析;
再者。由于存在浏览器缓存、本地缓存等多种不可控因素。不可避免还是会存在解析到备机IP;此方案不可行
2022.5.3更新:DNS服务器确实没法获取各个主机的状态,但是,浏览器却可以根据握手情况选择最优的服务器。
验证:新开一个域名对应2个IP地址(ip1,ip1),测试浏览器端访问结果。如果访问到ip1的服务器,则将ip1服务器nginx容器kill掉,亲测浏览器会解析到ip2;
如下图:

由于客户端先前DNS解析到131,故kill掉131后,刷新浏览器,在与131服务器建链失败后(推测是收到了[RST ACK]头),尝试与80(正常服务器)进行通讯,建立client hello;
故,不能通过nginx状态影响DNS服务器解析,但是可以通过nginx的状态影响浏览器DNS解析.
然而,我们的需求是通过nginx状态影响主备机的解析,无法直接kill掉备机,因为主备机都需要对外提供服务;所以需要进一步研究如何精确的控制nginx返回状态,在不停掉服务的情况下,让浏览器选择我们想让他选择的主机;
这种方案需要进一步验证;
要采用这种方案,思路应该是这样。
1、我需要找到一个控制点,在浏览器通过域名解析到达备机时的这个控制点的时候,能够让浏览器"知道"这个备机不可用,需要换一台服务器试试
2、由于备机要求能够通过ip访问,故我需要做访问控制,即这个控制点必须要能够进行条件判断。比如当我通过域名解析的时候,拒绝服务。而我通过Ip访问的时候,通过服务。
为了验证 1,方法是当浏览器解析到其中一台服务器时,尝试在该服务器nginx层 添加拒绝指令,以使浏览器觉得这个服务器不可用;我决定在nginx层进行相关控制,测试能否影响浏览器解析;
1.2.1 验证在nginx 层控制浏览器选择
指令通过如下方式实现;
server {
...
location / {
deny all;
return 503 ;
}
location /pathcanpass {
return 200;
}
}
查看抓包发现,浏览器与服务器交互的端口是443;由于我们的nginx容器是经过封装的,在server层的控制,监听的端口是8443(443端口经过linux操作系统转发过来的,所以中间还有一层没法控制的操作系统层转发);
server {
listen 8443:
deny all;
return 503 ;
}
控制层 | location | server |
---|
deny all ;return 503 | 失败 | 失败 |
deny all ;return 500 | 失败 | 失败 |
deny all ;return 403 | 失败 | 失败 |
deny all ;return 404 | 失败 | 失败 |
同时抓包分析,上述设置方式,无法使服务器发送[RST,ACK](连接重置)头:

只会给客户端发送[FIN,ACK]头(关闭连接)

补充发送RST 头的场景
1.connect一个不存在的端口;
2.向一个已经关掉的连接send数据;
3.向一个已经崩溃的对端发送数据(连接之前已经被建立);
4.close(sockfd)时,直接丢弃接收缓冲区未读取的数据,并给对方发一个RST。这个是由SO_LINGER选项来控制的;
5.a重启,收到b的保活探针,a发rst,通知b。
综上所述。在一对多域名解析情况下,浏览器进行轮询遍历,此时浏览器能够识别服务器最基本的端口状态,如果端口完全不存在,才会进行下一个IP的尝试,直到找到一个成功的ip并进行解析;
1.2.2 验证在防火墙端口层控制浏览器选择
这种方式是通过禁用443端口,来阻止一般用户用域名访问业务面服务,然后备机的其他维护型服务,由开发/运维通过指定端口访问。从而达到目的;应该有如下前提:
- 前提一、443端口阻止可以让域名无法解析到
- 前提二、维护服务端口转发需要带参数,让最终到达浏览器端时,脚本能够判断是来自于维护面访问,从而开放/屏蔽某些页面;
1、为了验证第一个前提,我们使用iptables禁用方法(8443即443端口,linux转发):
./iptables -w 5 -t filter -I FORWARD -p "tcp" -m multiport --dport "8443" -j REJECT
./iptables -w 5 -t filter -D FORWARD -p "tcp" -m multiport --dport "8443" -j REJECT
通过这种方式,测试当我将当前浏览器解析的战队加上443端口屏蔽时,可以引导浏览器尝试解析下一个ip;但是中间存在2个问题:
问题一、切换主备状态(端口禁用)浏览器解析特别慢。要持续近1分钟时间:

关于各个阶段的说明可以参考这篇文章:
https://juejin.cn/post/6844903801577537544
可以看到stalled阶段和 initail connection 阶段 耗时非常长;stalled阶段主要是TCP链接建立到真正可用的时间。我们猜想应该是发生了 tcp重传。
我们抓包发现
果然有大量重传;

initail connection 阶段是TCP3次握手时间。
后来修改屏蔽指令,指定 如下指令可以解决问题:
./iptables -w 5 -t filter -I FORWARD -p "tcp" -m multiport --dport "8443" -j --reject-with tcp-reset
问题二、切换主备状态(端口禁用)后,必须通过ctrl+f5刷新页面,否则存在部分资源用的是倒换前的缓存资源,如果2台服务器资源出现不一致,会导致页面出现不可预期的情况;

强制刷新页面可以解决;
解决上述两个问题后,我们就可以考虑使用端口屏蔽来影响浏览器解析了;那么方案就是这样的:
- 新增备站点访问端口,假设为8456;
- 主站点屏蔽8456,开放443端口,备站点屏蔽443端口,打开8456端口;
- 主备切换的时候,上述端口屏蔽规则互换;

方案二、在DNS解析到备机后,提供备机跳转解决方案
2.1 、在访问到备站点后,前端使用脚本/在备机nginx层进行域名重定向
访问到备站点的控制点,有3个;
- 第一个是请求到达备机nginx层,
- 第二个是备机页面脚本层(页面加载后)
- 第三个是前端登录后检查;
这三个检查点时间应该是越早,用户感知调整约少,对用户体验越好;
2.1.1 nginx层控制
请求到达nginx后,需要做跳转,前提需要知道主机IP。这里实现方案有2个:
- 集成njs或者lua模块进行Http请求(需要接口免鉴权)
- 倒换时手动刷新nginx配置 并进行reload
2.1.2 备机页面脚本层控制
当DNS解析到备机页面后,在页面能执行脚本的最早时机,通过免鉴权接口,获取主备状态进行跳转;
2.1.3 前端登录后检查层控制
当主备状态接口因安全原因无法免鉴权使用时,需要通过登录后脚本检查;此时需要做平滑重定向的方案有2个:
- 弹出页面,提示用户点击后跳转
- 登录的同时,获取主备状态,如果自己是备机,则重定向到主机ip,同时传递token,使用户无感知;前提:主备机token需要共享
2.1.4 问题:
不管上述哪种方式,都只能重定向到主机IP。无法重定向到域名。原因是域名重定向 还是会走DNS解析;如果解析到备机IP,会造成重定向死循环;如下图两种方案
DNS 模块包含浏览器缓存DNS、操作系统DNS和DNS服务器,指DNS解析过程
直接域名重定向
如图红线走向正常,蓝线就会导致重定向死锁

先ip重定向到主站点,再重定向到域名
如图红线走向正常,蓝线就会导致重定向死锁。
redirectflag是为了区分跳转请求和非跳转请求,只有对于跳转请求,才会重定向到domain.com;否则不跳转;

ip重定向(方案可行,不会造成死锁)

所以,按照这种方案,会造成一个致命问题,用户访问的是域名,结果浏览器地址栏可能会显示ip地址;
2.1.5 尝试解决ip地址反向转换域名
背景:浏览器输入ip地址 不会有DNS解析流程。为什么反查不了域名?
如果输入的是域名,先查找本地有没有缓存对应的地址和域名的映射,如果没有会将其发往DNS服务器查询对应的映射。也就是说getHostName可能会get到空值的情况,在这种情况下会自动向DNS服务器去反向查询域名,如果能查到就返回域名,查不到就直接把IP地址当成域名。由于安全问题(一开始我们用nslookup就可以看到返回的服务器并不是baidu,而是它的外壳),再加上网络过于复杂(IP相对域名来说太不稳定,域名经常好几年不变但是IP经常变化,导致了现在某个IP对应的域名过段时间或许就对到另一个域名上了;或者直接是域名不一样但是IP是一样的,这时候如果返回所有域名对安全性也没有保证),国内各大网站的服务器基本都不允许通过这种方式反向查询域名,所以基本上都查不到直接返回IP地址。但是有一个是绝对查得出来的,那就是本机上的IP地址(因为本机上IP的映射一定是保存在本机文件里面的,所以能找到)
尝试1 nginx 采用不同的方式重定向,看看是否有方法不改变浏览器域名
301-309状态码均尝试 不可使用;
尝试2 前端使用html5 history.replaceState特性修改地址栏
这种方法只能同域情况下修改url链接。但是无法跨域(ip切域名属于跨域)
该条路无法实现;
2.2 在nginx进行反向代理,代理所有备机请求到主机;
这种方案的思路是,利用nginx的反向代理,代理所有备机请求到主机,让用户访问到备机后,备机作为代理机,访问主机。从而让用户感知不到任何差异。

SMC3.0由于逻辑复杂,可能无法实现;
- 架构层面设计不合理:业务上,从只转发自身站点的请求,转变为需要转发到其他站点的请求;
- 实现层面:如何区分本站请求和域名转发的ip请求;通过备站点添加访问规则做条件代理,但是内部交互如果使用备站点ip,会导致请求被强制转发到主站点,要做大量适配;
2.3 新增nginx节点,DNS固定解析到改节点(1对1),再在该节点转发主备节点
这种方案,在不考虑容灾的一般架构下可以实现(使用nginx自带的负载均衡即可);但是对于考虑容灾的场景,新增的nginx节点无法做容灾管理。故不讨论;

三、结论
nginx反向代理在服务器架构简单的情况下,可以实现;不考虑容灾和架构破坏的情况下,nginx代理节点可以实现;如果对于备站点的访问方式没有强制要求,则可以通过443端口屏蔽,新开端口做为备站点访问方式,再在主备倒换的时候,进行端口屏蔽切换来实现;
2022.6.3日:打个广告,苏州华为终端BG面向社会招聘人才,Java /C C++ / Python / Javascript 。有兴趣来苏州的同学们 可以加我V 15850277051 ,有问必答!