备案域名指向一个网站,苏州新区做网站公司,深圳网站建设亿联时代,seo项目培训通信框架功能设计
功能描述
通信框架承载了业务内部各模块之间的消息交互和服务调用#xff0c;它的主要功能如下#xff1a;
基于 Netty 的 NIO 通信框架#xff0c;提供高性能的异步通信能力#xff1b;提供消息的编解码框架#xff0c;可以实现 POJO 的序列化和反序…通信框架功能设计
功能描述
通信框架承载了业务内部各模块之间的消息交互和服务调用它的主要功能如下
基于 Netty 的 NIO 通信框架提供高性能的异步通信能力提供消息的编解码框架可以实现 POJO 的序列化和反序列化消息内容的防篡改机制提供基于 IP 地址的白名单接入认证机制链路的有效性校验机制链路的断连重连机制
通信模型
1客户端发送应用握手请求消息携带节点 ID 等有效身份认证信息2服务端对应用握手请求消息进行合法性校验包括节点 ID 有效性校验、节点重复登录校验和 IP 地址合法性校验校验通过后返回登录成功的应用握手应答消息3链路建立成功之后客户端发送业务消息4链路成功之后服务端发送心跳消息5链路建立成功之后客户端发送心跳消息6链路建立成功之后服务端发送业务消息7服务端退出时服务端关闭连接客户端感知对方关闭连接后被动关闭客户端连接。备注需要指出的是协议通信双方链路建立成功之后双方可以进行全双工通信无论客户端还是服务端都可以主动发送请求消息给对方通信方式可以是 TWO WAY 或者 ONE WAY。双方之间的心跳采用 Ping-Pong 机制当链路处于空闲状态时客户端主动发送 Ping 消息给服务端服务端接收到 Ping 消息后发送应答消息 Pong 给客户端如果客户端连续发送 N 条 Ping 消息都没有接收到服务端返回的 Pong 消息说明链路已经挂死或者对方处于异常状态客户端主动关闭连接间隔周期 T 后发起重连操作直到重连成功。
消息定义
消息定义包含两部分消息头、消息体。在消息的定义上因为是同步处理模式不考虑应答消息需要填入请求消息 ID所以消息头中只有一个消息的 ID。如果要支持异步模式则请求消息头和应答消息头最好分开设计应答消息头中除了包括本消息的 ID 外还应该包括请求消息 ID以方便请求消息的发送方根据请求消息 ID 做对应的业务处理。消息体则支持 Java 对象类型的消息内容。
Netty 消息定义
名称类型长度描述headerHeader变长消息头定义bodyObject变长消息的内容
消息头定义Header
名称类型长度描述md5String变长消息体摘要缺省 MD5 摘要msgIDLong64消息的 IDTypeByte80业务请求消息1业务响应消息2业务 one way 消息3握手请求消息4握手应答消息5心跳请求消息6心跳应答消息PriorityByte8消息优先级0~255AttachmentMapString,Object变长可选字段用于扩展消息头
链路的建立
客户端的说明如下如果 A 节点需要调用 B 节点的服务但是 A 和 B 之间还没有建立物理链路则有调用方主动发起连接此时调用方为客户端被调用方为服务端。考虑到安全链路建立需要通过基于 Ip 地址或者号段的黑白名单安全认证机制作为样例本协议使用基于 IP 地址的安全认证如果有多个 Ip通过逗号进行分割。在实际的商用项目中安全认证机制会更加严格例如通过密钥对用户名和密码进行安全认证。客户端与服务端链路建立成功之后由客户端发送业务握手请求的认证消息服务端接收到客户端的握手请求消息之后如果 IP 校验通过返回握手成功应答消息给客户端应用层链路建立成功。握手应答消息中消息体为 byte 类型的结果0认证成功-1 认证失败服务端关闭连接。链路建立成功之后客户端和服务端就可以互相发送业务消息了在客户端和服务端的消息通信过程中业务消息体的内容需要通过 MD5 进行摘要防篡改。
可靠性设计
1心跳机制在凌晨等业务低谷时段如果发生网络闪断、连接被 Hang 住等问题时由于没有业务消息应用程序很难发现。到了白天业务高峰期时会发生大量的网络通信失败严重的会导致一段时间进程内无法处理业务消息。为了解决这个问题在网络空闲时采用心跳机制来检测链路的互通性一旦发现网络故障立即关闭链路主动重连。当读或者写心跳消息发生 I/O 异常的时候说明已经中断此时需要立即关闭连接如果是客户端需要重新发起连接。如果是服务端需要清空缓存的半包信息等到客户端重连。
2空闲的连接和超时检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务Netty 特地为它提供了几个 ChannelHandler 实现。IdleStateHandler 当连接空闲时间太长时将会触发一个 IdleStateEvent 事件。然后可以通过在 ChannelInboundHandler 中重写 userEventTriggered()方法来处理该 IdleStateEvent 事件。ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据则抛出一个 ReadTimeoutException 并关闭对应的 Channel。可以通过重写你的 ChannelHandler 中的 exceptionCaught()方法来检测该 Read-TimeoutException。
3重连机制如果链路中断等到 INTEVAL 时间后由客户端发起重连操作如果重连失败间隔周期 INTERVAL 后再次发起重连直到重连成功。为了保持服务端能够有充足的时间释放句柄资源在首次断连时客户端需要等待 INTERVAL 时间之后再发起重连而不是失败后立即重连。为了保证句柄资源能够及时释放无论什么场景下重连失败客户端必须保证自身的资源被及时释放包括但不限于 SocketChannel、Socket 等。重连失败后可以打印异常堆栈信息方便后续的问题定位。
4重复登录保护当客户端握手成功之后在链路处于正常状态下不允许客户端重复登录以防止客户端在异常状态下反复重连导致句柄资源被耗尽。服务端接收到客户端的握手请求消息之后对 IP 地址进行合法性校验如果校验成功在缓存的地址表中查看客户端是否已经登录如果登录则拒绝重复登录同时关闭 TCP 链路并在服务端的日志中打印握手失败的原因。客户端接收到握手失败的应答消息之后关闭客户端的 TCP 连接等待 INTERVAL 时间之后再次发起 TCP 连接直到认证成功。
实现
handler 示意图其中认证申请和认证检查可以在完成后移除。
1前期准备cn.firechou.nettyadv.vo 中定义了消息有关的实体类为了防篡改消息体需要进行摘要vo 包下提供了 EncryptUtils 类可以对消息体进行摘要目前支持 MD5、SHA-1 和 SHA-256 这三种缺省为 MD5其中 MD5 额外提供了加盐摘要。同时在 cn.firechou.nettyadv.kryocodec 中定义了有关序列化和反序列化的工具类和Handler本项目中序列化使用了 Kryo 序列化框架。
2服务端服务端中 NettyServe 类是服务端的主入口内部使用了 ServerInit 类进行 Handler 的安装。最先安装的当然是解决粘包和半包问题的 Handler很自然这里应该用 LengthFieldBasedFrameDecoder 进行解码为了实现方便我们也没有在消息报文中附带消息的长度由 Netty 帮我们在消息报文的最开始增加长度所以编码器选择了 LengthFieldPrepender。接下来自然就是序列化和反序列化直接使用我们在 kryocodec 下已经准备好的KryoDecoder 和 KryoEncoder 即可。服务端需要进行登录检查、心跳应答、业务处理对应着三个 handler于是我们分别安装了 LoginAuthRespHandler、HeartBeatRespHandler、ServerBusiHandler。为了节约网络和服务器资源如果客户端长久没有发送业务和心跳报文我们认为客户端出现了问题需要关闭这个连接我们引入 Netty 的 ReadTimeoutHandler当一定周期内默认值 50s我们设定为 15s没有读取到对方任何消息时会触发一个 ReadTimeouttException这时我们检测到这个异常需要主动关闭链路并清除客户端登录缓存信息等待客户端重连。
3客户端客户端的主类是 NettyClient并对外提供一个方法 send供业务使用内部使用了 ClientInit 类进行 Handler 的安装。最先安装的当然是解决粘包和半包问题的 Handler同样这里应该用 LengthFieldBasedFrameDecoder 进行解码编码器选择了 LengthFieldPrepender。接下来自然就是序列化和反序列化依然使用 KryoDecoder 和 KryoEncoder 即可。客户端需要主动发出认证请求和心跳请求。在 TCP 三次握手链路建立后客户端需要进行应用层的握手认证才能使用服务这个功能由 LoginAuthReqHandler 负责而这个 Handler 在认证通过后其实就没用了所以在认证通过后可以将这个 LoginAuthReqHandler 移除其实服务端的认证应答 LoginAuthRespHandler 同样也可以移除。对于发出心跳请求有两种实现方式一是定时发出本框架的第一个版本就是这种实现方式但是这种方式其实有浪费的情况因为如果客户端和服务器正在正常业务通信其实是没有必要发送心跳的所以第二种方式就是当链路写空闲时为了维持通道避免服务器关闭链接发出心跳请求。为了实现这一点我们首先在整个 pipeline 的最前面安装一个 CheckWriteIdleHandler 进行写空闲检测空闲时间定位8S取服务器读空闲时间15S的一半然后再安装一个 HearBeatReqHandler因为写空闲会触发一个 FIRST_WRITER_IDLE_STATE_EVENT 入站事件我们在 HearBeatReqHandler 的 userEventTriggered 方法中捕捉这个事件并发出心跳请求报文。考虑到在我们的实现中并没有双向心跳即是客户端向服务器发送心跳请求是服务器也向客户端发送心跳请求客户端这边同样需要检测服务器是否存活所以我们客户端这边安装了一个 ReadTimeoutHandler捕捉 ReadTimeoutException 后提示调用者并关闭通信链路触发重连机制。为了测试单独建立一个 BusiClient模拟业务方的调用。因为客户端的网络通信代码是在一个线程中单独启动的为了协调主线程和通信线程的工作我们引入了线程中的等待通知机制。
4测试
正常情况客户端宕机服务器应能清除客户端的缓存信息允许客户端重新登录服务器宕机客户端应能发起重连在 LoginAuthRespHandler 中进行注释可以模拟当服务器不处理客户端的请求时客户端在超时后重新进行登录。
5功能的增强作为一个通信框架支持诊断也是很重要的所以我们在服务端单独引入了一个 MetricsHandler可以提供目前在线 Channel 数、发送队列积压消息数、读取速率、写出速率相关数据以方便应用方对自己的应用的性能和繁忙程度进行检查和调整。当然对于一个通信框架还可以提供 SSL 安全访问、流控、I/O 线程和业务线程分离、参数的可配置化等等功能因为 Netty 对上述功能已经提供了很好的支持后面要学习的 Dubbo 框架源码分析中基本都有对应的实现。
面试问题分析
Netty 是如何解决 JDK 中的 Selector BUG 的
Selector BUGJDK NIO 的 BUG例如臭名昭著的 epoll bug它会导致 Selector 空轮询最终导致 CPU 100%。官方声称在 JDK1.6 版本的 update18 修复了该问题但是直到 JDK1.7 版本该问题仍旧存在只不过该 BUG 发生概率降低了一些而已它并没有被根本解决甚至 JDK1.8 的 131 版本中依然存在。JDK 官方认为这是 Linux Kernel 版本的 bug可以参见https://bugs.java.com/bugdatabase/view_bug.do?bug_id6403933https://bugs.java.com/bugdatabase/view_bug.do?bug_id2147719https://bugs.java.com/bugdatabase/view_bug.do?bug_id6670302https://bugs.java.com/bugdatabase/view_bug.do?bug_id6481709简单来说JDK 认为 linux 的 epoll 告诉我事件来了但是 JDK 没有拿到任何事件(READ、WRITE、CONNECT、ACCPET)但此时 select()方法不再选择阻塞了而是选择返回了 0于是就会进入一种无限循环导致 CPU 100%。这个问题的具体原因是在部分 Linux 的 2.6 的 kernel 中poll 和 epoll 对于突然中断的连接 socket 会对返回的 eventSet 事件集合置为 POLLHUP 或 POLLERReventSet 事件集合发生了变化这就可能导致 Selector 会被唤醒。但是这个时候 selector 的 select 方法返回 numKeys 是 0所以下面本应该对 key 值进行遍历的事件处理根本执行不了又回到最上面的 while(true) 循环循环往复不断的轮询直到 linux 系统出现 100%的 CPU 情况最终导致程序崩溃。Netty 解决办法对 Selector 的 select 操作周期进行统计每完成一次空的 select 操作进行一次计数若在某个周期内连续发生 N 次空轮询则触发了 epoll 死循环 bug。重建 Selector判断是否是其他线程发起的重建请求若不是则将原 SocketChannel 从旧的 Selector 上去除注册重新注册到新的 Selector 上并将原来的 Selector 关闭。具体代码在 NioEventLoop 的 select 方法中
如何让单机下 Netty 支持百万长连接
单机下能不能让我们的网络应用支持百万连接可以但是有很多的工作要做。
1操作系统首先就是要突破操作系统的限制。在 Linux 平台上无论编写客户端程序还是服务端程序在进行高并发 TCP 连接处理时最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制这是因为系统为每个 TCP 连接都要创建一个 socket 句柄每个 socket 句柄同时也是一个文件句柄。可使用 ulimit 命令查看系统允许当前用户进程打开的句柄数限制
$ ulimit -n
1024这表示当前用户的每个进程最多允许同时打开 1024 个句柄这 1024 个句柄中还得除去每个进程必然打开的标准输入标准输出标准错误服务器监听 socket 进程间通讯的 unix 或 socket 等文件那么剩下的可用于客户端 socket 连接的文件数就只有大概 1024-101014 个左右。也就是说缺省情况下基于 Linux 的通讯程序最多允许同时 1014 个 TCP 并发连接。对于想支持更高数量的 TCP 并发连接的通讯处理程序就必须修改 Linux 对当前用户的进程同时打开的文件数量。修改单个进程打开最大文件数限制的最简单的办法就是使用 ulimit 命令
$ ulimit –n 1000000如果系统回显类似于Operation not permitted之类的话说明上述限制修改失败实际上是因为指定的数值超过了 Linux 系统对该用户打开文件数的软限制或硬限制。因此就需要修改 Linux 系统对用户的关于打开文件数的软限制和硬限制。软限制soft limit是指 Linux 在当前系统能够承受的范围内进一步限制一个进程同时打开的文件数硬限制hardlimit是根据系统硬件资源状况主要是系统内存计算出来的系统最多可同时打开的文件数量。第一步修改/etc/security/limits.conf文件在文件中添加如下行 * soft nofile 1000000* hard nofile 1000000// *号表示修改所有用户的限制soft 和 hard 为两种限制方式其中 soft 表示警告的限制hard 表示真正限制nofile 表示打开的最大文件数。1000000 则指定了想要修改的新的限制值即最大打开文件数请注意软限制值要小于或等于硬限制。修改完后保存文件。第二步修改/etc/pam.d/login文件在文件中添加如下行
session required /lib/security/pam_limits.so 这是告诉 Linux 在用户完成系统登录后应该调用 pam_limits.so 模块来设置系统对该用户可使用的各种资源数量的最大限制包括用户可打开的最大文件数限制而 pam_limits.so 模块就会从/etc/security/limits.conf文件中读取配置来设置这些限制值。修改完后保存此文件。第三步查看 Linux 系统级的最大打开文件数限制使用如下命令 [spengas4 ~]$ cat /proc/sys/fs/file-max12158这表明这台 Linux 系统最多允许同时打开即包含所有用户打开文件数总和12158 个文件是 Linux 系统级硬限制所有用户级的打开文件数限制都不应超过这个数值。如果没有特殊需要不应该修改此限制除非想为用户级打开文件数限制设置超过此限制的值。如何修改这个系统最大文件描述符的限制呢修改 sysctl.conf 文件
vi /etc/sysctl.conf
# 在末尾添加
fs.file_max 1000000
# 立即生效
sysctl -p2Netty 调优设置合理的线程数对于线程池的调优主要集中在用于接收海量设备 TCP 连接、TLS 握手的 Acceptor 线程池( Netty 通常叫 boss NioEventLoop Group)上以及用于处理网络数据读写、心跳发送的 IO 工作线程池(Netty 通常叫 work Nio EventLoop Group)上。对于 Netty 服务端通常只需要启动一个监听端口用于端侧设备接入即可但是如果服务端集群实例比较少,甚至是单机或者双机冷备部署在端侧设备在短时间内大量接入时需要对服务端的监听方式和线程模型做优化,以满足短时间内例如 30s百万级的端侧设备接入的需要。服务端可以监听多个端口利用主从 Reactor 线程模型做接入优化前端通过 SLB 做 4 层门 7 层负载均衡。主从 Reactor 线程模型特点如下服务端用于接收客户端连接的不再是一个单独的 NIO 线程而是一个独立的NIO线程池Acceptor接收到客户端 TCP 连接请求并处理后(可能包含接入认证等)将新创建的 SocketChannel 注册到 I/O 线程池subReactor 线程池的某个 IO 线程由它负责 SocketChannel 的读写和编解码工作Acceptor 线程池仅用于客户端的登录、握手和安全认证等一旦链路建立成功就将链路注册到后端 sub reactor 线程池的 IO线程由 IO 线程负责后续的 IO 操作。对于 IO 工作线程池的优化可以先采用系统默认值即 CPU 内核数×2进行性能测试在性能测试过程中采集 IO 线程的 CPU 占用大小看是否存在瓶颈具体可以观察线程堆栈如果连续采集几次进行对比发现线程堆栈都停留在 Selectorlmpl.lockAndDoSelect则说明 IO 线程比较空闲无须对工作线程数做调整。如果发现 IO 线程的热点停留在读或者写操作或者停留在 ChannelHandler 的执行处则可以通过适当调大 Nio EventLoop 线程的个数来提升网络的读写性能。
心跳优化针对海量设备接入的服务端心跳优化策略如下。
要能够及时检测失效的连接并将其剔除防止无效的连接句柄积压导致 OOM 等问题设置合理的心跳周期防止心跳定时任务积压造成频繁的老年代 GC新生代和老年代都有导致 STW 的 GC不过耗时差异较大导致应用暂停使用 Netty 提供的链路空闲检测机制不要自己创建定时任务线程池加重系统的负担以及增加潜在的并发安全问题。
当设备突然掉电、连接被防火墙挡住、长时间 GC 或者通信线程发生非预期异常时会导致链路不可用且不易被及时发现。特别是如果异常发生在凌晨业务低谷期间当早晨业务高峰期到来时由于链路不可用会导致瞬间大批量业务失败或者超时这将对系统的可靠性产生重大的威胁。从技术层面看要解决链路的可靠性问题必须周期性地对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。心跳检测机制分为三个层面
TCP 层的心跳检测即 TCP 的 Keep-Alive 机制它的作用域是整个 TCP 协议栈。协议层的心跳检测主要存在于长连接协议中例如 MQTT。应用层的心跳检测它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
心跳检测的目的就是确认当前链路是否可用对方是否活着并且能够正常接收和发送消息。作为高可靠的 NIO 框架Netty 也提供了心跳检测机制。一般的心跳检测策略如下。
连续 N 次心跳检测都没有收到对方的 Pong 应答消息或者 Ping 请求消息则认为链路已经发生逻辑失效这被称为心跳超时。在读取和发送心跳消息的时候如果直接发生了 IO 异常说明链路已经失效这被称为心跳失败。无论发生心跳超时还是心跳失败都需要关闭链路由客户端发起重连操作保证链路能够恢复正常。
Netty 提供了三种链路空闲检测机制利用该机制可以轻松地实现心跳检测
读空闲链路持续时间 T 没有读取到任何消息写空闲链路持续时间 T 没有发送任何消息读写空闲链路持续时间 T 没有接收或者发送任何消息
对于百万级的服务器一般不建议很长的心跳周期和超时时长。
接收和发送缓冲区调优在一些场景下端侧设备会周期性地上报数据和发送心跳单个链路的消息收发量并不大针对此类场景可以通过调小 TCP 的接收和发送缓冲区来降低单个 TCP 连接的资源占用率。当然对于不同的应用场景收发缓冲区的最优值可能不同用户需要根据实际场景结合性能测试数据进行针对性的调优。
合理使用内存池随着 JVM 虚拟机和 JT 即时编译技术的发展对象的分配和回收是一个非常轻量级的工作。但是对于缓冲区 Buffer情况却稍有不同特别是堆外直接内存的分配和回收是一个耗时的操作。为了尽量重用缓冲区Netty 提供了基于内存池的缓冲区重用机制。在百万级的情况下需要为每个接入的端侧设备至少分配一个接收和发送 ByteBuf 缓冲区对象采用传统的非池模式每次消息读写都需要创建和释放 ByteBuf 对象如果有 100 万个连接每秒上报一次数据或者心跳就会有 100 万次/秒的 ByteBuf 对象申请和释放即便服务端的内存可以满足要求GC 的压力也会非常大。以上问题最有效的解决方法就是使用内存池每个 NioEventLoop 线程处理 N 个链路在线程内部链路的处理是串行的。假如 A 链路首先被处理它会创建接收缓冲区等对象待解码完成构造的 POJO 对象被封装成任务后投递到后台的线程池中执行然后接收缓冲区会被释放每条消息的接收和处理都会重复接收缓冲区的创建和释放。如果使用内存池则当 A 链路接收到新的数据报时从 NioEventLoop 的内存池中申请空闲的 ByteBuf解码后调用 release 将 ByteBuf 释放到内存池中供后续的 B 链路使用。Netty 内存池从实现上可以分为两类堆外直接内存和堆内存。由于 ByteBuf 主要用于网络 IO 读写因此采用堆外直接内存会减少一次从用户堆内存到内核态的字节数组拷贝所以性能更高。由于 DirectByteBuf 的创建成本比较高因此如果使用 DirectByteBuf则需要配合内存池使用否则性价比可能还不如 Heap Byte。Netty 默认的 IO 读写操作采用的都是内存池的堆外直接内存模式如果用户需要额外使用 ByteBuf建议也采用内存池方式如果不涉及网络 IO 操作只是纯粹的内存操作可以使用堆内存池这样内存的创建效率会更高一些。
IO 线程和业务线程分离如果服务端不做复杂的业务逻辑操作仅是简单的内存操作和消息转发则可以通过调大 NioEventLoop 工作线程池的方式直接在IO线程中执行业务 Channelhandler这样便减少了一次线程上下文切换性能反而更高。如果有复杂的业务逻辑操作则建议 IO 线程和业务线程分离对于 IO 线程由于互相之间不存在锁竞争可以创建一个大的 NioEvent Loop Group 线程组所有 Channel 都共享同一个线程池。对于后端的业务线程池则建议创建多个小的业务线程池线程池可以与 IO 线程绑定这样既减少了锁竞争又提升了后端的处理性能。
针对端侧并发连接数的流控无论服务端的性能优化到多少都需要考虑流控功能。当资源成为瓶颈或者遇到端侧设备的大量接入需要通过流控对系统做保护。流控的策略有很多种比如针对端侧连接数的流控在 Netty 中可以非常方便地实现流控功能新增一个 FlowControlchannelhandler然后添加到 ChannelPipeline 靠前的位置覆盖 channelActive()方法创建 TCP 链路后执行流控逻辑如果达到流控阈值则拒绝该连接调用 ChannelHandlerContext 的 close()方法关闭连接。
3JVM 层面相关性能优化当客户端的并发连接数达到数十万或者数百万时系统一个较小的抖动就会导致很严重的后果例如服务端的 GC导致应用暂停STW的 GC 持续几秒就会导致海量的客户端设备掉线或者消息积压一旦系统恢复会有海量的设备接入或者海量的数据发送很可能瞬间就把服务端冲垮。JVM 层面的调优主要涉及 GC 参数优化GC 参数设置不当会导致频繁 GC甚至 OOM 异常对服务端的稳定运行产生重大影响。
确定 GC 优化目标
GC垃圾收集有三个主要指标。吞吐量是评价 GC 能力的重要指标在不考虑 GC 引起的停顿时间或内存消耗时吞吐量是 GC 能支撑应用程序达到的最高性能指标。延迟GC 能力的最重要指标之一是由于 GC 引起的停顿时间优化目标是缩短延迟时间或完全消除停顿STW避免应用程序在运行过程中发生抖动。内存占用GC 正常时占用的内存量。JVM GC 调优的三个基本原则如下。Minor gc 回收原则每次新生代 GC 回收尽可能多的内存减少应用程序发生 Full gc 的频率。GC 内存最大化原则垃圾收集器能够使用的内存越大垃圾收集效率越高应用程序运行也越流畅。但是过大的内存一次 Full go 耗时可能较长如果能够有效避免 FullGC就需要做精细化调优。3 选 2 原则吞吐量、延迟和内存占用不能兼得无法同时做到吞吐量和暂停时间都最优需要根据业务场景做选择。对于大多数应用吞吐量优先其次是延迟。当然对于时延敏感型的业务需要调整次序。
确定服务端内存占用
在优化 GC 之前需要确定应用程序的内存占用大小以便为应用程序设置合适的内存提升 GC 效率。内存占用与活跃数据有关活跃数据指的是应用程序稳定运行时长时间存活的Java 对象。活跃数据的计算方式通过 GC 日志采集 GC 数据获取应用程序稳定时老年代占用的 Java 堆大小以及永久代元数据区占用的 Java 堆大小两者之和就是活跃数据的内存占用大小。
GC 优化过程
1、GC 数据的采集和研读2、设置合适的 JVM 堆大小3、选择合适的垃圾回收器和回收策略当然具体如何做请参考 JVM 相关课程。而且 GC 调优会是一个需要多次调整的过程期间不仅有参数的变化更重要的是需要调整业务代码。
什么是水平触发(LT)和边缘触发(ET)
Level_triggered(水平触发)当被监控的文件描述符上有可读写事件发生时epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完那么下次调用 epoll_wait()时它还会通知你在上没读写完的文件描述符上继续读写当然如果你一直不去读写它会一直通知你。Edge_triggered(边缘触发)当被监控的文件描述符上有可读写事件发生时epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完那么下次调用 epoll_wait()时它不会通知你也就是它只会通知你一次直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高系统不会充斥大量你不关心的就绪文件描述符。select()poll()模型都是水平触发模式信号驱动 IO 是边缘触发模式epoll()模型即支持水平触发也支持边缘触发默认是水平触发。JDK 中的 select 实现是水平触发而 Netty 提供的 Epoll 的实现中是边缘触发。
请说说 DNS 域名解析的全过程
本题其实是“浏览器中输入 URL 到返回页面的全过程”这个题目的衍生题
根据域名进行 DNS 域名解析拿到解析的 IP 地址建立 TCP 连接向 IP 地址发送 HTTP 请求服务器处理请求返回响应结果关闭 TCP 连接浏览器解析 HTML浏览器布局渲染
可见 DNS 域名解析是其中的一部分。DNS 一个由分层的服务系统大致说来有 3 种类型的 DNS 服务器根 DNS 服务器、顶级域(Top-Level DomainTLD DNS 服务器和权威 DNS 服务器。根 DNS 服务器截止到 2022 年 4 月 22 日有 1533 个根名字服务器遍及全世界可到 [https://root-servers.org/](https://root-servers.org/) 查询分布情况根名字服务器提供 TLD 服务器的 IP 地址。顶级域DNS服务器对于每个顶级域如 com、org、net、edu 和 gov和所有国家的顶级域如 uk、fr、ca 和 jp都有 TLD 服务器或服务器集群。TLD 服务器提供了权威 DNS 服务器的 IP 地址。权威 DNS 服务器在因特网上的每个组织机构必须提供公共可访问的 DNS 记录这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。一个组织机构能够选择实现它自己的权威 DNS 服务器以保存这些记录也可以交由商用 DNS 服务商存储在这个服务提供商的一个权威 DNS 服务器中比如阿里云旗下的中国万网。有另一类重要的 DNS 服务器称为本地 DNS 服务器 local DNS server。严格说来一个本地 DNS 服务器并不属于该服务器的层次结构但它对 DNS 层次结构是至关重要的。每个 ISP 都有一台本地 DNS 服务器。同时很多路由器中也会附带 DNS 服务。当主机发出 DNS 请求时该请求被发往本地 DNS 服务器它起着代理的作用并将该请求转发到 DNS 服务器层次结构中同时本地 DNS 服务器也会缓存 DNS 记录。所以一个 DNS 客户要决定主机名 www.baidu.com 的 IP 地址。粗略说来将发生下列事件。客户首先与根服务器之一联系它将返回顶级域名 com 的 TLD 服务器的 IP 地址。该客户则与这些 TLD 服务器之一联系它将为 baidu.com 返回权威服务器的 IP 地址。最后该客户与 baidu.com 权威服务器之一联系它为主机名 www.baidu.com 返回其 IP 地址。