Cloud Run 的強大之處在於「無極限自動擴展 (Autoscaling)」,但若了解的讀者肯定知道,帳單上費用名列前茅的總會出現Gemini API 的影子,若遭遇惡意流量狂刷,幾百台機器瞬間開出來,月底帳單會令人崩潰。為了保護 Gemini API 額度與錢包,我們從「基礎設施層」與「應用程式層」進行雙重防護。
在雲端架構中,防禦必須是多層次的:
在 Google Cloud CLI 執行更新指令,將 kakeru-ai-api 服務硬性限制最多開出 3 台機器:
gcloud run services update kakeru-ai-api \
--max-instances=3 \
--region=asia-east1
在 pom.xml 中加入 Bucket4j 套件,這是一個基於 Token Bucket 演算法的輕量級限流工具:
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.3.0</version>
</dependency>
重點在於:Token Bucket (5次/分鐘) 加上 X-Forwarded-For 真實 Client IP 解析。
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
// 1. Bucket 規則:容量 5 個,每分鐘補充 5 個 Token
private Bucket createNewBucket() {
Bandwidth limit = Bandwidth.classic(5, Refill.greedy(5, Duration.ofMinutes(1)));
return Bucket.builder().addLimit(limit).build();
}
// 2. 拆解 Cloud Run Reverse Proxy 轉發的真實 Client IP
private String extractClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isBlank()) {
return xForwardedFor.split(",")[0].trim(); // 取第一個原始 Client IP
}
return request.getRemoteAddr();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!request.getRequestURI().startsWith("/api/")) {
filterChain.doFilter(request, response);
return;
}
String ip = extractClientIp(request);
Bucket bucket = cache.computeIfAbsent(ip, k -> createNewBucket());
// 3. 嘗試消耗 1 個 Token,耗盡時回傳 HTTP 429
if (bucket.tryConsume(1)) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\": \"請求過於頻繁,請稍後再試。教練需要休息!\"}");
}
}
}
使用 Spring Boot MockMvc 驗證前 5 次 200 OK、第 6 次 429 攔截:
@Test
@DisplayName("測試同一 IP 前 5 次請求成功 (200),第 6 次請求觸發限流 (429)")
void rateLimit_ExceedFiveRequests_Returns429() throws Exception {
String clientIp = "192.168.1.100";
// 前 5 次:HTTP 200 OK
for (int i = 1; i <= 5; i++) {
mockMvc.perform(get("/hello").header("X-Forwarded-For", clientIp))
.andExpect(status().isOk());
}
// 第 6 次:HTTP 429 Too Many Requests
mockMvc.perform(get("/hello").header("X-Forwarded-For", clientIp))
.andExpect(status().isTooManyRequests())
.andExpect(jsonPath("$.error").value("請求過於頻繁,請稍後再試。教練需要休息!"));
}
打開終端機執行連續請求測試腳本:
for i in {1..10}; do
curl -i -X POST https://[Cloud-Run-URL]/generate \
-H "Content-Type: application/json" \
-d '{"targetRace":"全馬","currentLevel":"初學者"}'
done
HTTP/2 200 OK (成功取得 AI 課表)HTTP/2 429
content-type: application/json;charset=UTF-8
{"error": "請求過於頻繁,請稍後再試。教練需要休息!"}
request.getRemoteAddr() 可能取得的是代理層 IP,因此需要透過可信任的 X-Forwarded-For 取得 Client IP。不要無條件相信 Client 自己提供的 Header。
今天完成了 Day 13 的雙重流量防護後,KAKERU AI 教練 API 已經能在 Cloud Run 上穩定運作,並具備基礎的防禦機制,後端 API 的開發階段到此正式告一段落。
系統開發流程中,為了減少流程開發的步驟,肯定少不了自動化部署!!不過,為了優先驗證系統的整體可行性,打算先讓產品具備可操作的前端介面,藉此來測試前後端的真實串接狀況。因此,後端與前端的 CI/CD 自動化流水線(GitHub Actions 與 EAS Build),將會安排在系列文最後的 DevOps 階段一併實作。
明天(Day 14),專案將進入下個階段!!「前端開發與 AI 互動」。我們將暫停後端純文字互動的 Spring Boot 的開發,切換至 TypeScript 與 React Native,開始建立 KAKERU 跨平台手機 APP 的基礎環境與介面。明天見!