如何在Go语言中使用Redis连接池
创始人
2025-01-27 09:50:18
0次

 

一、关于连接池

一个数据库服务器只拥有有限的资源,并且如果你没有充分使用这些资源,你可以通过使用更多的连接来提高吞吐量。一旦所有的资源都在使用,那么你就不 能通过增加更多的连接来提高吞吐量。事实上,吞吐量在连接负载较大时就开始下降了。通常可以通过限制与可用的资源相匹配的数据库连接的数量来提高延迟和吞 吐量。

 

如果不使用连接池,那么,每次传输数据,我们都需要进行创建连接,收发数据,关闭连接。在并发量不高的场景,基本上不会有什么问题,一旦并发量上去了,那么,一般就会遇到下面几个常见问题:

  • 性能普遍上不去

  • CPU 大量资源被系统消耗

  • 网络一旦抖动,会有大量 TIME_WAIT 产生,不得不定期重启服务或定期重启机器

  • 服务器工作不稳定,QPS 忽高忽低

要想解决这些问题,我们就要用到连接池了。连接池的思路很简单,在初始化时,创建一定数量的连接,先把所有长连接存起来,然后,谁需要使用,从这里取走,干完活立马放回来。 如果请求数超出连接池容量,那么就排队等待、退化成短连接或者直接丢弃掉。

二、使用连接池遇到的坑

最近在一个项目中,需要实现一个简单的 Web Server 提供 Redis 的 HTTP interface,提供 JSON 形式的返回结果。考虑用 Go 来实现。

首先,去看一下 Redis 官方推荐的 Go Redis driver。官方 Star 的项目有两个:Radix.v2 和 Redigo。经过简单的比较后,选择了更加轻量级和实现更加优雅的 Radix.v2。

Radix.v2 包是根据功能划分成一个个的 sub package,每一个 sub package 在一个独立的子目录中,结构非常清晰。我的项目中会用到的 sub package 有 redis 和 pool。

由于我想让这种被 fork 的进程***简单点,做的事情单一一些,所以,在没有深入去看 Radix.v2 的 pool 的实现之前,我选择了自己实现一个 Redis pool。(这里,就不贴代码了。后来发现自己实现的 Redis pool 与 Radix.v2 实现的 Redis pool 的原理是一样的,都是基于 channel 实现的, 遇到的问题也是一样的。)

不过在测试过程中,发现了一个诡异的问题。在请求过程中经常会报 EOF 错误。而且是概率性出现,一会有问题,一会又好了。通过反复的测试,发现 bug 是有规律的,当程序空闲一会后,再进行连续请求,会发生3次失败,然后之后的请求都能成功,而我的连接池大小设置的是3。再进一步分析,程序空闲300秒 后,再请求就会失败,发现我的 Redis server 配置了 timeout 300,至此,问题就清楚了。是连接超时 Redis server 主动断开了连接。客户端这边从一个超时的连接请求就会得到 EOF 错误。

然后我看了一下 Radix.v2 的 pool 包的源码,发现这个库本身并没有检测坏的连接,并替换为新的连接的机制。也就是说我每次从连接池里面 Get 的连接有可能是坏的连接。所以,我当时临时的解决方案是通过增加失败后自动重试来解决了。不过,这样的处理方案,连接池的作用好像就没有了。技术债能早点 还的还是早点还上。

三、使用连接池的正确姿势

想到我们的 ngx_lua 项目里面也大量使用 redis 连接池,他们怎么没有遇到这个问题呢。只能去看看源码了。

经过抽象分离, ngx_lua 里面使用 redis 连接池部分的代码大致是这样的:

 

  1. server { 
  2.     location /pool { 
  3.         content_by_lua_block { 
  4.             local redis = require "resty.redis" 
  5.             local red = redis:new() 
  6.  
  7.             local ok, err = red:connect("127.0.0.1", 6379) 
  8.             if not ok then 
  9.                 ngx.say("failed to connect: ", err) 
  10.                 return 
  11.             end 
  12.  
  13.             ok, err = red:set("hello", "world") 
  14.             if not ok then 
  15.                 return 
  16.             end 
  17.  
  18.             red:set_keepalive(10000, 100) 
  19.         } 
  20.     } 
  21. } 

发现有个 set_keepalive 的方法,查了一下官方文档,方法的原型是 syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size) 貌似 max_idle_timeout 这个参数,就是我们所缺少的东西,然后进一步跟踪源码,看看里面是怎么保证连接有效的。

 

  1. function _M.set_keepalive(self, ...) 
  2.     local sock = self.sock 
  3.     if not sock then 
  4.         return nil, "not initialized" 
  5.     end 
  6.  
  7.     if self.subscribed then 
  8.         return nil, "subscribed state" 
  9.     end 
  10.  
  11.     return sock:setkeepalive(...) 
  12. end 

至此,已经清楚了,使用了 tcp 的 keepalive 心跳机制。

于是,通过与 Radix.v2 的作者一些讨论,选择自己在 redis 这层使用心跳机制,来解决这个问题。

四、***的解决方案

在创建连接池之后,起一个 goroutine,每隔一段 idleTime 发送一个 PING 到 Redis server。其中,idleTime 略小于 Redis server 的 timeout 配置。
连接池初始化部分代码如下:

  1. p, err := pool.New("tcp", u.Host, concurrency) 
  2. errHndlr(err) 
  3. go func() { 
  4.     for { 
  5.         p.Cmd("PING") 
  6.         time.Sleep(idelTime * time.Second) 
  7.     } 
  8. }() 

使用 redis 传输数据部分代码如下:

 

  1. func redisDo(p *pool.Pool, cmd string, args ...interface{}) (reply *redis.Resp, err error) { 
  2.     reply = p.Cmd(cmd, args...) 
  3.     if err = reply.Err; err != nil { 
  4.         if err != io.EOF { 
  5.             Fatal.Println("redis", cmd, args, "err is", err) 
  6.         } 
  7.     } 
  8.  
  9.     return 
  10. } 

其中,Radix.v2 连接池内部进行了连接池内连接的获取和放回,代码如下:

 

  1. // Cmd automatically gets one client from the pool, executes the given command 
  2. // (returning its result), and puts the client back in the pool 
  3. func (p *Pool) Cmd(cmd string, args ...interface{}) *redis.Resp { 
  4.     c, err := p.Get() 
  5.     if err != nil { 
  6.         return redis.NewResp(err) 
  7.     } 
  8.     defer p.Put(c) 
  9.  
  10.     return c.Cmd(cmd, args...) 
  11. } 

这样,我们就有了 keepalive 的机制,不会出现 timeout 的连接了,从 redis 连接池里面取出的连接都是可用的连接了。看似简单的代码,却***的解决了连接池里面超时连接的问题。同时,就算 Redis server 重启等情况,也能保证连接自动重连。

相关内容

热门资讯

PHP新手之PHP入门 PHP是一种易于学习和使用的服务器端脚本语言。只需要很少的编程知识你就能使用PHP建立一个真正交互的...
网络中立的未来 网络中立性是什... 《牛津词典》中对“网络中立”的解释是“电信运营商应秉持的一种原则,即不考虑来源地提供所有内容和应用的...
各种千兆交换机的数据接口类型详... 千兆交换机有很多值得学习的地方,这里我们主要介绍各种千兆交换机的数据接口类型,作为局域网的主要连接设...
粉嫩如何诠释霸道 东芝M805... “霸道粉”是个什么玩意东芝M805拿过来的时候,笔者扑哧笑了,不是笑这款笔记本,而是笑这款产品的颜色...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
什么是大数据安全 什么是大数据... 在《为什么需要大数据安全分析》一文中,我们已经阐述了一个重要观点,即:安全要素信息呈现出大数据的特征...
只需三步,让你掌握阿尔法路由器... 请参照以下阿尔法路由器设置方法,通过浏览器进入及设置宽带路由器。经过这三个步骤的设置,基本上能够熟悉...
如何更省钱? 近期特价热门硒鼓... 如今激光打印设备已经成为商务或企业的绝对主力办公设备,但采购者往往忽视了激光打印设备所用的耗材,相对...
全面诠释网络负载均衡 负载均衡的出现大大缓解了服务器的压力,更是有效的利用了资源,提高了效率。那么我们现在来说一下网络负载...
如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...