本题分别从如下三个方面来分享:
线上有一个业务,某个通服务通知 udp 客户端通过向 udp 服务端(某个硬件设备)发送 udp 包来进行用户上线操作
当同时有大量的请求打到 udp 服务端的时候,udp 服务端的回包可能会在网络环境中丢包,(udp 是不可靠的)导致 udp 客户端不能及时的收到 udp 服务端的回包,在短时间内,udp 客户端的句柄又没有得到复用或者释放,没有收到回包的句柄就一直阻塞在那里,最终导致句柄泄漏
那么可以如何解决呢?
显然,第一个解决方式治标不治本,改大句柄数,当请求量变大的时候,仍然会出现句柄泄漏的情况
第二种方式相对靠谱很多
那么对于连接池,我们实际上是可以自己来进行造轮子的,仅用于学习,实际使用的话,自然还是会去使用经过大众考研过的公共开源库,我们可以来基本的分析和研究一下一个连接池需要有些什么?
当然,我们自己来体会一下连接池以及演示上述 udp 的 demo,我们仅实现如下几个简单功能作为演示
对于池子中具体链接的销毁,池子的关闭,池子的扩缩容,以及其他高级使用,xdm 可以进行扩展
自定义连接池,实际上咱们是使用 chan 通道来进行实现,具体源码可以查看:https://github.com/qingconglaixueit/customer_pool/blob/master/customer_pool/pool.go
func (conn *MyConnPool) GetObject() (interface{}, error) {
return conn.getObject()
}
func (conn *MyConnPool) getObject() (interface{}, error) {
if conn.isClosed {
return nil, errors.New("pool is closed")
}
// 从通道里面读,如果通道里面没有则新建一个
select {
case object := <-conn.pool:
return object, nil
default:
}
// 校验当前的连接数是否大于最大连接数,若是,则还是需要从 pool 中取
// 此时使用 mutex 主要是为了锁 MyConnPool 的非通道的其他成员
conn.Lock()
if conn.currentConn >= conn.maxConn {
object := <-conn.pool
conn.Unlock()
return object, nil
}
// 逻辑走到此处需要新建对象放到 pool 中
object, err := conn.connFun()
if err != nil {
conn.Unlock()
return nil, fmt.Errorf("create conn error : %+v", err)
}
// 当前 pool 已有连接数+1
conn.currentConn++
conn.Unlock()
return object, nil
}
func (conn *MyConnPool) ReturnObject(object interface{}) error {
return conn.returnObject(object)
}
func (conn *MyConnPool) returnObject(object interface{}) error {
if conn.isClosed {
return errors.New("pool is closed")
}
conn.pool <- object
return nil
}
具体源码地址:https://github.com/qingconglaixueit/customer_pool/blob/master/main.go
type PoolTest struct {
Conn *net.UDPConn
}
var myPool *customer_pool.MyConnPool
func init() {
myPool = customer_pool.NewMyConnPool(3, 1, func() (interface{}, error) {
return connectUdp()
})
if myPool == nil {
log.Panicln("NewMyConnPool error")
return
}
log.Println("myPool == ", myPool)
}
// 创建连接函数
func connectUdp() (*PoolTest, error) {
// 创建一个 udp 句柄
log.Println(">>>>> 创建一个 udp 句柄 ... ")
// 连接服务器
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 9998,
})
if err != nil {
log.Println("Connect to udp server failed,err:", err)
return nil, err
}
log.Printf("<<<<<< new udp connect %+v", conn)
return &PoolTest{Conn: conn}, nil
}
最后补充上咱们的 main 函数,就可以进行测试验证了
func main() {
for i := 0; i < 10; i++ {
msg := fmt.Sprintf("send udp data is %d", i)
go SendMsg(msg)
}
time.Sleep(2 * time.Second)
}
启动咱们的 udp 客户端,和 udp 服务端,我们可以查看到如下效果
客户端效果:
同时启了 10 个协程,每一个协程都会去池子里面拿连接对象,如果池子有现成的则直接使用,如果没有现成的,那么就新建一个连接, 如果当前池子已创建连接已经等于最大值,那么只能等着池子中有连接归还的时候再进行分配
服务端效果:
可以看到服务端收到的 10 个请求,实际上只有 3 个句柄在多次请求
再一次印证了客户端实际上确实只创建了 3 次 udp 句柄
上述是自定义简单连接池的基本 demo,关于 udp 超时处理的内容就不做演示,感兴趣的 xdm 可以下载源码来进行查看效果
https://github.com/qingconglaixueit/customer_pool
当然,我们大致知道连接池基本是都有哪些组成部分,可以如何玩之后,我们来应用一个 golang 通用的连接池 go-commons-pool, 源码地址为:https://github.com/jolestar/go-commons-pool
应用 go-commons-pool 咱们的 demo 仅验证该库的通用和便捷,对于上述我们自定义的池子,咱们使用到的 udp 涉及到的代码,可以基本不用变动,直接使用 go-commons-pool 直接网上套即可
和咱们自定义池子不一样的地方
实际 demo 为:
此处初始化池子配置,咱们也是一样传入具体池子最大的对象数,使用池子的默认配置,传入咱们创建连接的具体函数 connectUdp()
对于到 main 函数 和 SendMsg 函数,咱们的用法和写法基本完全一致
自然,效果也是一样的
当然对于 go-commons-pool 池子还有其他很多有意思的东西,感兴趣的可以来一起阅读以下他的源码,如下为当前池子的基本数据结构和创建池子的代码,咱们可以根据这个结构来追以下代码
代码目录如下:
./pool.go
至此,本文结束
文中涉及到的源码地址:
感谢阅读,欢迎交流,点个赞,关注一波 再走吧
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
好了,本次就到这里
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是阿兵云原生,欢迎点赞关注收藏,下次见~
可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI