このエントリは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の枯渇から回避できた。