java系统找不到指定文件怎么解决
265
2022-10-01
Go 语言网络库 getty 的那些事
个人从事互联网基础架构系统研发十年余,包括我自己在内的很多朋友都是轮子党。
2011 年我在深圳某大厂干活时,很多使用 C 语言进行开发的同事都有一个自己的私人 SDK 库,尤其是网络通信库。个人刚融入这个环境时,觉得不能基于 epoll/iocp/kqueue 接口封装一个异步网络通信库,会在在同事面前矮人三分。现在想起来当时很多同事很大胆,把自己封装的通信库直接在测试生产环境上线使用,据说那时候整个公司投入生产环境运行的 RPC 通信库就有 197 个之多。
个人当时耗费两年周末休息时间造了这么一个私人 C 语言 SDK 库:包含所有 C++ STL 的 C 语言实现、定时器、TCP/UDP 通信库、输出速度可达 150MiB/s 的日志输出库、基于 CAS 实现的各种锁、规避了 ABA 问题的多生产者多消费者无锁队列 等等。自己当时不懂 PHP,其实稍微封装下就可以做出一个类似于 Swoole 的框架。如果一直坚持写下来,可能它也堪媲美老朋友郑树新老师的 ACL 库了。
自己 2014 年开始接触 Go 语言,经过一段时间学习之后,发现了它有 C 语言一样的特点:基础库太少 -- 又可以造轮子了。我清晰地记得自己造出来的第一个轮子每个 element 只使用一个指针的双向链表 xorlist【见参考 1】。
2016 年 6 月我在做一个即时通讯项目时,原始网关是一个基于 netty 实现的 Java 项目,后来使用 Go 语言重构时其 TCP 网路库各个接口实现就直接借鉴了 netty。同年 8 月份在其上添加了websocket 支持时,觉得 websocket提供的 onopen/onclose/onmessage 网络接口极其方便,就把它的网络接口改为 OnOpen/OnClose/OnMessage/OnError/OnCron 后,把全部代码放到了github上,并在小范围内进行了宣传【见参考2】。## 1 Getty 分层设计
Getty 严格遵循着分层设计的原则。主要分为数据交互层,业务控制层,网络层,同时还提供非常易于扩展的监控接口,其实就是对外暴露的网络库使用接口。
 as case 1. // The return value is (nil, 0, nil) as case 2. // The return value is (nil, pkgLen, nil) as case 3. // The return value is (pkg, pkgLen, nil) as case 4. // The handleTcpPackage may invoke func Read many times as case 5. Read(Session, []byte) (interface{}, int, error)}// Writer is used to marshal pkg and write to sessiontype Writer interface { // if @Session is udpGettySession, the second parameter is UDPContext. Write(Session, interface{}) ([]byte, error)}// package handler interfacetype ReadWriter interface { Reader Writer}````ReadWriter` 接口定义代码如上。`Read` 接口之所以有三个返回值,是为了处理 TCP 流粘包情况:
- 如果发生了网络流错误,如协议格式错误,返回 `(nil, 0, error)`- 如果读到的流很短,其头部 (header) 都无法解析出来,则返回 `(nil, 0, nil)`- 如果读到的流很短,可以解析出其头部 (header) 但无法解析出整个包 (package),则返回 `(nil, pkgLen, nil)`- 如果能够解析出一个完整的包 (package),则返回 `(pkg, 0, error)`### 1.2 业务控制层
业务控制层是 getty 设计的精华所在,由 Connection 和 Session 组成。- Connection 负责建立的 Socket 连接的管理,主要包括:连接状态管理,连接超时控制,连接重连控制,数据包的相关处理,如数据包压缩,数据包拼接重组等。- Session 负责客户端的一次连接建立的管理,记录着本次连接的状态数据,管理 Connection 的创建,关闭,控制数据的发送/接口的处理。#### 1.2.1 Session
Session 可以说是 getty 中最核心的接口了,每个 Session 代表着一次会话连接。向下,Session 对 go 内置的网络库做了完善的封装,包括对 net.Conn 的数据流读写、超时机制等。向上,Session 提供了业务可切入的接口,用户只需实现 EventListener 就可以将 getty 接入到自己的业务逻辑中。
目前 Session 接口的实现只有 session 结构体,Session 作为接口仅仅是提供了对外可见性以及遵循面向编程接口的机制,之后我们谈到 Session,其实都是在讲 session 结构体。
#### 1.2.2 Connection
Connection 对 Go 内置网络库进行了抽象封装,根据不同的通信模式,Connection 分别有三种实现:
- gettyTCPConn:底层是 *net.TCPConn- gettyUDPConn:底层是 *net.UDPConn- gettyWSConn:底层使用第三方库实现### 1.3 网络 API 接口 EventListener
本文开头提到,Getty 网络 API 接口命名是从 WebSocket 网络 API 接口借鉴而来。Getty 维护者之一 郝洪范 同学喜欢把它称为 “监控接口”,理由是:网络编程最麻烦的地方当出现问题时不知道如何排查,通过这些接口可以知道每个网络连接的每个阶段的状态。server.RunEventLoop(NewHelloServerSession)```
第一行非常明显是一个创建 server 的过程,options 是一个 `func (*ServerOptions)` 函数,用于给 server 添加一些额外功能设置,如启用 ssl,使用任务队列提交任务的形式执行任务等。
第二行的 `server.RunEventLoop(NewHelloServerSession)` 则是启动 server,同时也是整个 server 服务的入口,它的作用是监听某个端口(具体监听哪个端口可以通过 options 指定),并处理 client 发来的数据。RunEventLoop 方法需要提供一个参数 NewSessionCallback,该参数的类型定义如下:
```gotype NewSessionCallback func(Session) error```
这是一个回调函数,将在成功建立和 client 的连接后被调用,一般提供给用户用于设置网络参数,如设置连接的 keepAlive 参数、缓冲区大小、最大消息长度、read/write 超时时间等,但最重要的是,**用户需要通过该函数,为 session 设置好要用的 Reader、Writer 以及 EventListener。**
****至此,getty 中 server 的处理流程大体如下图:
、调用 `EventListener.OnMessage()` 接口进行逻辑处理;另一个 goroutine 负责发送网络字节流、调用 `EventListener.OnCron()` 执行定时逻辑。
后来出于提升网络吞吐的需要,getty 进行了一次大的优化:将逻辑处理这步逻辑从第一个 goroutine 任务中分离,添加 Goroutine Pool【下文简称 gr pool】专门处理网络逻辑。即网络字节流接收、逻辑处理和网络字节流发送都有单独的 goroutine 处理。
Gr Pool 成员有任务队列【其数目为 M】和 Gr 数组【其数目为 N】以及任务【或者称之为消息】,根据 N 的数目变化其类型分为可伸缩 gr pool 与固定大小 gr pool。可伸缩 Gr Pool 好处是可以随着任务数目变化增减 N 以节约 CPU 和内存资源。
#### 3.1.1 固定大小 Gr Pool
按照 M 与 N 的比例,固定大小 Gr Pool 又区分为 1:1、1:N、M:N 三类。
1:N 类型的 Gr Pool 最易实现,个人 2017 年在项目 `kafka-connect-elasticsearch` 中实现过此类型的 Gr Pool:作为消费者从 kafka 读取数据然后放入消息队列,然后各个 worker gr 从此队列中取出任务进行消费处理。
这种模型的 gr pool 整个 pool 只创建一个 chan, 所有 gr 去读取这一个 chan,其缺点是:队列读写模型是 一写多读,因为 go channel 的低效率【整体使用一个 mutex lock】造成竞争激烈,当然其网络包处理顺序更无从保证。
getty 初始版本的 gr pool 模型为 1:1,每个 gr 多有自己的 chan,其读写模型是一写一读,其优点是可保证网络包处理顺序性, 如读取 kafka 消息时候,按照 kafka message 的 key 的 hash 值以取余方式【hash(message key) % N】将其投递到某个 task queue,则同一 key 的消息都可以保证处理有序。但这种模型的缺陷:每个task 处理要有时间,此方案会造成某个 gr 的 chan 里面有 task 堵塞,就算其他 gr 闲着,也没办法处理之【任务处理“饥饿”】。
更进一步的 1:1 模型的改进方案:每个 gr 一个 chan,如果 gr 发现自己的 chan 没有请求,就去找别的 chan,发送方也尽量发往消费快的协程。这个方案类似于 go runtime 内部的 MPG 调度算法使用的 goroutine 队列,但其算法和实现会过于复杂。
getty 后来实现了 M:N 模型版本的 gr pool,每个 task queue 被 N/M 个 gr 消费,这种模型的优点是兼顾处理效率和锁压力平衡,可以做到总体层面的任务处理均衡,Task 派发采用 RoundRobin 方式。
```go// ┌───────┐ ┌───────┐ ┌───────┐ ┌─────────────────────────┐// │worker0│ │worker2│ │worker4│ ┌─┤ taskId % NumQueues == 0 │// └───────┘ └───────┘ └───────┘ │ └─────────────────────────┘// │ │ │ │// └───────consume───────┘ enqueue// ▼ task ╔══════════════════╗// ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │ ║ baseWorkerPool: ║// TaskQueue0 │t0│t1│t2│t3│t4│t5│t6│t7│t8│t9│◀─┘ ║ ║// ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ ║ *NumWorkers=6 ║// TaskQueue1 │t0│t1│t2│t3│t4│t5│t6│t7│t8│t9│◀┐ ║ *NumQueues=2 ║// └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │ ║ *QueueSize=10 ║// ▲ enqueue ╚══════════════════╝// ┌───────consume───────┐ task// │ │ │ │// ┌───────┐ ┌───────┐ ┌───────┐ │ ┌─────────────────────────┐// │worker1│ │worker3│ │worker5│ └──│ taskId % NumQueues == 1 │// └───────┘ └───────┘ └───────┘ └─────────────────────────┘```其整体实现如上图所示。具体代码实现请参见 `gr pool`【参考7】 连接中的 `TaskPool` 实现。
#### 3.1.2 无限制 Gr Pool
使用固定量资源的 gr pool,在请求量加大的情况下无法保证吞吐和 RT,有些场景下用户希望使用尽可能用尽所有的资源保证吞吐和 RT。
后来借鉴 "A Million WebSockets and Go" 一文【参考8】中的 “Goroutine pool” 实现了一个 可无限扩容的 gr pool。
具体代码实现请参见 `gr pool`【参考7】 连接中的 `taskPoolSimple` 实现。
#### 3.1.3 网络包处理顺序
`固定大小的 gr pool` 优点是限定了逻辑处理流程对机器 CPU/MEMORY 等资源的使用,而 `无限制 Gr Pool` 虽然保持了弹性但有可能耗尽机器的资源导致容器被内核杀掉。但无论使用何种形式的 `gr pool`,getty 无法保证网络包的处理顺序。
譬如 getty 服务端收到了同一个客户端发来的 A 和 B 两个网络包,gr pool 模型可能造成服户端先处理 B 包后处理 A 包。同样,客户端也可能先收到服务端对 B 包的 response,然后才收到 A 包的 response。
如果客户端的每次请求都是独立的,没有前后顺序关系,则带有 gr pool 特性的 getty 不考虑顺序关系是没有问题的。如果上层用户关注 A 和 B 请求处理的前后顺序,则可以把 A 和 B 两个请求合并为一个请求,或者把 gr pool 特性关闭。### 3.2 Lazy ReconnectGetty 中 session 代表一个网络连接,client 其实是一个网络连接池,维护一定数量的连接 session,这个数量当然是用户设定的。Getty client 初始版本【2018 年以前的版本】中,每个 client 单独启动一个 goroutine 轮询检测其连接池中 session 数量,如果没有达到用户设定的连接数量就向 server 发起新连接。
当 client 与 server 连接断开时,server 可能是被下线了,可能是意外退出,也有可能是假死。如果上层用户判定对端 server 确实不存在【如收到注册中心发来的 server 下线通知】后,调用 `client.Close()` 接口把连接池关闭掉。 如果上层用户没有调用这个接口把连接池关闭掉,client 就认为对端地址还有效,就会不断尝试发起重连,维护连接池。
综上,从一个旧 session 关闭到创建一个新 session,getty client 初始版本的重连处理流程是:
- 1 旧 session 关闭`网络接收 goroutine`;- 2 旧 session `网络发送 goroutine` 探测到 `网络接收 goroutine` 退出后终止网络发送,进行资源回收后设定当前 session 无效;- 3 client 的轮询 goroutine 检测到无效 session 后把它从 session 连接池删除;- 4 client 的轮询 goroutine 检测到有效 session 数目少于 getty 上层使用者设定的数目 且 getty 上层使用者没有通过 `client.Close()` 接口关闭连接池时,就调用连接接口发起新连接。上面这种通过定时轮询方式不断查验 client 中 session pool 中每个 session 有效性的方式,可称之为主动连接。主动连接的缺点显然是每个 client 都需要单独启用一个 goroutine。当然,其进一步优化手段之一是可以启动一个全局的 goroutine,定时轮询检测所有 client 的 session pool,不必每个 client 单独启动一个 goroutine。但是个人从 2016 年开始一直在思考一个问题:能否换一种 session pool 维护方式,去掉定时轮询机制,完全不使用任何的 goroutine 维护每个 client 的 session pool?
2018 年 5 月个人在一次午饭后遛弯时,把 getty client 的重连逻辑又重新梳理了一遍,突然想到了另一种方法,在步骤 2 中完全可以对 `网络发送 goroutine` 进行 “废物利用”,在这个 goroutine 标记当前 session 无效的逻辑步骤之后再加上一个逻辑:
- 1 如果当前 session 的维护者是一个 client【因为 session 的使用者也可能是 server】;- 2 且如果其当前 session pool 的 session 数量少于上层使用者设定的 session number;- 3 且如果上层使用者还没有通过 `client.Close()` 设定当前 session pool 无效【即当前 session pool 有效,或者说是对端 server 有效】- 4 满足上面三个条件,`网络发送 goroutine` 执行连接重连即可;- 5 新网络连接 session 建立成功且被加入 client 的 session pool 后,`网络发送 goroutine` 使命完成直接退出。
我把这种重连方式称之为 `lazy reconnect`,`网络发送 goroutine` 在其生命周期的最后阶段应该被称之为 `网络重连 goroutine`。通过 `lazy reconnect`这种方式,上述重连步骤 3 和 步骤 4 的逻辑被合入了步骤 2,client 当然也就没必要再启动一个额外的 goroutine 通过定时轮询的方式维护其 session pool 了。
- 第二个 goroutine 调用 `EventListener.OnMessage()` 接口进行逻辑处理- 第三个 goroutine 负责发送网络字节流、调用 `EventListener.OnCron()` 执行定时逻辑以及 `lazy reconnect`
在连接较少的情况下这个模型尚可稳定运行。但当集群规模到了一定规模,譬如每个服务端的连接数达 1k 以上时,单单网络连接就至少使用 3k 个 goroutine,这是对 CPU 计算资源和内存资源极大地浪费。上面三个 goroutine 中,第一个 goroutine 无可拆解,第二个 goroutine 实际是 gr pool 一部分,可优化的对象就是第三个 goroutine 的任务。
2020 年底 getty 维护团队首先把网络字节流任务放入了第二个 goroutine:处理完逻辑任务后立即进行同步网络发送。此处改进后,第三个 goroutine 就只剩下 `EventListener.OnCron()` 定时处理任务和 `lazy reconnect` 任务。这个定时逻辑其实可以抛给 getty 上层调用者处理,但出于方便用户和向后兼容地考虑,我们使用了另一种优化思路:引入时间轮管理定时心跳检测。
,兼容 Go 的 timer 所有原生接口,其优点是所有时间任务都在一个 goroutine 内执行。2020 年 12 月把它引入 getty 后,getty 所有的 `EventListener.OnCron()` 定时处理任务均交由 timer wheel 处理,第三个 goroutine 就可以完美地消失了【后续:两个月后发现 timer 库被另一个擅长拉 star 的 rpc 项目抄走了^+^】。
此时第三个 goroutine 就剩下最后一个任务:`lazy reconnect`。当第三个 goroutine 不存在后,这个任务完全可以放入第一个 goroutine:在当`网络字节流接收 goroutine` 检测到网络错误退出前的最后一个步骤,执行 `lazy reconnect`。
优化改进后的一个网络连接最多只使用两个 goroutine:
- 一个 goroutine 进行网络字节流的接收、调用 Reader 接口拆解出网络包 (package)、 `lazy reconnect`- 第二个 goroutine 调用 `EventListener.OnMessage()` 接口进行逻辑处理、发送网络字节流第二个 goroutine 来自 gr pool。考虑到 gr pool 中的 goroutine 都是可复用的公共资源,单个连接实际上只单独占用了第一个 goroutine。### 3.4 Getty 压测
Getty 维护团队的郝洪范同学,借鉴了 rpcx 的 benchmark 程序后实现了 getty benchmark (参考11),对优化后的 v1.4.3 版本进行过压测。压测环境如下:```bash系统:CentOS Linux release 7.5.1804 (Core)CPU:8核内存:16G类型:2 台腾讯云虚机,一台运行 100 个客户端,另一台运行一个服务端Go 版本:1.15.6服务访问方式:模拟局域网请求网络参数:每个客户端发送 100000 个请求,每个请求 915B```压测结果:
。这个压测当然没有压出 getty 的极限性能,但已经能够满足阿里主要场景下的使用需求。 ## 4 发展 timeline从我个人 2016 年时写 getty 开始,到目前有一个专门的开源团队维护 getty,getty 一路走来殊为不易。梳理其 timeline,其主要发展时间节点如下:
- 2016 年 6 月份开发出第一个生产可用版本,支持 TCP/websocket 两种通信协议,同年 10 月在 gocn 上发帖 [推广;- 2017 年 9 月时,实现了一个 Go 语言 timer 时间轮库 `timer wheel` [2018 年 3 月在其上加入 UDP 通信支持;- 2018 年 5 月实现了 `lazy reconnect`;- 2018 年 5 月支持至于 protobuf 和 json 的 RPC;- 2018 年 8 月加入基于 zookeeper 和 etcd 的服务注册和发现功能,取名 micro;- 2019 年 5 月 getty 的底层 tcp 通信实现被独立拆出迁入 github.com/dubbogo,后迁入 github.com/apache/dubbo-getty;- 2019 年 5 月 getty RPC 包被携程的两位同学迁入 `[构建了 dubbogo 基于 hessian2 协议的 RPC 层;- 2019 年 5 月,加入固定大小 goroutine pool;- 2019 年底,刘晓敏同学告知其基于 getty 实现了 seata-golang;- 2020 年 11 月,把网络发送与逻辑处理合并放入 gr pool 中处理;- 2021 年 5 月,完成定时器优化;
最后,还是如第三节 `优化` 开头部分所说,getty 维护团队不追求无意义的 benchmark 数据,不做无意义的炫技式优化,只根据生产环境需求来进行自身改进。只要维护团队在,getty 稳定性和性能定会越来越优秀。如果对 dubbo-go 感兴趣,欢迎加入搜索钉钉群号 23331795【或者扫描如下二维码】钉钉群与社区进行交流。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~