Julio Biason
5 years ago
2 changed files with 304 additions and 0 deletions
@ -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. |
@ -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…
Reference in new issue