千万级连接,知乎如何架构长连接网关?

说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到一线互联网企业如阿里、网易、有赞、希音、百度、滴滴的面试资格。

最近,尼恩指导一个小伙伴简历,写了一个《高性能长连接网关项目》,此项目帮这个小伙拿到 字节/阿里/微博/汽车之家 面邀, 所以说,这是一个牛逼的项目。

为了帮助大家拿到更多面试机会,拿到更多大厂offer,

尼恩决定:9月份給大家出一章视频介绍这个项目的架构和实操,《33章: 10Wqps 高并发 Netty网关架构与实操》,预计月底发布。然后,提供一对一的简历指导,这里简历金光闪闪、脱胎换骨。

《33章: 10Wqps 高并发 Netty网关架构与实操》 海报如下:

配合《33章: 10Wqps 高并发 Netty网关架构与实操》, 尼恩会梳理几个工业级、生产级网关案例,作为架构素材、设计的素材。

前面梳理了《日流量200亿,携程网关的架构设计》

这里又是一个漂亮的生产级案例:《知乎千万级并发的高性能长连接网关技术实践》,又一个非常 牛逼的工业级、生产级网关案例

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取

1、知乎千万级并发的高性能长连接网关技术实践

作者:知乎技术团队

几乎每个互联网公司都有一套长连接系统,它们在消息提示、实时通信、推送、直播弹幕、游戏、共享定位、股票行情等场景中得到应用。

随着公司规模的扩大和业务场景的复杂化,多个业务可能都需要同时使用长连接系统。

分别为各个业务设计长连接将会导致研发和维护成本大幅上升、资源浪费、增加客户端能耗、无法重复利用现有经验等问题。

共享长连接系统则需要协调不同系统间的认证、授权、数据隔离、协议扩展、消息送达保证等需求,在迭代过程中协议需要保持向前兼容,同时由于不同业务的长连接汇聚到一个系统,容量管理的难度也会相应增大。

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

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

2、我们怎么设计通讯协议?

2.1 业务解耦

支持多业务的长连接网关需要同时与多个客户端和多个业务后端进行对接,形成多对多的关系,他们之间仅依赖一条长连接进行通信。

在设计这种多对多的系统时,需要避免过度耦合。业务逻辑是动态调整的,如果将业务协议和逻辑与网关实现紧密结合,将会导致所有业务相互关联,协议升级和维护变得极其困难。

因此,我们尝试采用经典的发布订阅模型来实现长连接网关与客户端和业务后端的解耦,他们之间只需约定主题,便可自由地发布和订阅消息。传输的消息为纯二进制数据,网关无需关心业务方的具体协议规范和序列化方式。

2.2 如何进行客户端的权限控制?

我们采用发布订阅的方式解耦了网关与业务方的实现,然而,我们还需要控制客户端对主题(Topic)的发布订阅权限,防止数据污染或越权访问,无论是有意还是无意的。

比如,当一个讲师在知乎 Live 的 165218 频道进行演讲,客户端进入房间并尝试订阅 165218 频道的 Topic 时,知乎 Live 的后端就需要判断当前用户是否已经付费。在这种情况下,权限是非常灵活的,用户付费后才能订阅,否则就不能订阅。

关于权限的状态,只有知乎 Live 业务后端知道,网关无法独立作出判断。

因此,我们在 ACL 规则中设计了一个基于回调的鉴权机制,可以配置 Live 相关 Topic 的订阅和发布动作都通过 HTTP 回调给知乎 Live 的后端服务进行判断。

同时,根据我们对内部业务的观察,大部分场景下,业务只需要一个当前用户的私有主题来接收服务端下发的通知或消息。在这种情况下,如果让业务都设计回调接口来判断权限,将会非常繁琐。

因此,我们在 ACL 规则中设计了 Topic 模板变量,以降低业务方的接入成本。我们为业务方配置允许订阅的 Topic 中包含连接的用户名变量标识,表示只允许用户订阅或发送消息到自己的 Topic。

在这种情况下,网关可以在不与业务方通信的情况下,独立快速判断客户端是否有权限订阅或向 Topic 发送消息。

2.3 消息如何实现高可靠传输?

作为信息传输的关键节点,网关连接业务后端和客户端,转发信息时,必须确保传输过程中的可靠性。

尽管 TCP 可以确保传输的顺序和稳定性,但在 TCP 状态异常、客户端接收逻辑异常或发生了 Crash 等情况下,传输的信息可能会丢失。

为了确保下发或上传的信息能被对方正确处理,我们实现了回执和重传功能。在客户端收到并正确处理重要业务的信息后,需要发送回执,而网关会暂时保存客户端未接收的信息,并根据客户端的接收状况尝试重新发送,直至收到客户端的正确回执。

在面对服务端业务的高流量场景时,如果服务端给网关的每条信息都采用发送回执的方式,效率会较低。因此,我们也提供了基于消息队列的接收和发送方式,将在介绍发布订阅实现时作详细说明。

在设计通讯协议时,我们参照了 MQTT 规范,加强了认证和授权设计,实现了业务信息的隔离和解耦,确保了传输的可靠性。同时,保持了与 MQTT 协议一定程度上的兼容性,以便我们直接使用 MQTT 的各种客户端实现,降低业务方的接入成本。

3、我们怎么设计系统架构?

在设计项目整体架构时,我们优先考虑的是:

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

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

同时我们也需要快速地水平扩展能力。互联网场景下各种营销活动都可能导致连接数陡增,同时发布订阅模型系统中下发消息数会随着 Topic 的订阅者的个数线性增长,此时网关暂存的客户端未接收消息的存储压力也倍增。

将各个组件拆开后减少了进程内部状态,我们就可以将服务部署到容器中,利用容器来完成快速而且几乎无限制的水平扩展。

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

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

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

其中 Kafka 和 Redis 都是业界广泛使用的基础组件,它们在知乎都已平台化和容器化,它们也都能完成分钟级快速扩容。

4、如何构建长连接网关?

4.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。

4.2 内部消息传输的枢纽如何架构?

我们引入了业界广泛使用的消息队列 Kafka 来作为内部消息传输的枢纽。

前面提到了一些这么使用的原因:

  • 1)减少长连接 Broker 内部状态,让 Broker 可以无压力扩容;
  • 2)知乎内部已平台化,支持水平扩展。

还有一些原因是:

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

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

4.3 海量数据如何发布?

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

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

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

**情况二:**消息路由到 Kafka Topic,也被消费,普通的即时通讯场景,如下图所示。

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

**情况四:**消息路由到一个 Topic,然后从另一个 Topic 消费,用于消息需要过滤或者预处理的场景,如下图所示。

这套路由策略的设计灵活性非常高,可以解决几乎所有的场景的消息路由需求。同时因为发布订阅基于 Kafka,可以保证在处理大规模数据时的消息可靠性。

4.4 订阅

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

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

因为这个订阅关系是共享对象,当订阅和取消订阅发生时就会有连接尝试操作这个共享对象。为了避免并发写我们给 HashMap 加了锁,但这个全局锁的冲突非常严重,严重影响性能。

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

本地同时创建数百个 HashMap,当需要在某个 Key 上存取数据前通过 Hash 和取模找到其中一个 HashMap 然后进行操作,这样将全局锁分散到了数百个 HashMap 中,大大降低了操作冲突,也提升了整体的性能。

4.5 如何进行会话持久化?

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

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

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

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

4.6 如何使用滑动窗口进行QoS保障?

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

为了保证发送的效率,我们参考 TCP 的滑动窗口设计了并行发送的机制。我们设置一定的阈值为发送的滑动窗口,表示通道上可以同时有这么多条消息正在传输和被等待确认。

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

TCP 的滑动窗口内的 IP 报文无法保证顺序到达,而我们的通讯是基于 TCP 的所以我们的滑动窗口内的业务消息是顺序的,只有在连接状态异常、客户端逻辑异常等情况下才可能导致部分窗口内的消息乱序。

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

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

5、总结

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

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

说在最后:有问题可以找老架构取经

架构之路,充满了坎坷

架构和高级开发不一样 , 架构问题是open/开放式的,架构问题是没有标准答案的

正由于这样,很多小伙伴,尽管耗费很多精力,耗费很多金钱,但是,遗憾的是,一生都没有完成架构升级

所以,在架构升级/转型过程中,确实找不到有效的方案,可以来找40岁老架构尼恩求助.

前段时间一个小伙伴,他是跨专业来做Java,现在面临转架构的难题,但是经过尼恩几轮指导,顺利拿到了Java架构师+大数据架构师offer 。所以,如果遇到职业不顺,找老架构师帮忙一下,就顺利多了。

推荐阅读

百亿级访问量,如何做缓存架构设计

多级缓存 架构设计

消息推送 架构设计

阿里2面:你们部署多少节点?1000W并发,当如何部署?

美团2面:5个9高可用99.999%,如何实现?

网易一面:单节点2000Wtps,Kafka怎么做的?

字节一面:事务补偿和事务重试,关系是什么?

网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?

亿级短视频,如何架构?

炸裂,靠“吹牛”过京东一面,月薪40K

太猛了,靠“吹牛”过顺丰一面,月薪30K

炸裂了…京东一面索命40问,过了就50W+

问麻了…阿里一面索命27问,过了就60W+

百度狂问3小时,大厂offer到手,小伙真狠!

饿了么太狠:面个高级Java,抖这多硬活、狠活

字节狂问一小时,小伙offer到手,太狠了!

收个滴滴Offer:从小伙三面经历,看看需要学点啥?

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

猜你喜欢

转载自blog.csdn.net/crazymakercircle/article/details/132675308