fix: align api keepalive with gateway pool

This commit is contained in:
hectorzhao
2026-08-25 17:35:18 +08:00
parent 76e7c8c401
commit 394949f9f6
8 changed files with 64 additions and 1 deletions
+20
View File
@@ -0,0 +1,20 @@
import { configureApiHttpServerTimeouts } from './http-server-timeouts';
describe('configureApiHttpServerTimeouts', () => {
it('keeps the API connection alive longer than the Gateway idle pool', () => {
const server = { keepAliveTimeout: 0, headersTimeout: 0 };
expect(configureApiHttpServerTimeouts(server, {})).toEqual({
keepAliveTimeoutMs: 120_000,
headersTimeoutMs: 125_000,
});
expect(server).toEqual({ keepAliveTimeout: 120_000, headersTimeout: 125_000 });
});
it('keeps headers timeout above a configured keep-alive timeout', () => {
const server = { keepAliveTimeout: 0, headersTimeout: 0 };
expect(configureApiHttpServerTimeouts(server, {
API_HTTP_KEEP_ALIVE_TIMEOUT_MS: '90000',
API_HTTP_HEADERS_TIMEOUT_MS: '1000',
})).toEqual({ keepAliveTimeoutMs: 90_000, headersTimeoutMs: 91_000 });
});
});
+21
View File
@@ -0,0 +1,21 @@
import type { Server } from 'node:http';
const DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 120_000;
const DEFAULT_HEADERS_TIMEOUT_MS = 125_000;
export function configureApiHttpServerTimeouts(
server: Pick<Server, 'keepAliveTimeout' | 'headersTimeout'>,
env: NodeJS.ProcessEnv,
) {
const keepAliveTimeoutMs = positiveInteger(env.API_HTTP_KEEP_ALIVE_TIMEOUT_MS, DEFAULT_KEEP_ALIVE_TIMEOUT_MS);
const configuredHeadersTimeoutMs = positiveInteger(env.API_HTTP_HEADERS_TIMEOUT_MS, DEFAULT_HEADERS_TIMEOUT_MS);
const headersTimeoutMs = Math.max(configuredHeadersTimeoutMs, keepAliveTimeoutMs + 1_000);
server.keepAliveTimeout = keepAliveTimeoutMs;
server.headersTimeout = headersTimeoutMs;
return { keepAliveTimeoutMs, headersTimeoutMs };
}
function positiveInteger(value: string | undefined, fallback: number) {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
+3 -1
View File
@@ -7,6 +7,7 @@ import { AppModule } from './app.module';
import { MetricsService } from './metrics/metrics.service';
import { OpenApiModule } from './open-api/open-api.module';
import { configureHttpBodyParsers } from './http-body-limits';
import { configureApiHttpServerTimeouts } from './http-server-timeouts';
Object.defineProperty(BigInt.prototype, 'toJSON', {
configurable: true,
@@ -43,7 +44,8 @@ async function bootstrap() {
const port = Number(process.env.API_PORT ?? 3000);
// 生产环境只允许 Nginx 访问管理 API;显式绑定回环,避免默认的全网卡监听绕过入口鉴权与限流。
const host = process.env.API_HOST?.trim() || '127.0.0.1';
await app.listen(port, host);
const apiServer = await app.listen(port, host);
configureApiHttpServerTimeouts(apiServer, process.env);
const metrics = app.get(MetricsService);
const metricsHost = process.env.API_METRICS_HOST?.trim() || '127.0.0.1';
@@ -2167,3 +2167,4 @@
- 正在处理的企业必须从下一次领取候选中排除,避免同企业并发事务重新争抢账户锁;租约过期恢复、优先级/FIFO、`FOR UPDATE SKIP LOCKED`和逐请求幂等语义保持不变。
- 原生SQL对Prisma的无时区时间列统一使用`NOW() AT TIME ZONE 'UTC'`,覆盖Submit Outbox领取、租约、发布、重试及关联消息更新时间,禁止用数据库会话时区污染时延审计。
- 验收必须保持正价,分别执行单企业100/150 TPS和至少两个独立企业合计200 TPS,按非补发首次供应商Submit、回执、上行、计费、主备补发、业务拦截及全队列排空对账;触发拒绝、连接错误、持续积压、数据库异常或账务不一致立即停止。多Gateway P2不在本阶段范围。
- Gateway到主API的回环HTTP连接池空闲时长不得超过API服务端keep-alive生命周期;API默认keep-alive 120秒、headers timeout 125秒,并由发布环境显式设置和校验,防止负载期复用已被Node关闭的连接而把本可受理的Submit误回Result 9。
+1
View File
@@ -4885,3 +4885,4 @@ npm run verify:phase8
| TC-CMPP-PHASE5-014 | 不同企业正价并行 | 至少两个独立企业合计200 TPS;各企业账户冻结串行且企业间并行,单价均为325,分别对账消息、账单、首提和最终状态 |
| TC-CMPP-PHASE5-015 | Outbox UTC时间语义 | Asia/Shanghai数据库会话下领取、租约、发布、重试均写UTC无时区值;publishedAt-createdAt不再出现约8小时偏差 |
| TC-CMPP-PHASE5-016 | 微批发布边界与停止线 | 仅单Gateway;分别验证单企业100/150与多企业200,发生拒绝、连接错误、持续积压、数据库异常或账务不一致立即停止 |
| TC-CMPP-PHASE5-017 | Gateway到API长连接生命周期 | API keep-alive 120秒大于Gateway连接池90秒,headers timeout更大;跨越Node原默认5秒空闲边界后继续压测,不得出现loopback connection reset或Result 9 |
+4
View File
@@ -7,6 +7,8 @@ BRANCH="${BRANCH:-main}"
PUBLIC_HTTP_PORT="${PUBLIC_HTTP_PORT:-12026}"
API_PORT="${API_PORT:-3000}"
API_HOST="${API_HOST:-127.0.0.1}"
API_HTTP_KEEP_ALIVE_TIMEOUT_MS="${API_HTTP_KEEP_ALIVE_TIMEOUT_MS:-120000}"
API_HTTP_HEADERS_TIMEOUT_MS="${API_HTTP_HEADERS_TIMEOUT_MS:-125000}"
API_METRICS_HOST="${API_METRICS_HOST:-127.0.0.1}"
API_METRICS_PORT="${API_METRICS_PORT:-9464}"
API_ENABLE_SEND_WORKER="${API_ENABLE_SEND_WORKER:-true}"
@@ -200,6 +202,8 @@ write_env() {
NODE_ENV=production
API_PORT=${API_PORT}
API_HOST=${API_HOST}
API_HTTP_KEEP_ALIVE_TIMEOUT_MS=${API_HTTP_KEEP_ALIVE_TIMEOUT_MS}
API_HTTP_HEADERS_TIMEOUT_MS=${API_HTTP_HEADERS_TIMEOUT_MS}
API_METRICS_HOST=${API_METRICS_HOST}
API_METRICS_PORT=${API_METRICS_PORT}
API_ENABLE_SEND_WORKER=${API_ENABLE_SEND_WORKER}
+5
View File
@@ -19,6 +19,11 @@ set -a
source "$ENV_FILE"
set +a
if [[ ! "${API_HTTP_KEEP_ALIVE_TIMEOUT_MS:-}" =~ ^[1-9][0-9]*$ || ! "${API_HTTP_HEADERS_TIMEOUT_MS:-}" =~ ^[1-9][0-9]*$ || "${API_HTTP_HEADERS_TIMEOUT_MS}" -le "${API_HTTP_KEEP_ALIVE_TIMEOUT_MS}" ]]; then
echo "API_HTTP_KEEP_ALIVE_TIMEOUT_MS must be positive and API_HTTP_HEADERS_TIMEOUT_MS must be greater in $ENV_FILE." >&2
exit 1
fi
if [[ "${API_ENABLE_SEND_WORKER:-}" != "true" ]]; then
echo "API_ENABLE_SEND_WORKER=true is required in $ENV_FILE; refusing to deploy with SMS sending disabled." >&2
exit 1
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
const deploy = readFileSync(resolve(import.meta.dirname, 'production-deploy.sh'), 'utf8');
const bootstrap = readFileSync(resolve(import.meta.dirname, 'production-bootstrap.sh'), 'utf8');
const apiMain = readFileSync(resolve(import.meta.dirname, '../../api/src/main.ts'), 'utf8');
const apiTimeouts = readFileSync(resolve(import.meta.dirname, '../../api/src/http-server-timeouts.ts'), 'utf8');
const workerMain = readFileSync(resolve(import.meta.dirname, '../../api/src/send-worker.ts'), 'utf8');
const required = [
'compression_config=/etc/nginx/conf.d/cmpp-compression.conf',
@@ -26,6 +27,14 @@ for (const marker of ['API_HOST="${API_HOST:-127.0.0.1}"', 'API_HOST=${API_HOST}
if (!apiMain.includes("process.env.API_HOST?.trim() || '127.0.0.1'") || !apiMain.includes('app.listen(port, host)')) {
throw new Error('NestJS API must bind to API_HOST and default to loopback');
}
for (const marker of ['API_HTTP_KEEP_ALIVE_TIMEOUT_MS', 'API_HTTP_HEADERS_TIMEOUT_MS']) {
if (!`${deploy}\n${bootstrap}\n${apiTimeouts}`.includes(marker)) {
throw new Error(`production deployment is missing the API keep-alive contract: ${marker}`);
}
}
if (!apiMain.includes('configureApiHttpServerTimeouts(apiServer, process.env)')) {
throw new Error('NestJS API must apply the explicit keep-alive contract to its HTTP server');
}
for (const marker of [
'CMPP_INBOUND_FAST_PATH_ENABLED=true',