Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog

kiến trúc API Gateway

Tung Bui Thanh (TungDaDev)13 mins read

Api gateway.webp

Ở kiến trúc microservices, cho app mobile hay trình duyệt gọi thẳng vào cả chục service nội bộ là tự rước lấy một mớ liên lạc chằng chịt (communication explosion): client gánh độ trễ fan-out, cả cấu trúc mạng nội bộ phơi ra Internet, còn muốn đổi protocol giữa các team thì gần như không thể. API Gateway đứng ra làm cửa ngõ duy nhất: nó vẫn là reverse proxy, nhưng gánh thêm xác thực, rate limit, ngắt mạch, tracing cho cả hệ thống.

Khi hệ thống lớn dần từ vài service lên vài chục, vài trăm service, mọi request từ bên ngoài đều đi qua API Gateway. Chọn sai công nghệ hoặc thiết kế sai thì nó thành điểm hỏng duy nhất (Single Point of Failure, SPOF) và cái cổ chai độ trễ cho cả hệ thống.

Mình sẽ đi qua vai trò của API Gateway, đặt Spring Cloud Gateway, Kong và Envoy cạnh nhau, rồi dựng phần rate limiting phân tán và circuit breaker với Spring Cloud Gateway, kèm mấy cái bẫy cấu hình hay gặp.


# sự tiến hoá kiến trúc: direct access vs API gateway vs bff (backend-for-frontend)

# vì sao backend-for-frontend (bff) lại được chuộng?

App mobile chạy trên 4G/5G có nhu cầu khác hẳn web chạy trên cáp quang:

  • Mobile App: cần payload JSON thật gọn, gộp 5 API thành 1 lần gọi (aggregated endpoint) để đỡ tốn pin và băng thông.
  • Desktop Web: cần dữ liệu đầy đủ chi tiết, lại tận dụng được HTTP/2 đa luồng để tải song song.
  • Public Partner API: cần bảo mật chặt theo OAuth2 Client Credentials và rate limit nghiêm theo hợp đồng thương mại.

Cách hay gặp là dựng một tầng Edge Gateway dùng chung (lo TLS termination, chống DDoS, WAF), bên trong tách ra các BFF độc lập cho từng loại client. BFF nên do chính team làm client đó sở hữu, để đổi payload cho app mobile không phải xếp hàng chờ team khác. Cái giá là logic gộp dữ liệu bị lặp ở nhiều BFF, nên BFF chỉ nên làm việc gộp và định dạng, đừng để business logic trôi lên đây.


# ma trận đánh đổi 3 chiều: Spring cloud gateway vs kong vs envoy

Không có công nghệ nào "tốt nhất" cho mọi bài toán. Đặt ba cái tên hay gặp cạnh nhau:

# bảng so sánh chi tiết kỹ thuật

Tiêu chíSpring Cloud Gateway (v4+)Kong Gateway (v3+)Envoy Proxy
Ngôn ngữ & RuntimeJava / Netty (Non-blocking I/O)C / OpenResty (NGINX + LuaJIT)C++ Native
Tốn bao nhiêu memoryCao hơn (JVM, thường vài trăm MB)Thấp (vài chục MB mỗi worker NGINX)Thấp (tăng theo số route, cluster, kết nối)
Latency thêm vàoCao nhất trong ba (GC, reactive stack)Thấp, tăng theo số plugin bậtThấp, tăng theo số filter
Cách nạp cấu hìnhStatic YAML hoặc RouteLocator Java BeanDynamic DB-less (YAML) hoặc PostgreSQL ClusterDynamic xDS API (gRPC stream, không cần restart)
Viết plugin mở rộngViết bằng Java, dev Spring nào cũng quenViết bằng Lua, Go, Python, JavaScript hoặc WasmViết bằng C++ Filter, Lua hoặc WebAssembly (Wasm)
Hợp với aiDoanh nghiệp chạy chủ yếu Java / Spring BootDoanh nghiệp đa ngôn ngữ (polyglot), cần API MarketplaceCloud-Native, Kubernetes Service Mesh, gRPC Streaming

Bảng chỉ nói xu hướng. Con số memory, latency thật phụ thuộc rất nhiều vào số route, plugin, TLS và tải, nên muốn so thì benchmark trên đúng cấu hình của mình. Spring Cloud Gateway bây giờ cũng có hai bản: bản WebFlux (Netty, reactive) như bài này dùng, và bản Server MVC chạy trên Servlet, đi cùng virtual threads của Java 21 thì code blocking cũng không còn đáng sợ như trước.


# bất đồng bộ: vì sao non-blocking eventloop thắng?

Ngày xưa Netflix Zuul 1.x gần như là Gateway mặc định trong hệ sinh thái Spring. Nhưng Zuul 1.x bị bỏ dần vì dùng mô hình Thread-Per-Request (Blocking I/O):

Spring Cloud Gateway dựng trên Project Reactor và Netty:

  • Không cấp riêng một thread hệ điều hành cho mỗi kết nối HTTP.
  • Chỉ vài EventLoop Threads (Reactor Netty mặc định bằng số CPU core, tối thiểu 4) là giữ được hàng chục ngàn kết nối cùng lúc, mà RAM tốn rất ít.

Cái giá phải trả: tuyệt đối không được chạy code blocking (JDBC, gọi HTTP đồng bộ, Thread.sleep) trong filter, vì chỉ cần kẹt một event loop là hàng nghìn kết nối đang dựa vào nó cùng đứng theo. Cần thì đẩy sang Schedulers.boundedElastic().


# kiểm soát lưu lượng: distributed token bucket với Redis lua

Khi có nhiều pod Gateway chạy song song sau Load Balancer, rate limit cục bộ trên RAM của từng pod coi như vô dụng. Lúc này bắt buộc phải dùng Distributed Rate Limiting.

# sao bắt buộc phải dùng Redis lua script?

Nếu tách ra làm 2 lệnh:

  1. redis.get(key) để xem còn bao nhiêu token.
  2. redis.decr(key) để trừ token.

thì giữa 2 lệnh này sẽ dính race condition: hai pod cùng đọc thấy còn 1 token, cùng cho qua. Redis chạy Lua script một cách atomic: trong lúc script chạy, không lệnh nào khác chen vào được, nên phần tính token hồi lại và trừ token xảy ra trọn một lượt.

Vài điều nên biết thêm về RequestRateLimiter của Spring Cloud Gateway. Script dùng 2 key (số token còn lại và thời điểm nạp lần cuối) có hash tag {id}, nên chạy được trên Redis Cluster vì 2 key luôn nằm cùng slot. Response trả về các header X-RateLimit-Remaining, X-RateLimit-Replenish-Rate, X-RateLimit-Burst-Capacity, X-RateLimit-Requested-Tokens; Retry-After trong sơ đồ trên thì phải tự thêm. Và khi Redis lỗi, RedisRateLimiter mặc định cho request đi qua (fail-open) để Redis sập không kéo cả hệ thống sập theo. Với API thanh toán thì bạn nên cân nhắc xem chấp nhận điều đó không.


# tự hồi phục: distributed circuit breaker & fallback

Gateway cho doanh nghiệp phải chặn được lỗi dây chuyền (cascading failure). Khi một microservice phía sau chậm hoặc sập hẳn, Gateway phải ngắt mạch ngay và trả về dữ liệu dự phòng (fallback):


# blueprint triển khai production với Spring cloud gateway

Đây là cấu hình cho Spring Boot 3.4 (Spring Cloud 2024.0, Gateway 4.2), có Token Relay (OAuth2), Redis Rate Limiting và Resilience4j Circuit Breaker. Cần các starter spring-cloud-starter-gateway, spring-boot-starter-data-redis-reactive, spring-cloud-starter-circuitbreaker-reactor-resilience4j và spring-boot-starter-oauth2-client.

Lưu ý từ Spring Cloud 2025.0 (Boot 3.5) trở đi, starter đổi tên thành spring-cloud-starter-gateway-server-webflux và prefix cấu hình chuyển từ spring.cloud.gateway.* sang spring.cloud.gateway.server.webflux.*. Route theo prefix cũ chỉ còn log cảnh báo, riêng default-filters ở bản 4.3.0 bị bỏ qua im lặng, nên khi nâng version nhớ đổi prefix (OpenRewrite có recipe làm việc này).

spring:
  cloud:
    gateway:
      default-filters:
        # Không chuyển cookie (session của Gateway) xuống service phía sau
        - RemoveRequestHeader=Cookie
        - AddRequestHeader=X-Gateway-Processed, true
      routes:
        # Route 1: Order Service với Rate Limiting & Circuit Breaker
        - id: order-service-route
          uri: lb://order-service # Cân bằng tải nội bộ qua Spring Cloud LoadBalancer
          predicates:
            - Path=/api/v1/orders/**
            - Method=GET,POST,PUT,DELETE
          filters:
            # Gắn Circuit Breaker: Khi lỗi chuyển về /fallback/orders
            - name: CircuitBreaker
              args:
                name: orderServiceCircuitBreaker
                fallbackUri: forward:/fallback/orders
            # Gắn Distributed Rate Limiter dùng Redis
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100 # Tốc độ hồi phục: 100 tokens/giây
                redis-rate-limiter.burstCapacity: 200 # Dung lượng tối đa: Chịu đỉnh 200 requests/giây
                redis-rate-limiter.requestedTokens: 1
                key-resolver: '#{@userKeyResolver}' # Phân loại rate limit theo JWT User ID hoặc IP
            # Chuyển tiếp JWT Token xuống Microservice phía dưới
            - TokenRelay=
 
resilience4j:
  circuitbreaker:
    instances:
      orderServiceCircuitBreaker:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        failureRateThreshold: 50
        waitDurationInOpenState: 10000ms
        permittedNumberOfCallsInHalfOpenState: 5
        slowCallDurationThreshold: 2s
        slowCallRateThreshold: 50
  timelimiter:
    instances:
      orderServiceCircuitBreaker:
        timeoutDuration: 5s # Mặc định chỉ 1s, request chậm hơn sẽ bị cắt và rơi vào fallback

Mấy chỗ dễ vấp trong đoạn YAML này:

  • Filter CircuitBreaker đi kèm TimeLimiter của Resilience4j với timeout mặc định 1 giây. Không cấu hình timelimiter thì API nào chạy quá 1 giây cũng bị cắt và trả fallback, dù service phía sau vẫn đang xử lý.
  • fallbackUri: forward:/fallback/orders cần một endpoint /fallback/orders có thật ngay trong Gateway (một @RestController nhỏ).
  • TokenRelay dành cho trường hợp Gateway là OAuth2 client (người dùng đăng nhập qua Gateway, Gateway giữ access token trong session) và gắn token đó vào header Authorization khi gọi xuống. Nếu client tự gửi Authorization: Bearer <JWT> (Gateway đóng vai resource server) thì header đó vốn đã được chuyển tiếp nguyên vẹn, không cần TokenRelay.
  • burstCapacity là sức chứa của "xô" token, tức số request tối đa được dồn trong một giây; replenishRate là tốc độ trung bình được phép lâu dài.

Key Resolver lấy theo danh tính người dùng:

@Configuration
public class RateLimiterConfiguration {
 
    // Chỉ tin đúng 1 proxy (Load Balancer) đứng ngay trước Gateway
    private final XForwardedRemoteAddressResolver ipResolver =
            XForwardedRemoteAddressResolver.maxTrustedIndex(1);
 
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> exchange.getPrincipal()
                // Ưu tiên 1: rate limit theo User ID trích xuất từ JWT Token
                .map(principal -> "user:" + principal.getName())
                // Ưu tiên 2 (khách vãng lai): IP thật, đọc X-Forwarded-For có kiểm soát
                .switchIfEmpty(Mono.fromSupplier(() -> {
                    InetSocketAddress address = ipResolver.resolve(exchange);
                    return "ip:" + (address != null ? address.getAddress().getHostAddress() : "unknown");
                }));
    }
}

Đừng đọc thẳng header X-Forwarded-For rồi dùng làm key. Header đó client tự gửi được, kẻ tấn công chỉ cần mỗi request đổi một giá trị là mỗi lần lại có "xô" token mới, rate limit coi như không có. Nó còn có thể là một danh sách client, proxy1, proxy2. XForwardedRemoteAddressResolver.maxTrustedIndex(n) chỉ lấy giá trị do đúng n proxy tin cậy của mình gắn vào, tính từ phải sang. Tiền tố user: / ip: để user tên 10.0.0.1 không đụng key với IP 10.0.0.1. Còn nếu resolver trả về rỗng thì mặc định Gateway từ chối request với mã 403 (deny-empty-key).


# checklist trước khi vận hành (architect's checklist)

  • 1. High Availability (không có SPOF): cụm Gateway có ít nhất 3 pod, rải đều trên các Availability Zone khác nhau sau Cloud Load Balancer (AWS ALB / GCP GLB) chưa?
  • 2. Throttling & chống DDoS: đã rate limit ở cả 2 tầng chưa: tầng IP (chặn brute-force/DDoS) và tầng User ID / API Key (giữ quota nghiệp vụ)?
  • 3. Timeout bắt buộc: route nào đi qua Gateway cũng phải có connect-timeout và response-timeout (đặt chung ở httpclient hoặc riêng từng route qua metadata), con số tuỳ SLA của từng API, để không bị cạn kết nối. Nhớ để TimeLimiter của circuit breaker khớp với timeout này.
  • 4. Protocol Translation: đã dùng Gateway để chuyển từ HTTP/1.1 (client ngoài Internet) sang gRPC / HTTP/2 (giữa các microservice nội bộ) cho đỡ tốn băng thông chưa?
  • 5. Distributed Tracing Injection: Gateway có tự sinh X-Correlation-Id và gắn header W3C traceparent trước khi đẩy request vào mạng nội bộ không?

# lời kết

Một Gateway thiết kế tốt gánh hết mấy việc dùng chung (cross-cutting concerns) như security, rate limiting, tracing, nhờ vậy các team phía sau tập trung vào nghiệp vụ. Nhưng chính vì mọi thứ đi qua nó, Gateway cũng là chỗ mà một dòng cấu hình sai (timeout 1 giây mặc định, rate limit tin X-Forwarded-For) ảnh hưởng tới toàn bộ hệ thống.

Nếu đang dùng Spring Cloud Gateway, việc nên làm tiếp là đọc lại tài liệu chính thức đúng với version mình chạy, nhất là phần đổi prefix cấu hình từ 2025.0, rồi chạy thử rate limit và circuit breaker bằng một bài load test nhỏ trước khi lên production.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.