知乎技术分享:知乎千万级并发的高性能长链接网关技术实践

本文来自知乎官方技术团队的“知乎技术专栏”,感谢原做者faceair的无私分享。php

一、引言

实时的响应老是让人兴奋的,就如你在微信里看到对方正在输入,如你在王者峡谷里一呼百应,如大家在直播弹幕里不约而同的 666,它们的背后都离不开长链接技术的加持。

每一个互联网公司里几乎都有一套长链接系统,它们被应用在消息提醒、即时通信、推送、直播弹幕、游戏、共享定位、股票行情等等场景。而当公司发展到必定规模,业务场景变得更复杂后,更有多是多个业务都须要同时使用长链接系统。

业务间分开设计长链接会致使研发和维护成本陡增、浪费基础设施、增长客户端耗电、没法复用已有经验等等问题。共享长链接系统又须要协调好不一样系统间的认证、鉴权、数据隔离、协议拓展、消息送达保证等等需求,迭代过程当中协议须要向前兼容,同时由于不一样业务的长链接汇聚到一个系统致使容量管理的难度也会增大。

通过了一年多的开发和演进,通过咱们服务面向内和外的数个 App、接入十几个需求和形态万千的长链接业务、数百万设备同时在线、突发大规模消息发送等等场景的锤炼,咱们提炼出一个长链接系统网关的通用解决方案,解决了多业务共用长链接时遇到的种种问题。

知乎长链接网关致力于业务数据解耦、消息高效分发、解决容量问题,同时提供必定程度的消息可靠性保证。html

(本文同步发布于:www.52im.net/thread-2737…

程序员

二、相关文章

三、咱们怎么设计通信协议?

3.1 业务解耦

支撑多业务的长链接网关其实是同时对接多客户端和多业务后端的,是多对多的关系,他们之间只使用一条长链接通信。



这种多对多的系统在设计时要避免强耦合。业务方逻辑也是会动态调整的,若是将业务的协议和逻辑与网关实现耦合会致使全部的业务都会互相牵连,协议升级和维护都会异常困难。

因此咱们尝试使用经典的发布订阅模型来解耦长链接网关跟客户端与业务后端,它们之间只须要约定 Topic 便可自由互相发布订阅消息。传输的消息是纯二进制数据,网关也无需关心业务方的具体协议规范和序列化方式。



面试

3.2 权限控制

咱们使用发布订阅解耦了网关与业务方的实现,咱们仍然须要控制客户端对 Topic 的发布订阅的权限,避免有意或无心的数据污染或越权访问。

假如讲师正在知乎 Live 的 165218 频道开讲,当客户端进入房间尝试订阅 165218 频道的 Topic 时就须要知乎 Live 的后端判断当前用户是否已经付费。这种状况下的权限其实是很灵活的,当用户付费之后就能订阅,不然就不能订阅。权限的状态只有知乎 Live 业务后端知晓,网关没法独立做出判断。

因此咱们在 ACL 规则中设计了基于回调的鉴权机制,能够配置 Live 相关 Topic 的订阅和发布动做都经过 HTTP 回调给 Live 的后端服务判断。



同时根据咱们对内部业务的观察,大部分场景下业务须要的只是一个当前用户的私有 Topic 用来接收服务端下发的通知或消息,这种状况下若是让业务都设计回调接口来判断权限会很繁琐。

因此咱们在 ACL 规则中设计了 Topic 模板变量来下降业务方的接入成本,咱们给业务方配置容许订阅的 Topic 中包含链接的用户名变量标识,表示只容许用户订阅或发送消息到本身的 Topic。



此时网关能够在不跟业务方通讯的状况下,独立快速判断客户端是否有权限订阅或往 Topic 发送消息。

算法

3.3 消息可靠性保证


网关做为消息传输的枢纽,同时对接业务后端和客户端,在转发消息时须要保证消息在传输过程的可靠性。

TCP 只能保证了传输过程当中的顺序和可靠性,但遇到 TCP 状态异常、客户端接收逻辑异常或发生了 Crash 等等状况时,传输中的消息就会发生丢失。

为了保证下发或上行的消息被对端正常处理,咱们实现了回执和重传的功能。重要业务的消息在客户端收到并正确处理后须要发送回执,而网关内暂时保存客户端未收取的消息,网关会判断客户端的接收状况并尝试再次发送,直到正确收到了客户端的消息回执。



而面对服务端业务的大流量场景,服务端发给网关的每条消息都发送回执的方式效率较低,咱们也提供了基于消息队列的接收和发送方式,后面介绍发布订阅实现时再详细阐述。

在设计通信协议时咱们参考了 MQTT 规范(详见《扫盲贴:认识MQTT通讯协议》),拓展了认证和鉴权设计,完成了业务消息的隔离与解耦,保证了必定程度的传输可靠性。同时保持了与 MQTT 协议必定程度上兼容,这样便于咱们直接使用 MQTT 的各端客户端实现,下降业务方接入成本。编程

四、咱们怎么设计系统架构?

在设计项目总体架构时,咱们优先考虑的是:后端

  • 1)可靠性;
  • 2)水平扩展能力;
  • 3)依赖组件成熟度;
  • 4)简单才值得信赖。


为了保证可靠性,咱们没有考虑像传统长链接系统那样将内部数据存储、计算、消息路由等等组件所有集中到一个大的分布式系统中维护,这样增大系统实现和维护的复杂度。咱们尝试将这几部分的组件独立出来,将存储、消息路由交给专业的系统完成,让每一个组件的功能尽可能单一且清晰。

同时咱们也须要快速地水平扩展能力。互联网场景下各类营销活动均可能致使链接数陡增,同时发布订阅模型系统中下发消息数会随着 Topic 的订阅者的个数线性增加,此时网关暂存的客户端未接收消息的存储压力也倍增。将各个组件拆开后减小了进程内部状态,咱们就能够将服务部署到容器中,利用容器来完成快速并且几乎无限制的水平扩展。

最终设计的系统架构以下图:


系统主要由四个主要组件组成:缓存

  • 1)接入层使用 OpenResty 实现,负责链接负载均衡和会话保持;
  • 2)长链接 Broker,部署在容器中,负责协议解析、认证与鉴权、会话、发布订阅等逻辑;
  • 3)Redis 存储,持久化会话数据;
  • 4)Kafka 消息队列,分发消息给 Broker 或业务方。


其中 Kafka 和 Redis 都是业界普遍使用的基础组件,它们在知乎都已平台化和容器化 (详见:《Redis at Zhihu》、《知乎基于 Kubernetes 的 Kafka 平台的设计和实现》),它们也都能完成分钟级快速扩容。安全

五、咱们如何构建长链接网关?

5.1 接入层

OpenResty 是业界使用很是普遍的支持 Lua 的 Nginx 拓展方案,灵活性、稳定性和性能都很是优异,咱们在接入层的方案选型上也考虑使用 OpenResty。

接入层是最靠近用户的一侧,在这一层须要完成两件事:服务器

  • 1)负载均衡,保证各长链接 Broker 实例上链接数相对均衡;
  • 2)会话保持,单个客户端每次链接到同一个 Broker,用来提供消息传输可靠性保证。


负载均衡其实有不少算法都能完成,无论是随机仍是各类 Hash 算法都能比较好地实现,麻烦一些的是会话保持。

常见的四层负载均衡策略是根据链接来源 IP 进行一致性 Hash,在节点数不变的状况下这样能保证每次都 Hash 到同一个 Broker 中,甚至在节点数稍微改变时也能大几率找到以前链接的节点。

以前咱们也使用过来源 IP Hash 的策略,主要有两个缺点:

  • 1)分布不够均匀,部分来源 IP 是大型局域网 NAT 出口,上面的链接数多,致使 Broker 上链接数不均衡;
  • 2)不能准确标识客户端,当移动客户端掉线切换网络就可能没法链接回刚才的 Broker 了。


因此咱们考虑七层的负载均衡,根据客户端的惟一标识来进行一致性 Hash,这样随机性更好,同时也能保证在网络切换后也能正确路由。常规的方法是须要完整解析通信协议,而后按协议的包进行转发,这样实现的成本很高,并且增长了协议解析出错的风险。

最后咱们选择利用 Nginx 的 preread 机制实现七层负载均衡,对后面长链接 Broker 的实现的侵入性小,并且接入层的资源开销也小。

Nginx 在接受链接时能够指定预读取链接的数据到 preread buffer 中,咱们经过解析 preread buffer 中的客户端发送的第一个报文提取客户端标识,再使用这个客户端标识进行一致性 Hash 就拿到了固定的 Broker。

5.2 发布与订阅

咱们引入了业界普遍使用的消息队列 Kafka 来做为内部消息传输的枢纽。

前面提到了一些这么使用的缘由:

  • 1)减小长链接 Broker 内部状态,让 Broker 能够无压力扩容;
  • 2)知乎内部已平台化,支持水平扩展。


还有一些缘由是:

  • 1)使用消息队列削峰,避免突发性的上行或下行消息压垮系统;
  • 2)业务系统中大量使用 Kafka 传输数据,下降与业务方对接成本。


其中利用消息队列削峰好理解,下面咱们看一下怎么利用 Kafka 与业务方更好地完成对接。

5.3 发布

链接 Broker 会根据路由配置将消息发布到 Kafka Topic,同时也会根据订阅配置去消费 Kafka 将消息下发给订阅客户端。

路由规则和订阅规则是分别配置的,那么可能会出现四种状况。

状况一:消息路由到 Kafka Topic,但不消费,适合数据上报的场景,以下图所示。



状况二:消息路由到 Kafka Topic,也被消费,普通的即时通信场景,以下图所示。



状况三:直接从 Kafka Topic 消费并下发,用于纯下发消息的场景,以下图所示。



状况四:消息路由到一个 Topic,而后从另外一个 Topic 消费,用于消息须要过滤或者预处理的场景,以下图所示。



这套路由策略的设计灵活性很是高,能够解决几乎全部的场景的消息路由需求。同时由于发布订阅基于 Kafka,能够保证在处理大规模数据时的消息可靠性。

5.4 订阅

当长链接 Broker 从 Kafka Topic 中消费出消息后会查找本地的订阅关系,而后将消息分发到客户端会话。

咱们最开始直接使用 HashMap 存储客户端的订阅关系。当客户端订阅一个 Topic 时咱们就将客户端的会话对象放入以 Topic 为 Key 的订阅 Map 中,当反查消息的订阅关系时直接用 Topic 从 Map 上取值就行。

由于这个订阅关系是共享对象,当订阅和取消订阅发生时就会有链接尝试操做这个共享对象。为了不并发写咱们给 HashMap 加了锁,但这个全局锁的冲突很是严重,严重影响性能。

最终咱们经过分片细化了锁的粒度,分散了锁的冲突。

本地同时建立数百个 HashMap,当须要在某个 Key 上存取数据前经过 Hash 和取模找到其中一个 HashMap 而后进行操做,这样将全局锁分散到了数百个 HashMap 中,大大下降了操做冲突,也提高了总体的性能。

5.5 会话持久化

当消息被分发给会话 Session 对象后,由 Session 来控制消息的下发。

Session 会判断消息是不是重要 Topic 消息, 是的话将消息标记 QoS 等级为 1,同时将消息存储到 Redis 的未接收消息队列,并将消息下发给客户端。等到客户端对消息的 ACK 后,再将未确认队列中的消息删除。

有一些业界方案是在内存中维护了一个列表,在扩容或缩容时这部分数据无法跟着迁移。也有部分业界方案是在长链接集群中维护了一个分布式内存存储,这样实现起来复杂度也会变高。

咱们将未确认消息队列放到了外部持久化存储中,保证了单个 Broker 宕机后,客户端从新上线链接到其余 Broker 也能恢复 Session 数据,减小了扩容和缩容的负担。

5.6 滑动窗口

在发送消息时,每条 QoS 1 的消息须要被通过传输、客户端处理、回传 ACK 才能确认下发完成,路径耗时较长。若是消息量较大,每条消息都等待这么长的确认才能下发下一条,下发通道带宽不能被充分利用。

为了保证发送的效率,咱们参考 TCP 的滑动窗口设计了并行发送的机制(详见:《通俗易懂-深刻理解TCP协议(下):RTT、滑动窗口、拥塞处理》)。咱们设置必定的阈值为发送的滑动窗口,表示通道上能够同时有这么多条消息正在传输和被等待确认。



咱们应用层设计的滑动窗口跟 TCP 的滑动窗口实际上还有些差别。

TCP 的滑动窗口内的 IP 报文没法保证顺序到达,而咱们的通信是基于 TCP 的因此咱们的滑动窗口内的业务消息是顺序的,只有在链接状态异常、客户端逻辑异常等状况下才可能致使部分窗口内的消息乱序。

由于 TCP 协议保证了消息的接收顺序,因此正常的发送过程当中不须要针对单条消息进行重试,只有在客户端从新链接后才对窗口内的未确认消息从新发送。消息的接收端同时会保留窗口大小的缓冲区用来消息去重,保证业务方接收到的消息不会重复。

咱们基于 TCP 构建的滑动窗口保证了消息的顺序性同时也极大提高传输的吞吐量。

六、写在最后

知乎长链接网关由基础架构组 (Infra) 开发和维护,主要贡献者是@faceair@安江泽

基础架构组负责知乎的流量入口和内部基础设施建设,对外咱们奋斗在直面海量流量的的第一战线,对内咱们为全部的业务提供坚如磐石的基础设施,用户的每一次访问、每个请求、内网的每一次调用都与咱们的系统息息相关。

附录:更多网络编程相关资料

[1] 网络编程基础资料:
TCP/IP详解 - 第11章·UDP:用户数据报协议
TCP/IP详解 - 第17章·TCP:传输控制协议
TCP/IP详解 - 第18章·TCP链接的创建与终止
TCP/IP详解 - 第21章·TCP的超时与重传
技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)
通俗易懂-深刻理解TCP协议(上):理论基础
通俗易懂-深刻理解TCP协议(下):RTT、滑动窗口、拥塞处理
理论经典:TCP协议的3次握手与4次挥手过程详解
理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程
计算机网络通信协议关系图(中文珍藏版)
UDP中一个包的大小最大能多大?
P2P技术详解(一):NAT详解——详细原理、P2P简介
P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解
P2P技术详解(三):P2P技术之STUN、TURN、ICE详解
通俗易懂:快速理解P2P技术中的NAT穿透原理
高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少
高性能网络编程(二):上一个10年,著名的C10K并发链接问题
高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了
高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索
高性能网络编程(五):一文读懂高性能网络编程中的I/O模型
高性能网络编程(六):一文读懂高性能网络编程中的线程模型
鲜为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)
鲜为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)
鲜为人知的网络编程(三):关闭TCP链接时为何会TIME_WAIT、CLOSE_WAIT
鲜为人知的网络编程(四):深刻研究分析TCP的异常关闭
鲜为人知的网络编程(五):UDP的链接性和负载均衡
鲜为人知的网络编程(六):深刻地理解UDP协议并用好它
鲜为人知的网络编程(七):如何让不可靠的UDP变的可靠?
鲜为人知的网络编程(八):从数据传输层深度解密HTTP
网络编程懒人入门(一):快速理解网络通讯协议(上篇)
网络编程懒人入门(二):快速理解网络通讯协议(下篇)
网络编程懒人入门(三):快速理解TCP协议一篇就够
网络编程懒人入门(四):快速理解TCP和UDP的差别
网络编程懒人入门(五):快速理解为何说UDP有时比TCP更有优点
网络编程懒人入门(六):史上最通俗的集线器、交换机、路由器功能原理入门
网络编程懒人入门(七):深刻浅出,全面理解HTTP协议
网络编程懒人入门(八):手把手教你写基于TCP的Socket长链接
网络编程懒人入门(九):通俗讲解,有了IP地址,为什么还要用MAC地址?
技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解
让互联网更快:新一代QUIC协议在腾讯的技术实践分享
现代移动端网络短链接的优化手段总结:请求速度、弱网适应、安全保障
聊聊iOS中网络编程长链接的那些事
移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”
移动端IM开发者必读(二):史上最全移动弱网络优化方法总结
IPv6技术详解:基本概念、应用现状、技术实践(上篇)
IPv6技术详解:基本概念、应用现状、技术实践(下篇)
从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路
脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手
脑残式网络编程入门(二):咱们在读写Socket时,究竟在读写什么?
脑残式网络编程入门(三):HTTP协议必知必会的一些知识
脑残式网络编程入门(四):快速理解HTTP/2的服务器推送(Server Push)
脑残式网络编程入门(五):天天都在用的Ping命令,它究竟是什么?
脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?
以网游服务端的网络接入层设计为例,理解实时通讯的技术挑战
迈向高阶:优秀Android程序员必知必会的网络基础
全面了解移动端DNS域名劫持等杂症:技术原理、问题根源、解决方案等
美图App的移动端DNS优化实践:HTTPS请求耗时减少近半
Android程序员必知必会的网络通讯传输层协议——UDP和TCP
IM开发者的零基础通讯技术入门(一):通讯交换技术的百年发展史(上)
IM开发者的零基础通讯技术入门(二):通讯交换技术的百年发展史(下)
IM开发者的零基础通讯技术入门(三):国人通讯方式的百年变迁
IM开发者的零基础通讯技术入门(四):手机的演进,史上最全移动终端发展史
IM开发者的零基础通讯技术入门(五):1G到5G,30年移动通讯技术演进史
IM开发者的零基础通讯技术入门(六):移动终端的接头人——“基站”技术
IM开发者的零基础通讯技术入门(七):移动终端的千里马——“电磁波”
IM开发者的零基础通讯技术入门(八):零基础,史上最强“天线”原理扫盲
IM开发者的零基础通讯技术入门(九):无线通讯网络的中枢——“核心网”
IM开发者的零基础通讯技术入门(十):零基础,史上最强5G技术扫盲
IM开发者的零基础通讯技术入门(十一):为何WiFi信号差?一文即懂!
IM开发者的零基础通讯技术入门(十二):上网卡顿?网络掉线?一文即懂!
IM开发者的零基础通讯技术入门(十三):为何手机信号差?一文即懂!
IM开发者的零基础通讯技术入门(十四):高铁上无线上网有多难?一文即懂!
IM开发者的零基础通讯技术入门(十五):理解定位技术,一篇就够
百度APP移动端网络深度优化实践分享(一):DNS优化篇
百度APP移动端网络深度优化实践分享(二):网络链接优化篇
百度APP移动端网络深度优化实践分享(三):移动端弱网优化篇
技术大牛陈硕的分享:由浅入深,网络编程学习经验干货总结
可能会搞砸你的面试:你知道一个TCP链接上能发起多少个HTTP请求吗?
知乎技术分享:知乎千万级并发的高性能长链接网关技术实践
>> 更多同类文章 ……

[2] NIO异步网络编程资料:
Java新一代网络编程模型AIO原理及Linux系统AIO介绍
有关“为什么选择Netty”的11个疑问及解答
开源NIO框架八卦——究竟是先有MINA仍是先有Netty?
选Netty仍是Mina:深刻研究与对比(一)
选Netty仍是Mina:深刻研究与对比(二)
NIO框架入门(一):服务端基于Netty4的UDP双向通讯Demo演示
NIO框架入门(二):服务端基于MINA2的UDP双向通讯Demo演示
NIO框架入门(三):iOS与MINA二、Netty4的跨平台UDP双向通讯实战
NIO框架入门(四):Android与MINA二、Netty4的跨平台UDP双向通讯实战
Netty 4.x学习(一):ByteBuf详解
Netty 4.x学习(二):Channel和Pipeline详解
Netty 4.x学习(三):线程模型详解
Apache Mina框架高级篇(一):IoFilter详解
Apache Mina框架高级篇(二):IoHandler详解
MINA2 线程原理总结(含简单测试实例)
Apache MINA2.0 开发指南(中文版)[附件下载]
MINA、Netty的源代码(在线阅读版)已整理发布
解决MINA数据传输中TCP的粘包、缺包问题(有源码)
解决Mina中多个同类型Filter实例共存的问题
实践总结:Netty3.x升级Netty4.x遇到的那些坑(线程篇)
实践总结:Netty3.x VS Netty4.x的线程模型
详解Netty的安全性:原理介绍、代码演示(上篇)
详解Netty的安全性:原理介绍、代码演示(下篇)
详解Netty的优雅退出机制和原理
NIO框架详解:Netty的高性能之道
Twitter:如何使用Netty 4来减小JVM的GC开销(译文)
绝对干货:基于Netty实现海量接入的推送服务技术要点
Netty干货分享:京东京麦的生产级TCP网关技术实践总结
新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析
写给初学者:Java高性能NIO框架Netty的学习方法和进阶策略
少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别
史上最强Java NIO入门:担忧从入门到放弃的,请读这篇!
手把手教你用Netty实现网络通讯程序的心跳机制、断线重连机制
>> 更多同类文章 …

(本文同步发布于:www.52im.net/thread-2737…

相关文章
相关标签/搜索