如今的應用部署逐漸向微服務化發展,導致一個完整的事務往往會跨越很多的應用或服務,出於分布式鏈路跟蹤的需要,我們往往將從上游服務獲得的跟蹤請求報頭無腦地向下游服務進行轉發。本文介紹的這個名為HeaderForwarder的組件可以幫助我們完成針對指定HTTP請求報頭的自動轉發。本篇文章分為上下兩篇,上篇通過三個例子介紹HeaderForwarder的應用場景,下篇則介紹該組件的設計與實現。[源代碼從這里下載]
目錄
一、自動轉發指定的請求報頭
二、添加任意需要轉發的請求報頭
三、在非ASP.NET Core應用中使用
一、自動轉發指定的請求報頭
假設整個分布式調用鏈路由如下圖所示的三個應用構成。請求由控制台應用App1通過HttpClient向WebApp1(localhost:5000),該請求攜帶foo和bar兩個需要被轉發的跟蹤報頭。ASP.NET Core應用WebApp1在通過HttpClient調用WebApp2時,我們的組件會自動實現這對這兩個請求報頭的轉發。
如下所示的是作為下游應用的WebApp2的定義。如代碼片段所示,為了驗證指定的跟蹤報頭是否在WebApp1中被我們的組件成功轉發,我們將接收到的所有請求報頭拼接成一個字符串作為響應內容。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(web => web.Configure(app=>app.Run(Process))) .Build() .Run(); static Task Process(HttpContext httpContext) { var headerString = string.Join(";", httpContext.Request.Headers.Select(it => $"{it.Key}={it.Value}")); return httpContext.Response.WriteAsync(headerString); } } }
WebApp1的所有代碼定義如下。HeaderForwarder組件通過調用IHostBuilder的擴展方法UseHeaderForwarder進行注冊,在調用該方法的時候我們指定了需要轉發的請求報頭名稱(foo和bar)。在接收到請求之后,WebApp1會利用HttpClient調用WebApp2,並將得到結果作為相應的內容。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .UseHeaderForwarder(forwarder=>forwarder.AddHeaderNames("foo", "bar")) .ConfigureWebHostDefaults(web => web .ConfigureServices(svcs=>svcs.AddHttpClient()) .Configure(app => app.Run(Process))) .Build() .Run(); static async Task Process(HttpContext httpContext) { var httpClient = httpContext.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient(); var headerString = await httpClient.GetStringAsync("http://localhost:6000"); await httpContext.Response.WriteAsync(headerString); } } }
作為上游應用的App具有如下所示的定義。它直接利用HttpClient向WebApp1發送了一個請求,該請求攜帶了foo和bar這兩個需要WebApp1轉發的報頭。如果WebApp1完成了針對這兩個請求報頭的轉發,那么得到的響應內容將包含這兩個報頭的值,我們將這一驗證邏輯體現在兩個調試斷言中。
class Program { static async Task Main(string[] args) { var httpClient = new HttpClient(); var request = new HttpRequestMessage { RequestUri = new Uri("http://localhost:5000"), Method = HttpMethod.Get }; request.Headers.Add("foo", "123"); request.Headers.Add("bar", "456"); var response = await httpClient.SendAsync(request); var headers = (await response.Content.ReadAsStringAsync()).Split(";"); Debug.Assert(headers.Contains("foo=123")); Debug.Assert(headers.Contains("bar=456")); } }
二、添加任意需要轉發的請求報頭
上面我們演示了HeaderForwarder組件自動提取指定的報頭並自動轉發的功能,實際上該組件還可以幫助我們將任意的報頭添加到由HttpClient發出的請求消息中。假設WebApp1除了自動轉發的foo和bar報頭之外,還需要額外添加一個baz報頭,我們可以對程序作如下的修改。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .UseHeaderForwarder(forwarder => forwarder.AddHeaderNames("foo", "bar")) .ConfigureWebHostDefaults(web => web .ConfigureServices(svcs => svcs.AddHttpClient()) .Configure(app => app.Run(Process))) .Build() .Run(); static async Task Process(HttpContext httpContext) { using (new HttpInvocationContextScope()) { HttpInvocationContext.Current.AddOutgoingHeader("baz", "789"); var httpClient = httpContext.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient(); var headerString = await httpClient.GetStringAsync("http://localhost:6000"); await httpContext.Response.WriteAsync(headerString); } } } }
如上面的代碼片段所示,我們將針對HttpClient的調用置於HttpInvocationContextScope對象構建的上下文范圍中。在調用HttpClient發送請求之前,我們通過Current靜態屬性得到當前的HttpInvocationContext上下文,並通過調用其AddOutgoingHeader方法設置待轉發的baz報頭。為了驗證WebApp1針對baz報頭的轉發,我們將App的程序進行如下的改寫。
class Program { static async Task Main(string[] args) { var httpClient = new HttpClient(); var request = new HttpRequestMessage { RequestUri = new Uri("http://localhost:5000"), Method = HttpMethod.Get }; request.Headers.Add("foo", "123"); request.Headers.Add("bar", "456"); var response = await httpClient.SendAsync(request); var headers = (await response.Content.ReadAsStringAsync()).Split(";"); Debug.Assert(headers.Contains("foo=123")); Debug.Assert(headers.Contains("bar=456")); Debug.Assert(headers.Contains("baz=789")); } }
如果涉及到多個HTTP調用都需要對相同的報頭進行轉發,上面介紹的這種基於HttpInvocationContextScope/HttpInvocationContext的編程模式會變得很方便。
using (new HttpInvocationContextScope()) { HttpInvocationContext.Current .AddOutgoingHeader("foo", "123") .AddOutgoingHeader("bar", "456") .AddOutgoingHeader("baz", "789"); var result1 = await httpClient.GetStringAsync("http://www.foo.com/"); var result2 = await httpClient.GetStringAsync("http://www.bar.com/"); var result3 = await httpClient.GetStringAsync("http://www.baz.com/"); }
三、在非ASP.NET Core應用中使用
在ASP.NET Core應用中,HeaderForwarder是通過調用IHostBuilder的擴展方法UseHeaderForwarder進行注冊的,如果在控制台應用又該如何使用。其實很簡單,HeaderForwarder針對請求(通過HttpClient發送)報頭的添加是通過該注冊提供的一個HttpClientObserver對象提供的,它實現了IObserver<DiagnosticListener>接口,我們只需要對該對象進行注冊就可以了。
class Program { static async Task Main() { var httpClientObserver = new ServiceCollection() .AddHeaderForwarder() .BuildServiceProvider() .GetRequiredService<HttpClientObserver>(); DiagnosticListener.AllListeners.Subscribe(httpClientObserver); using (new HttpInvocationContextScope()) { HttpInvocationContext.Current .AddOutgoingHeader("foo", "123") .AddOutgoingHeader("bar", "456"); var headers = (await new HttpClient().GetStringAsync("http://locahost:5000")).Split(";"); Debug.Assert(headers.Contains("foo=123")); Debug.Assert(headers.Contains("bar=456")); Debug.Assert(headers.Contains("baz=789")); } } }
如上面的代碼片段所示,我們調用擴展方法AddHeaderForwarder將HeaderForwarder相關的服務注冊到創建的ServiceCollection對象上,並利用構建的IServiceProvider對象得到我們需要的HttpClientObserver對象,並將其添加到DiagnosticListener.AllListeners屬性的IObservable<DiagnosticListener>列表中。有了HttpClientObserver的加持,設置請求報頭的方式就可以通過上述的編程模式了。