04 | 连接池:别让连接池帮了倒忙

文章讨论了连接池在客户端SDK中的重要性,强调了检查SDK是否使用连接池以避免性能问题和线程安全风险。提到了TCP连接的创建成本和字节流特性,并举例展示了未使用连接池的Jedis库在多线程环境下的问题。此外,还讨论了连接池的配置调整,如最大连接数的影响,以及配置变更生效的验证。最后,文章通过一个配置错误导致大促期间应用故障的例子,强调了监控和验证配置的重要性。
摘要由CSDN通过智能技术生成

04 | 连接池:别让连接池帮了倒忙

连接池一般对外提供获得连接、归还连接的接口给客户端使用,并暴露最小空闲连接数、最大连接数等可配置参数,在内部则实现连接建立、连接心跳保持、连接管理、空闲连接回收、连接可用性检测等功能。

注意鉴别客户端 SDK 是否基于连接池

我们首先要确定客户端 SDK 是否是基于连接池技术实现的。我们知道,TCP 是面向连接的基于字节流的协议:

  1. 面向连接,意味着连接需要先创建再使用,创建连接的三次握手有一定开销;
  2. 基于字节流,意味着字节是发送数据的最小单元,TCP 协议本身无法区分哪几个字节是完整的消息体,也无法感知是否有多个客户端在使用同一个 TCP 连接,TCP 只是一个读写数据的管道。

如果客户端 SDK 没有使用连接池,而直接是 TCP 连接,那么就需要考虑每次建立 TCP 连接的开销,并且因为 TCP 基于字节流,在多线程的情况下对同一连接进行复用,可能会产生线程安全问题
TCP 连接的客户端 SDK,对外提供 API 的三种方式:

  1. 连接池和连接分离的 API
  2. 内部带有连接池的 API
  3. 非连接池的 API

在使用三方SDK 时,一定要先查看官方文档了解其最佳实践,或是在类似 Stackoverflow 的网站搜索
XXX threadsafe/singleton 字样看看大家的回复,也可以一层一层往下看源码,直到定位到原始 Socket 来判断 Socket 和客户端 API 的对应关系。

  1. 如果是分离方式,那么连接池本身一般是线程安全的,可以复用。每次使用需要从连接池获取连接,使用后归还,归还的工作由使用者负责。
  2. 如果是内置连接池,SDK 会负责连接的获取和归还,使用的时候直接复用客户端。
  3. 如果 SDK 没有实现连接池(大多数中间件、数据库的客户端 SDK 都会支持连接池),那通常不是线程安全的,而且短连接的方式性能不会很高,使用的时候需要考虑是否自己封装一个连接池。

案例:操作jedis库
启动两个线程,共享操作同一个 Jedis 实例,每一个线程循环 1000 次,分别读取Key 为 a 和 b 的 Value,判断是否分别为 1 和 2:

Jedis jedis = new Jedis("127.0.0.1", 6379);
new Thread(() -> {
  for (int i = 0; i < 1000; i++) {
    String result = jedis.get("a");
    if (!result.equals("1")) {
      log.warn("Expect a to be 1 but found {}", result);
      return;
    }
  }
}).start();
new Thread(() -> {
  for (int i = 0; i < 1000; i++) {
    String result = jedis.get("b");
    if (!result.equals("2")) {
      log.warn("Expect b to be 2 but found {}", result);
      return;
    }
  }
}).start();
TimeUnit.SECONDS.sleep(5);

执行程序多次,可以看到日志中出现了各种奇怪的异常信息,有的是读取 Key 为 b 的Value 读取到了 1,有的是流非正常结束,还有的是连接关闭异常:
分析源码:
Jedis 继承了 BinaryJedis,BinaryJedis 中保存了单个 Client 的实例,Client最终继承了 Connection,Connection 中保存了单个 Socket 的实例,和 Socket 对应的两个读写流。因此,一个 Jedis 对应一个 Socket 连接

在这里插入图片描述

private static JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);
new Thread(() -> {
  try (Jedis jedis = jedisPool.getResource()) {
    for (int i = 0; i < 1000; i++) {
      String result = jedis.get("a");
      if (!result.equals("1")) {
      log.warn("Expect a to be 1 but found {}", result);
      return;
      }
    }
  }
}).start();

最好通过 shutdownhook,在程序退出之前关闭 JedisPool:

@PostConstruct
public void init() {
  Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    jedisPool.close();
  }));
}

JedisPool 的 getResource 方法在拿到 Jedis 对象后,将自己设置为了连接池。连接池JedisPool,继承了 JedisPoolAbstract,而后者继承了抽象类 Pool,Pool 内部维护了Apache Common 的通用池 GenericObjectPool。JedisPool 的连接池就是基于GenericObjectPool 的。
Jedis 的 API 实现是连接池和连接分离的 API,JedisPool 是线程安全的连接池,Jedis 是非线程安全的单一连接

使用连接池务必确保复用

池一定是用来复用的,否则其使用代价会比每次创建单一对象更大。对连接池来说更是如此,原因如下:

  1. 创建连接池的时候很可能一次性创建了多个连接,大多数连接池考虑到性能,会在初始化的时候维护一定数量的最小连接
  2. 大多数的连接池都有闲置超时的概念。连接池会检测连接的闲置时间,定期回收闲置的连接,把活跃连接数降到最低(闲置)连接的配置值,减轻服务端的压力.

连接池的配置不是一成不变的

最大连接数不是设置得越大越好,太大的话,需要使用过多的资源来维护,会给服务端带来更大的压力。连接池最大连接数设置得太小,很可能会因为获取连接的等待时间太长,导致吞吐量低下,甚至超时无法获取连接。
这里要强调的是,修改配置参数务必验证是否生效,并且在监控系统中确认参数是否生效、是否合理。之所以要“强调”,是因为这里有坑。
应用准备针对大促活动进行扩容,把数据库配置文件中Druid 连接池最大连接数 maxActive 从 50 提高到了 100,修改后并没有通过监控验证,结果大促当天应用因为连接池连接数不够爆了。
经排查发现,当时修改的连接数并没有生效。原因是,应用虽然一开始使用的是 Druid 连接池,但后来框架升级了,把连接池替换为了 Hikari 实现,原来的那些配置其实都是无效的,修改后的参数配置当然也不会生效

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值