diff --git a/README.en.md b/README.en.md index 1f3dfeb..c48a5ca 100644 --- a/README.en.md +++ b/README.en.md @@ -125,8 +125,8 @@ services: - SUBSET_CACHE_MAX_SIZE=10485760 # Temp font retention (seconds), auto-deleted if unused. Default: 10800 (3h) - TEMP_RETENTION_SECONDS=10800 - # Max concurrent font subsetting. Default: 2 - - SUBSET_CONCURRENCY=2 + # Max concurrent font subsetting. Default: 4 + - SUBSET_CONCURRENCY=4 ``` ## API Reference diff --git a/README.md b/README.md index 37dff8c..09fb06e 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,8 @@ services: - SUBSET_CACHE_MAX_SIZE=10485760 # 临时字体保留时限(秒),超时未使用自动删除,默认 10800(3小时) - TEMP_RETENTION_SECONDS=10800 - # 字体裁剪最大并发数,默认 2(内存受限环境建议 1-3) - - SUBSET_CONCURRENCY=2 + # 字体裁剪最大并发数,默认 4(内存受限环境建议 2-3) + - SUBSET_CONCURRENCY=4 ``` ## API diff --git a/backend/config.ts b/backend/config.ts index 4673f2a..68927b3 100644 --- a/backend/config.ts +++ b/backend/config.ts @@ -25,10 +25,18 @@ export const subsetCacheMaxSize = parseInt(env.SUBSET_CACHE_MAX_SIZE ?? `${10 * * 字体子集化最大并发数 * * 字体裁剪是 CPU/内存密集操作,并发过多会导致 LLRT OOM 崩溃。 - * 默认 2:在 900M 内存限制下安全运行。 - * 服务器内存充足时可适当调大(如 4-8)。 + * 默认 4:在 900M 内存限制下安全运行。 + * 内存充裕可调大,内存紧张可调小到 2。 */ -export const subsetConcurrency = Math.max(1, parseInt(env.SUBSET_CONCURRENCY ?? "2", 10) || 2); +export const subsetConcurrency = Math.max(1, parseInt(env.SUBSET_CONCURRENCY ?? "4", 10) || 4); + +/** + * 队列等待超时(秒)—— 排队超过此时间返回 503,避免请求无限堆积 + * + * 50 并发场景下并发 4,每批 ~300ms,排队最久约 (50/4)*300ms ≈ 3.75s, + * 30 秒足够覆盖正常高峰。极端过载时快速失败让客户端重试。 + */ +export const subsetQueueTimeoutSeconds = Math.max(5, parseInt(env.SUBSET_QUEUE_TIMEOUT ?? "30", 10) || 30); /** 字体搜索目录(按优先级排序:admin > 普通 > 临时) */ export const fontDirs = ["font/admin", "font", "font/temp"] as const; diff --git a/backend/routes/config.ts b/backend/routes/config.ts index be3ee05..e630de1 100644 --- a/backend/routes/config.ts +++ b/backend/routes/config.ts @@ -1,5 +1,5 @@ import { jsonResponse } from "../shared"; -import { enableTempUpload, adminApiKey, tempRetentionSeconds, subsetConcurrency } from "../config"; +import { enableTempUpload, adminApiKey, tempRetentionSeconds, subsetConcurrency, subsetQueueTimeoutSeconds } from "../config"; /** GET /api/config — 返回公开配置 */ export async function handleGetConfig(req: Request, _res: Response) { @@ -13,6 +13,8 @@ export async function handleGetConfig(req: Request, _res: Response) { tempRetentionSeconds, /** 字体子集化最大并发数 */ subsetConcurrency, + /** 队列等待超时(秒) */ + subsetQueueTimeoutSeconds, }), }; } diff --git a/backend/routes/subset.ts b/backend/routes/subset.ts index 28e22f8..879d16b 100644 --- a/backend/routes/subset.ts +++ b/backend/routes/subset.ts @@ -3,7 +3,7 @@ import type { FontEditor } from "../../vendor/fonteditor-core/lib/ttf/font.js"; import { parseUrl, stats, subsetCache, findFontPath, readFontBuffer, markStatsDirty } from "../shared"; import { markFontUsed } from "../temp_cleaner"; import { withConcurrencyLimit } from "../subset_queue"; -import { subsetConcurrency } from "../config"; +import { subsetConcurrency, subsetQueueTimeoutSeconds } from "../config"; /** * 进程启动时戳(模块加载时取一次,进程重启即变化) @@ -121,7 +121,7 @@ export async function handleFontSubset(req: Request, res: Response) { outType: outType, sourceType: fontType, }); - }); + }, subsetQueueTimeoutSeconds * 1000); /** 排队超时,返回 503 让客户端重试 */ if (subsetResult === null) { diff --git a/backend/subset_queue.ts b/backend/subset_queue.ts index d337fe6..5b60377 100644 --- a/backend/subset_queue.ts +++ b/backend/subset_queue.ts @@ -8,8 +8,11 @@ * 等待超时则返回 503,避免请求无限堆积。 */ -/** 等待队列中的请求超时时间(毫秒),默认 10 秒 */ -const QUEUE_TIMEOUT_MS = 10_000; +/** + * 等待队列中的请求超时时间(秒),由 config.ts 通过环境变量控制 + * + * 在 subset.ts 中通过参数传入,不在此处硬编码。 + */ /** 当前正在执行的子集化数量 */ let activeCount = 0; @@ -25,11 +28,13 @@ let waitingCount = 0; * * @param maxConcurrency 最大并发数 * @param task 实际的子集化异步任务 + * @param queueTimeoutMs 排队等待超时(毫秒),超时返回 null * @returns 任务结果,或 null 表示排队超时 */ export async function withConcurrencyLimit( maxConcurrency: number, task: () => Promise, + queueTimeoutMs: number, ): Promise { /** 并发未满,直接执行 */ if (activeCount < maxConcurrency) { @@ -47,7 +52,7 @@ export async function withConcurrencyLimit( waitingCount++; try { /** 等待获取许可,或超时 */ - const acquired = await waitForSlot(QUEUE_TIMEOUT_MS); + const acquired = await waitForSlot(queueTimeoutMs); if (!acquired) { /** 排队超时,返回 null 让调用方返回 503 */ return null;