Routing, rate limit, xác thực, cân bằng tảiRouting, rate limits, authentication, load balancing
Mọi request từ web, mobile và đối tác đi qua một gateway (Kong, Spring Cloud Gateway hoặc Envoy). Gateway kiểm JWT bằng JWKS đã cache, đếm hạn mức trong Redis rồi chuyển tiếp tới service tương ứng. Bật “Token không hợp lệ” để xem request bị chặn ở biên; bật “Upstream sập” để xem gateway fail fast trong khi route khác vẫn chạy.Every request from web, mobile and partners goes through one gateway (Kong, Spring Cloud Gateway or Envoy). The gateway checks the JWT against a cached JWKS, counts the quota in Redis, then forwards to the matching service. Turn on “Invalid token” to see a request stopped at the edge; turn on “Upstream down” to see the gateway fail fast while other routes keep working.
1App mobile gọi lấy đơn 9812, kèm access token Keycloak.The mobile app fetches order 9812 with a Keycloak access token.
Tính giá, kiểm tồn kho hay gộp dữ liệu từ năm service trong plugin gateway biến nó thành một monolith khó deploy, ai sửa cũng phải đụng. Gateway chỉ làm việc chung cho mọi route: TLS, xác thực, giới hạn, định tuyến, header. Việc gộp dữ liệu cho một màn hình thuộc về BFF.Pricing, stock checks or merging data from five services inside gateway plugins turn it into a monolith that everyone must touch to ship. The gateway only does cross-cutting work: TLS, authentication, limits, routing, headers. Composing data for one screen belongs in a BFF.
Nếu service tin mọi request có header X-User-Id, một pod bị chiếm hoặc một lỗi cấu hình mạng là đủ để mạo danh. Gateway kiểm JWT để chặn rác sớm; service vẫn kiểm chữ ký, aud và quyền trên dữ liệu (zero trust). Gateway luôn xoá header X-User-Id do client gửi lên trước khi tự gắn.If services trust any request carrying X-User-Id, one compromised pod or a network misconfiguration is enough to impersonate users. The gateway checks the JWT to drop junk early; services still check the signature, aud and data-level permissions (zero trust). The gateway always strips any client-sent X-User-Id before setting its own.
Đếm trong bộ nhớ từng instance thì 6 instance cho thực tế 600 rps thay vì 100. Dùng token bucket trong Redis bằng một script Lua nguyên tử (Spring Cloud Gateway RedisRateLimiter, Kong rate-limiting policy=redis). Redis lỗi thì cho qua (fail open) và cảnh báo, trừ endpoint đăng nhập nên fail closed.Counting in each instance's memory means 6 instances actually allow 600 rps instead of 100. Use a Redis token bucket updated by one atomic Lua script (Spring Cloud Gateway RedisRateLimiter, Kong rate-limiting with policy=redis). When Redis fails, let traffic through (fail open) and alert, except for login endpoints, which should fail closed.
Không có timeout, request treo giữ connection và event loop cho tới khi gateway cạn tài nguyên, mọi route đều chậm. Mỗi route cần connect timeout, response timeout, pool kết nối riêng, health check chủ động và thụ động, và circuit breaker để fail fast bằng 503.Without timeouts, hanging requests hold connections and event-loop capacity until the gateway runs dry and every route slows down. Each route needs a connect timeout, a response timeout, its own connection pool, active and passive health checks, and a circuit breaker that fails fast with a 503.
Retry POST /orders khi upstream timeout có thể tạo hai đơn, vì request đầu có thể đã chạy xong. Chỉ retry GET, PUT, DELETE hoặc POST có Idempotency-Key, tối đa 1–2 lần, chỉ với lỗi kết nối và 503, và giữ retry ở một tầng duy nhất.Retrying POST /orders after an upstream timeout can create two orders, because the first request may have completed. Retry only GET, PUT, DELETE, or POST with an Idempotency-Key, at most 1–2 times, only on connection errors and 503s, and keep retries in a single layer.
Gateway là điểm đầu tiên thấy request nên sinh X-Request-Id (nếu client chưa gửi) và traceparent theo W3C, trả lại X-Request-Id cho client. Log của gateway và service cùng mang request_id, nên lỗi người dùng báo lên tra được ngay.The gateway is the first hop to see the request, so it creates X-Request-Id (unless the client sent one) and a W3C traceparent, and echoes X-Request-Id back to the client. Gateway and service logs share the request_id, so a user-reported error can be traced at once.
Web, mobile và đối tác cần dữ liệu khác nhau. Một gateway chung lo phần hạ tầng; mỗi client có thể có một BFF mỏng phía sau để gộp và cắt dữ liệu cho màn hình của mình, do chính team frontend đó sở hữu.Web, mobile and partners need different data. One shared gateway handles infrastructure concerns; each client may get a thin BFF behind it that composes and trims data for its own screens, owned by that frontend team.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
route | id, host, path_prefix, strip_prefix, upstream, timeout_ms, retries, retry_methods, auth, rate_limit_policy | Kong khai báo trong kong.yml (DB-less), Spring Cloud Gateway trong application.yml; quản lý bằng Git, review như codeKong declares it in kong.yml (DB-less), Spring Cloud Gateway in application.yml; kept in Git and reviewed like code |
upstream_target | upstream, target, weight, health, consecutive_failures, last_check_at | Active check mỗi 5 s, passive đánh dấu unhealthy sau 5 lỗi liên tiếpActive check every 5s; passive marks unhealthy after 5 consecutive failures |
api_key | key_hash, consumer, plan, rate_limit_rps, burst, scopes, revoked_at | Chỉ lưu SHA-256 của key; plan quyết định hạn mức, ví dụ partner-basic 100 rpsOnly the SHA-256 of the key is stored; the plan sets the quota, e.g. partner-basic 100 rps |
rl:{key} (Redis) | tokens, timestamp · TTL = burst / rate × 2 | Token bucket, cập nhật bằng một script Lua để đọc và trừ nguyên tửToken bucket updated by one Lua script so read-and-decrement is atomic |
Hai route có timeout, breaker và chính sách retry riêng; JWT được kiểm trước khi định tuyến, request id gắn cho mọi request. Go dựng cùng chuỗi bằng middleware quanh httputil.ReverseProxy.Two routes with their own timeout, breaker and retry policy; the JWT is checked before routing and every request gets a request id. Go builds the same chain with middleware around httputil.ReverseProxy.
@Configuration
public class GatewayConfig {
@Bean
RouteLocator routes(RouteLocatorBuilder rb, RedisRateLimiter limiter, KeyResolver bySubject) {
return rb.routes()
.route("orders", r -> r.path("/api/orders/**")
.filters(f -> f.stripPrefix(1)
.requestRateLimiter(c -> c.setRateLimiter(limiter).setKeyResolver(bySubject))
.circuitBreaker(c -> c.setName("orders").setFallbackUri("forward:/unavailable"))
.retry(c -> c.setRetries(1).setMethods(HttpMethod.GET)))
.metadata(RESPONSE_TIMEOUT_ATTR, 2000)
.uri("lb://order-service"))
.route("catalog", r -> r.path("/api/catalog/**")
.filters(f -> f.stripPrefix(1)
.circuitBreaker(c -> c.setName("catalog").setFallbackUri("forward:/unavailable")))
.metadata(RESPONSE_TIMEOUT_ATTR, 1000)
.uri("lb://catalog-service"))
.build();
}
@Bean
SecurityWebFilterChain security(ServerHttpSecurity http) {
return http
.authorizeExchange(a -> a.pathMatchers("/actuator/health/**").permitAll()
.anyExchange().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())) // jwk-set-uri from Keycloak
.build();
}
@Bean
RedisRateLimiter limiter() {
return new RedisRateLimiter(100, 200, 1); // 100 rps, burst 200
}
@Bean
KeyResolver bySubject() {
return ex -> ex.getPrincipal().map(Principal::getName).defaultIfEmpty("anonymous");
}
@Bean
GlobalFilter requestId() {
return (ex, chain) -> {
String id = Optional.ofNullable(ex.getRequest().getHeaders().getFirst("X-Request-Id"))
.orElseGet(() -> UUID.randomUUID().toString());
ServerWebExchange next = ex.mutate()
.request(r -> r.headers(h -> { h.remove("X-User-Id"); h.set("X-Request-Id", id); }))
.build();
next.getResponse().getHeaders().set("X-Request-Id", id);
return chain.filter(next);
};
}
}func NewGateway(v *jwt.Validator, rl *RateLimiter) http.Handler {
mux := http.NewServeMux()
mux.Handle("/api/orders/", route("http://order-service:8080", 2*time.Second))
mux.Handle("/api/catalog/", route("http://catalog-service:8080", time.Second))
// outermost first: request id → auth → rate limit → route
return chain(mux, withRequestID, withAuth(v), withRateLimit(rl))
}
func chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func withAuth(v *jwt.Validator) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, err := v.Validate(r.Context(), bearer(r)) // sig, exp, iss, aud; cached JWKS
if err != nil {
w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
r.Header.Set("X-User-Id", claims.Subject) // overwrites any client value
next.ServeHTTP(w, r)
})
}
}
func route(target string, timeout time.Duration) http.Handler {
u, _ := url.Parse(target)
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = &http.Transport{ResponseHeaderTimeout: timeout, MaxConnsPerHost: 200}
cb := gobreaker.NewTwoStepCircuitBreaker(gobreaker.Settings{
Name: target,
Timeout: 30 * time.Second, // OPEN → HALF_OPEN
ReadyToTrip: func(c gobreaker.Counts) bool { return c.ConsecutiveFailures >= 5 },
})
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
done, err := cb.Allow()
if err != nil { // breaker open: fail fast
w.Header().Set("Retry-After", "30")
http.Error(w, "upstream unavailable", http.StatusServiceUnavailable)
return
}
rec := &statusRecorder{ResponseWriter: w, code: http.StatusOK}
proxy.ServeHTTP(rec, r)
done(rec.code < 500)
})
}| Một gateway chungOne shared gateway | BFF cho từng clientA BFF per client | |
|---|---|---|
| Làm gìDoes | TLS, xác thực, giới hạn, định tuyến cho mọi clientTLS, authentication, limits and routing for every client | Gộp và cắt dữ liệu theo màn hình của web hoặc mobileComposes and trims data for the web or mobile screens |
| Ai sở hữuOwned by | Team platformThe platform team | Team frontend tương ứngThe matching frontend team |
| Rủi roRisk | Phình to khi nhét logic riêng của từng clientBloats when client-specific logic is pushed into it | Trùng lặp code, thêm một hop mạngDuplicated code and one more network hop |
| Chọn khiPick it when | Các client dùng API gần giống nhauClients consume nearly the same API | Mobile cần payload gọn, web cần màn hình gộp nhiều nguồnMobile needs small payloads and web needs screens built from many sources |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
Có. Gateway chặn rác sớm và giảm tải, nhưng service vẫn kiểm chữ ký, aud và quyền trên dữ liệu (user chỉ xem đơn của mình). Nếu không, ai vào được mạng nội bộ cũng mạo danh được. Kiểm JWT bằng JWKS cache tốn vài micro giây nên chi phí không đáng kể.Yes. The gateway drops junk early and saves load, but services still check the signature, aud and data-level permissions (a user only reads their own orders). Otherwise anyone inside the network can impersonate users. Checking a JWT against a cached JWKS costs microseconds, so the overhead is negligible.
Có nếu chỉ chạy một bản. Chạy nhiều instance không trạng thái sau load balancer L4 (NLB) ở nhiều AZ, cấu hình khai báo (Kong DB-less) để không phụ thuộc DB, giữ gateway mỏng để ít lý do phải deploy, và có canary khi đổi cấu hình route.It is if it runs as one copy. Run several stateless instances behind an L4 load balancer (NLB) across AZs, use declarative config (Kong DB-less) so it does not depend on a database, keep it thin so it rarely needs a deploy, and canary route-config changes.
Timeout theo route, pool kết nối riêng cho mỗi upstream, health check chủ động và thụ động để loại instance hỏng, circuit breaker theo route để trả 503 ngay. Retry chỉ cho thao tác idempotent và tối đa một lần.Per-route timeouts, a separate connection pool per upstream, active and passive health checks to drop broken instances, and a per-route circuit breaker that answers 503 at once. Retry only idempotent operations, at most once.
Gateway giới hạn thô theo API key, user hoặc IP để bảo vệ toàn hệ thống và chặn lạm dụng trước khi tốn tài nguyên. Service giới hạn theo nghiệp vụ, ví dụ 5 lần gửi OTP mỗi giờ cho một số điện thoại, vì chỉ service hiểu ý nghĩa đó.The gateway applies coarse limits per API key, user or IP to protect the whole system and stop abuse before it costs anything. The service applies business limits, such as 5 OTP sends per phone number per hour, because only the service understands that meaning.
Phím 1–4 để chọn đáp án, Enter để kiểm tra.Press 1–4 to choose an answer, Enter to check.
Ôn tập ngắt quãng (spaced repetition): thẻ được lặp lại sau 1 → 3 → 7 → 14 → 30 ngày để củng cố trí nhớ dài hạn.Spaced repetition: cards are repeated after 1 → 3 → 7 → 14 → 30 days to build long-term memory.