示例 socket.io 项目
编译
go get github.com/tangxinfa/envoy-socket.io-example cd $GOPATH/src/github.com/tangxinfa/envoy-socket.io-example glide install go build
运行
./envoy-socket.io-example -addr 127.0.0.1:8001 -logtostderr
用浏览器打开
http://localhost:8001
正常情况下,会收到
welcome
消息,表示socket.io
连接成功,可以在下方的编辑框输入内容,服务器会 echo 回来。这是客户端直接 socket.io 服务,接下来将展示使用 Envoy 做为前端代理来访问后端的 socket.io 服务。
修复 Envoy 的 Websocket 相关 Bug
之前的 Envoy 版本为
commit 3e43c2225c8882918b36b4b7c7bb55c6af2db929 Author: Greg Greenway <ggreenway@users.noreply.github.com> Date: Wed Nov 15 14:48:38 2017 -0800 Fix v2 TcpProxy config (#2065) Signed-off-by: Greg Greenway <ggreenway@apple.com>
存在两个问题导致 Websocket 不可用:
Connection 请求头包含多个值时未能正确处理,导致未正确判断出 Websocket 请求
Firefox 发起的 Websocket 请求 Connection 头的值为:
keep-alive, Upgrade
。
Envoy 向 Upstream 发起的 Websocket 请求多了一个
transfer-encoding: chunked
请求头由于请求体是空的,所以是一个无效的 HTTP 请求。
我提交了 Pull Request #2070 ,已合到 master
分支,该问题已修复。
将 Envoy 做为 socket.io 服务前端代理
启动 Envoy
使用是的修复 BUG 后的 Envoy。
~/Opensource/envoy/bazel-bin/source/exe/envoy-static --log-level trace --config-path ./envoy.json
用浏览器打开
http://localhost:9001
正常情况下,会收到
welcome
消息,表示 socket.io 连接成功,可以在下方的编辑框输入内容,服务器会 echo 回来。
将 Envoy 做为 socket.io 服务集群前端代理
在后台服务为集群的情况下,Envoy 会通过负载均衡将请求调度到所有服务结点,对于无状态服务,不管 Envoy 使用什么样的负载均衡策略都可以正常工作,但是对于有状态的服务,则要求将一个用户的请求总是调度到同一个服务结点。
Socket.IO never assumes that WebSocket will just work, because in practice there’s a good chance that it won’t. Instead, it establishes a connection with XHR or JSONP right away, and then attempts to upgrade the connection to WebSocket. Compared to the fallback method which relies on timeouts, this means that none of your users will have a degraded experience.
引用自 Socket.IO Polling vs. WebSocket Transport – on Balancing Methods。
socket.io 的 transports
选项默认值为 ['polling', 'websocket']
,也就是说首先发一个 HTTP 轮询请求,根据响应决定是否升级到使用 websocket
,这样在 Websocket 可用的情况下,会有两次 HTTP 交互,需确保两次 HTTP 交互调度到同一个服务结点,socket.io 连接才能建立成功。
Envoy 本身支持多种负载均衡策略,适合这里的场景是 Ring hash
。
Ring hash
The ring/modulo hash load balancer implements consistent hashing to upstream hosts. The algorithm is based on mapping all hosts onto a circle such that the addition or removal of a host from the host set changes only affect 1/N requests. This technique is also commonly known as “ketama” hashing. The consistent hashing load balancer is only effective when protocol routing is used that specifies a value to hash on. Currently the only implemented mechanism is to hash via HTTP header values in the HTTP router filter.
遗憾的是 Envoy 的 Websocket 实现过于简陋,当检测到 Websocket
升级请求时,它以 TcpProxy
的方式连接上游服务器,检测到非 Websocket
升级请求时,会正常地做为 HTTP 请求进行处理。由于 TcpProxy
只支持随机 Hash 算法选择上游服务结点,会导致默认情况下 socket.io 连接无法建立。
通过指定 socket.io 的 transports
选项值为 ['websocket', 'polling']
,也就是首先尝试建立 websocket
连接,失败时再降级为 polling
,能够解决这个问题。
Envoy 的 Ring hash
是根据配置的请求头来计算 Hash 值的,刚刚有一个按特定 Cookie 计算 Hash 值的
Pull Request Implement cookie hashing for v2 API #1766 已经合并到 master 分支,官方文档方面还没有更新,本文暂不采用。
一般 Web 前端都是通过自定义请求头或者 Cookie 中的特定字段来标识一个用户,由于 Cookie 可能会在交互过程中发生变化,因此不适合用于计算 Hash 值,而 Websocket 升级请求又不支持自定义请求头,因此这两种常用的方式失效了。
另一个合理的选择是通过 Referer
请求头来计算 Hash,这是客户端网页的地址,不会在交互过程中变化,只要想办法将用户的标识(如 ID)附在 URL 上,如:http://www.example.com/chat?uid=1234 ,就可以保证用户请求能够均衡地分布到所有服务结点上,同一用户的请求也会调度到同一个服务结点。
如下所示。
启动服务集群
./envoy-socket.io-example -127.0.0.1:8002 -logtostderr & ./envoy-socket.io-example -127.0.0.1:8003 -logtostderr & ./envoy-socket.io-example -127.0.0.1:8004 -logtostderr &
启动 Envoy
使用是的修复 BUG 后的 Envoy。
~/Opensource/envoy/bazel-bin/source/exe/envoy-static --log-level trace --config-path ./envoy2.json
用浏览器打开多个
http://localhost:9002/index2.html
页面正常情况下,每个页面都会收到
welcome
消息,表示 socket.io 连接成功,可以在下方的编辑框输入内容,服务器会 echo 回来。