Edge und Serverless hybrid: Code in der Nähe von Nutzern ausführen

1. Übersicht

Benutzer, die weit vom Server entfernt sind, erleben eine hohe Netzwerklatenz, egal wie schnell Ihr Code läuft. Das Hybrid-Modell aus Edge und Serverless löst dieses Problem: leichte Logik läuft an Punkten nahe bei Benutzern, schwere Teile laufen in Serverless-Funktionen in wenigen zentralen Regionen.

In diesem Artikel werden das Konzept, die Gründe für dieses Modell und die Aufteilung der Arbeit zwischen den beiden Schichten erklärt. Sie erhalten konkrete Beispiele mit Spring Boot 3.x, Spring Cloud Function und einer kleinen Edge Function. Am Ende folgen Fälle, in denen Sie dieses Modell nicht verwenden sollten.

2. Edge, Serverless und das Hybrid-Modell

2.1. Was ist Edge?

Edge ist eine verteilte Infrastruktur an vielen Orten, meist gebunden an CDN-Netzwerke. Früher wurde Edge hauptsächlich zum Caching statischer Dateien verwendet. Heute ermöglichen viele Plattformen die direkte Ausführung von Code im Edge, genannt Edge Functions.

Edge Runtimes haben üblicherweise starke Einschränkungen:

  • Die Ausführungszeit pro Request ist kurz.
  • Der Speicher ist klein, es gibt kein vollständiges Dateisystem.
  • Sie unterstützen normalerweise JavaScript/TypeScript oder WebAssembly, selten eine vollständige JVM.
  • Sie sind nicht geeignet, um lange Verbindungen zu traditionellen Datenbanken zu halten.

2.2. Was ist Serverless?

Serverless (Function-as-a-Service oder elastische Container) ermöglicht es, Code zu deployen, ohne Server zu verwalten. Die Plattform skaliert automatisch basierend auf der Anzahl der Requests, einschließlich Herunterskalierung auf 0. Dafür müssen Sie einen Cold Start akzeptieren, wenn neue Instanzen starten.

Im Vergleich zu Edge läuft Serverless normalerweise in weniger Regionen. Dafür hat es mehr Ressourcen, volle Java-Unterstützung und liegt näher bei Datenbanken.

2.3. Warum ein Hybrid-Modell?

Jede Schicht hat ihre eigenen Stärken:

┌──────────┐    nah    ┌──────────────┐   weiter  ┌────────────────────┐    ┌──────────┐
│ Benutzer │ ────────▶ │ Edge         │ ─────────▶ │ Serverless Origin  │ ─▶ │ Datenbank│
│          │ ◀──────── │ (Cache,      │ ◀───────── │ (Spring Boot /     │ ◀─ │          │
└──────────┘           │ Routing,     │            │  Spring Cloud Func)│    └──────────┘
                       │ Auth)        │            │                    │
                       └──────────────┘            └────────────────────┘
  • Edge verarbeitet schnelle Aufgaben, die nicht von zentralen Daten abhängen: Caching, Sprachweiterleitung, grundlegende Token-Überprüfung, Bot-Blocking, A/B-Testing.
  • Serverless Origin verarbeitet Geschäftslogik, die zentrale Daten braucht: Preisberechnung, Bestellerstellung, Zahlungsabwicklung.

Eine allgemeine Regel: Verschieben Sie lesehäufige und selten ändernde Teile zum Edge, behalten Sie schreibintensive und konsistenzabhängige Teile beim Origin.

3. Warum dieses Modell gerade Aufmerksamkeit erregt

Es gibt mehrere Hauptgründe:

  1. Wahrgenommene Latenz. Viele Requests werden direkt im Edge beantwortet, ohne den Origin zu erreichen. Benutzer weit weg vom zentralen Region profitieren am meisten.
  2. Weniger Last und Kosten beim Origin. Jeder Request, der im Edge gecacht wird, ist eine Serverless-Funktion, die nicht laufen muss.
  3. Personalisierung nach Standort. Edge weiß, woher Benutzer kommen, und kann Sprache, Währung oder Inhaltsversion passend auswählen.
  4. Datenspeicheranforderungen nach Region. Edge kann Requests zum Origin in der richtigen juristischen Region weiterleiten.
  5. Das Java-Ökosystem hat sich besser angepasst. Native Images mit GraalVM und schnelle Starttechniken reduzieren Cold Starts, weshalb Java mit Serverless besser funktioniert als früher.

4. Origin: Spring Boot „lehrt” Edge, wie Cache funktioniert

Das Hybrid-Modell beginnt beim Origin. Edge kann nur dann richtig cachen, wenn Origin einen klaren Cache-Control-Header zurückgibt. Betrachten Sie eine Produkt-Katalog-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));
    }
}

Der Controller gibt verschiedene Cache-Header für verschiedene Datentypen zurück:

@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"));
    }
}

Die Bedeutung jeder Direktive:

  • max-age=30: Der Browser darf die Cache-Version 30 Sekunden lang verwenden.
  • public: Gemeinsame Caches (Edge, CDN) dürfen die Response speichern.
  • s-maxage=300: Nur gemeinsame Caches speichern die Version 5 Minuten lang. Edge behält länger als der Browser, denn Sie können Edge-Cache löschen, nicht aber Browser-Cache auf Benutzern.
  • stale-while-revalidate=60: Wenn abgelaufen, serviert Edge immer noch die alte Version bis zu 60 Sekunden lang und aktualisiert sie im Hintergrund. Der Benutzer muss nicht auf den Origin warten.
  • Ein Warenkorb ist persönliche Daten, daher no-store, damit keine Ebene es speichert.

Prüfen Sie mit 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. Geschäftslogik in Serverless mit Spring Cloud Function

Rechnung, die zentrale Daten braucht, sollte in Serverless-Funktionen laufen. Spring Cloud Function ermöglicht es, Logik als reine Function zu schreiben, dann denselben Code lokal über HTTP oder über Adapter der Serverless-Plattform auszuführen.

Dependency hinzufügen (deklarieren Sie BOM spring-cloud-dependencies mit einer Version, die zu Ihrem Spring Boot passt):

<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>

Schreiben Sie eine Funktion, die eine Bestellung nach Land anbietet:

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));
        };
    }
}

Wenn Sie lokal laufen, exponiert spring-cloud-function-web automatisch das quoteOrder-Bean als Endpoint POST /quoteOrder:

curl -s -X POST http://localhost:8080/quoteOrder \
  -H "Content-Type: application/json" \
  -d '{"country":"VN","subtotal":150000}'
{"subtotal":150000,"tax":15000,"total":165000}

Logik liegt in einer reinen Function, unabhängig von HTTP oder Plattform. Damit können Sie zwischen Serverless-Anbietern wechseln, indem Sie nur den Adapter ändern, ohne die Geschäftslogik zu ändern.

Hinweis: Mit Java auf Serverless ist Cold Start ein echtes Problem. Halten Sie Funktionen schlank, minimieren Sie unnötige Beans und erwägen Sie native Images, falls die Plattform sie unterstützt.

6. Edge Function: Routing, frühes Blockieren und Weiterleitung

Die Edge-Ebene läuft normalerweise in JavaScript. Viele Edge-Runtimes folgen dem Standard-Fetch-API-Modell: erhalten Request, geben Response zurück. Das folgende Beispiel veranschaulicht die Idee. Die Art der Benennung von Standortheadern und die Bereitstellung hängen von Ihrer Plattform ab.

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. Weiterleitungsseite nach Sprache, kein Origin-Aufruf nötig
    if (url.pathname === "/") {
      const lang = country === "VN" ? "vi" : "en";
      return Response.redirect(`${url.origin}/${lang}/`, 302);
    }

    // 2. Blockiere früh Warenkorb-Anfragen ohne Token
    if (url.pathname.startsWith("/api/cart") && !request.headers.has("Authorization")) {
      return new Response("Unauthorized", { status: 401 });
    }

    // 3. Leite zum Origin weiter, mit Länderinformation
    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,
    }));
  },
};

Diese Edge-Funktion macht drei Dinge:

  1. Weiterleitung nach Land direkt im Edge. Anfragen der Homepage erreichen niemals den Origin.
  2. Frühes Blockieren von Anfragen ohne Token. Origin braucht nicht einmal eine Serverless-Funktion für eine garantiert abgelehnte Anfrage auszuführen. Die vollständige Token-Authentifizierung muss immer noch im Origin erfolgen.
  3. Weiterleitung der übrigen Anfragen. Die Response vom Origin trägt Cache-Control-Header aus Abschnitt 4, daher weiß die Cache-Ebene des Edge, welche Response gecacht werden und wie lange.

7. Testen des Hauptverhaltens

Der Vertrag zwischen Origin und Edge ist der Cache-Header. Daher sollten Sie Tests schreiben, um sicherzustellen, dass er nicht unbeabsichtigt geändert wird:

@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");
    }
}

Eine Serverless-Funktion ist nur eine Function, daher testen Sie sie wie normalen Java-Code:

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. Häufige Fehler und wann man das Modell nicht verwenden sollte

8.1. Häufige Fehler

  • Versehentliches Cachen persönlicher Daten. Wenn Sie no-store oder private für Responses, die vom Benutzer abhängen, vergessen, kann Edge den Warenkorb eines Benutzers für einen anderen servieren. Das ist der schlimmste Fehler.
  • Cache-Key stimmt nicht mit Personalisierung überein. Wenn der Origin verschiedenen Inhalt je nach X-Edge-Country gibt, aber der Cache-Key im Edge diesen Header nicht berücksichtigt, erhalten alle Benutzer dieselbe Version. Fügen Sie den passenden Vary-Header hinzu oder konfigurieren Sie den Cache-Key im Edge.
  • Direkte Datenbankaufrufe von Edge. Viele Requests gehen von mehreren Edge-Punkten zu einer Datenbank in einer Region. Die Latenz sinkt nicht, sondern steigt sogar, mit Risiko der Verbindungserschöpfung.
  • Doppelte Geschäftslogik. Preisberechnung wird einmal in JavaScript im Edge geschrieben, einmal in Java im Origin, und sie divergieren allmählich. Edge sollte nur Infrastruktur tun: Routing, Caching, Filterung.
  • Absolutes Vertrauen in Edge-Überprüfung. Token-Überprüfung im Edge ist nur zum frühen Filtern. Der Origin muss immer noch vollständig authentifizieren und autorisieren.

8.2. Wann man das Modell NICHT verwenden sollte

Hinweis: Das Hybrid-Modell erhöht die Anzahl der Schichten zum Debuggen, Deployen und Überwachen. Verwenden Sie es nur, wenn der Nutzen klar messbar ist.

  • Benutzer konzentrieren sich in einer Region, nahe beim Origin. Edge-Compute bringt dann wenig Nutzen. Ein einfacher CDN-Cache ist normalerweise ausreichend.
  • Die Anwendung schreibt hauptsächlich Daten oder braucht starke Konsistenz, wie Zahlungssysteme oder Bestandsverwaltung. Die meisten Anfragen müssen trotzdem zum Origin.
  • Workload läuft lange oder ist CPU-intensiv, wie Video-Verarbeitung oder große Berichte. Das passt nicht zu Edge-Grenzen und passt besser zu kontinuierlichen Services als Serverless-Funktionen.
  • Traffic ist stabil und durchgehend hoch. Ein kontinuierlich laufender Service kann einfacher und billiger sein als Serverless.
  • Das Team ist klein und hat keine gute Observability. Fehler, die sich über Edge, Serverless und Datenbank verteilen, lassen sich sehr schwer nachverfolgen, wenn Sie kein durchgängiges Tracing haben.

9. Fazit

Das Hybrid-Modell aus Edge und Serverless ersetzt nicht die traditionelle Architektur. Es ist eine Methode, um Arbeit nach Entfernung zu teilen: leichte, leseviel und datenzentralunabhängige Arbeit läuft im Edge; Geschäftslogik, die Konsistenz braucht, läuft im Serverless Origin in der Nähe der Datenbank.

In diesem Artikel haben Sie gesehen, dass der Spring Boot Origin das Edge-Caching über Cache-Control mit s-maxage und stale-while-revalidate kontrolliert. Sie haben auch Geschäftslogik mit Spring Cloud Function als reine Function geschrieben, um Tests und Plattformwechsel zu erleichtern, und Edge Functions verwendet, um Weiterleitung, frühes Blockieren und Request-Weiterleitung zu tun.

Beginnen Sie mit dem einfachsten Schritt: Setzen Sie die Cache-Header für jeden Endpoint richtig und messen Sie die Auswirkungen. Verschieben Sie Logik zum Edge nur, wenn die Metriken Ihres eigenen Systems zeigen, dass es sinnvoll ist.

edge computingserverlessspring bootspring cloud functionarchitektur