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:
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:
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:
A função correspondente PT_insert_values_list::contextualize
no MySQL 5.7 é a seguinte:
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
:
std::deque (double-ended queue) is an indexed sequence container that allows fast insertion and deletion at both its
beginning and its end. In addition, insertion and deletion at either end of a deque never invalidates pointers or
references to the rest of the elements.
As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence
of individually allocated fixed-size arrays, with additional bookkeeping, which means indexed access to deque must
perform two pointer dereferences, compared to vector's indexed access which performs only one.
The storage of a deque is automatically expanded and contracted as needed. Expansion of a deque is cheaper than the
expansion of a std::vector because it does not involve copying of the existing elements to a new memory location. On
the other hand, deques typically have large minimal memory cost; a deque holding just one element has to allocate its
full internal array (e.g. 8 times the object size on 64-bit libstdc++; 16 times the object size or 4096 bytes,
whichever is larger, on 64-bit libc++).
The complexity (efficiency) of common operations on deques is as follows:
Random access - constant O(1).
Insertion or removal of elements at the end or beginning - constant O(1).
Insertion or removal of elements - linear O(n).
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.
The implementation is the same as classic std::deque: Elements are held in blocks of about 1 kB each.
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:
Vamos examinar a operação execute
do núcleo do commit MTR no MySQL 8.0.40:
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:
A função rec_init_offsets_comp_ordinary
no MySQL 8.0.40 é a seguinte:
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:
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:
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
.
A função row_sel_field_store_in_mysql_format_func
não pode ser inlined devido à presença do código ib::fatal
.
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.
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.
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.
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.
Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis