使用Netty,我们到底在开发些什么?
在java界,netty无疑是开发网络应用的拿手菜。你不需要太多关注复杂的nio模型和底层网络的细节,使用其丰富的接口,可以很容易的实现复杂的通讯功能。 和golang的网络模块相比,netty还是太过臃肿。不过java类框架就是这样,属于那种离了IDE就无法存活的编码语言。 最新的netty版本将模块分的非常细,如果不清楚每个模块都有什么内容,直接使用netty-all即可。 单纯从使用方面来说,netty是非常简单的,掌握ByteBuf、Channel、Pipeline、Event模型等,就可以进行开发了。你会发现面试netty相关知识,没得聊。但Netty与其他开发模式很大不同,最主要的就是其异步化。异步化造成的后果就是编程模型的不同,同时有调试上的困难,对编码的要求比较高,因为bug的代价与业务代码的bug代价不可同日而语。 但从项目来说,麻雀虽小五脏俱全,从业务层到服务网关,以及各种技术保障,包括监控和配置,都是需要考虑的因素。netty本身占比很小。 本文将说明使用netty开发,都关注哪些通用的内容,然后附上单机支持100w连接的linux配置。本文并不关注netty的基础知识。 协议开发 网络开发中最重要的就是其通讯格式,协议。我们常见的protobuf、json、avro、mqtt等,都属于此列。协议有语法、语义、时序三个要素。 我见过很多中间件应用,采用的是redis协议,而后端落地的却是mysql;也见过更多的采用mysql协议实现的各种自定义存储系统,比如proxy端的分库分表中间件、tidb等。 我们常用的redis,使用的是文本协议;mysql等实现的是二进制协议。放在netty中也是一样,实现一套codec即可(继承Decoder或Encoder系列)。netty默认实现了dns、haproxy、http、http2、memcache、mqtt、redis、smtp、socks、stomp、xml等协议,可以说是很全了,直接拿来用很爽。 一个可能的产品结构会是这样的,对外提供一致的外观,核心存储却不同: 文本协议在调试起来是比较直观和容易的,但安全性欠佳;而二进制协议就需要依赖日志、wireshark等其他方式进行分析,增加了开发难度。传说中的粘包拆包,就在这里处理。而造成粘包的原因,主要是由于缓冲区的介入,所以需要约定双方的传输概要等信息,netty在一定程度上解决了这个问题。 每一个想要开发网络应用的同学,心里都埋了一颗重新设计协议的梦想种子。但协议的设计可以说是非常困难了,要深耕相应业务,还要考虑其扩展性。如没有特别的必要,建议使用现有的协议。 连接管理功能 做Netty开发,连接管理功能是非常重要的。通信质量、系统状态,以及一些黑科技功能,都是依赖连接管理功能。 无论是作为服务端还是客户端,netty在创建连接之后,都会得到一个叫做Channel的对象。我们所要做的,就是对它的管理,我习惯给它起名叫做ConnectionManager。 管理类会通过缓存一些内存对象,用来统计运行中的数据。比如面向连接的功能:包发送、接收数量;包发送、接收速率;错误计数;连接重连次数;调用延迟;连接状态等。这会频繁用到java中concurrent包的相关类,往往也是bug集中地。 但我们还需要更多,管理类会给予每个连接更多的功能。比如,连接创建后,想要预热一些功能,那这些状态就可以参与路由的决策。通常情况下,将用户或其他元信息也attach到连接上,能够多维度的根据条件筛选一些连接,进行批量操作,比如灰度、过载保护等,是一个非常重要的功能。 管理后台可以看到每个连接的信息,筛选到一个或多个连接后,能够开启对这些连接的流量录制、信息监控、断点调试,你能体验到掌控一切的感觉。 管理功能还能够看到系统的整个运行状态,及时调整负载均衡策略;同时对扩容、缩容提供数据依据。 心跳检测 应用协议层的心跳是必须的,它和tcp keepalive是完全不同的概念。 应用层协议层的心跳检测的是连接双方的存活性,兼而连接质量,而keepalive检测的是连接本身的存活性。而且后者的超时时间默认过长,完全不能适应现代的网络环境。 心跳就是靠轮训,无论是服务端,还是客户端比如GCM等。保活机制会在不同的应用场景进行动态的切换,比如程序唤起和在后台,轮训的策略是不一样的。 Netty内置通过增加IdleStateHandler产生IDLE事件进行便捷的心跳控制。你要处理的,就是心跳超时的逻辑,比如延迟重连。但它的轮训时间是固定的,无法动态修改,高级功能需要自己定制。 在一些客户端比如Android,频繁心跳的唤起会浪费大量的网络和电量,它的心跳策略会更加复杂一些。 边界 优雅退出机制 Java的优雅停机通常通过注册JDK ShutdownHook来实现。 Runtime.getRuntime().addShutdownHook(); 一般通过kill -15进行java进程的关闭,以便在进程死亡之前进行一些清理工作。 注意:kill -9 会立马杀死进程,不给遗言的机会,比较危险。 虽然netty做了很多优雅退出的工作,通过EventLoopGroup的shutdownGracefully方法对nio进行了一些状态设置,但在很多情况下,这还不够多。它只负责单机环境的优雅关闭。 流量可能还会通过外层的路由持续进入,造成无效请求。我的通常做法是首先在外层路由进行一次本地实例的摘除,把流量截断,然后再进行netty本身的优雅关闭。这种设计非常简单,即使没有重试机制也会运行的很好,前提是在路由层需要提前暴露相关接口。 异常处理功能 netty由于其异步化的开发方式,以及其事件机制,在异常处理方面就显得异常重要。为了保证连接的高可靠性,许多异常需要静悄悄的忽略,或者在用户态没有感知。 netty的异常会通过pipeline进行传播,所以在任何一层进行处理都是可行的,但编程习惯上,习惯性抛到最外层集中处理。 为了最大限度的区别异常信息,通常会定义大量的异常类,不同的错误会抛出不同的异常。发生异常后,可以根据不同的类型选择断线重连(比如一些二进制协议的编解码紊乱问题),或者调度到其他节点。 功能限制 指令模式 网络应用就该干网络应用的事,任何通讯都是昂贵的。在《Linux之《荒岛余生》(五)网络篇》中,我们谈到百万连接的服务器,广播一个1kb消息,就需要1000M的带宽,所以并不是什么都可以放在网络应用里的。 一个大型网络应用的合理的思路就是值发送相关指令。客户端在收到指令以后,通过其他方式,比如http,进行大型文件到获取。很多IM的设计思路就是如此。 指令模式还会让通讯系统的扩展性和稳定性得到保证。增加指令可以是配置式的,立即生效,服务端不需要编码重启。 稳定性保证 网络应用的流量一般都是非常大的,并不适合全量日志的开启。应用应该只关注主要事件的日志,关注异常情况下的处理流程,日志要打印有度。 网络应用也不适合调用其他缓慢的api,或者任何阻塞I/O的接口。一些实时的事件,也不应该通过调用接口吐出数据,可以走高速mq等其他异步通道。 (编辑:ASP站长网) |