diff --git a/content/code/microservices-self-healing.md b/content/code/microservices-self-healing.md new file mode 100644 index 0000000..a0c6cb2 --- /dev/null +++ b/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? + + + +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. diff --git a/content/code/microservices-self-healing.pt.md b/content/code/microservices-self-healing.pt.md new file mode 100644 index 0000000..ae5db40 --- /dev/null +++ b/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? + + + +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.