Análise Aprofundada da Degradação de Desempenho do MySQL 8.0

Os usuários tendem a notar uma queda no desempenho de baixa concorrência mais facilmente, enquanto as melhorias no desempenho de alta concorrência são frequentemente mais difíceis de perceber. Portanto, manter o desempenho de baixa concorrência é crucial, pois afeta diretamente a experiência do usuário e a disposição para atualizar [1].

De acordo com um extenso feedback dos usuários, após a atualização para o MySQL 8.0, os usuários geralmente perceberam uma queda no desempenho, particularmente em operações de inserção em lote e junção. Essa tendência de queda tornou-se mais evidente em versões mais altas do MySQL. Além disso, alguns entusiastas e testadores do MySQL relataram degradação de desempenho em múltiplos testes do sysbench após a atualização.

Esses problemas de desempenho podem ser evitados? Ou, mais especificamente, como devemos avaliar cientificamente a tendência contínua de queda de desempenho? Essas são questões importantes a serem consideradas.

Embora a equipe oficial continue a otimizar, a deterioração gradual do desempenho não pode ser ignorada. Em certos cenários, pode parecer haver melhorias, mas isso não significa que o desempenho em todos os cenários esteja igualmente otimizado. Além disso, também é fácil otimizar o desempenho para cenários específicos à custa da degradação do desempenho em outras áreas.

As Causas Raiz da Queda de Desempenho do MySQL

No geral, à medida que mais recursos são adicionados, a base de código cresce e, com a contínua expansão da funcionalidade, o desempenho se torna cada vez mais difícil de controlar.

Os desenvolvedores do MySQL frequentemente não percebem a queda no desempenho, uma vez que cada adição ao código resulta em apenas uma pequena diminuição na performance. No entanto, ao longo do tempo, essas pequenas quedas se acumulam, levando a um efeito cumulativo significativo, que faz com que os usuários percebam uma degradação de desempenho notável nas versões mais recentes do MySQL.

Por exemplo, a figura a seguir mostra o desempenho de uma simples operação de junção única, com o MySQL 8.0.40 apresentando uma queda de desempenho em comparação com o MySQL 8.0.27:

Figure 1. Significant decline in join performance in MySQL 8.0.40.

A figura a seguir mostra o teste de desempenho de inserção em lote sob concorrência única, com a queda de desempenho do MySQL 8.0.40 em comparação com a versão 5.7.44:

Figure 2. Significant decline in bulk insert performance in MySQL 8.0.40.

Das duas gráficos acima, pode-se ver que o desempenho da versão 8.0.40 não é bom.

A seguir, vamos analisar a causa raiz da degradação do desempenho no MySQL a partir do nível de código. Abaixo está a função PT_insert_values_list::contextualize no MySQL 8.0:

C++

 

A função correspondente PT_insert_values_list::contextualize no MySQL 5.7 é a seguinte:

C++

 

Da comparação de código, o MySQL 8.0 parece ter um código mais elegante, aparentemente fazendo progresso.

Infelizmente, muitas vezes, são precisamente as motivações por trás dessas melhorias de código que levam à degradação do desempenho. A equipe oficial do MySQL substituiu a estrutura de dados anterior List por um deque, que se tornou uma das causas raiz da gradual degradação do desempenho. Vamos dar uma olhada na documentação do deque:

Markdown

 

Conforme mostrado na descrição acima, em casos extremos, a retenção de um único elemento requer a alocação de todo o array, resultando em uma eficiência de memória muito baixa. Por exemplo, em inserções em massa, onde um grande número de registros precisa ser inserido, a implementação oficial armazena cada registro em um deque separado. Mesmo que o conteúdo do registro seja mínimo, um deque ainda precisa ser alocado. A implementação do deque do MySQL aloca 1KB de memória para cada deque para suportar buscas rápidas.

Plain Text

 

A implementação oficial utiliza 1KB de memória para armazenar informações de índice, e mesmo que o comprimento do registro não seja grande, mas haja muitos registros, os endereços de acesso à memória podem se tornar não contíguos, levando a uma baixa amizade com o cache. Este design foi destinado a melhorar a amizade com o cache, mas não foi totalmente eficaz.

Vale ressaltar que a implementação original usava uma estrutura de dados Lista, onde a memória era alocada por meio de um pool de memória, proporcionando um certo nível de amizade com o cache. Embora o acesso aleatório seja menos eficiente, otimizar o acesso sequencial aos elementos da Lista melhora significativamente o desempenho.

Durante a atualização para o MySQL 8.0, os usuários observaram uma significativa queda no desempenho da inserção em lote, e uma das principais causas foi a substancial mudança nas estruturas de dados subjacentes.

Além disso, enquanto a equipe oficial melhorou o mecanismo de log de refazimento, isso também levou a uma diminuição na eficiência da operação de commit de MTR. Comparado ao MySQL 5.7, o código adicionado reduz significativamente o desempenho dos commits individuais, mesmo que o throughput de escrita geral tenha sido grandemente melhorado.

Vamos examinar a operação central execute do commit MTR no MySQL 5.7.44:

C++

 

Vamos examinar a operação execute do núcleo do commit MTR no MySQL 8.0.40:

C++

 

Em comparação, é claro que no MySQL 8.0.40, a operação execute no commit MTR se tornou muito mais complexa, com mais etapas envolvidas. Essa complexidade é uma das principais causas da queda no desempenho de gravação de baixa concorrência.

Em particular, as operações m_impl->m_log.for_each_block(write_log) e log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn) têm um overhead significativo. Essas mudanças foram feitas para melhorar o desempenho de alta concorrência, mas ocorreram à custa do desempenho de baixa concorrência.

A priorização do log redo para o modo de alta concorrência resulta em baixo desempenho para cargas de trabalho de baixa concorrência. Embora a introdução de innodb_log_writer_threads tenha sido destinada a mitigar problemas de desempenho de baixa concorrência, ela não afeta a execução das funções acima. Uma vez que essas operações se tornaram mais complexas e exigem commits MTR frequentes, o desempenho ainda caiu significativamente.

Vamos dar uma olhada no impacto do recurso de adição/remoção instantânea no desempenho. Abaixo está a função rec_init_offsets_comp_ordinary no MySQL 5.7:

C++

 

A função rec_init_offsets_comp_ordinary no MySQL 8.0.40 é a seguinte:

C++

 

A partir do código acima, é claro que, com a introdução do recurso de adição/exclusão instantânea de colunas, a função rec_init_offsets_comp_ordinary tornou-se significativamente mais complexa, introduzindo mais chamadas de função e adicionando um switch statement que impacta severamente a otimização de cache. Como essa função é chamada com frequência, ela impacta diretamente o desempenho da atualização de índices, inserções em lote e junções, resultando em uma grande queda de desempenho.

Além disso, a queda de desempenho no MySQL 8.0 não se limita ao acima; há muitas outras áreas que contribuem para a degradação geral do desempenho, especialmente o impacto na expansão de funções inline. Por exemplo, o código a seguir afeta a expansão de funções inline:

C++

 

De acordo com nossos testes, a instrução ib::fatal interfere severamente na otimização inline. Para funções frequentemente acessadas, é aconselhável evitar instruções que interfiram na otimização inline.

Em seguida, vamos analisar um problema semelhante. A função row_sel_store_mysql_field é chamada com frequência, sendo row_sel_field_store_in_mysql_format uma função de destaque dentro dela. O código específico é o seguinte:

C++

 

A função row_sel_field_store_in_mysql_format chama, em última instância, a função row_sel_field_store_in_mysql_format_func.

C++

 

A função row_sel_field_store_in_mysql_format_func não pode ser inlined devido à presença do código ib::fatal.

C++

 

Funções ineficientes frequentemente chamadas, executadas dezenas de milhões de vezes por segundo, podem impactar severamente o desempenho de junção.

Vamos continuar a explorar as razões para a queda de desempenho. A seguinte otimização oficial de desempenho é, na verdade, uma das causas raiz da queda no desempenho de junção. Embora certas consultas possam ser melhoradas, ainda são algumas das razões para a degradação do desempenho das operações de junção comuns.

GitHub Flavored Markdown

 

Os problemas do MySQL vão além disso. Como mostrado nas análises acima, a queda de desempenho no MySQL não é sem causa. Uma série de pequenos problemas, quando acumulados, pode levar a uma degradação de desempenho perceptível que os usuários experimentam. No entanto, esses problemas são frequentemente difíceis de identificar, tornando-os ainda mais difíceis de resolver.

O chamado ‘otimização prematura’ é a raiz de todos os males, e não se aplica ao desenvolvimento do MySQL. O desenvolvimento de banco de dados é um processo complexo, e negligenciar o desempenho ao longo do tempo torna as melhorias de desempenho subsequentes significativamente mais desafiadoras.

Soluções para Mitigar a Queda de Desempenho do MySQL

As principais razões para a queda no desempenho de gravação estão relacionadas a problemas de commit do MTR, adição/remover colunas instantaneamente e vários outros fatores. Esses são difíceis de otimizar de maneiras tradicionais. No entanto, os usuários podem compensar a queda de desempenho por meio da otimização PGO. Com uma estratégia adequada, o desempenho pode geralmente ser mantido estável.

Para a degradação do desempenho de inserções em lote, nossa versão de código aberto [2] substitui a deque oficial por uma implementação de lista aprimorada. Isso aborda principalmente questões de eficiência de memória e pode aliviar parcialmente a queda de desempenho. Ao combinar a otimização PGO com nossa versão de código aberto, o desempenho de inserções em lote pode se aproximar do MySQL 5.7.

Figure 3. Optimized MySQL 8.0.40 with PGO performs roughly on par with version 5.7.

Os usuários também podem aproveitar múltiplas threads para processamento em lote concorrente, utilizando totalmente a concorrência aprimorada do log de redo, o que pode impulsionar significativamente o desempenho de inserções em lote.

Quanto aos problemas de atualização de índice, devido à inevitável adição de novo código, a otimização PGO pode ajudar a mitigar esse problema. Nossa versão PGO [2] pode aliviar significativamente essa questão.

Para o desempenho de leitura, particularmente o desempenho de junção, fizemos melhorias substanciais, incluindo a correção de problemas de inline e outras otimizações. Com a adição do PGO, o desempenho de junção pode aumentar em mais de 30% em comparação com a versão oficial.

Figure 4. Using PGO, along with our optimizations, can lead to significant improvements in join performance.

Continuaremos a investir tempo na otimização do desempenho de baixa concorrência. Esse processo é longo, mas envolve inúmeras áreas que precisam de melhorias.

A versão de código aberto está disponível para testes, e os esforços persistirão para melhorar o desempenho do MySQL.

Referências

[1] Bin Wang (2024). A Arte de Resolver Problemas em Engenharia de Software: Como Melhorar o MySQL.

[2] Aprimorado para MySQL · GitHub

Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis