微軟用它取代了Nginx,吞吐量提升了百分之八十!

微軟用它取代了Nginx,吞吐量提升了百分之八十!

Azure 應用服務用 YARP 取代了 Nginx,獲得了 80% 以上的吞吐量。他們每天處理 160B 多個請求 (1.9 m RPS)。這是微軟一項了不起的技術創新。

最後更新 2024/1/14 上午6:19
tokengo
預計閱讀 7 分鐘
分類
.NET
標籤
.NET C# 技術更新

大佬消息截圖

Azure 應用服務用 YARP 取代了 Nginx,獲得了 80% 以上的吞吐量。他們每天處理 160B 多個請求(1.9 m RPS)。這是微軟的一項了不起的技術創新。

首先我們來介紹一下什麼是 Yarp

Yarp 是什麼?

YARP(Yet Another Reverse Proxy)是一個開源的、高效能的反向代理程式庫,由 Microsoft 開發,使用 C# 語言編寫。它旨在作為 .NET 平台上構建反向代理伺服器的基礎。YARP 主要針對 .NET 5 及以上版本,允許開發者在 .NET 應用程式中輕鬆地實現反向代理的功能。

YARP 的主要特點和功能:

  1. 模組化與可擴充性: YARP 設計成高度模組化的,這意味著可以根據需求替換或擴充內部元件,如 HTTP 請求路由、負載平衡、健康檢查等。
  2. 效能: YARP 針對高效能進行了最佳化,利用了 .NET 的非同步程式設計模型和高效的 IO 操作,以處理大量並行連線。
  3. 設定驅動: YARP 的行為可以透過設定來控制,支援從檔案、資料庫或其他來源動態載入設定。
  4. 路由: 可以基於各種參數(如路徑、標頭、查詢參數)設定請求路由規則。
  5. 負載平衡: 內建多種負載平衡策略,如輪詢、最少連線、隨機選擇等,並且可以自訂負載平衡策略。
  6. 健康檢查: 支援後端服務的健康檢查,以確保請求只會被轉發到健康的後端服務執行個體。
  7. 轉換器: 允許對請求和回應進行轉換,如修改標頭、路徑或查詢參數。
  8. 工作階段親和性: 支援工作階段親和性(Session Affinity),確保來自同一用戶端的請求被傳送到相同的後端服務執行個體。

使用 YARP 的一些場景:

  • 反向代理: 在用戶端和後端服務之間提供一個中間層,用於請求轉發和負載平衡。
  • API 閘道: 作為微服務架構中的 API 閘道,提供路由、鑑權、監控等功能。
  • 邊緣服務: 在應用程式和外部世界之間提供安全層,處理 SSL 終止、請求限制等工作。

Yarp 簡單的使用

建立一個 WebApi 的專案

安裝 NuGet 套件

<ItemGroup>
 <PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
</ItemGroup>

開啟 appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ReverseProxy": {
    "Routes": {
      "route1": {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "destination1": {
            "Address": "https://cn.bing.com/"
          }
        }
      }
    }
  }
}

開啟 Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

然後啟動專案,存取我們的 api 就會被代理轉發到 bing

成功代理到必應

Yarp 工具代理使用

下面我們再提供一個在中介軟體使用 yarp 的方式

我們需要用到 IHttpForwarder

先修改 Program.cs 在這裡我們註入了 HttpForwarder,然後提供一個 Run 中介軟體,在中介軟體中手動指定了端點的位址 https://cn.bing.com/ 然後我們啟動一下專案。

using Yarp.ReverseProxy.Forwarder;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpForwarder(); // 注入IHttpForwarder
var app = builder.Build();

var httpMessage = new HttpMessageInvoker(new HttpClientHandler());

app.Run((async context =>
{
    var httpForwarder = context.RequestServices.GetRequiredService<IHttpForwarder>();
    var destinationPrefix = "https://cn.bing.com/";

    await httpForwarder.SendAsync(context, destinationPrefix, httpMessage);
}));

app.Run();

也是一樣會被代理過去,但是與簡單使用不一樣的是我們是在程式碼層面控制代理的。

程式碼控制反代效果一樣

使用 yarp 修改 Bing 的回應內容

我們繼續基於上面的代理使用進行修改 bing 的相應內容!

開啟 Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpForwarder(); // 注入IHttpForwarder
var app = builder.Build();

var httpMessage = new HttpMessageInvoker(new HttpClientHandler()
{
    // 忽略https錯誤
    ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
    AllowAutoRedirect = false,
    AutomaticDecompression = DecompressionMethods.GZip,
    UseCookies = false,
    UseProxy = false,
    UseDefaultCredentials = true,
});
var destinationPrefix = "https://cn.bing.com/";

var bingTransformer = new BingTransformer();

app.Run((async context =>
{
    var httpForwarder = context.RequestServices.GetRequiredService<IHttpForwarder>();
    await httpForwarder.SendAsync(context, destinationPrefix, httpMessage, new ForwarderRequestConfig(),
        bingTransformer);
}));

app.Run();

建立 BingTransformer.cs

public class BingTransformer : HttpTransformer
{
    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest,
        string destinationPrefix,
        CancellationToken cancellationToken)
    {
        var uri = RequestUtilities.MakeDestinationAddress(destinationPrefix, httpContext.Request.Path,
            httpContext.Request.QueryString);
        proxyRequest.RequestUri = uri;
        proxyRequest.Headers.Host = uri.Host;
        await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);
    }

    public override async ValueTask<bool> TransformResponseAsync(HttpContext httpContext,
        HttpResponseMessage? proxyResponse,
        CancellationToken cancellationToken)
    {
        await base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken);

        if (httpContext.Request.Method == "GET" &&
            httpContext.Response.Headers["Content-Type"].Any(x => x.StartsWith("text/html")))
        {
            var encoding = proxyResponse.Content.Headers.FirstOrDefault(x => x.Key == "Content-Encoding").Value;
            if (encoding?.FirstOrDefault() == "gzip")
            {
                var content = proxyResponse?.Content.ReadAsByteArrayAsync(cancellationToken).Result;
                if (content != null)
                {
                    var result = Encoding.UTF8.GetString(GZipDecompressByte(content));
                    result = result.Replace("国内版", "Token Bing 搜索 - 国内版");
                    proxyResponse.Content = new StringContent(GZipDecompressString(result));
                }
            }
            else if (encoding.FirstOrDefault() == "br")
            {
                var content = proxyResponse?.Content.ReadAsByteArrayAsync(cancellationToken).Result;
                if (content != null)
                {
                    var result = Encoding.UTF8.GetString(BrDecompress(content));
                    result = result.Replace("国内版", "Token Bing 搜索 - 国内版");
                    proxyResponse.Content = new ByteArrayContent(BrCompress(result));
                }
            }
            else
            {
                var content = proxyResponse?.Content.ReadAsStringAsync(cancellationToken).Result;
                if (content != null)
                {
                    content = content.Replace("国内版", "Token Bing 搜索 - 国内版");
                    proxyResponse.Content = new StringContent(content);
                }
            }
        }

        return true;
    }

    /// <summary>
    /// 解壓GZip
    /// </summary>
    /// <param name="bytes"></param>
    /// <returns></returns>
    public static byte[] GZipDecompressByte(byte[] bytes)
    {
        using var targetStream = new MemoryStream();
        using var compressStream = new MemoryStream(bytes);
        using var zipStream = new GZipStream(compressStream, CompressionMode.Decompress);
        using (var decompressionStream = new GZipStream(compressStream, CompressionMode.Decompress))
        {
            decompressionStream.CopyTo(targetStream);
        }

        return targetStream.ToArray();
    }

    /// <summary>
    /// 解壓GZip
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    public static string GZipDecompressString(string str)
    {
        using var compressStream = new MemoryStream(Encoding.UTF8.GetBytes(str));
        using var zipStream = new GZipStream(compressStream, CompressionMode.Decompress);
        using var resultStream = new StreamReader(new MemoryStream(compressStream.ToArray()));
        return resultStream.ReadToEnd();
    }

    /// <summary>
    /// Br壓縮
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static byte[] BrCompress(string str)
    {
        using var outputStream = new MemoryStream();
        using (var compressionStream = new BrotliStream(outputStream, CompressionMode.Compress))
        {
            compressionStream.Write(Encoding.UTF8.GetBytes(str));
        }

        return outputStream.ToArray();
    }

    /// <summary>
    /// Br解壓
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static byte[] BrDecompress(byte[] input)
    {
        using (var inputStream = new MemoryStream(input))
        using (var outputStream = new MemoryStream())
        using (var decompressionStream = new BrotliStream(inputStream, CompressionMode.Decompress))
        {
            decompressionStream.CopyTo(outputStream);
            return outputStream.ToArray();
        }
    }
}

得到的效果我們將 国内版 修改成了 Token Bing 搜索 - 国内版

修改回應內容結果

Yarp 相關資料

技術交流群:737776595

官方文件:https://microsoft.github.io/reverse-proxy/articles/getting-started.html

來自 token 的分享

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2026/2/7

AOT使用經驗總結

從專案建立伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 AOT 發布測試。

繼續閱讀