【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

京东的 Netty 实践,京麦 TCP 网关长连接容器架构

  • 2018-01-02
  • 本文字数:4148 字

    阅读完需:约 14 分钟

京麦从 2014 年构建网关,从 HTTP 网关发展到 TCP 网关。在 2016 年重构完成基于 Netty4.x+Protobuf3.x 实现对接 PC 和 App 上下行通信的高可用、高性能、高稳定的 TCP 长连接网关。本文重点介绍京麦 TCP 网关的背景、架构及 Netty 的应用实践。

背景

早期京麦搭建 HTTP 和 TCP 长连接功能主要用于消息通知的推送,并未应用于 API 网关。随着逐步对 NIO 的深入学习和对 Netty 框架的了解,以及对系统通信稳定能力越来越高的要求,开始有了采用 NIO 技术应用网关实现 API 请求调用的想法,最终在 2016 年实现,并完全支撑业务化运行。

由于诸多的改进,包括 TCP 长连接容器、Protobuf 的序列化、服务泛化调用框架等等,性能比 HTTP 网关提升 10 倍以上,稳定性也远远高于 HTTP 网关。

架构

基于 Netty 构建京麦 TCP 网关的长连接容器,作为网关接入层提供服务 API 请求调用。

一、网络结构

客户端通过域名 + 端口访问 TCP 网关,域名不同的运营商对应不同的 VIP,VIP 发布在 LVS 上,LVS 将请求转发给后端的 HAProxy,再由 HAProxy 把请求转发给后端的 Netty 的 IP+Port。

LVS 转发给后端的 HAProxy,请求经过 LVS,但是响应是 HAProxy 直接反馈给客户端的,这也就是 LVS 的 DR 模式。

二、TCP 网关长连接容器架构

TCP 网关的核心组件是 Netty,而 Netty 的 NIO 模型是 Reactor 反应堆模型(Reactor 相当于有分发功能的多路复用器 Selector)。每一个连接对应一个 Channel(多路指多个 Channel,复用指多个连接复用了一个线程或少量线程,在 Netty 指 EventLoop),一个 Channel 对应唯一的 ChannelPipeline,多个 Handler 串行的加入到 Pipeline 中,每个 Handler 关联唯一的 ChannelHandlerContext。

TCP 网关长连接容器的 Handler 就是放在 Pipeline 的中。我们知道 TCP 属于 OSI 的传输层,所以建立 Session 管理机制构建会话层来提供应用层服务,可以极大的降低系统复杂度。

所以,每一个 Channel 对应一个 Connection,一个 Connection 又对应一个 Session,Session 由 Session Manager 管理,Session 与 Connection 是一一对应的,Connection 保存着 ChannelHandlerContext(ChannelHanderContext 可以找到 Channel),Session 通过心跳机制来保持 Channel 的 Active 状态。

每一次 Session 的会话请求(ChannelRead)都是通过 Proxy 代理机制调用 Service 层,数据请求完毕后通过写入 ChannelHandlerConext 再传送到 Channel 中。数据下行主动推送也是如此,通过 Session Manager 找到 Active 的 Session,轮询写入 Session 中的 ChannelHandlerContext,就可以实现广播或点对点的数据推送逻辑。

Netty 的应用实践

京麦 TCP 网关使用 Netty Channel 进行数据通信,使用 Protobuf 进行序列化和反序列化,每个请求都将被封装成 Byte 二进制字节流,在整个生命周期中,Channel 保持长连接,而不是每次调用都重新创建 Channel,达到链接的复用。

一、TCP 网关 Netty Server 的 IO 模型

  1. 创建 ServerBootstrap,设定 BossGroup 与 WorkerGroup 线程池。
  2. bind 指定的 port,开始侦听和接受客户端链接。(如果系统只有一个服务端 port 需要监听,则 BossGroup 线程组线程数设置为 1。)
  3. 在 ChannelPipeline 注册 childHandler,用来处理客户端链接中的请求帧。

二、TCP 网关的线程模型

TCP 网关使用 Netty 的线程池,共三组线程池,分别为 BossGroup、WorkerGroup 和 ExecutorGroup。其中,BossGroup 用于接收客户端的 TCP 连接,WorkerGroup 用于处理 I/O、执行系统 Task 和定时任务,ExecutorGroup 用于处理网关业务加解密、限流、路由,及将请求转发给后端的抓取服务等业务操作。

NioEventLoop 是 Netty 的 Reactor 线程,其角色:

  1. Boss Group:作为服务端 Acceptor 线程,用于 accept 客户端链接,并转发给 WorkerGroup 中的线程。
  2. Worker Group:作为 IO 线程,负责 IO 的读写,从 SocketChannel 中读取报文或向 SocketChannel 写入报文。
  3. Task Queue/Delay Task Queue:作为定时任务线程,执行定时任务,例如链路空闲检测和发送心跳消息等。

三、TCP 网关执行时序图

其中步骤一至步骤九是 Netty 服务端的创建时序,步骤十至步骤十三是 TCP 网关容器创建的时序。

  • 步骤一:创建 ServerBootstrap 实例,ServerBootstrap 是 Netty 服务端的启动辅助类。
  • 步骤二:设置并绑定 Reactor 线程池,EventLoopGroup 是 Netty 的 Reactor 线程池,EventLoop 负责所有注册到本线程的 Channel。
  • 步骤三:设置并绑定服务器 Channel,Netty Server 需要创建 NioServerSocketChannel 对象。
  • 步骤四:TCP 链接建立时创建 ChannelPipeline,ChannelPipeline 本质上是一个负责和执行 ChannelHandler 的职责链。
  • 步骤五:添加并设置 ChannelHandler,ChannelHandler 串行的加入 ChannelPipeline 中。
  • 步骤六:绑定监听端口并启动服务端,将 NioServerSocketChannel 注册到 Selector 上。
  • 步骤七:Selector 轮训,由 EventLoop 负责调度和执行 Selector 轮询操作。
  • 步骤八:执行网络请求事件通知,轮询准备就绪的 Channel,由 EventLoop 执行 ChannelPipeline。
  • 步骤九:执行 Netty 系统和业务 ChannelHandler,依次调度并执行 ChannelPipeline 的 ChannelHandler。
  • 步骤十:通过 Proxy 代理调用后端服务,ChannelRead 事件后,通过发射调度后端 Service。
  • 步骤十一:创建 Session,Session 与 Connection 是相互依赖关系。
  • 步骤十二:创建 Connection,Connection 保存 ChannelHandlerContext。
  • 步骤十三:添加 SessionListener,SessionListener 监听 SessionCreate 和 SessionDestory 等事件。

四、TCP 网关源码分析

1. Session 管理

Session 是客户端与服务端建立的一次会话链接,会话信息中保存着 SessionId、连接创建时间、上次访问事件,以及 Connection 和 SessionListener,在 Connection 中保存了 Netty 的 ChannelHandlerContext 上下文信息。Session 会话信息会保存在 SessionManager 内存管理器中。

创建 Session 的源码

通过源码分析,如果 Session 已经存在销毁 Session,但是这个需要特别注意,创建 Session 一定不要创建那些断线重连的 Channel,否则会出现 Channel 被误销毁的问题。因为如果在已经建立 Connection(1) 的 Channel 上,再建立 Connection(2),进入 session.close 方法会将 cxt 关闭,Connection(1) 和 Connection(2) 的 Channel 都将会被关闭。在断线之后再建立连接 Connection(3),由于 Session 是有一定延迟,Connection(3) 和 Connection(1/2) 不是同一个,但 Channel 可能是同一个。

所以,如何处理是否是断线重练的 Channel,具体的方法是在 Channel 中存入 SessionId,每次事件请求判断 Channel 中是否存在 SessionId,如果 Channel 中存在 SessionId 则判断为断线重连的 Channel。

2. 心跳

心跳是用来检测保持连接的客户端是否还存活着,客户端每间隔一段时间就会发送一次心跳包上传到服务端,服务端收到心跳之后更新 Session 的最后访问时间。在服务端长连接会话检测通过轮询 Session 集合判断最后访问时间是否过期,如果过期则关闭 Session 和 Connection,包括将其从内存中删除,同时注销 Channel 等。

通过源码分析,在每个 Session 创建成功之后,都会在 Session 中添加 TcpHeartbeatListener 这个心跳检测的监听,TcpHeartbeatListener 是一个实现了 SessionListener 接口的守护线程,通过定时休眠轮询 Sessions 检查是否存在过期的 Session,如果轮训出过期的 Session,则关闭 Session。

同时,注意到 session.connect 方法,在 connect 方法中会对 Session 添加的 Listeners 进行添加时间,它会循环调用所有 Listner 的 sessionCreated 事件,其中 TcpHeartbeatListener 也是在这个过程中被唤起。

3. 数据上行

数据上行特指从客户端发送数据到服务端,数据从 ChannelHander 的 channelRead 方法获取数据。数据包括创建会话、发送心跳、数据请求等。这里注意的是,channelRead 的数据包括客户端主动请求服务端的数据,以及服务端下行通知客户端的返回数据,所以在处理 object 数据时,通过数据标识区分是请求 - 应答,还是通知 - 回复。

4. 数据下行

数据下行通过 MQ 广播机制到所有服务器,所有服务器收到消息后,获取当前服务器所持有的所有 Session 会话,进行数据广播下行通知。如果是点对点的数据推送下行,数据也是先广播到所有服务器,每天服务器判断推送的端是否是当前服务器持有的会话,如果判断消息数据中的信息是在当前服务,则进行推送,否则抛弃。

通过源码分析,数据下行则通过 NotifyProxy 的方式发送数据,需要注意的是 Netty 是 NIO,如果下行通知需要获取返回值,则要将异步转同步,所以 NotifyFuture 是实现 java.util.concurrent.Future 的方法,通过设置超时时间,在 channelRead 获取到上行数据之后,通过 seq 来关联 NotifyFuture 的方法。

下行的数据通过 TcpConnector 的 send 方法发送,send 方式则是通过 ChannelHandlerContext 的 writeAndFlush 方法写入 Channel,并实现数据下行,这里需要注意的是,之前有另一种写法就是 cf.await,通过阻塞的方式来判断写入是否成功,这种写法偶发出现 BlockingOperationException 的异常。

使用阻塞获取返回值的写法

关于 BlockingOperationException 的问题我在 StackOverflow 进行提问,非常幸运的得到了 Norman Maurer(Netty 的核心贡献者之一)的解答。

最终结论大致分析出,在执行 write 方法时,Netty 会判断 current thread 是否就是分给该 Channe 的 EventLoop,如果是则行线程执行 IO 操作,否则提交 executor 等待分配。当执行 await 方法时,会从 executor 里 fetch 出执行线程,这里就需要 checkDeadLock,判断执行线程和 current threads 是否时同一个线程,如果是就检测为死锁抛出异常 BlockingOperationException。

总结

本篇文章粗浅地向大家介绍了京麦 TCP 网关中使用 Netty 实现长连接容器的架构,对涉及 TCP 长连接容器搭建的关键点一一进行了阐述,以及对源码进行简单地分析。在京麦发展过程里 Netty 还有很多的实践应用,例如 Netty4.11+HTTP2 实现 APNs 的消息推送等等。

作者介绍

张松然,京东商城商家研发部架构师。丰富的构建高性能高可用大规模分布式系统的研发、架构经验。2013 年加入京东,目前负责京麦服务网关的系统研发工作。

感谢雨多田光对本文的审校。

2018-01-02 17:0111986

评论 1 条评论

发布
用户头像
感谢作者的分享。由于客户端连接到了集群中未知的一台机器上,我们要给这个客户端发送消息,知乎采用Kafka的发布订阅,不知道京麦是怎么做的?还有,当集群缩容的时候,是服务端主动断开连接,让客户端重新发起请求吗?
2019-09-17 23:46
回复
没有更多了
发现更多内容

数据分析与可视化在企业中的日常应用

夏日星河

数字经济催生的低代码开发浪潮,JNPF带你轻松实现应用程序拓展!

引迈信息

可观测性Trace全量存储——之开篇

乘云 DataBuff

【有奖体验】这个 AI 智能回答,就一个字“绝”!

阿里巴巴云原生

阿里云 AI 云原生

直播平台源码功能分享:直播回放功能的实现

山东布谷科技

软件开发 直播 源码搭建 直播平台源码

AI药物研发大赛培训来啦!清华博导讲解,高分基线等你来Fork!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 百度飞桨

直播预约 | 邀您共同探讨“云XR技术如何改变元宇宙的虚拟体验”

3DCAT实时渲染

元宇宙 VR虚拟现实 云XR技术

直播系统聊天技术(九):千万级实时直播弹幕的技术实践

JackJiang

网络编程 即时通讯 IM

vivo 自研鲁班分布式 ID 服务实践

vivo互联网技术

雪花算法 分布式ID UidGenerator Tinyid Leaf

BI商业智能工具成宠儿,企业降本增效优质工具

夜雨微澜

如何从消失的异常堆栈定位线上问题 | 京东云技术团队

京东科技开发者

缓存 TCP 异常堆栈 企业号 6 月 PK 榜

强化学习从基础到进阶--案例与实践含面试必知必答[9]:稀疏奖励、reward shaping、curiosity、分层强化学习HRL

汀丶人工智能

人工智能 深度学习 强化学习 6 月 优质更文活动 分层强化学习

推进绿色数据中心建设,宁畅发布“无忧焕液计划”

Geek_2d6073

Java反射源码学习之旅 | 京东云技术团队

京东科技开发者

Java java反射 企业号 6 月 PK 榜

互联网大厂2700道Java高频面试题(2023年最新版)不管你工作几年,都可以看看!

采菊东篱下

java面试

全球NDR市场“客户之声”首次发布:那些最挑剔的用户都选了谁?

Geek_2d6073

IPQ9554-IPQ6010 supports QCN9274-QCN9074 to achieve triple-band 2.4G, 5G, 6E, up to 9.6 Gbps rate

wifi6-yiyi

WIFI 6e WiFi7

京东到家小程序-在性能及多端能力的探索实践 | 京东云技术团队

京东科技开发者

小程序 性能优化 后端 多端开发 企业号 6 月 PK 榜

Apifox:与 OpenAI 接口调试的最佳拍档

Apifox

开发 API openai ChatGPT chatgpt api

AIGC+设计|AI卖画,卖的是创意还是生意?

TE智库

人工智能 AIGC 生成式AI 平面设计

typescript的必要性及使用 | 京东云技术团队

京东科技开发者

JavaScript 前端 企业号 6 月 PK 榜

CFFF在复旦上线 中国高校可以在世界带好头!

新云力量

阿里云 复旦大学 智算平台

CFFF部署在公共云上意义重大

新云力量

阿里云 复旦大学 智算平台

强化学习从基础到进阶--案例与实践含面试必知必答[10]:模仿学习、行为克隆、逆强化学习、第三人称视角模仿学习、序列生成和聊天机器人

汀丶人工智能

人工智能 深度学习 强化学习 模仿学习 逆强化学习

浅谈 ByteHouse Projection 优化实践

不在线第一只蜗牛

bytehouse

玖章算术与百度智能云达成合作,「NineData SQL 开发」成为百度智能云主推的数据库工具

Baidu AICLOUD

SpringBoot 2 种方式快速实现分库分表,轻松拿捏!

程序员小富

分库分表 springboot ShardingSphere

深入了解 KaiwuDB 负载行为数据采集

KaiwuDB

数据采集 KaiwuDB KAP

芯片中的上百亿个晶体管是如何设计的?

博文视点Broadview

PoseiSwap IDO、IEO 结束,即将登录 BNB Chain

西柚子

瓴羊Quick BI:财务报表分析工具的重要应用

巷子

京东的Netty实践,京麦TCP网关长连接容器架构_架构_张松然_InfoQ精选文章