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


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

  1. 消息路由到 Kafka Topic,但不消费,适合数据上报的场景 。
  2. 消息路由到 Kafka Topic,也被消费,普通的即时通讯场景 。
  3. 直接从 Kafka Topic 消费并下发,用于纯下发消息的场景 。
  4. 消息路由到一个 Topic,然后从另一个 Topic 消费,用于消息需要过滤或者预处理的场景 。
这套路由策略的设计灵活性非常高,可以解决几乎所有的场景的消息路由需求 。同时因为发布订阅基于 Kafka,可以保证在处理大规模数据时的消息可靠性 。
订阅当长连接 Broker 从 Kafka Topic 中消费出消息后会查找本地的订阅关系,然后将消息分发到客户端会话 。
我们最开始直接使用 HashMap 存储客户端的订阅关系 。当客户端订阅一个 Topic 时我们就将客户端的会话对象放入以 Topic 为 Key 的订阅 Map 中,当反查消息的订阅关系时直接用 Topic 从 Map 上取值就行 。
因为这个订阅关系是共享对象,当订阅和取消订阅发生时就会有连接尝试操作这个共享对象 。为了避免并发写我们给 HashMap 加了锁,但这个全局锁的冲突非常严重,严重影响性能 。
最终我们通过分片细化了锁的粒度,分散了锁的冲突 。
本地同时创建数百个 HashMap,当需要在某个 Key 上存取数据前通过 Hash 和取模找到其中一个 HashMap 然后进行操作,这样将全局锁分散到了数百个 HashMap 中,大大降低了操作冲突,也提升了整体的性能 。
会话持久化当消息被分发给会话 Session 对象后,由 Session 来控制消息的下发 。
Session 会判断消息是否是重要 Topic 消息,是的话将消息标记 QoS 等级为 1,同时将消息存储到 Redis 的未接收消息队列,并将消息下发给客户端 。等到客户端对消息的 ACK 后,再将未确认队列中的消息删除 。
有一些业界方案是在内存中维护了一个列表,在扩容或缩容时这部分数据没法跟着迁移 。也有部分业界方案是在长连接集群中维护了一个分布式内存存储,这样实现起来复杂度也会变高 。
我们将未确认消息队列放到了外部持久化存储中,保证了单个 Broker 宕机后,客户端重新上线连接到其他 Broker 也能恢复 Session 数据,减少了扩容和缩容的负担 。
滑动窗口在发送消息时,每条 QoS 1 的消息需要被经过传输、客户端处理、回传 ACK 才能确认下发完成,路径耗时较长 。如果消息量较大,每条消息都等待这么长的确认才能下发下一条,下发通道带宽不能被充分利用 。
为了保证发送的效率,我们参考 TCP 的滑动窗口设计了并行发送的机制 。我们设置一定的阈值为发送的滑动窗口,表示通道上可以同时有这么多条消息正在传输和被等待确认 。
知乎千万级高性能长连接网关是如何搭建的

文章插图
 
我们应用层设计的滑动窗口跟 TCP 的滑动窗口实际上还有些差异 。
TCP 的滑动窗口内的 IP 报文无法保证顺序到达,而我们的通讯是基于 TCP 之所以我们的滑动窗口内的业务消息是顺序的,只有在连接状态异常、客户端逻辑异常等情况下才可能导致部分窗口内的消息乱序 。
因为 TCP 协议保证了消息的接收顺序,所以正常的发送过程中不需要针对单条消息进行重试,只有在客户端重新连接后才对窗口内的未确认消息重新发送 。消息的接收端同时会保留窗口大小的缓冲区用来消息去重,保证业务方接收到的消息不会重复 。
我们基于 TCP 构建的滑动窗口保证了消息的顺序性同时也极大提升传输的吞吐量 。
写在最后基础架构组负责知乎的流量入口和内部基础设施建设,对外我们奋斗在直面海量流量的的第一战线,对内我们为所有的业务提供坚如磐石的基础设施,用户的每一次访问、每一个请求、内网的每一次调用都与我们的系统息息相关 。




推荐阅读