3 Recomendações para Consumir Microsserviços com Segurança e Resiliência

Uma arquitetura baseada em microsseriços exige que prestemos atenção tanto na escrita destes, quanto nos códigos que os consomem.

Neste post, compartilho três recomendações sobre como consumir microsserviços de forma elegante e adequada.

O código-fonte para o exemplo deste post está disponível em meu github.

Se tiver interesse em entender mais sobre microsserviços, recomendo que acesse o Guia de Conteúdo para Microsserviços deste site.

1. Abstraia o interface para seu microsserviço

O acesso a um microsserviço deve ser abstraído da lógica principal de sua aplicação.

 

Minha recomendação é criar interfaces apropriadas para o negócio, isolando o código faz chamadas remotas.

public interface IEchoService
{
    Task Echo(string message,
        CancellationToken cancellationToken = default(CancellationToken)
        );
}

Dessa forma, o código que irá consumir o microsserviço ficará muito mais simples de ler e entender.

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task Echo(EchoViewModel model)
{
    model.Result = await _echoService.Echo(model.Message);
    return View(model);
}

O que temos é uma chamada assíncrona simples. Veja como não há nenhuma menção ao fato de estar sendo feito um request para outro serviço.

Outra vantagem dessa abordagem, é que fica mais simples escrever testes de unidade. Afinal, não estamos “acoplando” nosso código a conexões HTTP.

2. Isole requisições HTTP em um cliente apropriado

Chamadas HTTP para um microsserviço exigem configurações apropriadas de segurança e tratamento de falhas. Recomendo implementar essa lógica uma única vez reaproveitando a implementação sempre que necessário.

Atualmente, venho adotando uma versão adaptada do código de exemplo fornecido pela Microsoft.

public class StandardHttpClient : IHttpClient, IDisposable
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly HttpClient _httpClient;

    public StandardHttpClient(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
        _httpClient = new HttpClient();
    }

    public async Task GetStringAsync(
        string uri, 
        Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken)
        )
    {
        var message = new HttpRequestMessage(HttpMethod.Get, uri)
            .CopyAuthorizationHeaderFrom(_httpContextAccessor.HttpContext)
            .Apply(authorization);

        var response = await _httpClient.SendAsync(message, cancellationToken);

        if (response.StatusCode == HttpStatusCode.InternalServerError)
        {
            throw new HttpRequestException();
        }

        return await response.Content.ReadAsStringAsync();
    }

    public async Task PutAsync(
        string uri, 
        T item, string requestId = null, 
        Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Put, uri)
            .CopyAuthorizationHeaderFrom(_httpContextAccessor.HttpContext)
            .Apply(authorization);
            
        requestMessage.Content = new StringContent(
            JsonConvert.SerializeObject(item), 
            System.Text.Encoding.UTF8, "application/json"
            );
            
        if (requestId != null)
        {
            requestMessage.Headers.Add("x-requestid", requestId);
        }

        var response = await _httpClient.SendAsync(requestMessage, cancellationToken);

        if (response.StatusCode == HttpStatusCode.InternalServerError)
        {
            throw new HttpRequestException();
        }

        return response;
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}

Chamo especial atenção, nesse código, para a utilização da chave de identificação no PUT (adicionando o cabeçalho x-requestid). É parte da implementação do lado cliente para minha recomendação de como lidar com falhas de conexão.

Quanto a autorização, tento obter do próprio contexto sempre que possível.

public static HttpRequestMessage CopyAuthorizationHeaderFrom(
    this HttpRequestMessage request,
    HttpContext context
)
{
    var authorizationHeader = context
        .Request
        .Headers["Authorization"];

    if (!string.IsNullOrEmpty(authorizationHeader))
    {
        request.Headers.Add("Authorization", new string[] { authorizationHeader });
    }

    return request;
}

Se não estiver disponível, dou opção para o código consumidor prover a informação de autenticação apropriada.

public struct Authorization
{
    public string Token { get; }
    public string Method { get; }

    public Authorization(
        string token = null,
        string method = "Bearer"
    )
    {
        Token = token;
        Method = method;
    }

    public static readonly Authorization Empty = new Authorization();

    public static implicit operator Authorization(string token) =>
        new Authorization(token);

    public bool IsEmpty => Token == null;
}

3. Implemente os padrões Retry e Circuit Breaker de forma transparente

Como destaquei em um post anterior, no lado cliente (microsserviço ou aplicativo que está fazendo uma requisição) é fundamental implementar:

  • estratégias de proteção ao servidor (microsserviço que está sendo consumido), como a implementação do padrão Circuit Breaker
  • estretégias de repetição de requisição, como a implementação do padrão Retry

Tenho feito essa implementação com um “adapter” para o cliente Http que suporta políticas do Polly. Mais uma vez, é importante destacar que esta é uma implementação adaptada de um código de exemplo fornecido pela Microsoft.

public delegate IEnumerable PolicyFactory(string origin);

public class ResilientHttpClient : IHttpClient
{
    private readonly PolicyFactory _policyFactory;
    private readonly StandardHttpClient _standardHttpClient;

    private readonly ConcurrentDictionary<string, PolicyWrap> _policyWrappers =
        new ConcurrentDictionary<string, PolicyWrap>();

    public ResilientHttpClient(
        PolicyFactory policyFactory,
        IHttpContextAccessor accessor
        )
    {
        _policyFactory = policyFactory;
        _standardHttpClient = new StandardHttpClient(accessor);

    }

    public Task GetStringAsync(string uri, Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken)) => HttpInvoker(
        new Uri(uri).GetOriginFromUri(),
        () => _standardHttpClient.GetStringAsync(
            uri, authorization, cancellationToken
        )
    );

    public Task PutAsync(string uri, T item, string requestId = null, Authorization authorization = default(Authorization),
        CancellationToken cancellationToken = default(CancellationToken)) => HttpInvoker(
        new Uri(uri).GetOriginFromUri(),
        () => _standardHttpClient.PutAsync(
            uri, item, requestId, authorization, cancellationToken
        )
    );
        
    private async Task HttpInvoker(string origin, Func<Task> action)
    {
        var normalizedOrigin = origin?.Trim()?.ToLower();

        if (!_policyWrappers.TryGetValue(normalizedOrigin, out var policyWrap))
        {
            var policies = _policyFactory(normalizedOrigin).ToArray();
            policyWrap = Policy.WrapAsync(policies);
            _policyWrappers.TryAdd(normalizedOrigin, policyWrap);
        }

        return await policyWrap.ExecuteAsync(action, new Context(normalizedOrigin));
    }
}

public static class UriExtensions
{
    public static string GetOriginFromUri(this Uri uri) =>
        $"{uri.Scheme}://{uri.DnsSafeHost}:{uri.Port}";
}

Repare que tanto o Retry quanto o Circuit Breaker são injetados através da especificação de um Delegate.

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//services.AddSingleton<IHttpClient, StandardHttpClient>();
services.AddSingleton(sp => (PolicyFactory)((origin) =>
{
    var logger = sp.GetRequiredService<ILogger>();
    return new Policy[]
    {
        Policy.Handle()
            .WaitAndRetryAsync<HttpRequestException>(
                // number of retries
                6,
                // exponential backofff
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                // on retry
                (exception, timeSpan, retryCount, context) =>
                {
                    var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " +
                                $"of {context.PolicyKey} " +
                                $"at {context.ExecutionKey}, " +
                                $"due to: {exception}.";
                    logger.LogWarning(msg);
                    logger.LogDebug(msg);
                }),
        Policy.Handle()
            .CircuitBreakerAsync<HttpRequestException>( 
                // number of exceptions before breaking circuit
                5,
                // time circuit opened before retry
                TimeSpan.FromMinutes(1),
                (exception, duration) =>
                {
                    // on circuit opened
                    logger.LogTrace("Circuit breaker opened");
                },
                () =>
                {
                    // on circuit closed
                    logger.LogTrace("Circuit breaker reset");
                })
    };
}));
services.AddSingleton<IHttpClient, ResilientHttpClient>();

Concluindo

A escrita de código que consome microsserviços não é necessariamente difícil. Mas, precisa ser cuidadosa para evitar excessiva complexidade acidental. Minha abordagem, tem sido construir abstrações e implementar meu código através dessas abstrações.

  • Abstraindo o acesso ao microsserviço em uma classe de serviço com interface amigável
  • Abstraindo complexidades de acesso Http através de um cliente especializado com customizações e padrões que estou adotando
  • Abstraindo implementação de padrões de resiliência através do uso de uma biblioteca apropriada.

Comentários?
Capa: Nicolas Picard

Compartilhe este insight:

4 respostas

  1. Muito bom seu artigo Elemar. Parabéns!

    O que vejo geralmente é que o consumo de outros serviços é algo negligenciado pelos desenvolvedores, não se preocupando se o serviço que será consumido realmente está disponível, ou se a rede pode apresentar algum problema. E aí temos sistemas pobres e problemáticos.

    Conforme você mencionou uma simples implementação de Circuit Breaker e Retry com Polly pode ajudar nessa questão.

    Abraços!

  2. Oi Elemar,

    Muito bom ver essa série de artigos relacionados a construção de microsserviços.

    Olhei o seu exemplo e tenho algumas questões que gostaria de discutir.

    Primeiro, vejo que todas as requisições utilizarão a política de “retry”. Porém, isso não seria um problema para as operações não idempotentes?
    Eu li em outro artigo seu que você evita utilizar o método POST e prefere o PUT com “x-requestid” para não executar duas vezes a mesma requisição. Sendo assim, o uso do “x-requestid” não deveria ser obrigatório para que o “retry” possa ser utilizado com segurança? Além disso, quem fizer uso desse ResilientHttpClient deverá saber que um Bad Request na verdade pode significar sucesso (considerando o seu exemplo no artigo LIDANDO COM FALHAS DE CONEXÃO EM MICROSSERVIÇOS).

    *Você também utiliza o POST para a criação de recursos?

    A outra questão é sobre o uso do ConcurrentDictionary. Me parece ser possível que N políticas sejam criadas para a mesma origem. Eu entendo que apenas a primeira será inserida no dicionário, porém não seria mais adequado o seguinte código:

    if (!_policyWrappers.TryGetValue(normalizedOrigin, out var policyWrap))
    {
    var policies = _policyFactory(normalizedOrigin).ToArray();
    policyWrap = Policy.WrapAsync(policies);
    if(!_policyWrappers.TryAdd(normalizedOrigin, policyWrap))
    {
    policyWrap = _policyWrappers[normalizedOrigin];
    }
    }

    Acho que era isso…
    Abs

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

Sometimes it is not enough to know if two strings are equals or not. Instead, we need to get an...
O que mais me agrada no C4 Model é a forma como detalhes vão sendo explicitados na medida em que...
This is the first of a series of blog posts sharing some knowledge about how to develop a real-world software...
O comportamento que descrevo nesse post é extremamente nocivo a carreira de todo técnico. Geralmente resulta em desgaste desnecessário e...
Software em funcionamento é mais relevante que documentação abrangente. Concordo com esse princípio expresso no manifesto ágil. Entretanto, acho que...
Nem todos os problemas podem ser resolvidos da mesma forma. Nem toda ferramenta é apropriada para todo tipo de trabalho....
× Precisa de ajuda?