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