🌐

Cloud RunでDirect VPC Egressを使うことの利点、注意点

に公開

このエントリーは一休.com Advent Calendar 2024の13日目の記事になります。


今年の4月にCloud RunのDirect VPC EgressがGAしました。
今まで、Cloud RunからVPC内のリソースにアクセスする場合、 VPCアクセスコネクタでプロキシしなければなりませんでした。
Direct VPC Egressを使うと、コネクタなしで、直接VPC内のリソースにアクセスにアクセスできます。
わたしたちも、Direct VPC Egressへの移行を行いました。いくつか注意点があったので、記録に残しておきます。

Direct VPC Egressの利点は?

注意点の前に利点を確認します。
VPCアクセスコネクタとの違いはこの比較表にある通りです。
※執筆時点でこの比較表では、まだ、Direct VPC Egressがプレビュー状態である、と書かれていますが、おそらくこのページが古いと思われます。

利点として大きいのは、レイテンシの低下とスループットの向上でしょうか。また、VPCアクセスコネクタは、実体はVMのクラスタなので、コネクタを使わないなら、そのぶんコスト削減できます。

ここでは、実際Direct VPC Egressを使うとどのくらいレイテンシが改善するのか、Golangのコードで確かめてみたいと思います。
コードは、以下。

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/jackc/pgx/v5"
)

var conn *pgx.Conn

func main() {

	conn, _ = pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
	defer conn.Close(context.Background())

	log.Print("starting server...")
	http.HandleFunc("/", handler)

	// Determine port for HTTP service.
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("defaulting to port %s", port)
	}

	// Start HTTP server.
	log.Printf("listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	s := time.Now()
	var description string
	err := conn.QueryRow(context.Background(), "select name from merchant").Scan(&description)
	if err != nil {
		fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
		os.Exit(1)
	}
	fmt.Fprintf(w, "name is %s!\n", description)
	fmt.Printf("process time: %s\n", time.Since(s))
}


このコードは、Google Cloud Runのサンプルコードを改変し、かなり簡易なやり方ですが、VPC内のDBへのSQL発行の速度を計測するように改変したものです。具体的な改変点は以下。

  • jackc/pgxを導入して、VPC内にあるCloud SQL(PostgreSQL)へselect文を投げられるようにした。
  • SQLクエリの実行をtime.Sinceで計測して、標準出力に出す。

これをgcloud run deploy --source .でデプロイし、k6を使って一定のリクエストを送り(k6 run --vus 1 --duration 30s script.js)、process timeを集計してみました。
結果は以下。

  • VPCアクセスコネクタの場合は、
    • 平均値: 2.2189 ms
    • 中央値: 2.1348 ms
    • 最小値: 2.0042 ms
    • 最大値: 3.9656 ms
  • Direct VPC Egressの場合は、
    • 平均値: 1.3716 ms
    • 中央値: 1.3085 ms
    • 最小値: 1.2212 ms
    • 最大値: 3.0441 ms

実行しているSQLがかなり軽いものなので、絶対値としては、大きな違いはありませんが、相対的にみると、平均値/中央値ともに、Direct VPC Egressが、50%程度VPCアクセスコネクタを下回っています。たしかに、レイテンシが改善しているといえそうです。

また、VPCアクセスコネクタを運用しているとたまにコネクタで障害?が起きて、Cloud Run側のエラーレートが一斉に上がる、ということが何度かおきました。が、Direct VPC Egressにしたところそのようなことが一切なくなり、ストレスなく運用できています。これも大きな利点だと感じて言います。

Direct VPC Egressの注意点

最大インスタンス数の上限は100

この記事を執筆している2024/12時点ですが、最大インスタンス数(yamlのautoscaling.knative.dev/maxScale)は、100が上限になるようです。100以上を指定するとリビジョンのデプロイが失敗します。

アプリケーションによっては、厳しい制約になるかもしれません。どこかの段階で制約がなくなるといいな、と思います。

Cloud NATのポート枯渇に注意

外向きの通信のIPアドレスを固定したい、という動機で、Cloud RunのすべてのトラフィックをVPCに送っているケースは多いと思います。この場合、Cloud NATに指定したのIPアドレスで外部へ通信するようになります。
このとき、Cloud NATのポートについて意識する必要があります。

  • Cloud NATは、NATの1つのIPアドレスのポートをNATに接続するVMに割り当てます。
  • ひとつのNATのIPアドレスあたり64,512のポートが使えます。割り当てるポートが枯渇してしまうと通信障害になってしまいます。
  • ポートの割り当てには静的割り当てと動的割り当てがあります。
    • 静的割り当てがデフォルトです。これは、接続元のVMに均一に一定数のポートを割り当てます。
    • 動的割り当ては、接続元のVMに最小値に設定したポート数を割り当て、枯渇しそうになると、割り当てを2倍にする、ということを最大ポート数に設定した値に達するまで繰り返します。

さて、VPCアクセスコネクタからDirect VPC Engressへ移行する場合、考慮しなければならないのは以下。

  • VPCアクセスコネクタがNATに接続する場合、最大でスケールアウトしても10VMです。なので、静的ポート割り当てで、ある程度大き目のポートを割り当てれば、ポート枯渇は、起きにくいです。
    • もちろん、コネクタ自体をたくさん作っている場合は枯渇の可能性があります。しかし、コネクタのVMは結構性能がよいので、それほどたくさんコネクタを作る必要はないはずです。私たちもコネクタのスケールアウトを経験することはありませんでした。
    • また、router.googleapis.com/nat/port_usageというメトリクスでVMあたりのポート利用の最大値が補足できます。これを見ながら調整すれば、安全です。
  • が、Direct VPC Engressの場合、Cloud RunのひとつのインスタンスがNATに直接接続するようになります。
  • サービスの特性にもよりますが、B to C向けのサービスの場合は、インスタンスは、水平にスムーズにスケールアウトすることを前提にキャパシティプランニング/負荷対策をしています。よって、VPCアクセスコネクタの最大でも10台、という接続インスタンス数の前提では全然足りません。
  • 必然的にCloud NATのポート割り当てを再考する必要が生まれました。
  • また、Cloud Runが直接確保するポートは、router.googleapis.com/nat/port_usageには、反映されないようです。これが、Direct VPC Engressにした場合のポート割り当ての見積もりや運用を難しくする、ということもわかりました。

では、ポート割り当てについて最終的にどうしたか、というと以下。

  • VPCアクセスコネクタからDirect VPC Engressへ切り替えるのに合わせて、静的ポート割り当てをやめ、動的ポート割り当てに変更しました。
    • 静的だと、たいして通信が発生しないインスタンスにも無駄にポートを割り当ててしまうので、不効率だと判断しました。
    • 動的にして、最小ポート数を可能な限り小さい値にすることで、インスタンス数が急増しても枯渇が起きにくい、という設計を志向しました。
    • 最終的な最小/最大ポート数は、router.googleapis.com/nat/port_usageから推計して決めました。
  • 合わせて、以下も実施しました。
    • NATのPublic IPアドレスを増やすことで確保できるポート数をより多くしました。
    • かならずしも、すべてのトラフィックをVPCに流す必要のないCloud Runインスタンス(=外向きの通信のIPアドレスを固定する必要がないサービスのインスタンス)については、プライベートなIPの場合のみVPCにトラフィックを流すようにしました。これによって、無駄にNAT経由で通信しないようにして、ポートの利用を抑制しました。

以上の変更を行い、Direct VPC Engressへ移行したところ、安全に事故なく移行することができました。ただ、VMにおけるrouter.googleapis.com/nat/port_usageと同等のメトリクスが取得できない、という点に不安が残りますので、これは改善を期待したいです。
※サポートケースを作成して、値を教えてもらうことはできました。


一休では、ともに良いサービスをつくっていく仲間を募集中です。クラウドインフラの運用やSREに興味がある方はぜひ募集ください。

https://hrmos.co/pages/ikyu/jobs/1693126708022206468

カジュアル面談もやっています。

https://www.ikyu.co.jp/recruit/engineer/

Discussion