Substituído pelo pool de conexão HikariCP, muito rápido!

fundo

Em nossa codificação usual, normalmente salvamos alguns objetos, o que considera principalmente o custo de criação do objeto.

Por exemplo, como recursos de thread, recursos de conexão de banco de dados ou conexões TCP, etc., a inicialização de tais objetos geralmente leva muito tempo. Se solicitado e destruído com frequência, consumirá muitos recursos do sistema e causará perda desnecessária de desempenho .

E esses objetos têm uma característica marcante, ou seja, podem ser reciclados e usados ​​repetidamente por meio de trabalhos leves de reinicialização.

Neste momento, podemos usar um pool virtual para economizar esses recursos e, quando os utilizarmos, podemos obter rapidamente um do pool.

Em Java, a tecnologia de pooling é amplamente utilizada. Os mais comuns incluem pool de conexão de banco de dados, pool de threads, etc. Este artigo se concentra no pool de conexões e no pool de threads, que apresentaremos em blogs subsequentes.

Pacote de pool comum Commons Pool 2

Vejamos primeiro o Commons Pool 2, um pacote de pooling comum em Java, para entender a estrutura geral de um pool de objetos.

De acordo com nossas necessidades de negócios, o uso desse conjunto de APIs pode implementar facilmente o gerenciamento do pool de objetos.

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

GenericObjectPool é a classe principal do pool de objetos. Ao passar uma configuração de pool de objetos e uma fábrica de objetos, um pool de objetos pode ser criado rapidamente.

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

Recomende um tutorial mais completo do Spring Boot de código aberto e gratuito:

https://github.com/javastacks/spring-boot-best-practice

O caso

Jedis, um cliente comum do Redis, usa Commons Pool para gerenciar o pool de conexões, o que pode ser considerado uma prática recomendada. A figura abaixo é o bloco de código principal dos Jedis usando a fábrica para criar objetos.

O método principal da classe de fábrica de objetos é makeObject, seu valor de retorno é do tipo PooledObject e o objeto pode ser simplesmente empacotado e retornado usando new DefaultPooledObject<>(obj).

redis.clients.jedis.JedisFactory, use a fábrica para criar objetos.

@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;
  }
}

Vamos apresentar novamente o processo de geração de objetos, conforme mostrado na figura abaixo, ao adquirir um objeto, ele primeiro tentará retirar um do pool de objetos, se não houver nenhum objeto livre no pool de objetos, use o método fornecido pelo classe de fábrica para gerar uma nova.

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;
            }
        }
        //此处省略若干行
    }
    //此处省略若干行
}

Onde o objeto existe? A responsabilidade desse armazenamento é assumida por uma estrutura chamada LinkedBlockingDeque, que é uma fila bidirecional.

A seguir, observe as principais propriedades de 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;

Existem muitos parâmetros. Para entender o significado dos parâmetros, vamos primeiro examinar o ciclo de vida de um objeto agrupado em todo o pool.

Conforme mostrado na figura abaixo, existem duas operações principais do pool: uma é o thread de negócios e a outra é o thread de detecção.

Quando o conjunto de objetos é inicializado, três parâmetros principais devem ser especificados:

  • maxTotal O limite superior de objetos gerenciados no pool de objetos
  • maxIdle número máximo ocioso
  • minIdle número mínimo ocioso

Entre eles, maxTotal está relacionado ao thread de negócios.Quando o thread de negócios deseja obter o objeto, ele primeiro verificará se há um objeto ocioso.

Se houver, retorne um; caso contrário, entre na lógica de criação. Neste ponto, se o número do pool atingir o valor máximo, a criação falhará e um objeto vazio será retornado.

Quando um objeto é adquirido, existe um parâmetro muito importante, ou seja, o tempo máximo de espera (maxWaitMillis), que tem um impacto relativamente grande no desempenho do lado da aplicação. O parâmetro padrão é -1, o que significa que o tempo limite nunca expirará até que um objeto esteja livre.

Conforme mostrado na figura abaixo, se a criação do objeto for muito lenta ou o uso estiver muito ocupado, o thread de negócios continuará a bloquear (o padrão blockWhenExhausted é verdadeiro), o que fará com que os serviços normais falhem na execução.

Questões de entrevista

O entrevistador geral perguntará: Quão grande você definirá o parâmetro de tempo limite? Normalmente defino o tempo máximo de espera como o atraso máximo que a interface pode tolerar.

O arranjo mais abrangente das últimas perguntas da entrevista sobre Java: https://www.javastack.cn/mst/

Por exemplo, se o tempo de resposta de um serviço normal for de cerca de 10 ms, ele ficará travado quando atingir 1 segundo, portanto, este parâmetro pode ser definido como 500 ~ 1000 ms.

Após o tempo limite, NoSuchElementException será lançado e a solicitação falhará rapidamente sem afetar outros threads de negócios.Essa ideia de Fail Fast é amplamente utilizada na Internet.

Os parâmetros com a palavra evcit tratam principalmente da remoção de objetos. Além de serem caros para inicializar e destruir, os objetos em pool também ocupam recursos do sistema em tempo de execução.

Por exemplo, o pool de conexões ocupará várias conexões e o pool de threads aumentará a sobrecarga de agendamento. Sob condições repentinas de tráfego, a empresa solicitará recursos de objeto além da situação normal e os colocará no pool. Quando esses objetos não são mais usados, precisamos limpá-los.

Objetos que excederem o valor especificado pelo parâmetro minEvictableIdleTimeMillis serão reciclados à força. Este valor é 30 minutos por padrão; o parâmetro softMinEvictableIdleTimeMillis é semelhante, mas só será removido quando o número atual de objetos for maior que minIdle, portanto a ação anterior é mais violento Alguns.

Existem também 4 parâmetros de teste: testOnCreate, testOnBorrow, testOnReturn e testWhileIdle, especificando respectivamente se a validade dos objetos agrupados deve ser verificada durante a criação, aquisição, retorno e detecção de inatividade.

Ativar essas verificações pode garantir a disponibilidade de recursos, mas consumirá desempenho, portanto o padrão é falso.

No ambiente de produção, recomenda-se definir apenas testWhileIdle como verdadeiro e ajustar o intervalo de detecção ociosa (timeBetweenEvictionRunsMillis), como 1 minuto, para garantir a disponibilidade e eficiência dos recursos.

Teste JMH

Qual é o tamanho da diferença de desempenho entre usar um pool de conexões e não usar um pool de conexões?

A seguir está um exemplo simples de teste JMH (consulte warehouse), que executa uma operação simples de configuração e define um valor aleatório para a chave do redis.

@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);

   @Benchmark
   public void testPool() {
       Jedis jedis = pool.getResource();
       jedis.set("a", UUID.randomUUID().toString());
       jedis.close();
   }

   @Benchmark
   public void testJedis() {
       Jedis jedis = new Jedis("localhost", 6379);
       jedis.set("a", UUID.randomUUID().toString());
       jedis.close();
   }
   //此处省略若干行
}

Use o meta-gráfico para traçar os resultados do teste e exibi-los conforme mostrado na figura abaixo: você pode ver que o método do pool de conexões é usado e sua taxa de transferência é 5 vezes maior que a do método do pool de conexões sem uso!

Conjunto de conexões de banco de dados HikariCP

HikariCP vem do japonês "光る", que significa leve, o que significa que o software funciona tão rápido quanto a velocidade da luz. É o pool de conexão de banco de dados padrão no SpringBoot.

O banco de dados é um componente que usamos frequentemente em nosso trabalho. Existem muitos pools de conexões de clientes projetados para o banco de dados. Seu princípio de design é basicamente o mesmo que mencionamos no início deste artigo, o que pode efetivamente reduzir os recursos para criação e destruindo conexões de banco de dados.

O mesmo pool de conexões, seu desempenho também é diferente, a imagem abaixo é um gráfico de teste oficial do HikariCP, você pode ver seu excelente desempenho, o código de teste oficial JMH veja Github.

A pergunta geral da entrevista é esta: Por que o HikariCP é rápido?

Existem três aspectos principais:

  • Ele usa FastList em vez de ArrayList e reduz a operação de verificação fora dos limites inicializando o valor padrão
  • Otimizou e simplificou o bytecode e reduziu a perda de desempenho do proxy dinâmico usando Javassist, como o uso de instruções invocadas estáticas em vez de instruções invocadas virtuais
  • Implementou um ConcurrentBag sem bloqueios, reduzindo a competição de bloqueios em cenários simultâneos

Algumas operações de otimização de desempenho do HikariCP são muito dignas de nossa referência. Nos blogs a seguir analisaremos detalhadamente vários cenários de otimização.

O pool de conexões de banco de dados também enfrenta o problema de um valor máximo (maximumPoolSize) e um valor mínimo (minimumIdle). Há também uma pergunta de entrevista de alta frequência aqui: Qual o tamanho que você costuma definir para o pool de conexões?

Muitos estudantes pensam que quanto maior o tamanho do conjunto de conexões, melhor. Alguns alunos até definem esse valor para mais de 1000, o que é um mal-entendido.

De acordo com a experiência, apenas 20 a 50 conexões de banco de dados são suficientes. O tamanho específico deve ser ajustado de acordo com os atributos do negócio, mas é definitivamente inapropriado ser muito grande.

HikariCP oficialmente não recomenda definir o valor de mínimoIdle, ele será definido com o mesmo tamanho de máximoPoolSize por padrão. Se os recursos de conexão do servidor de banco de dados estiverem relativamente ociosos, você também poderá remover a função de ajuste dinâmico do pool de conexões.

Além disso, de acordo com o tipo de consulta e transação do banco de dados, vários pools de conexões de banco de dados podem ser configurados em um aplicativo. Poucas pessoas conhecem essa técnica de otimização. Deixe-me descrevê-la brevemente aqui.

Geralmente existem dois tipos de negócios: um requer tempo de resposta rápido e retorna os dados ao usuário o mais rápido possível; o outro pode ser executado lentamente em segundo plano, o que leva muito tempo e não requer muita pontualidade.

Se esses dois tipos de negócios compartilharem um pool de conexões de banco de dados, será fácil competir por recursos, o que, por sua vez, afetará a velocidade de resposta da interface.

Embora os microsserviços possam resolver essa situação, a maioria dos serviços não possui essa condição e o pool de conexões pode ser dividido neste momento.

Conforme mostrado na figura, no mesmo negócio, de acordo com os atributos do negócio, dividimos dois pools de conexões para fazer frente a esta situação.

HikariCP também mencionou outro ponto de conhecimento, no protocolo JDBC4, a validade da conexão pode ser detectada através de Connection.isValid().

Desta forma, não precisamos definir muitos parâmetros de teste e o HikariCP não fornece tais parâmetros.

conjunto de buffers de resultado

Ao chegar aqui, você descobrirá que existem muitas semelhanças entre Pool e Cache.

Uma coisa em comum entre eles é que os objetos são processados ​​e armazenados em uma área de velocidade relativamente alta. Habitualmente penso nos caches como objetos de dados e nos objetos do pool como objetos de execução. Os dados no cache têm um problema de taxa de acerto, enquanto os objetos no pool geralmente são pares.

Considere o seguinte cenário, jsp fornece a função dinâmica de páginas da web, que podem ser compiladas em arquivos de classe após a execução para acelerar a execução; ou, algumas plataformas de mídia converterão regularmente artigos populares em páginas HTML estáticas, apenas por meio do balanceamento de carga do nginx pode lidar com altas solicitações simultâneas (separação dinâmica e estática).

Nestes momentos, é difícil dizer se isso é uma otimização para cache ou pooling para objetos. Em essência, eles apenas salvam o resultado de uma determinada etapa de execução, para que não precisem começar tudo de novo na próxima. momento em que são acessados.

Normalmente chamo essa técnica de Result Cache Pool, que é uma combinação de vários métodos de otimização.

resumo

Deixe-me resumir brevemente os pontos principais deste artigo: Começamos com Commons Pool 2, o pacote de pooling mais comum em Java, apresentamos alguns detalhes de sua implementação e explicamos a aplicação de alguns parâmetros importantes.

Jedis é encapsulado com base no Commons Pool 2. Através do teste JMH, descobrimos que após o pool de objetos, há uma melhoria de desempenho de quase 5 vezes.

Em seguida, apresentei o HikariCP, que é muito rápido no pool de conexões de banco de dados. Ele é baseado na tecnologia de pooling e tem maior melhoria de desempenho por meio de habilidades de codificação. HikariCP é uma das bibliotecas de classes em que me concentro. Também recomendo que você participe. a lista de tarefas.

Em geral, ao encontrar os seguintes cenários, você pode considerar o uso do pooling para aumentar o desempenho do sistema:

  • A criação ou destruição de objetos requer mais recursos do sistema
  • A criação ou destruição de objetos leva muito tempo, requer operações complicadas e uma longa espera
  • Depois que o objeto é criado, ele pode ser usado repetidamente por meio de algumas redefinições de estado

Após agrupar objetos, apenas a primeira etapa de otimização é habilitada. Para alcançar o desempenho ideal, alguns parâmetros-chave do pool devem ser ajustados.Um tamanho razoável do pool mais um período de tempo limite razoável podem fazer com que o pool jogue um valor maior. Semelhante à taxa de acertos do cache, o monitoramento do pool também é muito importante.

Conforme mostrado na figura abaixo, você pode ver que o número de conexões no pool de conexões do banco de dados permanece em um nível alto por um longo tempo sem ser liberado, e o número de threads em espera aumenta drasticamente, o que pode nos ajudar a localizar rapidamente a transação problema do banco de dados.

Na codificação normal, existem muitos cenários semelhantes. Por exemplo, pool de conexões Http, Okhttp e Httpclient fornecem o conceito de pool de conexões, você pode analisá-lo por analogia, o foco também está no tamanho e no tempo limite da conexão.

O middleware subjacente, como o RPC, também geralmente usa tecnologia de pool de conexões para acelerar a aquisição de recursos, como pool de conexões Dubbo, comutação Feign para httppclient e outras tecnologias.

Você descobrirá que o design do pool em diferentes níveis de recursos é semelhante. Por exemplo, o pool de threads usa uma fila para armazenar tarefas em buffer na segunda camada e fornece várias estratégias de rejeição. Apresentaremos o pool de threads em artigos subsequentes.

Esses recursos do pool de threads também podem ser usados ​​como referência na tecnologia do pool de conexões para aliviar o estouro de solicitação e criar algumas estratégias de estouro.

Na realidade, fazemos o mesmo. Então, como fazê-lo? Quais são as práticas? Esta parte fica para todos pensarem.

Declaração de direitos autorais: Este artigo é o artigo original do blogueiro CSDN "Philadelphia Migrant Worker" e segue o acordo de direitos autorais CC 4.0 BY-SA. Anexe o link da fonte original e esta declaração para reimpressão.

Link original: https://blog.csdn.net/monarch91/article/details/123867269

Recomendação recente de artigos interessantes:

1. Mais de 1.000 perguntas e respostas de entrevistas sobre Java (versão mais recente de 2022)

2. Brilhante! As corrotinas Java estão chegando. . .

3. Tutorial do Spring Boot 2.x, muito abrangente!

4. Não encha a tela com explosões e explosões, experimente o modo decorador, esse é o jeito elegante! !

5. A versão mais recente do "Manual de Desenvolvimento Java (Edição Songshan)", baixe rapidamente!

Sinta-se bem, não esqueça de curtir + encaminhar!

Acho que você gosta

Origin blog.csdn.net/youanyyou/article/details/132617954
Recomendado
Clasificación