Edge と Serverless のハイブリッド: ユーザーの近くでコードを実行
1. 概要
ユーザーがサーバーから遠く離れている場合、コードがどれだけ高速に実行されても、ネットワーク遅延は高いままです。Edge と Serverless のハイブリッドモデルがこれを解決します。軽いロジックの一部はユーザーの近くのポイントで実行され、重い部分は中央の数個のリージョンの Serverless 関数で実行されます。
この記事では、このコンセプトについて説明し、なぜこのモデルが注目されているのか、そして 2 つのレイヤー間で作業を分割する方法を説明します。Spring Boot 3.x、Spring Cloud Function、および小さなエッジ関数の具体的な例を示します。記事の最後に、このモデルを使用すべきではないケースを説明します。
2. Edge、Serverless、ハイブリッドモデル
2.1. Edge とは何か
Edge は複数の場所に分散しているインフラストラクチャで、通常は CDN ネットワークに関連付けられています。以前は、Edge は主に静的ファイルをキャッシュするために使用されていました。今日、多くのプラットフォームが Edge でコードを直接実行できるようにしており、これを Edge 関数 と呼びます。
Edge runtime は通常、厳しい制限を持っています:
- リクエストごとの実行時間は短い。
- メモリが小さく、完全なファイルシステムがない。
- 通常、JavaScript/TypeScript または WebAssembly をサポートしており、フル JVM を実行することはめったにない。
- 従来型のデータベースへの長時間接続を保つのには適していない。
2.2. Serverless とは何か
Serverless(Function-as-a-Service または自動スケーリングコンテナ)は、サーバーを管理することなくコードをデプロイできます。プラットフォームはリクエスト数に応じて自動的にスケーリングし、0 までスケールダウンできます。その代わりに、新しいインスタンスが起動するときの Cold Start を受け入れる必要があります。
Edge と比べると、Serverless は通常、より少ないリージョンで実行されます。その代わりに、より多くのリソースを持ち、完全な Java サポートを提供し、データベースに近い位置にあります。
2.3. ハイブリッドモデルが必要な理由
各レイヤーには独自の強みがあります:
┌──────────┐ 近い ┌──────────────┐ さらに遠い ┌────────────────────┐ ┌──────────┐
│ ユーザー │ ────────▶ │ Edge │ ─────────▶ │ Serverless origin │ ─▶ │ Database │
│ │ ◀──────── │ (cache, ルート│ ◀───────── │ (Spring Boot / │ ◀─ │ │
└──────────┘ │ 指定, auth) │ │ Spring Cloud Func)│ └──────────┘
└──────────────┘ └────────────────────┘
- Edge は、速度が必要で中央データに依存しない作業を処理します: キャッシュ、言語に基づくリダイレクト、トークンの事前チェック、ボットブロック、A/B テスト。
- Serverless Origin は、一貫したデータを必要とするビジネスロジックを処理します: 価格計算、注文作成、支払い。
一般的な原則: 読み取りが多く、変更が少ない部分を Edge に移動し、書き込みと一貫性が必要な部分をオリジンに保つ。
3. なぜこのモデルが注目されているのか
いくつかの主な理由があります:
- レイテンシの認識. 多くのリクエストは Edge で直接応答され、Origin へ行く必要がありません。中央リージョンから遠いユーザーが最も恩恵を受けます。
- オリジンの負荷とコスト削減. Edge でキャッシュされるリクエストはすべて、Serverless 関数が実行される必要がない 1 回です。
- 場所に基づくパーソナライズ. Edge はユーザーがどこから来たかを知っているため、言語、通貨、または適切なコンテンツバージョンを選択できます。
- 地域ごとのデータストレージ要件. Edge は、リクエストを正しい法定地域にあるオリジンにルーティングできます。
- Java エコシステムの改善. GraalVM による Native Image と高速スタートアップの技術は Cold Start を削減し、Java が以前よりも Serverless に適しています。
4. Origin: Spring Boot が Edge にキャッシュ方法を「教える」
ハイブリッドモデルは Origin から始まります。Edge は Origin が明確な Cache-Control ヘッダーを返す場合のみ正しくキャッシュします。製品カタログ API の例を考えてみましょう:
public record Product(String id, String name, BigDecimal price) {}
@Component
public class ProductCatalog {
private final Map<String, Product> products = Map.of(
"p-1", new Product("p-1", "Nón lá Huế", new BigDecimal("150000")),
"p-2", new Product("p-2", "Áo bà ba", new BigDecimal("320000"))
);
public Optional<Product> findById(String id) {
return Optional.ofNullable(products.get(id));
}
}
Controller は各データ型に対して異なるキャッシュヘッダーを返します:
@RestController
@RequestMapping("/api")
public class ProductController {
private final ProductCatalog catalog;
public ProductController(ProductCatalog catalog) {
this.catalog = catalog;
}
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable String id) {
return catalog.findById(id)
.map(product -> ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofSeconds(30))
.cachePublic()
.sMaxAge(Duration.ofMinutes(5))
.staleWhileRevalidate(Duration.ofMinutes(1)))
.body(product))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/cart")
public ResponseEntity<List<String>> getCart(@RequestHeader("Authorization") String token) {
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore())
.body(List.of("p-1"));
}
}
各指示の意味:
max-age=30: ブラウザーは 30 秒間キャッシュを使用できます。public: 共有キャッシュ (Edge、CDN) がレスポンスを保存できます。s-maxage=300: 共有キャッシュのみが 5 分間キャッシュを保持します。Edge はブラウザーより長く保持します。理由は、Edge でキャッシュを主動的に削除できますが、ユーザーのマシン上のキャッシュは削除できないからです。stale-while-revalidate=60: 期限が切れると、Edge は最大 60 秒間古いバージョンを返し続け、バックグラウンドで更新します。ユーザーは Origin を待つ必要がありません。- ショッピングカートは個人データなので、
no-storeを使用して、どのレイヤーでも保存されないようにします。
curl で確認します:
curl -i http://localhost:8080/api/products/p-1
HTTP/1.1 200
Cache-Control: max-age=30, public, s-maxage=300, stale-while-revalidate=60
Content-Type: application/json
{"id":"p-1","name":"Nón lá Huế","price":150000}
5. Spring Cloud Function による Serverless でのビジネスロジック
中央データを必要とする計算部分は Serverless 関数で実行する必要があります。Spring Cloud Function は、純粋な Function としてロジックを記述し、同じコードをローカル HTTP または Serverless プラットフォームの adapter を通じて実行できるようにします。
依存関係を追加します (使用している Spring Boot と互換性のあるバージョンで spring-cloud-dependencies BOM を宣言します):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
</dependencies>
国に応じた注文見積もり関数を記述します:
public record OrderRequest(String country, BigDecimal subtotal) {}
public record OrderQuote(BigDecimal subtotal, BigDecimal tax, BigDecimal total) {}
@Configuration
public class PricingFunctions {
private static final Map<String, BigDecimal> TAX_RATES = Map.of(
"VN", new BigDecimal("0.10"),
"SG", new BigDecimal("0.09")
);
@Bean
public Function<OrderRequest, OrderQuote> quoteOrder() {
return request -> {
BigDecimal rate = TAX_RATES.getOrDefault(request.country(), BigDecimal.ZERO);
BigDecimal tax = request.subtotal().multiply(rate).setScale(0, RoundingMode.HALF_UP);
return new OrderQuote(request.subtotal(), tax, request.subtotal().add(tax));
};
}
}
ローカルで実行する場合、spring-cloud-function-web は quoteOrder bean を POST /quoteOrder endpoint で自動的に公開します:
curl -s -X POST http://localhost:8080/quoteOrder \
-H "Content-Type: application/json" \
-d '{"country":"VN","subtotal":150000}'
{"subtotal":150000,"tax":15000,"total":165000}
ロジックは純粋な Function に含まれており、HTTP やプラットフォームに依存しません。これにより、adapter を変更するだけで Serverless プロバイダー間を切り替えることができ、ビジネスロジックを修正する必要がありません。
注意: Serverless 上の Java では、Cold Start は実際の問題です。関数を簡潔に保ち、不要な bean を制限し、プラットフォームがサポートしている場合は Native Image を検討してください。
6. Edge 関数: ルーティング、事前ブロック、フォワード
Edge レイヤーは通常 JavaScript で実行されます。多くの Edge Runtime は標準的な Web Fetch API パターンに従っています: Request を受け取り、Response を返します。以下の例はこの考え方を示しています。場所ヘッダーの命名方法とデプロイ方法は、使用しているプラットフォームに依存します。
const ORIGIN = "https://origin.nonla.example";
export default {
async fetch(request) {
const url = new URL(request.url);
const country = request.headers.get("x-edge-country") ?? "VN";
// 1. 言語に基づいてホームページをリダイレクト、origin の呼び出しは不要
if (url.pathname === "/") {
const lang = country === "VN" ? "vi" : "en";
return Response.redirect(`${url.origin}/${lang}/`, 302);
}
// 2. トークンなしのショッピングカートリクエストを事前にブロック
if (url.pathname.startsWith("/api/cart") && !request.headers.has("Authorization")) {
return new Response("Unauthorized", { status: 401 });
}
// 3. 国情報を含めて origin にフォワード
const headers = new Headers(request.headers);
headers.set("X-Edge-Country", country);
return fetch(new Request(ORIGIN + url.pathname + url.search, {
method: request.method,
headers,
body: request.body,
}));
},
};
この Edge 関数は 3 つのことを行います:
- 国に基づいてリダイレクト Edge で直接。ホームページへのリクエストは Origin に到達することはありません。
- トークンがないリクエストを事前にブロック します。Origin は確実に拒否されるリクエストのために Serverless 関数の実行回数を節約できます。完全なトークン認証は依然として Origin で行う必要があります。
- 残りのリクエストをフォワード します。Origin からのレスポンスはセクション 4 の
Cache-Controlヘッダーを含んでいるため、Edge のキャッシュレイヤーは、どのレスポンスをキャッシュして、どのくらいの期間キャッシュするかを知ります。
7. 主な動作のテスト
Origin と Edge の間の契約は、キャッシュヘッダーです。したがって、このヘッダーが意図せずに変更されないようにテストを記述します:
@WebMvcTest(ProductController.class)
@Import(ProductCatalog.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void productResponseIsCacheableAtEdge() throws Exception {
MvcResult result = mockMvc.perform(get("/api/products/p-1")).andReturn();
assertThat(result.getResponse().getStatus()).isEqualTo(200);
assertThat(result.getResponse().getHeader("Cache-Control"))
.contains("public", "s-maxage=300", "stale-while-revalidate=60");
}
@Test
void cartResponseIsNeverStored() throws Exception {
MvcResult result = mockMvc.perform(get("/api/cart")
.header("Authorization", "Bearer demo"))
.andReturn();
assertThat(result.getResponse().getHeader("Cache-Control")).isEqualTo("no-store");
}
}
Serverless 関数は単なる Function なので、通常の Java コードと同じようにテストできます:
class PricingFunctionsTest {
private final Function<OrderRequest, OrderQuote> quoteOrder =
new PricingFunctions().quoteOrder();
@Test
void addsVietnameseTax() {
OrderQuote quote = quoteOrder.apply(new OrderRequest("VN", new BigDecimal("150000")));
assertThat(quote.tax()).isEqualByComparingTo("15000");
assertThat(quote.total()).isEqualByComparingTo("165000");
}
@Test
void unknownCountryHasNoTax() {
OrderQuote quote = quoteOrder.apply(new OrderRequest("XX", new BigDecimal("100")));
assertThat(quote.total()).isEqualByComparingTo("100");
}
}
8. よくあるエラーと使用すべきでない場合
8.1. よくあるエラー
- 個人データを誤ってキャッシュする. ユーザーに依存するレスポンスで
no-storeまたはprivateを忘れた場合、Edge はあるユーザーのショッピングカートを別のユーザーに返す可能性があります。これは最も深刻なエラーです。 - キャッシュキーがパーソナライズ方法と一致しない. Origin が
X-Edge-Countryに基づいて異なるコンテンツを返すが、Edge のキャッシュキーがそのヘッダーを考慮しない場合、すべてのユーザーは同じバージョンを受け取ります。適切なVaryヘッダーを追加するか、Edge でキャッシュキーを構成します。 - Edge からデータベースを直接呼び出す. 複数の Edge ポイントから 1 つのリージョンのデータベースへのリクエストがあります。レイテンシは減少せず、増加し、接続が枯渇するリスクがあります。
- ビジネスロジックの重複. 価格計算ルールを Edge で JavaScript で 1 回、Origin で Java で 1 回記述してから、徐々に乖離します。Edge はインフラストラクチャ作業のみを行うべきです: ルーティング、キャッシュ、フィルタリング。
- Edge のチェックを完全に信頼する. Edge のトークンチェックは早期フィルタリング用です。Origin は依然として完全な認証と認可を実行する必要があります。
8.2. 使用すべきでない場合
注意: ハイブリッドモデルは、デバッグ、デプロイ、監視する必要があるレイヤー数を増やします。測定可能な利益が明確な場合にのみ使用します。
- ユーザーが 1 つの地域に集中している、Origin リージョンの近く。Edge compute はこの場合、ほとんど利益をもたらしません。通常、静的 CDN キャッシュで十分です。
- アプリケーションが主に書き込み用途、または強い一貫性が必要な場合、例えば支払いシステムまたは在庫管理。ほとんどのリクエストは依然として Origin に移動する必要があります。
- 長時間実行または CPU 集約的なワークロード、ビデオ処理や大規模レポートなど。これらは Edge の制限に適合しておらず、通常は Serverless 関数よりも継続的に実行されるサービスに適しています。
- トラフィックが安定し、継続的に高い. 継続的に実行されるサービスは Serverless よりも単純で安価な場合があります。
- 小さなチーム、observability が不十分. Error は Edge、Serverless、Database に広がっており、エンドツーエンドのトレーシングがない場合、追跡が非常に困難になります。
9. 結論
Edge と Serverless のハイブリッドモデルは、従来型のアーキテクチャに取って代わるものではありません。それは距離に基づいた作業分割の方法です: 軽い、読み取りが多く、中央データを必要としない作業は Edge で実行され、一貫性が必要なビジネスロジックはデータベースの近くの Serverless Origin で実行されます。
この記事では、Origin の Spring Boot が s-maxage と stale-while-revalidate を含む Cache-Control で Edge のキャッシュを制御する方法を見ました。Spring Cloud Function を使って Function としてビジネスロジックを記述して、テストとプラットフォーム移行を容易にし、その後 Edge 関数を使用してリダイレクト、事前ブロック、リクエスト転送を実行しました。
最もシンプルなステップから始めてください: 各エンドポイントに対して正しくキャッシュヘッダーを設定し、有効性を測定します。システムのデータが価値があることを示す場合にのみ、ロジックを Edge に移動します。