zoukankan      html  css  js  c++  java
  • 解剖HttpClientFactory,自由扩展HttpMessageHandler

    前言

      .NetCore2.1新推出HttpClientFactory工厂类, 替代了早期的HttpClient, 并新增了弹性Http调用机制 (集成Policy组件)。

    替换的初衷还是简单说下:

    ①  using(var client= new HttpClient()) 调用Dispose()方法,并不会很快释放底层Socket连接, 同时新建Socket需要时间,这在高并发场景下Socket耗尽。 传送门

    ②  由于①很多人会想到用单例或静态类构建HttpClient实例,但是这里还有一个坑,HttpClient 会忽略DNS的变化。 传送门

    HttpClientFactory 以一种模块化、可命名、弹性可预期的方式重建了HttpClient的使用方式。

    现在的HttpClientFactory以依赖注入的方式集成到.NETCore 框架:

    // 截取自Startup.cs文件服务配置部分
    public void ConfigureServices(IServiceCollection services)
    {
                services.AddHttpClient("bce-request", x =>
                       x.BaseAddress = new Uri(Configuration.GetSection("BCE").GetValue<string>("BaseUrl")))
                    .ConfigurePrimaryHttpMessageHandler(_ => new BceAuthClientHandler()
                   {
                       AccessKey = Configuration.GetSection("BCE").GetValue<string>("AccessKey"),
                       SerectAccessKey = Configuration.GetSection("BCE").GetValue<string>("SecretAccessKey"),
                       AllowAutoRedirect = true,
                       UseDefaultCredentials = true
                   })
                   .SetHandlerLifetime(TimeSpan.FromHours(12))
                   .AddPolicyHandler(GetRetryPolicy(3));
    }
    
    static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(int  retry)
    {
          var retryPolicy = HttpPolicyExtensions
                    .HandleTransientHttpError()
                    .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
                    .WaitAndRetryAsync(retry, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
                return retryPolicy;
    }
    HttpClientFactory典型用法

    使用时从 IHttpClientFactory工厂创建所需HttpClient实例,发起业务端请求。

    以下是利用NLog观察到的文件日志:

    19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[18}].[]
    Start processing HTTP request GET http://localhost:5000/v1/eqid/b827a9400004132a000000065dc26470 
    19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.ClientHandler].[18}].[]
    Sending HTTP request GET http://localhost:5000/v1/eqid/b827a9400004132a000000065dc26470 
    19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.ClientHandler].[34}].[]
    Received HTTP response after 174.5088ms - OK 
    19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[34}].[]
    End processing HTTP request after 211.1478ms - OK 

    头脑风暴

      观察上面单次请求的日志,由外层LogicHandler和内层ClientHandler 日志头组成。 这样的日志可以想象到有2个问题:

     ① 在高并发使用HttpClient,日志条数众多,没有类似TraceId 这样的机制定位 某次HttpClient调用的完整日志。 

     ②  若是微服务/ 分布式调用,可能还有 将本次HttpClient调用日志与后置api日志 结合分析的需求, 这个日志也支持不了。

    因此本文打算重新自定义HttpClientFactory日志处理器(给请求的全部日志设置TraceId),实际上CustomLoggingHttpMessageHandler只是一个引子,掌握如何扩展才是关键。

    结合我给出的典型用法来看IHttpClientFactory组件原理:

     示例中System.Net.Http.HttpClient.bce-request.LogicalHandlerSystem.Net.Http.HttpClient.bce-request.ClientHandler 日志头即是来自LoggingScopeHttpMessageHandler ,LoggingHttpMessageHandler 两个处理器,

     给出手绘的UML类图: 

     

    本次要扩展的入口便是 IHttpMessageHandlerFilter接口, 核心是自定义DelegatingHandler日志处理器

    https://github.com/dotnet/extensions/blob/master/src/HttpClientFactory/Http/src/Logging/LoggingHttpMessageHandlerBuilderFilter.cs

    编程实践

       如以上分析,

    P1  实现 IHttpMessageHandlerFilter接口,在接口中移除默认的两个日志处理器

        public class TraceIdLoggingMessageHandlerFilter : IHttpMessageHandlerBuilderFilter
        {
            private readonly ILoggerFactory _loggerFactory;
    
            public TraceIdLoggingMessageHandlerFilter(ILoggerFactory loggerFactory)
            {
                _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
            }
    
            public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
            {
                if (next == null)
                {
                    throw new ArgumentNullException(nameof(next));
                }
    
                return (builder) =>
                {
                    // Run other configuration first, we want to decorate.
                    next(builder);
    
                    var outerLogger =_loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{builder.Name}.LogicalHandler");
                    builder.AdditionalHandlers.Clear();
                    builder.AdditionalHandlers.Insert(0,new CustomLoggingScopeHttpMessageHandler(outerLogger));
                };
            }
        }

    P2  实现带有TraceId能力的HttpClient 日志处理器, 并加入到 IHttpMessageHandlerFilter接口实现类

    public class CustomLoggingScopeHttpMessageHandler : DelegatingHandler
        {
            private readonly ILogger _logger;
    
            public CustomLoggingScopeHttpMessageHandler(ILogger logger)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            }
    
            protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                CancellationToken cancellationToken)
            {
                if (request == null)
                {
                    throw new ArgumentNullException(nameof(request));
                }
    
                using (Log.BeginRequestPipelineScope(_logger, request))
                {
                    Log.RequestPipelineStart(_logger, request);
                    var response = await base.SendAsync(request, cancellationToken);
                    Log.RequestPipelineEnd(_logger, response);
    
                    return response;
                }
            }
    
            private static class Log
            {
                private static class EventIds
                {
                    public static readonly EventId PipelineStart = new EventId(100, "RequestPipelineStart");
                    public static readonly EventId PipelineEnd = new EventId(101, "RequestPipelineEnd");
                }
    
                private static readonly Func<ILogger, HttpMethod, Uri, string, IDisposable> _beginRequestPipelineScope =
                    LoggerMessage.DefineScope<HttpMethod, Uri, string>(
                        "HTTP {HttpMethod} {Uri} {CorrelationId}");
    
                private static readonly Action<ILogger, HttpMethod, Uri, string, Exception> _requestPipelineStart =
                    LoggerMessage.Define<HttpMethod, Uri, string>(
                        LogLevel.Information,
                        EventIds.PipelineStart,
                        "Start processing HTTP request {HttpMethod} {Uri} [Correlation: {CorrelationId}]");
    
                private static readonly Action<ILogger, HttpStatusCode,string,Exception> _requestPipelineEnd =
                    LoggerMessage.Define<HttpStatusCode,string>(
                        LogLevel.Information,
                        EventIds.PipelineEnd,
                        "End processing HTTP request - {StatusCode}, [Correlation: {CorrelationId}]");
    
                public static IDisposable BeginRequestPipelineScope(ILogger logger, HttpRequestMessage request)
                {
                    var correlationId = GetCorrelationIdFromRequest(request);
                    return _beginRequestPipelineScope(logger, request.Method, request.RequestUri, correlationId);
                }
    
                public static void RequestPipelineStart(ILogger logger, HttpRequestMessage request)
                {
                    var correlationId = GetCorrelationIdFromRequest(request);
                    _requestPipelineStart(logger, request.Method, request.RequestUri, correlationId, null);
                }
    
                public static void RequestPipelineEnd(ILogger logger, HttpResponseMessage response)
                {
                    var correlationId = GetCorrelationIdFromRequest(response.RequestMessage);
                    _requestPipelineEnd(logger, response.StatusCode, correlationId, null);
                }
    
                private static string GetCorrelationIdFromRequest(HttpRequestMessage request)
                {
                    string correlationId;
                    if (request.Headers.TryGetValues("X-Correlation-ID", out var values))
                        correlationId = values.First();
                    else
                       {correlationId = Guid.NewGuid().ToString(); request.Headers.Add("X-Correlation-ID",correlationId);}
              return correlationId; }
    }
    }

       以上TraceId的实现思路,参考了我前一篇博文《被忽略的TraceId,可以用起来了》的思路,为每次HttpClient调用过程设定  全局唯一的GUID标记, 后置api服务可酌情修改以上代码处理。

    其中写入日志的代码Copy自HttpClientFactory源代码。

    P3  在DI框架中替换原有的 IHttpMessageHandlerFilter 实现

    services.Replace(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, TraceIdLoggingMessageHandlerFilter>());

      发起两次HttpClient请求, 输出的日志如下:

    19/12/04 12:59:22 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[17}].[]
    Start processing HTTP request GET http://localhost:5000/v1/eqid/ad78deef00444ed7000000035de704e8 [Correlation: 03de676d-680e-4a92-aef5-749bcc3ba499] 
    19/12/04 12:59:22 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[4}].[]
    End processing HTTP request - OK, [Correlation: 03de676d-680e-4a92-aef5-749bcc3ba499] 
    19/12/04 12:59:48 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[17}].[]
    Start processing HTTP request GET http://localhost:5000/v1/eqid/8ea0c3b66b60f0ff100000005de704fb [Correlation: 6f14393a-3a2b-45c4-a9b4-0b4ab874ef1d] 
    19/12/04 12:59:48 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[42}].[]
    End processing HTTP request - OK, [Correlation: 6f14393a-3a2b-45c4-a9b4-0b4ab874ef1d] 

    可以看到每次请求的开始和结束都带上了设定的guid TraceId。

    值得提醒的是:

     ① 这个TraceId 可以使用你业务上独具一格的标记,这样在排查时, 能根据上游业务更好的追踪日志。

     ② 现在这个TraceId位于LogMessage,实际上可以为nlog自定义LogoutRenderer,将该TraceId放在显著位置,便于ETL等日志集成框架过滤。

    That's All, 本次为解决HttpClientFactory日志无追踪机制的探索,思考 + 实践 + UML制图。

    实现CustomLoggingScopeHttpMessageHandler只是扩展HttpClientFactory能力的一个引子,如何扩展HttpClientFactory能力才是关键,希望能给大家一些启发。 

    --------------------------------------------------------------2019.12.06 更新------------------------

    实际上HttpClientFactory内原生LoggingHandler是支持LoggingScope, 在Console 输出如下:

    info: System.Net.Http.HttpClient.bce-request.LogicalHandler[100]
          => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
          Start processing HTTP request GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
    info: System.Net.Http.HttpClient.bce-request.ClientHandler[100]
          => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
          Sending HTTP request GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
    
    info: System.Net.Http.HttpClient.bce-request.ClientHandler[101]
          => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
          Received HTTP response after 195.1112ms - OK
    info: System.Net.Http.HttpClient.bce-request.LogicalHandler[101]
          => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
          End processing HTTP request after 232.4906ms - OK

    Scope需要LoggingProvider 支持,而我们使用的NLog不支持scope, 所以最上面的nlog 文件日志没有输出Scope。

    这就引出了本文的目的,所以本文通过解构HttpClientFactory的HttpMessageHandler,为请求响应添加TraceId, 当然你也可以根据HttpClient业务加入其它HttpMessageHandler

  • 相关阅读:
    String Kernel SVM
    基因组印记
    用Js的eval解析JSON中的注意点
    struts2中<s:select>标签的使用
    如何在Linux中使用cron命令
    怎样解决MySQL数据库主从复制延迟的问题
    PMON failed to acquire latch, see PMON dump
    java中对List中对象排序实现
    C语言typedef关键字
    企业级内部信息统一搜索解决方案
  • 原文地址:https://www.cnblogs.com/JulianHuang/p/11982021.html
Copyright © 2011-2022 走看看