知乎千万级高性能长连接网关是如何搭建的( 二 )


在设计通讯协议时我们参考了 MQTT 规范,拓展了认证和鉴权设计,完成了业务消息的隔离与解耦,保证了一定程度的传输可靠性 。同时保持了与 MQTT 协议一定程度上兼容,这样便于我们直接使用 MQTT 的各端客户端实现,降低业务方接入成本 。
我们怎么设计系统架构?
在设计项目整体架构时,我们优先考虑的是:

  • 可靠性
  • 水平扩展能力
  • 依赖组件成熟度
简单才值得信赖 。
为了保证可靠性,我们没有考虑像传统长连接系统那样将内部数据存储、计算、消息路由等等组件全部集中到一个大的分布式系统中维护,这样增大系统实现和维护的复杂度 。我们尝试将这几部分的组件独立出来,将存储、消息路由交给专业的系统完成,让每个组件的功能尽量单一且清晰 。
同时我们也需要快速的水平扩展能力 。互联网场景下各种营销活动都可能导致连接数陡增,同时发布订阅模型系统中下发消息数会随着 Topic 的订阅者的个数线性增长,此时网关暂存的客户端未接收消息的存储压力也倍增 。将各个组件拆开后减少了进程内部状态,我们就可以将服务部署到容器中,利用容器来完成快速而且几乎无限制的水平扩展 。
最终设计的系统架构如下图:
知乎千万级高性能长连接网关是如何搭建的

文章插图
 
系统主要由四个主要组件组成:
  1. 接入层使用 OpenResty 实现,负责连接负载均衡和会话保持
  2. 长连接 Broker,部署在容器中,负责协议解析、认证与鉴权、会话、发布订阅等逻辑
  3. redis 存储,持久化会话数据
  4. Kafka 消息队列,分发消息给 Broker 或业务方
其中 Kafka 和 Redis 都是业界广泛使用的基础组件,它们在知乎都已平台化和容器化,它们也都能完成分钟级快速扩容 。
我们如何构建长连接网关?
接入层
【知乎千万级高性能长连接网关是如何搭建的】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 。
发布与订阅我们引入了业界广泛使用的消息队列 Kafka 来作为内部消息传输的枢纽 。
前面提到了一些这么使用的原因:
  • 减少长连接 Broker 内部状态,让 Broker 可以无压力扩容
  • 知乎内部已平台化,支持水平扩展
还有一些原因是:
  • 使用消息队列削峰,避免突发性的上行或下行消息压垮系统
  • 业务系统中大量使用 Kafka 传输数据,降低与业务方对接成本
其中利用消息队列削峰好理解,下面我们看一下怎么利用 Kafka 与业务方更好地完成对接 。
发布长连接 Broker 会根据路由配置将消息发布到 Kafka Topic,同时也会根据订阅配置去消费 Kafka 将消息下发给订阅客户端 。


推荐阅读