Análise detalhada 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 melhorias no desempenho de alta concorrência muitas vezes são 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 se tornou mais evidente em versões superiores do MySQL. Além disso, alguns entusiastas e testadores do MySQL relataram degradação de desempenho em vários 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 no 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 que há 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 às custas da degradação do desempenho em outras áreas.

As Causas Raiz da Queda de Desempenho do MySQL

De maneira 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 muitas vezes não percebem a queda de desempenho, já que cada adição ao código resulta em apenas uma pequena diminuição no desempenho. No entanto, com o tempo, essas pequenas quedas se acumulam, levando a um efeito cumulativo significativo, que faz com que os usuários percebam uma degradação notável no desempenho nas versões mais novas 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.

Agora, vamos analisar a causa raiz da degradação de desempenho no MySQL a partir do nível do 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++

 

Pela 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 List anterior por um deque, que se tornou uma das causas raiz da gradual degradação de desempenho. Vamos dar uma olhada na documentação do deque:

Markdown

 

Conforme mostrado na descrição acima, em casos extremos, reter um único elemento requer alocar o array inteiro, 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 deve 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 usa 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 amigabilidade com o cache. Este design foi destinado a melhorar a amigabilidade 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 amigabilidade com o cache. Embora o acesso aleatório seja menos eficiente, a otimização para o acesso sequencial aos elementos da Lista melhora significativamente o desempenho.

Ao atualizar para o MySQL 8.0, os usuários observaram uma queda significativa no desempenho da inserção em lote, e uma das principais causas foi a mudança substancial 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 MTR. Comparado ao MySQL 5.7, o código adicionado reduz significativamente o desempenho de commits individuais, embora o throughput de escrita geral tenha sido muito melhorado.

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

C++

 

Vamos examinar a operação execute principal do commit MTR no MySQL 8.0.40:

C++

 

Em comparação, fica 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 isso veio com o custo do desempenho de baixa concorrência.

A priorização do log de refazimento no modo de alta concorrência resulta em um desempenho ruim 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, isso não afeta a execução das funções acima. Como 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, fica claro que com a introdução do recurso de adição/remover coluna instantânea, a função rec_init_offsets_comp_ordinary se tornou visivelmente mais complexa, introduzindo mais chamadas de função e adicionando uma instrução switch que impacta severamente a otimização de cache. Como essa função é chamada com frequência, ela impacta diretamente o desempenho da atualização do índice, inserções em lote e joins, resultando em uma grande queda de desempenho.

Além disso, a queda de desempenho no MySQL 8.0 não se limita ao que foi mencionado acima; existem 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 seguinte código 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 acessadas com frequência, é 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 hotspot 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 row_sel_field_store_in_mysql_format_func.

C++

 

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

C++

 

Funções ineficientes chamadas com frequência, executando dezenas de milhões de vezes por segundo, podem impactar severamente o desempenho de joins.

Vamos continuar explorando os motivos para a queda de desempenho. A seguinte otimização de desempenho oficial é, na verdade, uma das principais causas da queda no desempenho de junção. Embora certas consultas possam ser melhoradas, elas ainda são algumas das razões para a degradação de 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 ocorre sem motivo. Uma série de pequenos problemas, quando acumulados, podem levar a uma degradação de desempenho perceptível que os usuários experimentam. No entanto, essas questões muitas vezes são difíceis de identificar, tornando-as ainda mais difíceis de resolver.

A chamada “otimização prematura” é a raiz de todo mal, e não se aplica no 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 escrita estão relacionadas a problemas de commit MTR, adição/exclusão instantânea de colunas e vários outros fatores. Estes são difíceis de otimizar de forma tradicional. No entanto, os usuários podem compensar a queda de desempenho através da otimização PGO. Com uma estratégia adequada, o desempenho geralmente pode ser mantido estável.

Para a degradação de desempenho na inserção 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 da inserção em lote pode se equiparar ao 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 melhoria de concorrência do log de refazimento, o que pode impulsionar significativamente o desempenho da inserção em lote.

Em relação a problemas de atualização de índice, devido à adição inevitável 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 desempenho de leitura, especialmente desempenho de junção, fizemos melhorias substanciais, incluindo a correção de problemas inline e outras otimizações. Com a adição de PGO, o desempenho de junção pode ser aumentado 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 da Resolução de Problemas em Engenharia de Software: Como Tornar o MySQL Melhor.

[2] Otimizado para MySQL · GitHub

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