App ServiceでSNAT Port枯渇を避けたい

このエントリは2022/05/16現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容からの乖離が発生する可能性があります。

例によって以下のような問い合わせが届いた。

問い合わせとその背景

現在Azure App Service上にJavaアプリケーションを展開している。VNetには接続しておらず、シンプルなアプリケーションなのだが、断続的にSNAT Port枯渇のエラーが出てしまう。なお、OkHttpの接続プールを構成して利用している。

VNet統合してNAT Gatewayを構成すれば、SNAT Port枯渇のエラーが出なくなることは承知しているが、できる限りコードで対応したい。どうしたらよいか?

Azure App Serviceの各インスタンスには、当初128 個の SNAT ポートが事前に割り当てられているが、これを超えるような接続(Connection)を作ると、SNATポートが枯渇して外部接続ができなくなる、という事象が発生するので、そうしないようにコードで対応する必要がある、という有名な話に対して、Javaだったらどうすればいいか、というもの。

もちろん、Azureインフラ側で緩和することもできて、VNet接続しているなら、NAT Gatewayを組み合わせるとか、接続相手がAzure PaaSであれば、Service EndpointやPrivate Endpointを使う、という方法も利用できる。ただ、Azure FunctionsのConsumption SKUではVNet統合が構成できないので、その場合にはコードで対応するしかない。

.NET (C#) の場合

以下のようなガイド(ベストプラクティス)が公式ドキュメントに掲載されていたり、サポートチームのブログにも記載があったりする。

Azure App Service での断続的な送信接続エラーのトラブルシューティング / Troubleshooting intermittent outbound connection errors in Azure App Service
https://docs.microsoft.com/azure/app-service/troubleshoot-intermittent-outbound-connection-errors
App Service における SNAT ポート枯渇問題とその解決方法
https://jpazpaas.github.io/blog/2021/09/29/app-service-snat.html

具体的には、「usingで囲まない」というのがお約束。つまり、

using (var client = new HttpClient())
{
    var response = await client.GetAsync(url);
    ....
}

ではなく、

public class GoodController : ApiController
{
    private static readonly HttpClient HttpClient;
    static GoodController()
    {
        HttpClient = new HttpClient();
    }
}

のように使い回すのがお作法。

Javaの場合

Javaでも基本的な考え方は同じ。今回の問い合わせ主のコードを見たところ、JAX-RSベースのコードであった。OkHttpの構成は別のクラス(HttpConnectionClient)でやっているよう。

@Path("/api")
public class Caller {
    ...
    @GET
    @Path("/start")
    @Produces(MediaType.APPLICATION_JSON)
    public Response2Client startApi() {
        HttpConnectionClient httpConnectionClient = HttpConnectionClient.getInstance();
        Request request = httpConnectionClient.getRequest();
        OkHttpClient okHttpClient = httpConnectionClient.getOkHttpClient();
        Response2Client response2Client = new Response2Client();
        try(Response response = okHttpClient.newCall(request).execute()) {
            // Handling called API response
            ...
        } catch (IOException e) {
            response2Client.setMessage(e.getMessage());
        }
        return response2Client;
    }
    ...
}

OkHttpの設定をしているクラス(HttpConnectionClient)は以下のよう。

public class HttpConnectionClient {
    private final OkHttpClient okHttpClient;
    private final Request request;
    private static final class HttpConnectionClientHolder {
        private static final HttpConnectionClient instance = new HttpConnectionClient();
    }
    public static HttpConnectionClient getInstance() {
        return HttpConnectionClientHolder.instance;
    }
    private HttpConnectionClient() {
        ConnectionPool connectionPool = new ConnectionPool(MAX_CONNECTION, 10, TimeUnit.MINUTES);
        okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
            .readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
            .writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
            .connectionPool(connectionPool)
            .build();
        request = new Request.Builder()
            .url(URL)
            .get()
            .addHeader("Connection", "close")
            .build();
    }
    public OkHttpClient getOkHttpClient() {
        return okHttpClient;
    }
    public Request getRequest() {
        return request;
    }
}

クラスの構成はさておき、ここで問題になるのは、HttpConnectionClientの21行目にある Connection: close の指定。これがあると、いくら接続プールを作っていても、接続をクローズしてしまう。

HTTP/2による通信

もう一つの方法はHTTP/2を使う、というもの。HTTP/2であれば同一ピアに対する通信の場合、1個のTCP接続だけで複数の通信を多重化できるため、SNAT Port枯渇にはなりづらい。もちろん、1対多の通信が発生する場合にはこの限りではないが、対向のAPIとの通信でHTTP/2が利用できるなら、回避策の一つとして検討の価値がある。以下はAzure PortalでSNAT Portの枯渇状況を確認したもの。黄色でマークした時間帯にHTTP/2で他のAPIを呼び出す負荷テストを実行したのだが、接続は増えているものの、SNAT Portの枯渇は発生していないことがわかる。

問い合わせ主の対応

今回はHTTP 1.1を使っての接続であったため、HttpConnectionClientクラスの25行目の設定を Connection: keep-alive に変更してもらった。これにより接続が短命でなくなったので、SNAT Portの枯渇から回避できた。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中