一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

浅谈 Websocket 和反向代理实践

时间:2016-08-23 编辑:简简单单 来源:一聚教程网


目录

什么是 Websocket
为什么使用 Websocket
性能理论分析
Websocket 服务端和反代实践
对反代性能进行测试
Websocket 和 HTTP
什么是 Websocket

Websocket 是起初由 HTML5 定义的一个建立在单 TCP 连接上的全双工通信协议,后从 HTML5 规范独立并由 RFC 6445 标准化,但仍被习惯性地称为 HTML5 Websocket。

Websocket 工作在 HTTP 的80和443端口并使用前缀ws://或者wss://(with ssl) 进行协议标注,但是实际上这个协议和 HTTP 并没有什么关联性,参见 RFC 6455 Section 1.7 的说明,在建立时,使用 HTTP/1.1 101状态码进行协议切换,当前标准不支持两个客户端之间不借助 HTTP 直接建立 Websocket 连接,参见 StackOverflow:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
服务端返回响应:


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

为什么是 Websocket

由于基本的 HTTP 是半双工的请求-响应的模式,客户端发起一个请求后服务端才能返回一个响应,在设计之初是没有考虑服务端主动推送数据、没有考虑数据实时更新的情况的;在 Websocket 出现之前,为了保持数据的实时更新和服务端主动推送数据给客户端,最早也是最简单粗暴的实现是让客户端不断发送请求(即轮询),后来出现了 Comet 模型和 SSE(Server Sent Event),Comet 是让服务器在没有接到浏览器显式请求的情况下,实现推送数据的一类技术的总称,是一类模式而不是一个标准,其原理是大多都是服务端延迟完成 HTTP 的响应,又可以分为长轮询和流两种实现方式。

Polling(轮询):客户端定期发起请求,无论请求的内容是否存在,服务器马上返回响应。对于出现时间不可预测的内容,轮询效率很低。
Long-Polling(长轮询):包括插入 标签实现长轮询和 XMLHttpRequest(XHR)客户端发送请求,服务器保留连接和请求持续一段时间,如果需要的内容在预定时间内出现则返回,如需要的内容没有出现,服务器将关闭请求。但是当消息量较大时,长轮询相比轮询没有性能提升,反而可能因为需要维护长连接造成性能比轮询更差。
Streaming(流):包括隐藏 iframe 和 XHR,客户端发送请求,服务器保持连接,并持续发回响应,如 Google talk 使用了嵌入隐藏 iframe,而 Gmail 使用的是 XHR。
但是这些技术都是基于 HTTP 的,而 HTTP/1.1 的标准 RFC 7230 并不建议客户端打开过多的连接,基于 HTTP 的传输技术不仅实现繁杂、开销较大(比如频繁的 TCP 握手和 HTTP header 传递),而且总是会受到 HTTP 单向传输和连接数的限制。

一言蔽之,HTTP 不是为实时全双工的目的而设计的协议,在 HTTP 的基础上模拟全双工制约太大,因此要提高性能一个独立于 HTTP 的协议是必要的,使用独立的协议可以完全摆脱这些限制。

性能分析

先前提到,在 HTTP 下实现双向持续通信一大问题就是 HTTP 有 header,有时 header 比需要传输的消息本身还大,Websocket 刚被提出时,号称只有2字节的额外开销,能将 HTTP 的延迟降低到三分之一,一个很大的原因就是 Websocket 有很小的 overhead,payload 大小不同的时候的开销如下表:

PAYLOAD CLIENT-TO-SERVER SERVER-TO-CLIENT
< 126 6 2
< 64k 8 4
< 2**63 12 8
此外,Websocket 允许传输二进制内容,减少了转码的开销。

关于性能开销的分析参考了 http://tavendo.com/blog/post/dissecting-websocket-overhead/

可以看到,大体的结论是,TCP 上的开销比 Websocket 本身的开销要大的多。

在 Websocket 和 Comet 的对比测试中,Websocket 针对大量用户请求很小的消息这样一个场景,比轮询极大的减少了非必需的网络传输,提高了带宽利用率:


nginx 官方也做过 nginx 对 Websocket 反代的性能测试:NGINX WebSocket Performance

搭建 Websocket 服务端和代理服务端

关于 Websocket 的服务端实现,W3C 给出了 Websocket API,但是到现在实现 Websocket 的通用服务端较少,考虑到 Websocket 这个东西更多的是工具而不是目的,一个纯粹的 Socket Server 实际上并没有什么实际作用,更多的是实现了 Websocket 封装的后端语言的库,JS 有 socket.io、Python 有 tornado 都实现了 Websocket 库。

Websocket 的实现方式是通过发送 Upgrade 头,但是 Upgrade 和 Connection 都是逐跳(hop-by-hop)的 header,即只在一跳内有效而不会被传递到源站,这个 header 在正常情况下经过代理的时候会被去掉,因此需要在反代服务器上做一点设置。

Nginx 在博客上给出了一个实践样例:Using Nginx as a Websocket Proxy

Forward Proxy: 前向代理模式下,客户端将主动识别代理并使用 CONNECT 方法让服务器 向源站打开隧道避免此问题;
Shell

CONNECT example.com:80 HTTP/1.1
Host: example.com

Reverse Proxy: 反向代理模式下,由于客户端不能察觉代理的存在,nginx 从1.3.13版本之后使用了一个特殊的模式,当从源站接到 HTTP 101 时,nginx 将维持客户端和服务端之间的连接,因为 Upgrade 头不能传递到后端,需要在手动添加 header:
Shell

location /chat/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}


一个更加优雅的解决:使用 map 指令,map 是ngx_http_map_module中的指令,可以将变量组合成为新的变量,下面的配置根据客户端传来的连接中是否带有 Upgrade 头来决定是否给源站传递 Connection 头:
Shell

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        ...

        location /socket/ {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }


默认情况下,连接将会在无数据传输60秒后关闭,proxy_read_timeout 参数可以延长这个时间;或者源站通过定期发送 ping 帧以保持连接并确认连接是否还在使用。
使用 nginx 对 Websocket 进行反代

之前提到,Websocket 是工具性协议,而其开源实现大多是基于后端语言如 Python 和 nodejs,关于选用什么样的服务端和客户端,nginx 官网有过示例,在这里我们大体遵从这篇文章的示范进行测试。需要注意,由于 nodejs 在社区发展中遇到过分支间的斗争,版本号存在历史遗留的一个超大跨度问题,加上 nodejs 本身更新迭代很快,Debian8 默认源里的 nodejs 版本号尚在 0.x 不足以支持本次测试的需要,而主流 LTS 支持版本已经在4.x,我们使用 nodejs 官方目前建议的方法进行安装 4.x 版本的 nodejs:

curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
apt-get update && apt-get install -y nodejs
执行npm install -g ws wscat使用 npm 安装ws和wscat,其中,ws是 nodejs 的 Websocket 实现,我们借助他搭建简单的 Websocket echo server;wscat是一个可执行的 Websocket 客户端,我们用来调试 Websocket 服务是否正常。

完成安装后实现一个简单的服务端:

Shell

console.log("Server started");
var Msg = '';
var WebSocketServer = require('ws').Server
    , wss = new WebSocketServer({port: 8010});
    wss.on('connection', function(ws) {
        ws.on('message', function(message) {
        console.log('Received from client: %s', message);
        ws.send('Server received from client: ' + message);
    });
 });


这个简单的服务端实现的是向客户端返回客户端发送是消息,执行node server.js运行这个简单的 echo 服务。

使用wscat --connect ws://127.0.0.1:8010,输入任意内容进行测试,得到相同返回则说明运行正常。再使用上面的 nginx 配置对 simple server 进行反代,端口开在8020。

常见的压力测试工具 Apache JMeter 使用插件后可以对 Websocket 进行测试,但是这个插件较老,而且 JMeter 本身使用比较麻烦。而类似 ab 的轻量级测试工具目前发现的有 thor 和 Artillery,这两工具都是 nodejs 写的,nginx.com 在测试中使用的是 thor。

Thor:

可以定义并发度
输出结果简洁明了

针对 Websocket,没有 HTTP 功能
Artillery:

可以自定义 payload、多后端带权重轮询
不能定义并发度
也可以测试 HTTP、支持 cookie
使用 Artillery 进行测试:artillery quick --duration 60 --rate [number] ws://127.0.0.1:8020或thor --amount 10000 --concurrent [number] ws://127.0.0.1:8020,二者 在简单模式下发送的 payload 不相同,结果不能直接进比较。

由于 Websocket 原理,总请求数增加的时候会吃满一个网卡上的所有端口导致测试结束,而且测试中发现,传输速率较大时,前端 nginx 的 CPU 占有率较低,反而是后端 node 把一个 CPU 吃满。

主要原因还是源站过于简单,而由于 Websocket 的工具性质,也很难有代表性的性能评价标准,只能知道 nginx 在作为 Websocket 反代的时候能轻松抗住上万并发。

使用 haproxy 对 Websocket 进行反代

除了 nginx 之外,实际上还有 haproxy 也可以进行 Websocket 代理,haproxy 上这个功能出现的比 nginx 还要稍微早一点(haproxy 在2012年就有了 Websocket 反代支持,nginx 是在2013年初),且 haproxy 在两种模式下都支持 Websocket:TCP 模式下转发协议包,HTTP 模式下代替客户端和源站进行 Websocket 握手,本部分参考:Websocket load-balancing with haproxy-Blog.haproxy.com

 

haproxy 的 TCP mode 基本没有什么讨论的,HTTP mode 则有点像 nginx 的模式,通过 判断Sec-WebSocket-Protocol header,haproxy 可以转发到不同的后端。


defaults
  mode http
  log global
  option httplog
  option  http-server-close
  option  dontlognull
  option  redispatch
  option  contstats
  backlog 10000
  timeout client          25s
  timeout connect          5s
  timeout server          25s
  timeout tunnel        3600s
  timeout tarpit          60s
  option forwardfor
 
frontend ft_web
  bind *:8020 name http
  maxconn 10000
  default_backend bk_web
 
backend bk_web
  balance roundrobin
  server websrv1 127.0.0.1:8010 maxconn 10000

同样使用 Thor 和 Artillery 进行测试,nginx、haproxy 和直接连接性能并无明显区别,也可能是服务端写的很简单的缘故。

HTTP & Websocket

Websocket 出现的一个最大的原因就是 HTTP 是半双工的,Websocket 解决了服务端主动向客户推送数据的难题,但是 HTTP/2 出现了,联想到之前 HTTP/2 的 server push 特性,那么有了 HTTP/2 之后是否还需要 Websocket 这样基于 TCP 的独立双工协议,InfoQ 和 Stackoverflow 的结论都是认为 HTTP/2 不能代替 Websocket 这样的推送技术,主要原因是 HTTP/2 的 Server push 实际上是通过服务端在主页响应之前,通过一个 PUSH PROMISE 告诉客户端有哪些比较重要资源需要预先加载,只能被浏览器执行用于加载文件;如果客户端没有请求,服务端并不能主动推流,实际上并没有改变 HTTP 半双工协议的性质。

热门栏目