>  기사  >  Java  >  SpringBoot HikariCP 연결 풀을 만드는 방법

SpringBoot HikariCP 연결 풀을 만드는 방법

PHPz
PHPz앞으로
2023-05-25 13:11:48943검색

공통 객체 풀링 패키지 Commons Pool 2

일반적인 객체 풀 구조를 이해하기 위해 먼저 Java에서 공통 객체 풀링 패키지 Commons Pool 2를 공부해 보겠습니다.

비즈니스 요구에 따라 이 API 세트를 사용하면 개체 풀링 관리를 쉽게 구현할 수 있습니다.

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

GenericObjectPool은 객체 풀의 핵심 클래스입니다. 객체 풀과 객체 팩토리의 구성을 전달하면 객체 풀을 빠르게 생성할 수 있습니다.

public GenericObjectPool( 
            final PooledObjectFactory<T> factory, 
            final GenericObjectPoolConfig<T> config)

Case

Redis의 공통 클라이언트 Jedis는 Commons Pool을 사용하여 연결 풀을 관리하는데 이는 모범 사례라고 할 수 있습니다. 다음 그림은 Jedis가 팩토리를 사용하여 객체를 생성하는 주요 코드 블록입니다.

객체 팩토리 클래스의 주요 메소드는 makeObject입니다. 반환 값은 PooledObject 유형입니다. 객체는 new DefaultPooledObject(obj)를 사용하여 간단히 패키징하고 반환할 수 있습니다.

redis.clients.jedis.JedisFactory는 팩토리를 사용하여 객체를 생성합니다.

@Override
public PooledObject<Jedis> makeObject() throws Exception {
  Jedis jedis = null;
  try {
    jedis = new Jedis(jedisSocketFactory, clientConfig);
    //主要的耗时操作
    jedis.connect();
    //返回包装对象
    return new DefaultPooledObject<>(jedis);
  } catch (JedisException je) {
    if (jedis != null) {
      try {
        jedis.quit();
      } catch (RuntimeException e) {
        logger.warn("Error while QUIT", e);
      }
      try {
        jedis.close();
      } catch (RuntimeException e) {
        logger.warn("Error while close", e);
      }
    }
    throw je;
  }
}

객체 생성 과정을 다시 소개하자면, 아래 그림과 같이 객체를 획득할 때 먼저 객체 풀에서 사용 가능한 객체를 꺼내려고 시도합니다. 새로운 클래스를 생성하기 위해 팩토리 클래스에서 제공됩니다.

public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
    //此处省略若干行
    while (p == null) {
        create = false;
        //首先尝试从池子中获取。
        p = idleObjects.pollFirst();
        // 池子里获取不到,才调用工厂内生成新实例
        if (p == null) {
            p = create();
            if (p != null) {
                create = true;
            }
        }
        //此处省略若干行
    }
    //此处省略若干行
}

물체는 어디에 존재하나요? 이 저장 책임은 양방향 대기열인 LinkedBlockingDeque라는 구조에 의해 수행됩니다.

다음으로 GenericObjectPoolConfig의 주요 속성을 살펴보겠습니다.

// GenericObjectPoolConfig本身的属性
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
// 其父类BaseObjectPoolConfig的属性
private boolean lifo = DEFAULT_LIFO;
private boolean fairness = DEFAULT_FAIRNESS;
private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
private long minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private long evictorShutdownTimeoutMillis = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
private long softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
private EvictionPolicy<T> evictionPolicy = null; 
// Only 2.6.0 applications set this 
private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME;
private boolean testOnCreate = DEFAULT_TEST_ON_CREATE;
private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
private boolean testOnReturn = DEFAULT_TEST_ON_RETURN;
private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;

매개변수가 많습니다. 매개변수의 의미를 이해하기 위해 먼저 전체 풀에서 풀링된 개체의 수명 주기를 살펴보겠습니다.

아래 그림에 표시된 것처럼 풀에는 두 가지 주요 작업이 있습니다. 하나는 비즈니스 스레드이고 다른 하나는 감지 스레드입니다.

SpringBoot HikariCP 연결 풀을 만드는 방법

객체 풀을 초기화할 때 세 가지 주요 매개변수를 지정해야 합니다.

  • maxTotal 객체 풀에서 관리되는 객체의 상한

  • maxIdle 최대 유휴 수

  • minIdle 최소 number of 유휴

maxTotal은 비즈니스 스레드와 관련되어 있습니다. 비즈니스 스레드가 개체를 얻으려고 하면 먼저 유휴 개체가 있는지 감지합니다.

있는 경우 하나를 반환하고, 그렇지 않은 경우 생성 로직을 입력하세요. 풀이 최대 크기에 도달하면 개체 생성이 실패하고 빈 개체가 반환됩니다.

객체를 획득할 때 매우 중요한 매개변수가 있는데, 바로 최대 대기 시간(maxWaitMillis)입니다. 이 매개변수는 애플리케이션 성능에 상대적으로 큰 영향을 미칩니다. 이 매개변수의 기본값은 -1입니다. 이는 객체가 자유로워질 때까지 시간 초과가 발생하지 않음을 의미합니다.

아래 그림과 같이 객체 생성이 매우 느리거나 사용이 매우 바쁜 경우 비즈니스 스레드가 계속 차단되어(blockWhenExhausted의 기본값은 true임) 정상적인 서비스 실행에 실패하게 됩니다.

SpringBoot HikariCP 연결 풀을 만드는 방법

인터뷰 질문

저는 보통 인터뷰 중에 "시간 제한 매개변수를 얼마나 오래 설정하시겠습니까?"라는 질문을 받습니다. 제 접근 방식은 최대 대기 시간을 인터페이스가 허용할 수 있는 최대 지연으로 설정하는 것입니다.

예를 들어 일반적인 서비스 응답 시간이 10ms 정도인데 1초에 도달하면 막히는 느낌이 든다면 이 매개변수를 500~1000ms로 설정해도 괜찮습니다.

시간 초과 후에 NoSuchElementException 예외가 발생하고 다른 비즈니스 스레드에 영향을 주지 않고 요청이 빠르게 실패합니다. 이 빠른 실패 아이디어는 인터넷에서 널리 사용됩니다.

evcit라는 단어가 포함된 매개변수는 주로 객체 제거를 처리하는 데 사용됩니다. 풀링된 개체의 생성 및 삭제 작업은 시간을 소비할 뿐만 아니라 런타임 중에 시스템 리소스도 차지합니다.

예를 들어 연결 풀은 여러 연결을 차지하며 스레드 풀은 예약 오버헤드 등을 증가시킵니다. 비즈니스에서 버스트 트래픽이 발생하면 일반 요구 사항을 초과하는 풀의 개체 리소스를 적용합니다. 이러한 개체가 더 이상 사용되지 않으면 정리해야 합니다.

minEvictableIdleTimeMillis 매개변수에 지정된 값을 초과하는 개체는 강제로 재활용됩니다. 이 값은 기본적으로 30분입니다. SoftMinEvictableIdleTimeMillis 매개변수는 비슷하지만 현재 개체 수가 minIdle보다 큰 경우에만 제거됩니다. 행동은 더욱 폭력적이어야 합니다.

또한 4개의 테스트 매개변수인 testOnCreate, testOnBorrow, testOnReturn 및 testWhileIdle이 있으며 각각 생성, 획득, 반환 및 유휴 감지 중에 풀링된 개체의 유효성을 확인할지 여부를 지정합니다.

이러한 탐지를 활성화하면 리소스의 효율성을 보장할 수 있지만 성능이 소모되므로 기본값은 false입니다.

프로덕션 환경에서는 리소스 가용성과 효율성을 보장하기 위해 testWhileIdle만 true로 설정하고 유휴 감지 간격(timeBetweenEvictionRunsMillis)을 1분 등으로 조정하는 것이 좋습니다.

JMH 테스트

연결 풀링을 사용하는 것과 연결 풀링을 사용하지 않는 것 사이의 성능 차이는 얼마나 됩니까?

다음은 Redis 키에 임의의 값을 설정하기 위해 간단한 설정 작업을 수행하는 간단한 JMH 테스트 예제(웨어하우스 참조)입니다.

@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
public class JedisPoolVSJedisBenchmark { 
   JedisPool pool = new JedisPool("localhost", 6379);
@Benchmarkpublicvoid testPool() { 
    Jedis jedis = pool.getResource(); jedis.set("a", UUID.randomUUID().toString()); 
    jedis.close();
}
@Benchmarkpublicvoid testJedis() { 
    Jedis jedis = new Jedis("localhost",6379); 
    jedis.set("a", UUID.randomUUID().toString()); 
    jedis.close(); 
}//此处省略若干行}

将测试结果使用 meta-chart 作图,展示结果如下图所示,可以看到使用了连接池的方式,它的吞吐量是未使用连接池方式的 5 倍!

SpringBoot HikariCP 연결 풀을 만드는 방법

数据库连接池 HikariCP

HikariCP 源于日语“光る”,光的意思,寓意软件工作速度和光速一样快,它是 SpringBoot 中默认的数据库连接池。

数据库是我们工作中经常使用到的组件,针对数据库设计的客户端连接池是非常多的,它的设计原理与我们在本文开头提到的基本一致,可以有效地减少数据库连接创建、销毁的资源消耗。

同是连接池,它们的性能也是有差别的,下图是 HikariCP 官方的一张测试图,可以看到它优异的性能,官方的 JMH 测试代码见 Github。

SpringBoot HikariCP 연결 풀을 만드는 방법

一般面试题是这么问的:HikariCP 为什么快呢?

主要有三个方面:

  • 它使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作

  • 优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令

  • 实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争

HikariCP 对性能的一些优化操作,是非常值得我们借鉴的,在之后的博客中,我们将详细分析几个优化场景。

数据库连接池同样面临一个最大值(maximumPoolSize)和最小值(minimumIdle)的问题。一个在面试中经常被问到的问题是:你通常会将连接池设置为多大?

许多学生误以为,连接池设置得越大越好,有些学生甚至将该值设置为1000以上。

根据经验,数据库连接,只需要 20~50 个就够用了。具体的大小,要根据业务属性进行调整,但大得离谱肯定是不合适的。

HikariCP 官方是不推荐设置 minimumIdle 这个值的,它将被默认设置成和 maximumPoolSize 一样的大小。如果你的数据库Server端的连接资源空闲很多,你可以考虑禁用连接池的动态调整功能。

另外,根据数据库查询和事务类型,一个应用中是可以配置多个数据库连接池的,这个优化技巧很少有人知道,在此简要描述一下。

业务类型通常有两种:一种需要快速的响应时间,把数据尽快返回给用户;另外一种是可以在后台慢慢执行,耗时比较长,对时效性要求不高。

如果这两种业务类型,共用一个数据库连接池,就容易发生资源争抢,进而影响接口响应速度。

虽然微服务能够解决这种情况,但大多数服务是没有这种条件的,这时就可以对连接池进行拆分。

如图,在同一个业务中,根据业务的属性,我们分了两个连接池,就是来处理这种情况的。

SpringBoot HikariCP 연결 풀을 만드는 방법

HikariCP 还提到了另外一个知识点,在 JDBC4 的协议中,通过 Connection.isValid() 就可以检测连接的有效性。

这样,我们就不用设置一大堆的 test 参数了,HikariCP 也没有提供这样的参数。

结果缓存池

到了这里你可能会发现池(Pool)与缓存(Cache)有许多相似之处。

它们有一个共同点,即将经过加工的对象存储在相对高速的区域中。我习惯性将缓存看作是数据对象,而把池中的对象看作是执行对象。缓存中的数据有一个命中率问题,而池中的对象一般都是对等的。

考虑下面一个场景,jsp 提供了网页的动态功能,它可以在执行后,编译成 class 文件,加快执行速度;再或者,一些媒体平台,会将热门文章,定时转化成静态的 html 页面,仅靠 nginx 的负载均衡即可应对高并发请求(动静分离)。

这些时候,你很难说清楚,这是针对缓存的优化,还是针对对象进行了池化,它们在本质上只是保存了某个执行步骤的结果,使得下次访问时不需要从头再来。

我通常把这种技术叫作结果缓存池(Result Cache Pool),属于多种优化手段的综合。

小结

下面我来简单总结一下本文的内容重点:我们从 Java 中最通用的公用池化包 Commons Pool 2 说起,介绍了它的一些实现细节,并对一些重要参数的应用做了讲解。

Jedis 就是在 Commons Pool 2 的基础上封装的,通过 JMH 测试,我们发现对象池化之后,有了接近 5 倍的性能提升。

接下来介绍了数据库连接池中速度很快的 HikariCP ,它在池化技术之上,又通过编码技巧进行了进一步的性能提升,HikariCP 是我重点研究的类库之一,我也建议你加入自己的任务清单中。

总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:

  • 对象的创建或者销毁,需要耗费较多的系统资源

  • 对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待

  • 对象创建后,通过一些状态重置,可被反复使用

将对象池化之后,只是开启了第一步优化。要想达到最优性能,就不得不调整池的一些关键参数,合理的池大小加上合理的超时时间,就可以让池发挥更大的价值。和缓存的命中率类似,对池的监控也是非常重要的。

如下图,可以看到数据库连接池连接数长时间保持在高位不释放,同时等待的线程数急剧增加,这就能帮我们快速定位到数据库的事务问题。

SpringBoot HikariCP 연결 풀을 만드는 방법

平常的编码中,有很多类似的场景。比如 Http 连接池,Okhttp 和 Httpclient 就都提供了连接池的概念,你可以类比着去分析一下,关注点也是在连接大小和超时时间上。

在底层的中间件,比如 RPC,也通常使用连接池技术加速资源获取,比如 Dubbo 连接池、 Feign 切换成 httppclient 的实现等技术。

你会发现,在不同资源层面的池化设计也是类似的。在后续的文章中,我们将介绍线程池的功能,例如利用队列对任务进行二层缓冲、提供多种拒绝策略等。

线程池的这些特性,你同样可以借鉴到连接池技术中,用来缓解请求溢出,创建一些溢出策略。

现实情况中,我们也会这么做。那么具体怎么做?有哪些做法?这部分内容就留给大家思考了。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ykq</groupId>
    <artifactId>qy151-springboot-vue</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>qy151-springboot-vue</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>1.9.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

위 내용은 SpringBoot HikariCP 연결 풀을 만드는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제