服务框架的核心
大型服务框架的核心: RPC 通信
微服务的核心是远程通信和服务治理
远程通信提供了服务之间通信的桥梁, 服务治理提供了服务的后勤保障
服务的拆分增加了通信的成本, 因此远程通信很容易成为系统瓶颈
在满足一定的服务治理需求的前提下, 对远程通信的性能需求是技术选型的主要影响因素
很多微服务框架中的服务通信是基于 RPC 通信实现的
在没有进行组件扩展的前提下, Spring Cloud 是基于 Feign 组件实现 RPC 通信(基于 HTTP+JSON 序列化)
Dubbo 是基于 SPI 扩展了很多 RPC 通信框架, 包括 RMI,Dubbo,Hessian 等(默认为 Dubbo+Hessian 序列化)
性能测试
基于 Dubbo:2.6.4, 单一 TCP 长连接 + Protobuf(响应时间和吞吐量更优), 短连接的 HTTP+JSON 序列化
RPC 通信
架构演化
无论是微服务, SOA, 还是 RPC 架构, 都是分布式服务架构, 都需要实现服务之间的互相通信, 通常把这种通信统称为 RPC 通信
概念
RPC:Remote Process Call, 远程服务调用, 通过网络请求远程计算机程序服务的通信技术
RPC 框架封装了底层网络通信和序列化等技术
只需要在项目中引入各个服务的接口包, 就可以在代码中调用 RPC 服务(如同调用本地方法一样)
- RMI
- RMI:Remote Method Invocation
RMI 是 JDK 自带的 RPC 通信框架, 已经成熟地应用于 EJB 和 Spring, 是纯 Java 网络分布式应用系统的核心解决方案
RMI 实现了一台虚拟机应用对远程方法的调用可以同对本地方法调用一样, RMI 封装好了远程通信的具体细节
实现原理
RMI 远程代理对象是 RMI 中最核心的组件, 除了对象本身所在的虚拟机, 其他虚拟机也可以调用此对象的方法
这些虚拟机可以分布在不同的主机上, 通过远程代理对象, 远程应用可以用网络协议和服务进行通信
高并发下的性能瓶颈
Java 默认序列化
RMI 的序列化方式采用的是 Java 默认序列化, 性能不好, 而且不支持跨语言
TCP 短连接
RMI 是基于 TCP 短连接实现的, 在高并发情况下, 大量请求会带来大量 TCP 连接的创建和销毁, 非常消耗性能
阻塞式网络 IO
Socket 编程中使用传统的 IO 模型, 在高并发场景下基于短连接实现的网络通信就很容易产生 IO 阻塞, 性能将大打折扣
优化路径
TCP / UDP
网络传输协议有 TCP 和 UDP, 两个协议都是基于 Socket 编程
基于 TCP 协议实现的 Socket 通信是有连接的
传输数据要通过三次握手来实现数据传输的可靠性, 而传输数据是没有边界的, 采用的是字节流模式
基于 UDP 协议实现的 Socket 通信, 客户端不需要建立连接, 只需要创建一个套接字发送数据给服务端
基于 UDP 协议实现的 Socket 通信具有不可靠性
UDP 发送的数据采用的是数据报模式, 每个 UDP 的数据报都有一个长度, 该长度与数据一起发送到服务端
为了保证数据传输的可靠性, 通常情况下会采用 TCP 协议
在局域网且对数据传输的可靠性没有要求的情况下, 可以考虑使用 UDP 协议, UDP 协议的效率比 TCP 协议高
长连接
服务之间的通信不同于客户端与服务端之间的通信
由于客户端数量众多, 基于短连接实现请求, 可以避免长时间地占用连接, 导致系统资源浪费
服务之间的通信, 连接的消费端不会像客户端那么多, 但消费端向服务端请求的数量却一样多
基于长连接实现, 可以省去大量建立 TCP 连接和关闭 TCP 连接的操作, 从而减少系统的性能消耗, 节省时间
优化 Socket 通信
传统的 Socket 通信主要存在 IO 阻塞, 线程模型缺陷以及内存拷贝等问题, Netty4 对 Socket 通信编程做了很多方面的优化
实现非阻塞 IO: 多路复用器 Selector 实现了非阻塞 IO 通信
高效的 Reactor 线程模型
Netty 使用了主从 Reactor 多线程模型
主线程: 用于客户端的连接请求操作, 一旦连接建立成功, 将会监听 IO 事件, 监听到事件后会创建一个链路请求
链路请求将会注册到负责 IO 操作的 IO 工作线程上, 由 IO 工作线程负责后续的 IO 操作
Reactor 线程模型解决了在高并发的情况下, 由于单个 NIO 线程无法监听海量客户端和满足大量 IO 操作造成的问题
4. 串行设计
服务端在接收消息之后, 存在着编码, 解码, 读取和发送等链路操作
如果这些操作基于并行实现, 无疑会导致严重的锁竞争, 进而导致系统的性能下降
为了提升性能, Netty 采用串行无锁化完成链路操作, 提供了 Pipeline, 实现链路的各个操作在运行期间不会切换线程
5. 零拷贝
数据从内存发到网络中, 存在两次拷贝, 先是从用户空间拷贝到内核空间, 再从内核空间拷贝到网络 IO
NIO 提供的 ByteBuffer 可以使用 Direct Buffer 模式
直接开辟一个非堆物理内存, 不需要进行字节缓冲区的二次拷贝, 可以直接将数据写入到内核空间
6. 优化 TCP 参数配置, 提高网络吞吐量, Netty 可以基于 ChannelOption 来设置
TCP_NODELAY: 用于控制是否开启 Nagle 算法
Nagle 算法通过缓存的方式将小的数据包组成一个大的数据包, 从而避免大量发送小的数据包, 导致网络阻塞
在对时延敏感的应用场景, 可以选择关闭该算法
SO_RCVBUF / SO_SNDBUF:Socket 接收缓冲区和发送缓冲区的大小
SO_BACKLOG: 指定客户端连接请求缓冲队列的大小
服务端处理客户端连接请求是按顺序处理的, 同一时间只能处理一个客户端连接
当有多个客户端进来的时候, 服务端将不能处理的客户端连接请求放在队列中等待处理
SO_KEEPALIVE
连接会检查长时间没有发送数据的客户端的连接状态, 检测到客户端断开连接后, 服务端将回收该连接
将该值设置得小一些, 可以提高回收连接的效率
定制报文格式
设计一套报文, 用于描述具体的校验, 操作, 传输数据等内容
为了提高传输效率, 可以根据实际情况来设计, 尽量实现报体小, 满足功能, 易解析等特性
字段长度 (字节) 备注魔数 4 协议的标识, 类似于字节码的魔数, 通常为固定数字版本号 1 序列化算法 1Protobuf / Thrift 指令 1 类似于 HTTP 中的增删改查数据长度 4 数据 N
编解码
实现一个通信协议, 需要兼容优秀的序列化框架
如果只是单纯的数据对象传输, 可以选择性能相对较好的 Protobuf 序列化, 有利于提高网络通信的性能
Linux 的 TCP 参数设置
三次握手
四次挥手
配置项
- fs.file-max = 194448 / ulimit
- NET.ipv4.tcp_keepalive_time
- NET.ipv4.tcp_max_syn_backlog
- NET.ipv4.ip_local_port_range
- NET.ipv4.tcp_max_tw_buckets
- NET.ipv4.tcp_tw_reuse
备注
1.Linux 默认单个进程可以打开的文件数量上限为 1024,Socket 也是文件
2. 与 Netty 的 SO_KEEPALIVE 配置项的作用一致
3.SYN 队列的长度, 加大队列长度, 可以容纳更多等待连接的网络连接数
4. 客户端连接服务器时, 需要动态分配源端口号, 该配置项表示向外连接的端口范围
5. 当一个连接关闭时, TCP 会通过四次挥手来完成一次关闭连接操作, 在请求量比较大的情况下, 消费端会有大量 TIME_WAIT 状态的连接, 该参数可以限制 TIME_WAIT 状态的连接数量, 如果 TIME_WAIT 的连接数量超过该值, TIME_WAIT 将会立即被清除掉并打印警告信息
6. 客户端每次连接服务器时, 都会获得一个新的源端口以实现连接的唯一性, 在 TIME_WAIT 状态的连接数量过大的情况下, 会增加端口号的占用时间, 由于处于 TIME_WAIT 状态的连接属于关闭连接, 所以新创建的连接可以复用该端口号
来源: http://network.51cto.com/art/201909/603490.htm