No ano passado, a equipe de engenharia da Uber publicou um artigo sobre seu novo mecanismo de redução de carga projetado para sua arquitetura de microsserviços.
Este artigo é muito interessante sob várias perspectivas. Então, fiz algumas anotações enquanto lia para captar meu entendimento e anotar coisas que gostaria de me aprofundar mais tarde, caso não obtivesse as respostas no final. Descobri várias vezes que esta é a melhor maneira de aprender coisas novas.
O que me emocionou desde o início foi a referência a princípios centenários utilizados para construir esta solução. Isso é algo que adoro – pegar emprestado conceitos/ideias de diferentes áreas e adaptá-los para resolver um problema em um domínio diferente.
Se a resiliência e a estabilidade do sistema são do seu interesse, recomendo também a leitura do excelente livro 'Release It!' por Michael T. Nygard.
É antigo, mas é bom — um livro que investiga estratégias, padrões e orientações práticas para a construção de sistemas de software resilientes e estáveis, enfatizando como lidar com falhas de maneira eficaz.
A Uber implementou uma nova solução de redução de carga chamada Cinnamon que aproveita um controlador PID (o mecanismo centenário) para decidir quais solicitações devem ser processadas ou descartadas por um serviço com base na carga atual do serviço e na prioridade da solicitação.
Não envolve nenhum ajuste no nível de serviço (embora eu tivesse uma dúvida sobre isso), é automaticamente adaptável e muito mais eficiente que a solução anterior QALM. Lembre-se também que a arquitetura de microsserviços do Uber não é para os fracos de coração…
Um controlador PID é um instrumento usado em aplicações de controle industrial para regular temperatura, vazão, pressão, velocidade e outras variáveis de processo. Os controladores PID (derivativo integral proporcional) usam um mecanismo de feedback de malha de controle para controlar variáveis de processo e são os controladores mais precisos e estáveis.
Se você quiser mais informações sobre esse conceito centenário, acesse a Wikipedia.
Agora, de volta ao artigo. PID significa Proporcional, Integral e Derivativo. No caso deles, eles utilizam um componente conhecido como controlador PID para monitorar a saúde de um serviço (solicitações de entrada) com base em três componentes (ou medidas).
O termo “proporcional” indica que a ação tomada é proporcional ao erro atual. Em termos simples, isto significa que a correção aplicada é diretamente proporcional à diferença entre o estado desejado e o estado real. Se o erro for grande, a ação corretiva será proporcionalmente grande.
Quando um endpoint está sobrecarregado, a goroutine em segundo plano começa a monitorar a entrada e a saída de solicitações na fila de prioridade.
Portanto, o componente Proporcional (P) no deslastre de carga ajusta a taxa de rejeição com base na distância entre o tamanho da fila atual e o tamanho da fila alvo ou desejado. Se a fila for maior que o desejado, ocorre mais derramamento; se for menor, a queda é reduzida.
Esse é o meu entendimento sobre isso.
A função do controlador PID é minimizar o número de solicitações enfileiradas, enquanto a função do sintonizador automático é maximizar o rendimento do serviço, sem sacrificar (muito) as latências de resposta.
Embora o texto não mencione explicitamente “Integral (I)” no contexto do tamanho da fila, ele indica que a função do controlador PID é minimizar o número de solicitações enfileiradas. A minimização de solicitações enfileiradas está alinhada com o objetivo do componente Integral de resolver erros acumulados ao longo do tempo.
Para determinar se um endpoint está sobrecarregado, monitoramos a última vez que a fila de solicitações esteve vazia e, se não tiver sido esvaziada nos últimos 10 segundos, consideramos que o endpoint está sobrecarregado (inspirado no Facebook).
No load shedder, pode estar associado a decisões relacionadas ao comportamento histórico da fila de solicitações, como o tempo desde a última vez que ela ficou vazia.
Honestamente, isso não está totalmente claro para mim. É um pouco frustrante, devo dizer. Embora mencionem o aproveitamento de um mecanismo centenário, teria sido útil se declarassem explicitamente qual parte corresponde a quê ou como funciona. Não quero diminuir o valor de seu artigo incrível. Esse é apenas o meu desabafo aqui… Afinal, sou francês… ;)
Acho que este é mais fácil de identificar.
Em um controlador PID (Proporcional-Integral-Derivativo) clássico, a ação “Derivada (D)” é particularmente útil quando você deseja que o controlador antecipe o comportamento futuro do sistema com base na taxa atual de mudança do erro. Ajuda a amortecer as oscilações e melhorar a estabilidade do sistema.
No contexto do load shedder e do controlador PID mencionados no artigo, o componente Derivativo é provavelmente empregado para avaliar a rapidez com que a fila de solicitações está sendo preenchida. Ao fazer isso, auxilia na tomada de decisões que visam manter um sistema estável e evitar mudanças repentinas ou imprevisíveis.
O componente rejeitador tem duas responsabilidades: a) descobrir se um endpoint está sobrecarregado e b), se um endpoint estiver sobrecarregado, eliminar uma porcentagem das solicitações para garantir que a fila de solicitações seja a menor possível. Quando um endpoint está sobrecarregado, a goroutine em segundo plano começa a monitorar a entrada e a saída de solicitações na fila de prioridade. Com base nesses números, ele usa um controlador PID para determinar a proporção de solicitações a serem descartadas. O controlador PID é muito rápido (pois são necessárias poucas iterações) para encontrar o nível correto e, uma vez esgotada a fila de solicitações, o PID garante que reduzamos a proporção apenas lentamente.
No contexto mencionado, o controlador PID é usado para determinar a proporção de solicitações a serem descartadas quando um endpoint está sobrecarregado e monitora a entrada e saída de solicitações. O componente derivativo do controlador PID, que responde à taxa de mudança, está implicitamente envolvido na avaliação da rapidez com que a fila de solicitações está sendo preenchida ou esgotada. Isso ajuda na tomada de decisões dinâmicas para manter a estabilidade do sistema.
No contexto da determinação da sobrecarga, o componente integral pode estar associado ao rastreamento de quanto tempo a fila de solicitações está em um estado não vazio. Isto está alinhado com a ideia de acumular a integral do sinal de erro ao longo do tempo.
“Integral – com base em quanto tempo a solicitação está na fila…”
O componente derivativo, por outro lado, está relacionado à taxa de variação. Ele responde à rapidez com que o estado da fila de solicitações está mudando.
“Derivativo – rejeição com base na rapidez com que a fila está enchendo…”
O componente Integral enfatiza a duração do estado não vazio, enquanto o componente Derivativo considera a taxa na qual a fila está mudando.
No final do jogo, eles utilizam essas três medidas para determinar o curso de ação para uma solicitação.
A questão que tenho é como eles combinam esses três componentes, se é que combinam. Estou curioso para entender como eles os monitoram também.
Mesmo assim, acho que entendi a ideia…
O endpoint na borda é anotado com a prioridade da solicitação e é propagado da borda para todas as dependências downstream via Jaeger . Ao propagar essas informações, todos os serviços da cadeia de solicitações saberão a importância da solicitação e o quão crítica ela é para nossos usuários.
O primeiro pensamento que vem à mente é que ele se integraria perfeitamente a uma arquitetura de malha de serviço.
Aprecio o conceito de empregar rastreamento de serviço distribuído e cabeçalhos para propagar a prioridade da solicitação. Nesse sentido, por que optar por uma biblioteca compartilhada com essa dependência adicionada a cada microsserviço, em vez de colocá-la fora do serviço, talvez como um plugin do Istio? Considerando os benefícios que oferece: ciclos independentes de lançamento/implantação, suporte poliglota, etc.
Aqui estão algumas reflexões adicionais:
Bem, sou tendencioso, pois não sou um grande fã de bibliotecas compartilhadas, até porque acho que elas complicam o processo de lançamento/implantação. No entanto, não tenho certeza se há um aspecto de configuração específico do serviço a ser considerado. Talvez eles configurem quanto tempo o serviço deve esperar para começar a processar uma consulta e concluí-la?
Talvez um aspecto que valha a pena testar seja o processo de tomada de decisão do ejetor.
Pelo que entendi, ele determina se uma solicitação deve ser rejeitada com base no controlador PID, que está localizado no serviço. Existe uma opção para uma abordagem mais global? Por exemplo, se for conhecido que um dos serviços downstream no pipeline está sobrecarregado (devido ao seu próprio controlador PID), algum serviço upstream poderia decidir rejeitar a solicitação antes que ela atinja esse serviço sobrecarregado (o que poderia estar n passos adiante no caminho)?
Esta decisão pode ser baseada no valor retornado pelo controlador PID ou pelo sintonizador automático do serviço downstream.
Agora, estou refletindo sobre vários aspectos mencionados enquanto eles encerram o artigo e fornecem alguns números para mostrar a eficiência de seu sistema, o que é bastante impressionante
Eles mencionam em algum momento que 'Cada solicitação tem um tempo limite de 1 segundo'.
Executamos testes de 5 minutos, onde enviamos uma quantidade fixa de RPS (ex: 1.000), onde 50% do tráfego é nível 1 e 50% é nível 5. Cada solicitação tem um timeout de 1 segundo.
É comum em sistemas distribuídos associar uma solicitação a um prazo ou prazo de expiração específico, sendo cada serviço ao longo do caminho de processamento responsável por fazer cumprir esse limite de tempo. Se o tempo de expiração for atingido antes da solicitação ser concluída, qualquer serviço da cadeia terá a opção de abortar ou rejeitar a solicitação.
Presumo que esse tempo limite de 1 segundo esteja anexado à solicitação, e cada serviço, dependendo de onde estivermos nesse prazo, pode decidir abortar a solicitação. Esta é uma medida global porque é agregada através dos serviços. Acho que isso está de acordo com o que eu disse anteriormente sobre ter uma visão global da integridade completa do sistema ou das dependências para decidir abortar a solicitação o mais rápido possível se ela não tiver a chance de ser concluída devido a um dos serviços desativados. caminho.
A 'saúde' dos serviços downstream (compreendendo dados de seus controladores PID locais) poderia ser retornada como cabeçalhos anexados às respostas e usada para construir um disjuntor/mecanismo de eliminação preventiva antecipada mais evoluído?
Por fim, estou curioso para saber mais sobre a abordagem anterior porque, com base na descrição dada neste artigo, ela parece correta.
Quando você examina as medidas de goodput e latências, não há dúvida sobre qual delas, QALM ou Cinnamon, tem o melhor desempenho. Observe que eles mencionam um link para a abordagem QALM no artigo. Provavelmente deveria começar a partir daí ;)
Como sempre, essas abordagens não são para todos. A arquitetura e a carga do Uber são próprias. Na verdade, estou impaciente para ler os próximos artigos desta série, especificamente para aprender mais sobre o controlador PID e o sintonizador automático.
Com o Cinnamon construímos um load shedder eficiente que utiliza técnicas centenárias para definir dinamicamente limites de rejeição e estimativa da capacidade dos serviços. Ele resolve os problemas que observamos com o QALM (e, portanto, com qualquer deslastre de carga baseado em CoDel), ou seja, que o Cinnamon é capaz de:
- Encontre rapidamente uma taxa de rejeição estável
- Ajustar automaticamente a capacidade do serviço
- Ser usado sem definir nenhum parâmetro de configuração
- Incorrer em sobrecarga muito baixa
O interessante dessa abordagem é que eles consideram todas as solicitações a serem processadas para decidir o que fazer para cada nova solicitação de entrada, pois utilizam uma fila (prioritária). Como mencionei, estou curioso para saber se o mecanismo também poderia levar em conta a saúde de todos os serviços dependentes com base nas mesmas medidas PID…
Existem outros aspectos interessantes neste artigo, como a forma como medem o efeito das suas estratégias e a comparação com a abordagem anterior. No entanto, não requer de mim notas mais detalhadas do que as já apresentadas. Portanto, recomendo fortemente que você leia o artigo original .
Achou este artigo útil? Siga-me no Linkedin , Hackernoon e Medium ! Por favor 👏 este artigo para compartilhá-lo!
Também publicado aqui.