
Azure App Service は YARP で Nginx を置き換え、80% 以上のスループット向上を達成しました。1 日あたり 160B 以上のリクエスト(1.9M RPS)を処理しています。これは Microsoft の素晴らしい技術革新です。
まず Yarp とは何かを紹介します。
Yarp とは?
YARP(Yet Another Reverse Proxy)は、Microsoft が開発したオープンソースの高性能リバースプロキシライブラリで、C# で記述されています。.NET プラットフォーム上でリバースプロキシサーバーを構築するための基盤として設計されています。YARP は主に .NET 5 以降を対象としており、開発者が .NET アプリケーション内で簡単にリバースプロキシ機能を実装できるようにします。
YARP の主な特徴と機能:
- モジュール性と拡張性: YARP は高度にモジュール化されて設計されており、HTTP リクエストルーティング、ロードバランシング、ヘルスチェックなどの内部コンポーネントを必要に応じて置き換えたり拡張したりできます。
- パフォーマンス: YARP は高パフォーマンス向けに最適化されており、.NET の非同期プログラミングモデルと効率的な IO 操作を活用して、大量の同時接続を処理します。
- 設定駆動: YARP の動作は設定によって制御でき、ファイル、データベース、その他のソースから動的に設定をロードすることができます。
- ルーティング: パス、ヘッダー、クエリパラメータなどのさまざまなパラメータに基づいてリクエストルーティングルールを設定できます。
- ロードバランシング: ラウンドロビン、最小接続数、ランダム選択など、複数のロードバランシング戦略が組み込まれており、カスタム戦略も定義できます。
- ヘルスチェック: バックエンドサービスのヘルスチェックをサポートし、正常なバックエンドサービスのインスタンスにのみリクエストが転送されるようにします。
- トランスフォーマー: リクエストとレスポンスの変換(ヘッダー、パス、クエリパラメータの変更など)を許可します。
- セッションアフィニティ: セッションアフィニティをサポートし、同じクライアントからのリクエストを同じバックエンドサービスのインスタンスに送信します。
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 からの共有