1. 背景介绍
基于websocket的及时通信中,客户端与服务端建立ws连接后,服务端将业务继续传递到下一级业务服务系统Business server后,下一级服务系统处理完毕后,要将结果反馈给客户端,而此时的客户端ws服务器存在多个实例时,处理方式上存在几种策略:
1) 比较常见的是基于ip哈希;
2)基于参数的hash;
这些,一般情况下可以满足业务需求,但是呢,在ws服务动态增减的时候,可能就会出现消息丢失。例如:当前有3台websocket的tomcat服务器,有一个ws连接建立在tomcat1上面,基于轮询的模式下,且websocket前端采用了心跳机制时,客户端的重连机制会让这个客户端连接转而连接到其他两个tomcat当中之一,例如tomcat2,而客户是无感知的, 请参照websocket连接的后台反向代理问题。若这个时候Business server回调及时通信系统IM后(HTTP),不做特别处理,在轮询的方式下(或者hash方式),会出现消息去往的websocket的tomcat服务器不是这个回复消息该去往的tomcat服务器。导致回复消息无法到达该客户的websocket连接通道上,从而出现消息丢失的问题。
如上图所示:客户请求如红色线条,起初,经过LB-》nginx1-》websocket server1-》other business server. 但是处理过程中,websocket server1宕机了,或者其他什么原因,不能对外服务了,客户端的心跳机制,使得websocket重连,连到了websocket server2上了。此时,other business server处理完了客户的请求,回调IM系统,投递客户请求的答案,有可能出现上图绿色的箭头所示,这个时候,该客户的消息是没有办法投递出去的,因为websocket的长连接是在客户端和websocket server2.
即使还有一种,就是基于redis的订阅发布,进行消息回传给ws服务器,这种做法,要增加redis服务器的压力,且可能存在多次发布操作,性能不好,放弃。
这里要介绍的是,基于http的接口调用,通过url后面带上query信息,指定ws所在的服务器的ip和port信息。然后在nginx上做特殊配置,实现消息回传时,指定调用ws服务器集群中的服务器,因为这个服务器上存在websocket的连接实例。
nginx的配置如下:
upstream ims_svr {
server 10.130.215.143:;
server 10.130.215.144:;
} #用于BI回调IMS系统定位消息具体去往那个tomcat服务,涉及websocket的连接要原路来还要原路回去
upstream 10.130.215.1438080 {
server 10.130.215.143:8080;
}
upstream 10.130.215.1448080 {
server 10.130.215.144:8080;
} server {
listen ;
server_name localhost; #charset koi8-r; #access_log logs/host.access.log main;
default_type text/html; location / {
root html;
index index.html index.htm;
} location /IMS {
proxy_pass http://ims_svr;
proxy_set_header Host $host:$server_port;
proxy_set_header Remote_Addr $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
} location ~ /IMS/(replyMessage|callback) {
proxy_pass http://$arg_ip$arg_port;
proxy_set_header Host $host:$server_port;
proxy_set_header Remote_Addr $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
上面的配置中,技巧就在于获取query中的ip字段和port字段的值,拼接在一起构成upstream的block名称。在proxy_pass的代理下,就可以去往特定服务器,即连接有websocket连接的那个tomcat上。
注意:这里你可能会说,websocket server同样会出现宕机啊,在other Business server回调的时候,对的。我们可以在other Business server和websocket server之间搭建redis服务器,作为信息中心节点,通过visistorId+sessionid作为redis的key,ip_port值作为这个key对应的键值,当other Business server回调IM前,获取一下这个visistorId在当前sessionid下的websocket建立在那个websocket server上,即获取IP和Port信息。在回调的HTTP接口的URL部分附着query信息,例如?ip=1.1.1.1&port=8080.
如上图所示的案例,客户请求经过LB-》nginx1-》websocket server1-》other business server-》LB-》nginx2-》websocket server1-》resp去往客户端(基于websocket长连接),因为HTTP请求,在LB上是基于轮询的,所以,HTTP回调的response信息,也可能是经过nginx1的,这个时候,基于我们的nginx配置规则,依然会使得resp反馈去往websocket server1上,保障resp响应一定是在websocket连接建立的通道上回复给客户端,确保resp消息不会丢失。
测试案例截图:
到此,整个方案介绍完毕,是不是觉得nginx很厉害?的确,nginx现在的web应用领域占有率非常高。
另外,这里要注意一个小细节:就是nginx获取query部分的变量时,$arg_<xxx>这里的xxx部分,不要出现下划线“_”了,否则会导致query部分的变量获取不到,我经历过这个血的教训。
例如我开始的配置如下:
upstream ims_svr {
server 10.130.215.143:;
server 10.130.215.144:;
} #用于BI回调IMS系统定位消息具体去往那个tomcat服务,涉及websocket的连接要原路来还要原路回去
upstream 10.130.215.143_8080 {
server 10.130.215.143:;
}
upstream 10.130.215.144_8080 {
server 10.130.215.144:;
} server {
listen ;
server_name localhost; #charset koi8-r; #access_log logs/host.access.log main;
default_type text/html; location / {
root html;
index index.html index.htm;
} location /IMS {
proxy_pass http://ims_svr;
proxy_set_header Host $host:$server_port;
proxy_set_header Remote_Addr $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
} location ~ /IMS/(replyMessage|callback) {
proxy_pass http://$arg_ip_$arg_port;
proxy_set_header Host $host:$server_port;
proxy_set_header Remote_Addr $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
测试过程中,nginx后台报错如下:
access.log
10.130.207.217 - - [/Mar/::: +] "POST /IMS/replyMessage?ip=10.130.215.143&port=8080 HTTP/1.1" "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36"
error.log
// :: [error] #: * no resolver defined to resolve 8080, client: 10.130.207.217, server: localhost, request: "POST /IMS/replyMessage?ip=10.130.215.143&port=8080 HTTP/1.1", host: "10.130.207.217"
这里显然是$arg_ip_$arg_port值没有计算对。根本原因是$arg_ip_搞错了
切记,切记nginx的query的arg取值规则。