fix: 并发默认调到4,队列超时改为可配(30s)

- SUBSET_CONCURRENCY 默认 2 → 4(900M 内存够用)
- 新增 SUBSET_QUEUE_TIMEOUT 环境变量(默认30秒,原硬编码10秒)
- 压测发现10秒超时太短:50并发只有8成功,其余排队超时
This commit is contained in:
崮生(子虚) 2026-07-30 21:46:21 +08:00
parent 65ba984972
commit b86d2fcb53
6 changed files with 28 additions and 13 deletions

View File

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

View File

@ -125,8 +125,8 @@ services:
- SUBSET_CACHE_MAX_SIZE=10485760
# 临时字体保留时限(秒),超时未使用自动删除,默认 108003小时
- TEMP_RETENTION_SECONDS=10800
# 字体裁剪最大并发数,默认 2内存受限环境建议 1-3
- SUBSET_CONCURRENCY=2
# 字体裁剪最大并发数,默认 4内存受限环境建议 2-3
- SUBSET_CONCURRENCY=4
```
## API

View File

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

View File

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

View File

@ -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) {

View File

@ -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<T>(
maxConcurrency: number,
task: () => Promise<T>,
queueTimeoutMs: number,
): Promise<T | null> {
/** 并发未满,直接执行 */
if (activeCount < maxConcurrency) {
@ -47,7 +52,7 @@ export async function withConcurrencyLimit<T>(
waitingCount++;
try {
/** 等待获取许可,或超时 */
const acquired = await waitForSlot(QUEUE_TIMEOUT_MS);
const acquired = await waitForSlot(queueTimeoutMs);
if (!acquired) {
/** 排队超时,返回 null 让调用方返回 503 */
return null;