今天要介绍的问题,是一个相对来说比较经典的问题,问题表面看不是很复杂的问题,但是反映出的背后通信逻辑,其实还是比较有意义的。
websocket协议是当前绝大部分浏览器都支持的长连接协议,是HTTP协议的升级版协议,解决HTTP协议的单向通信的弊端,WS(websocket协议的简写)协议,基于长连接,实现客户端和服务端之间的双向通信,这个技术在我们项目里面,主要用来解决客服系统问题,即客户发送的消息以及坐席回复的消息之间,可以实时交互。关于这个协议的更多介绍,请参考官方网站,这里不多做介绍。
下面说下我们项目中,研究websocket连接,在集群环境下,如何解决后端tomcat上只有一个连接实例问题。我们的实验环境,topo架构如下:
结合上面的topo架构,客户端的请求,只有在涉及到websocket的连接的时候,才会从Client LB一直走到Server LB的后面tomcat上。除此之外,其他的客户端请求,只会到client app对应的tomcat即停止。
0.环境信息
1).客户端后台nginx,充当Client LB
这里的客户端,主要指类似聊天窗的渲染以及基本的和客户端相关的逻辑主体。我们的实验中,客户端,主要就是一个聊天窗口和附着在这个上面的辅助应用。
[root@localhost sbin]# ./nginx -V nginx version: openresty/1.11.2.2 built by gcc 4.1.2 20080704 (Red Hat 4.1.2-51) built with OpenSSL 0.9.8e-fips-rhel5 01 Jul 2008 TLS SNI support disabled configure arguments: --prefix=/usr/local/nginx --with-cc-opt=-O2 --add-module=../ngx_devel_kit-0.3.0 --add-module=../echo-nginx-module-0.60 --add-module=../xss-nginx-module-0.05 --add-module=../ngx_coolkit-0.2rc3 --add-module=../set-misc-nginx-module-0.31 --add-module=../form-input-nginx-module-0.12 --add-module=../encrypted-session-nginx-module-0.06 --add-module=../srcache-nginx-module-0.31 --add-module=../ngx_lua-0.10.7 --add-module=../ngx_lua_upstream-0.06 --add-module=../headers-more-nginx-module-0.32 --add-module=../array-var-nginx-module-0.05 --add-module=../memc-nginx-module-0.17 --add-module=../redis2-nginx-module-0.13 --add-module=../redis-nginx-module-0.3.7 --add-module=../rds-json-nginx-module-0.14 --add-module=../rds-csv-nginx-module-0.07 --with-ld-opt=-Wl,-rpath,/usr/local/luajit/lib --with-http_realip_module --with-pcre --add-module=/usr/local/openresty-1.11.2.2/bundle/ngx_cache_purge-2.3 --add-module=/usr/local/openresty-1.11.2.2/bundle/nginx_upstream_check_module-0.3.0 --with-http_ssl_module
2)服务端后台nginx,充当Server LB
这里主要指websocket消息的通信处理逻辑,即客户端收到客户的消息后要通过客户端后台反向代理推送给的目的服务地址。
[root@bogon nginx]# ./sbin/nginx -V nginx version: openresty/1.11.2.2 built by gcc 4.8.5 20150623 (Red Hat 4.8.5-11) (GCC) built with OpenSSL 1.0.1e-fips 11 Feb 2013 TLS SNI support enabled configure arguments: --prefix=/usr/local/openresty/nginx --with-cc-opt=-O2 --add-module=../ngx_devel_kit-0.3.0 --add-module=../echo-nginx-module-0.60 --add-module=../xss-nginx-module-0.05 --add-module=../ngx_coolkit-0.2rc3 --add-module=../set-misc-nginx-module-0.31 --add-module=../form-input-nginx-module-0.12 --add-module=../encrypted-session-nginx-module-0.06 --add-module=../srcache-nginx-module-0.31 --add-module=../ngx_lua-0.10.7 --add-module=../ngx_lua_upstream-0.06 --add-module=../headers-more-nginx-module-0.32 --add-module=../array-var-nginx-module-0.05 --add-module=../memc-nginx-module-0.17 --add-module=../redis2-nginx-module-0.13 --add-module=../redis-nginx-module-0.3.7 --add-module=../rds-json-nginx-module-0.14 --add-module=../rds-csv-nginx-module-0.07 --with-ld-opt=-Wl,-rpath,/usr/local/openresty/luajit/lib --add-module=/opt/nginx-rtmp-module-master --with-http_ssl_module
1.客户端应用对应的后台nginx的反向代理配置
upstream ims_client { server 10.90.9.20:7080; server 10.90.9.20:8080; } upstream ims_svr { server 10.90.7.10:9091; } server { listen 9090; server_name localhost; default_type text/html; proxy_send_timeout 3000; proxy_read_timeout 3000; location /IMS_SVR/websocket { 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 { proxy_pass http://ims_client; 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; } }
对应的客户端应用,在我本地开发机器上,启动了两个tomcat,一个端口7080,另外一个8080,也就是上面nginx配置中的ims_client部分,当客户端请求IMS这个客户端应用时,客户端的界面渲染以及在客户端本地能完成的工作,都在这个应用里面。只有客户端涉及到和websocket连接有关的请求,会被反向代理到ims_svr对应的服务入口上。如上面的nginx配置,ims_svr对应的服务入口,其实就是websocket后端服务端的负载均衡入口地址,这里10.90.7.10:9091其实就是另外一个nginx服务的入口地址。
2. 服务端应用对应的后台nginx的反向代理配置
upstream ims_svr { server 10.90.9.20:9080; server 10.90.9.20:9081; #hash $query_string; #hash $request_uri; #hash $arg_userId; } server { listen 9091; server_name localhost; default_type text/html; location /IMS_SVR/websocket { 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"; } }
这里,主要是将websocket的请求反向代理到后端具体的tomcat服务器上,websocket的对应tomcat服务器有2个,也是本地基于不同端口启动的9080和9081两个tomcat应用。
这次实验的目的,主要是想知道,nginx配置的轮询的方式做反向代理,WS连接,是否也会在后端服务器之间不停的切换,造成后端tomcat上存在多个连接实例peer,因为长连接通信,是存在两个对端实例的。若出现tomcat端出现多个客户端的连接对端peer,那么,就不符合我们业务的需要了,即浪费资源,又不便于逻辑管理。
我们的测试过程中,发现,不论客户端HTML5页面如何刷新,最终websocket总是在同一个tomcat上。这个是非常重要的结论。另外,当Server 端的tomcat停掉一个,比如9080对应的app停掉,客户端HTML5,因为websocket页面js里面添加心跳机制,实现重连,客户在无感知的情况下,websocket重新连接上来,接到了server app 9081上面来了。
客户端聊天界面如下:
通过这次实验得出如下结论:
1. 基于websocket长连接的通信,可以不用通过客户id等指定唯一字段信息进行hash来锁定客户端和websocket服务端之间的peer到peer的映射关系,这个映射关系,应该是在websocket的配置,即nginx中配置代码段中红色部分指定的。
2. 基于websocket的长连接的反向代理,客户端和服务端的反向代理nginx配置逻辑要一致,保证连接正常通信。
3. 基于websocket的长连接的通信,前端的业务层面的heartbeat心跳机制,是非常有用的,这个保证某个websocket后端服务出现异常的时候,客户可以无感知的继续得到服务。