Como foi salvo nossa sexta-feira negra

Anteriormente, falamos sobre como, à medida que a carga cresceu, abandonamos gradualmente o uso do Python no back-end de serviços críticos de produção, substituindo-o pelo Go. E hoje eu, Denis Girko, líder da equipe de desenvolvimento Madmin, quero compartilhar detalhes: como e por que isso aconteceu no exemplo de um dos serviços mais importantes para o nosso negócio - calcular o preço considerando descontos em cupons.



A mecânica de trabalhar com cupons provavelmente é representada por quem já fez compras pelo menos uma vez em lojas on-line. Em uma página especial ou diretamente na cesta, você insere o número do cupom e os preços são recalculados de acordo com o desconto prometido. O cálculo depende do tipo de desconto que o cupom oferece - em porcentagem, na forma de um valor fixo ou usando alguma outra matemática (por exemplo, levamos em consideração pontos do programa de fidelidade, promoções de lojas, tipos de mercadorias etc.). Naturalmente, o pedido já é emitido com novos preços.

Os negócios estão encantados com todos esses mecanismos de trabalho com preços, mas queremos falar sobre o serviço de um ponto de vista um pouco diferente.

Como isso funciona


Para os preços, considerando todas essas dificuldades no back-end, agora temos um serviço separado. No entanto, ele nem sempre foi independente. O serviço apareceu um ou dois anos após o início da loja online e, em 2016, fazia parte de um grande monólito Python que incluía uma grande variedade de componentes para atividades de marketing (Madmin). Posteriormente, ele se destacou como um "bloco" independente, enquanto avançava para a arquitetura de microsserviço.

Como é geralmente o caso dos monólitos, Madmin foi modificado e correspondeu parcialmente a um grande número de desenvolvedores. Bibliotecas de terceiros foram integradas lá, o que simplificou o desenvolvimento, mas muitas vezes não teve o melhor efeito no desempenho. No entanto, naquela época, não nos preocupávamos realmente com a resistência a cargas pesadas durante as vendas, pois o serviço fazia um excelente trabalho. Mas 2016 mudou tudo.



Nos EUA, a "Black Friday" é conhecida desde os anos 60 do século passado. Na Rússia, começou a ser lançado na década de 2010, enquanto a ação tinha que ser criada do zero - o mercado não estava pronto para isso. No entanto, os esforços dos organizadores não foram em vão e, a cada ano, o tráfego de usuários em nosso site aumentava durante os dias de vendas. E, portanto, nossa colisão com a carga, excessiva para essa versão do serviço de cálculo de preços, era apenas uma questão de tempo.

Sexta-feira negra de 2016. E nós dormimos demais com ela


Como a ideia de venda funcionou em todo o seu potencial, a “Black Friday” difere de qualquer outro dia do ano em que, à meia-noite, uma audiência semanal aproximadamente do site chega à loja. Este é um período difícil para todos os serviços. Mesmo naqueles que operam sem problemas ao longo do ano, às vezes surgem problemas.

Agora estamos nos preparando para cada nova “Black Friday”, imitando a carga esperada, mas em 2016 ainda agimos de forma diferente. Ao testar o Madmin antes de um dia importante, testamos a resistência à carga usando cenários de comportamento do usuário em dias regulares. Como se viu, esse teste não reflete bem a situação real, porque na "Black Friday" há muitas pessoas com o mesmo cupom. Como resultado, o serviço de cálculo de preços que leva em consideração esse desconto, incapaz de lidar com uma carga de três vezes (em comparação com os dias normais), bloqueou nossa capacidade de atender os clientes por duas horas no pico mais quente da venda.

O serviço "foi" uma hora antes da meia-noite. Tudo começou com uma interrupção na conexão com o banco de dados (MySQL naquela época), após o qual nem todas as cópias em execução do serviço de cálculo de preços puderam se conectar novamente. E aqueles que ainda estavam conectados não suportaram a carga e pararam de responder, ficando presos nas travas da base.

Por coincidência, o júnior permaneceu de serviço na época, que no momento da queda do serviço estava a caminho da casa do escritório. Ele só poderia se conectar ao problema quando chegasse ao local e chamasse a "artilharia pesada" - o oficial de serviço de emergência. Juntos, eles normalizaram a situação, porém, somente após duas horas.

Quando o processo começou, começaram a se abrir detalhes sobre quão subóptimo era o serviço. Por exemplo, descobriu-se que, para calcular um cupom, foram feitas 28 consultas no banco de dados (não é de surpreender que tudo funcionasse com 100% de utilização da CPU). Os usuários mencionados acima com o mesmo cupom Black Friday não simplificaram a situação, principalmente porque tínhamos um contador de aplicativos para todos os cupons - portanto, cada uso aumentava a carga consultando esse contador.

O ano de 2016 nos proporcionou muita reflexão - principalmente sobre como ajustar nosso trabalho com cupons e testes para que essa situação não ocorra novamente. E em números que sexta-feira é melhor descrita por esta figura:


Os resultados da sexta-feira negra de 2016

Sexta-feira negra de 2017. Estávamos nos preparando seriamente, mas ...


Tendo recebido uma boa lição, nos preparamos com antecedência para a próxima “Black Friday”, reconstruindo e otimizando seriamente o serviço. Por exemplo, finalmente criamos dois tipos de cupons: limit e ilimitado - para evitar bloqueios no acesso simultâneo ao banco de dados, removemos a entrada do banco de dados do script para aplicar o cupom popular. Paralelamente, 1 a 2 meses antes da Black Friday, passamos do MySQL para o PostgreSQL no serviço, que, juntamente com a otimização do código, reduziu o número de chamadas do banco de dados de 28 para 4 a 5. Essas melhorias nos permitiram estender o serviço de teste aos requisitos do SLA - resposta em 3 segundos, percentil 95 a 600 RPS.

Não tendo idéia de quanto nossas melhorias aceleraram o trabalho da versão antiga do serviço em produção, na época duas versões do código Python estavam sendo preparadas para a Black Friday de uma só vez - uma versão existente altamente otimizada e um código completamente novo escrito do zero. Na produção, o segundo foi lançado, que foi testado antes deste dia e noite. No entanto, como se viu "em batalha", um pouco sub-testado.

No dia da "emergência", com a chegada do fluxo principal de clientes, a carga no serviço começou a crescer exponencialmente. Alguns pedidos foram processados ​​até dois minutos. Devido ao longo processamento de algumas solicitações, a carga em outros trabalhadores aumentou.

Nossa principal tarefa era servir um tráfego tão valioso para os negócios. Mas ficou óbvio que "fundição com ferro" não resolve o problema e a qualquer momento o número de trabalhadores ocupados chegará a 100%. Não sabendo então o que exatamente estávamos enfrentando, decidimos ativar o harakiri no uWSGI e simplesmente definir solicitações longas (que são processadas por mais de 6 segundos) para liberar recursos para as normais. E realmente ajudou a resistir - os trabalhadores começaram a ser libertados apenas alguns minutos antes de ficarem completamente exaustos.

Um pouco mais tarde, descobrimos a situação ... Aconteceu que eram pedidos com cestas muito grandes - de 40 a 100 mercadorias - e com um cupom específico com restrições no intervalo. Essa situação foi mal resolvida pelo novo código. Ele mostrou um trabalho incorreto com a matriz, que se transformou em recursão infinita. É curioso que testemos um caso com cestas grandes, mas não em combinação com um cupom complicado. Como solução, simplesmente mudamos para uma versão diferente do código. É verdade que isso aconteceu três horas antes do final da sexta-feira negra. A partir desse momento, todas as cestas começaram a ser processadas corretamente. E, apesar de termos concluído o plano de vendas na época, evitamos problemas globais milagrosos devido à carga cinco vezes no dia normal.

Black Friday 2018


Em 2018, para serviços altamente carregados que atendem ao site, gradualmente começamos a implementar o Go. Dado o histórico das sextas-feiras anteriores, o serviço de cálculo de descontos foi um dos primeiros candidatos ao processamento.



É claro que poderíamos salvar a versão do Python que já foi "testada em batalha" e, antes da nova "Black Friday", poderíamos desativar bibliotecas pesadas e lançar código não ideal. No entanto, Golang já havia se enraizado nessa época e parecia mais promissor.

Mudamos para um novo serviço neste verão; portanto, antes da próxima venda, conseguimos testá-lo bem, inclusive com um perfil de carga crescente.

Durante os testes, verificou-se que a fraqueza em termos de altas cargas continua sendo nossa base. Transações muito longas levaram ao fato de que selecionamos todo o conjunto de conexões e as solicitações foram enfileiradas. Portanto, tivemos que refazer um pouco a lógica do aplicativo, reduzindo ao mínimo o uso do banco de dados (referindo-se a ele apenas quando não há nada a ver sem ele) e armazenando em cache os diretórios do banco de dados e os dados dos cupons populares na Black Friday.

É verdade que este ano nos enganamos com as previsões de carga para cima: estávamos nos preparando para um aumento de 6 a 8 vezes nos picos e obtivemos um bom trabalho de serviços apenas para esse volume de solicitações (caches adicionais, funções experimentais desativadas antecipadamente, simplificamos algumas coisas, implementamos nós adicionais do Kubernetes e até servidores de banco de dados para réplicas, que no final não eram necessárias). De fato, o aumento no interesse do usuário foi menor, então tudo correu normalmente. O tempo de resposta do serviço não excedeu 50 ms no percentil 95.

Para nós, uma das características mais importantes é como o aplicativo é escalado quando não há recursos suficientes para uma cópia. O Go usa recursos de hardware com mais eficiência, portanto, com a mesma carga, você precisa executar menos cópias (atendendo a mais solicitações nos mesmos recursos de hardware). Este ano, no auge da venda, estavam funcionando 16 instâncias do aplicativo, que processavam uma média de 300 solicitações por segundo com picos de até 400 solicitações por segundo, aproximadamente duas vezes maior que a carga usual. Observe que no ano passado, um serviço Python exigiu 102 instâncias.

Parece que o serviço Go desde a primeira abordagem fechou todas as nossas necessidades. Mas Golang não é uma "solução completa para todos os problemas". Não poderia prescindir de alguns recursos. Por exemplo, tivemos que limitar o número de encadeamentos que o serviço pode iniciar no nó do multiprocessador Kubernetes, para que, ao dimensioná-lo, não interfira nos aplicativos "vizinhos" na produção (por padrão, o Go não tem limite de quantos processadores serão necessários). Para isso, definimos o GOMAXPROCS em todos os aplicativos em movimento. Teremos o maior prazer em comentar o quão útil isso foi - em nossa equipe, essa foi apenas uma das hipóteses sobre como lidar com a degradação dos "vizinhos".

Outra "configuração" é o número de conexões mantidas como Keep-Alive. Clientes HTTP e DB regulares no Go, por padrão, mantêm apenas duas conexões; portanto, se houver muitas solicitações simultâneas e você precisar economizar no tráfego da configuração da conexão TCP, faz sentido aumentar esse valor configurando MaxIdleConnsPerHost e SetMaxIdleConns, respectivamente.

No entanto, mesmo com essas “reviravoltas” manuais, a Golang nos forneceu uma grande margem de desempenho para vendas futuras.

Source: https://habr.com/ru/post/pt434208/


All Articles