WebSocket技术分享
背景
为什么要用消息推送?主要是目前我们面授业务和ulcd很多场景,需要对消息的实时性要求高,比如学员上课老师发送课程资料,学员签到大屏显示签到状态,学员考试练习前后,大屏实时考试练习状态,统计结果。如果不采用消息推送方式,无法保证实时性,用户体验差。
为什么要选用WebSocket,适用场景?
传统方式采用的,长轮询,短轮询的方式: 短轮询(Polling):循环发起http请求,每次请求都是立即返回结果,不管有没有获取到数据都立即返回,需要不断的请求获取新的数据。 优点:它适用于一些实时数据请求,配合轮询来进行新旧数据的更替。 缺点:效率非常低,浪费资源 (带宽和计算资源),有一定延迟、服务器压力较大,每次请求和响应可能包含很长的头部,并且大部分是无效请求。 长轮询(long Polling):循环发起HTTP 请求,每次请求如果没有数据,就先挂起这个连接(死循环或者sleep),直到有数据之后再返回。 优点:长轮询省去了大量无效请求,减少了服务器压力和一定的网络带宽的占用。 缺点:需要保持大量的连接。 WebSocket:最后就出现了websocket这种长连接,基于流的方式,在服务器往客户端推送,这个方向的流实时性比较好。客户端发送一次http 请求,服务器响应请求,双方建立持久连接,并进行双向数据传输,后面不进行HTTP连接,而是使用TCP连接。
WebSocket在实时通信领域运用的比较多,比如社交聊天、弹幕、多玩家游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、基于位置的应用、在线教育、智能家居等需要高实时的场景。
简介
WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层(七层协议),并且都依赖于传输层的TCP协议(四层协议)。 虽然它们不同,但是RFC 6455规范中规定:WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介),从而使其与HTTP协议兼容。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。
优缺点
优点 ? 服务器推。数据可以随时从服务器发送到客户端,甚至不需要客户端请求。称为服务器推送。 客户端需要快速知道服务器上发生的事情(例如收到新的聊天消息或更新的新价格)。客户端不用必须通过每隔几秒发出一次 http 请求来定期轮询方式,效率非常低。 ? 较少的控制开销。Websocket只是用http建立一下连接 连上后就是tcp通信了,不需要额外消耗。 ? 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求,服务端才能响应,延迟明显更少;即使长轮询比较,其也能在短时间内更多次地传递数据。 ? 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。 ? 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。 ? 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。 缺点 ? 少部分浏览器不支持,浏览器支持的程度与方式有区别。 ? 不兼容低版本的IE。
握手协议
? WebSocket 是独立的、创建在TCP上的协议。 ? Websocket 通过 HTTP/1.1 协议的101状态码进行握手。 ? 为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”。 ? HTTP 协议老的标准是HTTP/1.0,为了提高系统的效率,HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求 ? http1.0 协议被抱怨最多的就是连接无法复用,为了克服HTTP 1.0的这个缺陷,HTTP 1.1支持持久连接(HTTP/1.1的默认模式使用带流水线的持久连接),在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。 ? HTTP2.0 协议特点多路复用,二进制分帧,首部压缩(Header Compression),服务端推送。
例子
? 一个典型的Websocket握手请求如下:
? Connection必须设置Upgrade,表示客户端希望连接升级。 ? Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。 ? Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行Base64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。 ? Sec-WebSocket-Version 表示支持的Websocket版本。 ? 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。
业界开源解决方案有哪些?为什么要自研?
解决方案
- 长轮询,短轮询(Ajax),定时刷新单方通讯
- websocket实时双方通讯。
- emq (遵循MQTT等协议的MQTT broker的 mq。号称一台服务支持百万级连接)
使用WebSocket主要是技术成熟,WebSocket 直接使用 TCP 连接保持全双工的传输,可以有效地减少连接的建立,实现真正的服务器通信,对于有低延迟有要求的应用是一个很好的选择。目前浏览器对 WebSocket 的支持程度已经很好,还有微信小程序的平台支持,这种可以极大提高客户端体验的通信方式将会变得更加主流。 为什么要封装公共服务?
- 首先:WebSocket技术栈不统一,既有基于Netty实现的,也有基于Web容器实现的,给开发和维护带来困难;
- 其次:WebSocket实现分散在在各个工程中,与业务系统强耦合,如果有其他业务需要集成WebSocket,面临着重复开发的窘境,浪费成本、效率低下;
- 第三:WebSocket是有状态协议的,客户端连接服务器时只和集群中一个节点连接,数据传输过程中也只与这一节点通信。WebSocket集群需要解决会话共享的问题。如果只采用单节点部署,虽然可以避免这一问题,但无法水平扩展支撑更高负载,有单点的风险;
- 最后:缺乏监控与报警,虽然可以通过Linux的Socket连接数大致评估WebSocket长连接数,但数字并不准确,也无法得知用户数等具有业务含义的指标数据;无法与现有的微服务监控整合,实现统一监控和报警。
自研的架构是怎么设计的?有哪些特性?
整体架构
架构流程如下:
- 客户端与网关任一实例握手建立起长连接,实例将其加入到内存维护的长连接队列。客户端定时向服务端发送心跳消息,如果超过设定的时间仍没有收到心跳,则认为客户端与服务端的长连接已断开,服务端会关闭连接,清理内存中的会话。
- 当业务系统需要向客户端推送数据时,通过网关提供的HTTP接口将数据发向网关。
- 网关在接收到推送请求后,将消息写入RocketMQ。
- 网关作为消费者,以广播模式消费消息,所有实例节点都会接收到消息。
- 消费实例接收到消息后,根据消息路由key匹配到要推送的客户端channel,推送给客户端,如果匹配不到则忽略。
网关以多实例节点方式构成集群,每节点负责一部分长连接,可实现负载均衡,当面对海量连接时,也可以通过增加节点的方式分担压力,实现水平扩展。 同时,当节点出现宕机时,客户端会尝试重新与其他节点握手建立长连接,保证服务整体的可用性。 问题: - 由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的问题。
如果要做到消息有序消费(根据业务分区,根据规则),MQ有序就放到一个分区,如果没有就自动分配,实时性取决于消费能力,消费采用多线程批量消费。
会话管理
- WebSocket长连接建立起来后,会话维护在各实例的内存中。SessionManager组件负责管理会话,内部使用了哈希表维护了UID与UserSession的关系。
- UserSession代表用户维度的会话,一个用户可能会同时建立多个长连接,因此UserSession内部同样使用了一个哈希表维护Channel与ChannelSession的关系。
- 为了避免用户无限制的创建长连接,UserSession在内部的ChannelSession超过一定数量后,会将最早建立的ChannelSession关闭,减少服务器资源占用。SessionManager、UserSession、ChannelSession的关系如下图所示。
监控与报警
-
为了了解集群建立了多少长连接、包含了多少用户,网关提供了基本的监控与报警能力。 -
接入监控系统,apm等等。 持久化和补偿策略 -
websocketapi推送数据到mq前先持久化DB -
websocketapi将数据发送到mq队列 -
websocket消费者将消息推送给客户端 -
websocket消费者异步修改消息状态 -
补偿服务定时检查异常消息补偿重试
消息路由策略
-
Client 和WebsocketServer建立连接,会将连接参数注册keys注册器,keys同时绑定到UserChannel保存到内存中。 -
WebsocketServer接收到要推送的数据时,首先会将数据交给Keys注册器,注册器进行路由计算得到routing key,然后根据routing key筛选出符合条件的UserChannel,推送到对应的客户端。
如何使用?
负载均衡几种方式 Nginx反向代理 Nginx 代理 WebSocket 的要点是设置Upgrade和Connection响应头。 配置 Nginx 根据Upgrade(即$http_upgrade)来设置Connection: 如果请求头中有Upgrade,就直接设置到响应头中,并把Connection设置为upgrade。如
WebSocket 请求头会带上Upgrade: websocket,则响应头有 Upgrade: websocket Connection: upgrade 否则把Connection设置为close。如普通HTTP请求。 最终 Nginx 配置如下: nginx.conf 中 http 配置
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
conf.d下的.conf具体配置文件
server {
listen 8000;
location / {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
map指令的作用: 该作用主要是根据客户端请求中
h
t
t
p
u
p
g
r
a
d
e
的
值
,
来
构
造
改
变
http_upgrade 的值,来构造改变
httpu?pgrade的值,来构造改变connection_upgrade的值,即根据变量
h
t
t
p
u
p
g
r
a
d
e
的
值
创
建
新
的
变
量
http_upgrade的值创建新的变量
httpu?pgrade的值创建新的变量connection_upgrade, 创建的规则就是{}里面的东西。其中的规则没有做匹配,因此使用默认的,即 $connection_upgrade 的值会一直是 upgrade。然后如果 $http_upgrade为空字符串的话,那值会是 close。 Haproxy反向代理 具体配置例子
JSON
ASDFA
frontend WSS_SSL
bind *:443 ssl crt /etc/ssl/name.pem
mode http
routing based on websocket protocol header
acl hdr_connection_upgrade hdr(Connection) -i upgrade
acl hdr_upgrade_websocket hdr(Upgrade) -i websocket
use_backend wss_srv if hdr_connection_upgrade hdr_upgrade_websocket
backend wss_srv
balance roundrobin
cookie SERVERID
server host1 192.168.1.10:80 cookie host1 maxconn 50000 check inter 10s rise 3 fall 3
其他代理方式 客户端如何连接? 连接参数 https://confluence.yunxuetang.com.cn/pages/viewpage.action?pageId=70521202
心跳续约
响应结构体
各个业务项目线如何接入?
性能测试报告
测试环境 JMeter性能测试 2台实例
压测2万请求
? 服务器两台实例,受到默认句柄限制情况下,前压测2万请求,错误率为29% ? 错误原因是最大句柄数限制了连接数量,因此后续的请求都会拒绝 压测3万请求
stable服务器两台实例,默认句柄是情况下,优化后压测3万请求,错误率为0% 压测4万请求
stable服务器两台实例,默认句柄是情况下,优化后压测4万请求,错误率为0.01% 压测10万请求
stable服务器两台实例,默认句柄是情况下,优化后压测10万请求,错误率为2.16% 性能瓶颈
- 内核瓶颈,在线用户达百万时,实时数据量非常之大,linux内核发送tcp的极限包频率是100万/秒。
a. 优化:消息合并,减小网络小包推送,同一秒内的多条消息合并成1条推送,减小内核压力。 - cpu瓶颈
a. 浏览器与服务器通常采取json格式通讯。 b. json编码特别消耗cpu资源。 c. 向100万在线推送资源,json需要100万次encode。 优化:编码前置:1次编码+100万次推送。 - 锁瓶颈
a. 需要维护在线用户(100万在线),通常是个哈希字典结构。 b. 推送消息遍历整个集合,顺序发送消息,耗时极长。 c. 推送期间,客户端正常上下线,集合面临不停的修改,需要上锁,这期间客户端拿不到锁会无法正常上下线。 优化:读写锁代替互斥锁。 - 单机瓶颈,维护海量长连接会花费不少的内存,消息推送瞬时会消耗大量的cpu资源,消息推送瞬时带宽高达400-600MB(4-6Gbits)。
优化:主要的技术瓶颈要采用分布式架构。 - 消息处理速度
消息的处理速度影响数据展示的实时性,需要保证数据无积压,多业务互不影响,多机构互不影响 优化:根据业务和机构进行消息的水平和垂直拆分,提高消息的消费速度和数据隔离。
参数调优: Nginx参数:
Nginx
server {
listen 801;
server_name localhost;
location /ws/{
proxy_connect_timeout 900;
proxy_read_timeout 900;
proxy_send_timeout 900;
proxy_pass http://localhost:8333/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
worker_rlimit_nofile 65535;
events {
worker_connections 65535;
}
Linux 操作系统参数
TCP参数
EMQ Docs
net.ipv4.tcp_max_tw_buckets = 20000 设置系统的TIME_WAIT的数量,如果超过默认值则会被立即清除 net.core.somaxconn = 65535 定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数 net.ipv4.tcp_max_syn_backlog = 262144 对于还未获得对方确认的连接请求,可保存在队列中的最大数目 fs.file-max = 6815744 系统所有进程一共可以打开的文件数量 net.ipv4.tcp_tw_recycle = 0 能够更快地回收TIME-WAIT套接字。此选项会导致处于NAT网络的客户端超时,建议为0 net.core.netdev_max_backlog = 30000 在每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
Netty参数
Netty通信网络参数配置 Netty服务端/客户端网络通信过程中常用的参数:
Name Associated setter method
"writeBufferHighWaterMark" 默认64 * 1024(用法未知)
"writeBufferLowWaterMark" 默认32 * 1024(用法未知)
"writeSpinCount" 默认16(重复写次数,用法未知)
"broadcast" true / false多播模式(UDP适用)
"interface" 多播数据包的网络接口地址
"loopbackModeDisabled" 实际调用的是channel.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, loopbackModeDisabled);仅针对JDK7+有效
"networkInterface" 实际调用的是channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);仅针对JDK7+有效
"reuseAddress" 地址是否可复用(UDP socket address绑定时用到)
"receiveBufferSize" 数据包接收大小
"receiveBufferSizePredictor" 数据包接收大小:默认设置为FixedReceiveBufferSizePredictor(768),超过后丢弃
"receiveBufferSizePredictorFactory" 似乎与上面的功能相同,设置方式:new FixedReceiveBufferSizePredictorFactory(1024)
"sendBufferSize" 发送数据包大小
"timeToLive" JDK7+版本有效
"trafficClass" 0<=tc<=255
bufferFactory" 用于创建ChannelBuffer的工厂,默认HeapChannelBufferFactory
"connectTimeoutMillis" 连接超时时间(毫秒)
"pipelineFactory" 仅适用于child channel创建时有效
"keepAlive" 启用/禁用Nagle算法
"soLinger" Socket关闭时的延迟时间(单位:秒)
"tcpNoDelay" 启用/禁用Nagle算法
待办任务
- 压测和调优:压测消费能力,MQ队列,客户端连接数,消息实时性,持久性;
- 个性化推送:根据业务方需求,支持定制化的推送方式,比如顺序推送、失效补偿等;
- 优化业务方接入工作:接入简单,不需要额外配置;
- 多泳道支持:WebsocketServer消费服务、MQ队列根据项目或者机构泳道隔离,各个项目和机构只消费自己的业务消息,业务拆分,提高消息处理能力。
- 高可用:目标高可用支持百万级连接,低延迟,高可用,水平扩容,垂直扩容;
|