Browse Source

Last part of my discussion about microservices: self-healing

master
Julio Biason 4 years ago
parent
commit
cdbd5d1156
  1. 150
      content/code/microservices-self-healing.md
  2. 154
      content/code/microservices-self-healing.pt.md

150
content/code/microservices-self-healing.md

@ -0,0 +1,150 @@
+++
title = "Self-Healing Microservices"
date = 2020-01-03
[taxonomies]
tags = ["microservices", "healing", "artifacts"]
+++
All the [previous](@/code/microservices-artifact-input-state.md)
[discussions](@/code/microservices-artifact-ejection.md) I bought about
microservices was just a prelude to something I still don't have a fixed
solution: how do microservices "heal" themselves in case of missing data?
<!-- more -->
Quick recap before jumping into the problem: Microservices produce artifacts;
artifacts are either send downstream through some message broker to other
microservices or kept in the same microservice for future requests;
microservices can listen to more than one data input to build their artifact.
Previously I mentioned an example of a score microservice that produces an
artifact with the current game score for each team and the names of the
players that scored. This microservice could listen to:
1. The teams queue: this may be needed so we can show the team name or its
abbreviation in the score request; once a team appears in the championship,
the microservice adds it to its state for future reference[^1].
2. The players queue: same as above, so the microservice can return the player
name, nickname, shirt number or something player related if the player
scores; again, it keeps listening to the player queue and stores the
players in its state.
3. The match queue: if a match will happen, it has to have a score, probably
starting at 0 with no players in the goal list; this is just to avoid any
issues with the services requesting scores of matches that didn't start
or haven't had any goals yet; in any case, the artifact will be ready to be
retrieved.
4. The narration queue: by listening to the narration queue, the score
microservice will detect any goals, update its state and generate the new
artifact.
The keyword to take from the above list is "could": Depending on the way the
microservice _and_ the messages are built, it may not be necessary to have all
this.
## Using full-blown messages
Let's start with the easiest way to avoid listening to all those queues:
Full-blown messages.
In a full-blown message, all the related data is sent along with the main
information. Using the previous example, the service could listen to just the
match and narration queue, but expect that the "NewMatch" message will contain
the names of the teams, their abbreviation, logo, probably some id and so on;
the same for the "NewNarration" message: it will contain the player name,
nickname, shirt name, player id and so on.
The problem with full-blown messages is that they tend to become bigger and
bigger: As more microservices are plugged in the system, more fields may be
required -- and dropped by services that don't need those fields.
The pro side of full-blown messages is that a microservice will always have
all the information necessary, while keep the number of listening queues low.
This would also help if you just add a new service in the pool: if it starts
with a blank state, it will be able to build all the state from scratch,
'cause all the information is _already there_.
## Listen to base queues, request the rest
Almost like the solution before, the service would listen to the narrations
and matches, but once it detects a missing information (for example, the
narration event says player with ID, but this ID doesn't exist in its state),
the service would request the more "stale" information (players, teams and
products are not added all the time, for example) for some other service and
fill the lacking information in its state.
This means that this microservice now, instead of knowing only about queues,
now has to have information about other services (the ones that process and
store the "stale" data) and their interfaces -- and, in general, it would also
require some service discovery in the system. Those microservices would be
the "two faced" type of microservice, which receives information, store in the
state, build the artifact but also has an interface for it to be retrieved
instead of simply receiving, processing and passing it along. Caching would
also be advised here, so one service can't flood the other requesting the same
data over and over -- and updates from time to time would make sense in some
situations.
The messages are shorter now ('cause you receive only the team/player ID
instead of everything) and retrieval of information happen when necessary, but
where you reduce the number of listeners, you increase the number of requests.
As will full-blown messages, a new service can easily build its own state from
scratch without any issues -- it will do a lot of requests, but it will,
eventually, have all the necessary information.
## Listen to all
This is exactly same solution as presented in the example above: the
microservice keeps listening to the queues of all related events and build the
state with them.
One problem with this solution: since the queues are asynchronous,
there could be a problem with the ordering of the data, with goals coming
before players (for different reasons). In this case... what would the service
do? Reject the goal in the hopes the player will appear, to avoid any
inconsistencies, and that the message broker requeue the event?
One solution would have services that, along with this one, listen to one
specific data: the score microservice listens to all four, but one
microservice listens only to the player queue. This service would
process things way faster than the score, and serve as sort of "fallback" in
case some data is missing, kinda like the solution above. This will reduce the
traffic in the network, but it'd create have duplicate data in different
services -- although that last point shouldn't be a problem in the first
place.
New services will find it problematic, 'cause although they are receiving the
main data, they are were not alive when the more "stale" data was processed;
they will need to either communicate with other services to get this
information, or someone
will have to manually duplicate the sources.
## Single queue
I just describe solutions in which every data has its own queue, but what if
we put _all_ the events in the same queue? This way, order is assured (players
will be queue before the goals, and the services will process players before
they even see there is a goal).
This reduces the number of listeners, but it requires some good
message design, specially in statically typed languages, which usually require
a well-defined structure for serialization and deseralization.
But it solves almost everything else: there is no issue with the processing
order, the number of listeners is low and the messages are small. But it will
also make new services suffer from the lack of stale data, forcing them to
communicate with other services or to have the data manually copied when they
are brought up.
# The best one?
Honestly, I have no idea. I'm siding with "Full-blown messages" simply 'cause
it simplifies the structure of the services, even knowing that network is not
free; if I used some non-statically typed language, I'd probably side with the
single queue one. But, again, I don't think there is any "one size fits all".
Probably there are more architectural options for this, and those are the ones
I can remember discussing with my coworkers.
[^1]: It's worth noting that the microservice may simply drop some of the
information. For example, if the artifact produced only requires the
abbreviated name, it full name may be completely dropped from the state.

154
content/code/microservices-self-healing.pt.md

@ -0,0 +1,154 @@
+++
title = "Microserviços com 'Auto Cura'"
date = 2020-01-03
[taxonomies]
tags = ["microserviços", "auto cura", "artefatos"]
+++
As [discussões](@/code/microservices-artifact-input-state.pt.md)
[anteriores](@/code/microservices-artifact-ejection.pt.md) que eu levantei
sobre microserviços foram um prelúdio para uma coisa que eu não consegui uma
solução perfeita: como é que microserviços se "curam" quando faltam dados?
<!-- more -->
Pequena recapitulação antes de falar sobre o problema: Microserviços produzem
artefatos; artefatos ou são enviados para frente por um message broker para
outros serviços ou mantidos no mesmo microserviço para requisições futuras;
microserviços podem escutar mais de uma fonte de dados para construir seus
artefatos.
Anteriormente eu mencionei um exemplo de um microserviço de placares que
produz um artefato com o placar de cada time e o nome dos jogadores que
fizeram os gols. Esse microserviço poderia ouvir:
1. A fila de times: pode ser necessária para que possamos mostrar o nome ou
sigla do time na requisição de placar; uma vez que um time aparece num
campeonato, o microserviço adiciona o mesmo no seu estado para referência
futura[^1].
2. A file da jogadores: o mesmo que acima, para que o microserviço possa
retornar o nome, apelido, número da camisa ou alguma coisa relacionada com
o jogador; de novo, o serviço fica escutando a fila de jogadores e os
guarda em seu estado.
3. A fila de partidas: se uma partida for acontecer, ela tem que ter um
placar, provavelmente começando com 0 sem nenhum jogador na lista de gols;
isso é feito apenas para evitar problemas com serviços pedindo placares de
partidas que ainda não começaram ou que não tiveram gols ainda; de qualquer
forma, o artefato necessário já vai estar pronto para ser entregue.
4. A fila de narrações: escutando a fila de narrações, o microserviço de
placar irá detectar gols, atualizar seu estado e produzir o artefato
atualizado.
A palavra chave da lista acima é "poderia": dependendo da forma como os
microserviços _e_ as mensagens são construídas, pode não ser necessário ter
acesso a tudo isso.
## Usando mensagens completas
Vamos começar com a forma mais simples de evitar escutar todas essas filas:
utilizando mensagens completas.
Numa mensagem completa, todos os campos relacionados são enviados junto com a
informação principal. Usando o exemplo acima, o serviço poderia ouvir apenas
as filas de partidas e narração, mas esperar que a mensagem de "NovaPartida"
teria os nomes dos tipos, suas siglas, escudos, provavelmente o ID e assim
pode diante; da mesma forma para a mensagem de "NovaNarração": ela contém o
nome do jogador, o apelido, número da camisa, ID e assim pode diante.
O problema com mensagens completas é que elas tentem a ficarem maiores com o
tempo: Com mais microserviços sendo adicionados ao sistema, mais campos vai
sendo necessários -- e ignorados por serviços que não os precisam.
O lado positivo de mensagens completas é que um microserviço sempre terá toda
a informação necessária, mantendo o número de filas a serem escutadas baixo.
Esse formato também facilita a adição de outros serviços no sistema: se o
mesmo começar com um estado em branco, ele poderá construir o mesmo a partir
do zero, porque toda a informação _já está lá_.
## Escutar as filas básicas, pedir o resto
Quase como a solução acima, o serviço escuta apenas as filas de narrações e
partidas, mas uma vez que detecta alguma informação faltante (por exemplo, o
evento de narração cita um jogador, mas esse jogador não existe no estado), o
serviço faria uma requisição por essa informação mais "fria" (jogadores, times
e produtos não são atualizados com muita frequência, por exemplo) para outro
serviço e preencheria essa informação no seu estado.
Isso significa que esse microserviço agora, ao invés de saber apenas como
escutar filas, também precisa ter informações de outros serviços (aqueles que
processam e armazenam os dados frios) e suas interfaces -- e, de forma geral,
também requisitaria um serviço de descoberta presente no sistema. Esses
microserviços seriam aqueles de "duas caras", que recebem informações,
armazenam o estado, produzem o artefato mas tem uma interface de requisições
ao invés de simplesmente receber, processar e passar pra frente. Fazer cache
aqui também seria recomendado, para que um serviço não faça um "flood" de
requisições da mesma informação -- e atualizações de tempo em tempo podem
fazer sentido em algumas situações.
As mensagens seria menores (porque é enviado apenas o ID do time/jogador) e a
recuperação de informações acontece apenas quando necessária, mas onde é
reduzido o número de escutas nas filas, é aumentado o número de requisições.
Assim como na utilização de mensagens completas, um novo serviço poderia
facilmente construir seu estado a partir do zero sem qualquer problema -- irá
fazer um monte de requisições, mas terá, eventualmente, todas as informações
necessárias.
## Escutas todas
Essa é exatamente a solução apresentada no exemplo acima: o microserviço fica
escutando todas os eventos das filas com eventos relacionados e constrói o
estado a partir deles.
Um problema dessa solução: uma vez que as filas são assíncronas, pode
acontecer um problema com a ordenação dos dados, com gols chegando antes dos
jogadores (por vários motivos). Nesse caso... o que o serviço faz? Rejeita o
gol na esperança que o jogador apareça, para evitar uma inconsistência dos
dados, e o que o message broker coloque o evento novamente no fim da fila?
Uma solução seriam serviços que, junto com este, escutem por um dado
específico: o microserviço de placares escuta as quatro filas citadas, mas há
um microserviço escutando apenas a fila de jogadores. Esse serviço iria
processar os dados mais rapidamente que o placar, e serviria como "fallback"
no caso de dados faltantes, como na solução acima. Isso reduziria o tráfego de
rede, mas iria gerar dados duplicados em serviços diferentes -- embora esse
último ponto não deveria ser um problema em primeiro lugar.
Novos serviços iriam encontrar problemas, porque apesar de receberem
novos dados, eles não estavam presentes quando os dados frios foram
processados; eles vão precisar se comunicar com outros serviços para recuperar
essa informação, ou alguém teria que manualmente copiar os dados.
## Fila única
As soluções acima trabalham com cada dado em sua própria fila, mas e se
pudéssemos colocar _todos_ os eventos na mesma fila? Dessa forma, a ordenação
é assegurada (jogadores são sempre enfileirados antes dos gols, e os serviços
irão processar os jogadores antes de sequer verem que há um gol).
Isso reduz o número de filas a serem ouvidas, mas requer um bom design de
mensagens, especialmente se for utilizada alguma linguagem de tipagem
estática, que normalmente requer uma estrutura bem definida para serialização
e desserialização.
Mas ao mesmo tempo, resolve praticamente todos os problems: não existe
problema com a ordem de processamento, o número de filas a serem ouvidas é
baixo e as mensagens pequenas. Mas também faz com que novos serviços sofram
com a falta de dados frios, forçando-os a comunicar com outros serviços ou
terem os dados copiados manualmente quando levantados.
# E qual o melhor?
Honestamente, não faço ideia. Eu tenho uma certa preferência pelas mensagens
completas simplesmente porque simplifica a estrutura dos serviços, mesmo
sabendo que rede não é de graça; se eu usasse uma linguagem dinâmica, eu
provavelmente utilizaria a fila única. Mas, de novo, não acho que haja um
"tamanho único para todos".
Provavelmente existem outras opções arquiteturais para resolver esses
problemas, mas essas são as que eu consigo lembrar das conversas que tivemos
no trabalho.
[^1]: Vale notar que o microserviço pode simplesmente ignorar parte da
informação. Por exemplo, se o artefato produzido tem apenas a sigla do time,
o serviço pode remover o nome completamente de seu estado.
Loading…
Cancel
Save