feat: 提升并发容量默认配置
This commit is contained in:
@@ -1067,6 +1067,12 @@ async function clearFeishuCache() {
|
|||||||
v-model="systemSettingValues[setting.key]"
|
v-model="systemSettingValues[setting.key]"
|
||||||
/>
|
/>
|
||||||
<small>{{ setting.description }}</small>
|
<small>{{ setting.description }}</small>
|
||||||
|
<small
|
||||||
|
v-if="setting.valueHint"
|
||||||
|
class="setting-value-hint"
|
||||||
|
>
|
||||||
|
{{ setting.valueHint(systemSettingValues[setting.key]) }}
|
||||||
|
</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface SystemSettingDefinition {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
options?: SystemSettingOption[];
|
options?: SystemSettingOption[];
|
||||||
description: string;
|
description: string;
|
||||||
|
valueHint?: (value: SystemSettingValue) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemSettingSection {
|
export interface SystemSettingSection {
|
||||||
@@ -181,28 +182,29 @@ export const systemSettingSections: SystemSettingSection[] = [
|
|||||||
key: "chat_max_active_requests",
|
key: "chat_max_active_requests",
|
||||||
label: "问答最大并发数",
|
label: "问答最大并发数",
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: 2,
|
defaultValue: 30,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 1000,
|
max: 1000,
|
||||||
description: "同一后端进程内同时进入飞书和模型生成链路的最大请求数。",
|
description: "用户正式问答的全局执行上限;Redis 可用时由所有后端实例共享,超过后进入排队。",
|
||||||
|
valueHint: concurrentCapacityHint,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "chat_max_queue_size",
|
key: "chat_max_queue_size",
|
||||||
label: "问答最大排队数",
|
label: "问答最大排队数",
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: 20,
|
defaultValue: 90,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 10000,
|
max: 10000,
|
||||||
description: "超过最大并发后允许等待的请求数量,超过后直接提示稍后再试。",
|
description: "超过最大并发后允许等待的请求数量。默认按并发数的 3 倍预留,避免课程结束后的集中提问被直接拒绝。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "chat_queue_timeout_seconds",
|
key: "chat_queue_timeout_seconds",
|
||||||
label: "问答排队超时(秒)",
|
label: "问答排队超时(秒)",
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: 60,
|
defaultValue: 90,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 3600,
|
max: 3600,
|
||||||
description: "请求在排队中超过该时间后自动结束并提示用户稍后再试。",
|
description: "请求在排队中超过该时间后自动结束。默认 90 秒可覆盖 30 并发下约三轮请求释放。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "chat_active_lease_seconds",
|
key: "chat_active_lease_seconds",
|
||||||
@@ -292,4 +294,16 @@ export const systemSettingSections: SystemSettingSection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function concurrentCapacityHint(value: SystemSettingValue) {
|
||||||
|
const concurrency = Math.max(1, Number(value) || 1);
|
||||||
|
const lower = roundDownToTen((concurrency * 120) / 25 / 1.4);
|
||||||
|
const upper = roundDownToTen((concurrency * 120) / 25 / 1.2);
|
||||||
|
const capacity = lower === upper ? `约 ${lower} 人` : `约 ${lower}–${upper} 人`;
|
||||||
|
return `容量估算:按单次问答约 25 秒并预留 20%–40% 波动,当前 ${concurrency} 并发可承接 2 分钟内${capacity}集中提问。实际能力还受模型服务商额度和回答时长影响。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundDownToTen(value: number) {
|
||||||
|
return Math.max(1, Math.floor(value / 10) * 10);
|
||||||
|
}
|
||||||
|
|
||||||
export const systemSettingDefinitions = systemSettingSections.flatMap((section) => section.settings);
|
export const systemSettingDefinitions = systemSettingSections.flatMap((section) => section.settings);
|
||||||
|
|||||||
@@ -1725,6 +1725,14 @@ textarea {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-field > .setting-value-hint {
|
||||||
|
padding: 9px 11px;
|
||||||
|
border: 1px solid #cde6dc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3faf7;
|
||||||
|
color: #246b52;
|
||||||
|
}
|
||||||
|
|
||||||
.setting-field .el-input-number,
|
.setting-field .el-input-number,
|
||||||
.setting-field .el-input,
|
.setting-field .el-input,
|
||||||
.setting-field .el-select {
|
.setting-field .el-select {
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""raise the default chat concurrency to thirty
|
||||||
|
|
||||||
|
Revision ID: 0028_chat_concurrency
|
||||||
|
Revises: 0027_content_generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0028_chat_concurrency"
|
||||||
|
down_revision = "0027_content_generation"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
config = sa.table(
|
||||||
|
"sys_system_config",
|
||||||
|
sa.column("config_key", sa.String(length=100)),
|
||||||
|
sa.column("config_value", sa.Text()),
|
||||||
|
sa.column("description", sa.String(length=255)),
|
||||||
|
)
|
||||||
|
connection = op.get_bind()
|
||||||
|
_set_default(
|
||||||
|
connection,
|
||||||
|
config,
|
||||||
|
key="chat_max_active_requests",
|
||||||
|
old_default="2",
|
||||||
|
new_default="30",
|
||||||
|
description="用户正式问答的全局执行上限;Redis 可用时由所有后端实例共享,超过后进入排队。",
|
||||||
|
)
|
||||||
|
_set_default(
|
||||||
|
connection,
|
||||||
|
config,
|
||||||
|
key="chat_max_queue_size",
|
||||||
|
old_default="20",
|
||||||
|
new_default="90",
|
||||||
|
description="超过最大并发后允许等待的请求数量。默认按并发数的 3 倍预留。",
|
||||||
|
)
|
||||||
|
_set_default(
|
||||||
|
connection,
|
||||||
|
config,
|
||||||
|
key="chat_queue_timeout_seconds",
|
||||||
|
old_default="60",
|
||||||
|
new_default="90",
|
||||||
|
description="请求在排队中超过该时间后自动结束。默认可覆盖约三轮请求释放。",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# 运行时配置可能已经被管理员调整,降级代码版本时不覆盖现场值。
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _set_default(
|
||||||
|
connection,
|
||||||
|
config,
|
||||||
|
*,
|
||||||
|
key: str,
|
||||||
|
old_default: str,
|
||||||
|
new_default: str,
|
||||||
|
description: str,
|
||||||
|
) -> None:
|
||||||
|
current = connection.execute(
|
||||||
|
sa.select(config.c.config_value).where(config.c.config_key == key)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if current is None:
|
||||||
|
connection.execute(
|
||||||
|
config.insert().values(
|
||||||
|
config_key=key,
|
||||||
|
config_value=new_default,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if str(current).strip() == old_default:
|
||||||
|
connection.execute(
|
||||||
|
config.update()
|
||||||
|
.where(config.c.config_key == key)
|
||||||
|
.values(config_value=new_default, description=description)
|
||||||
|
)
|
||||||
@@ -59,9 +59,9 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
default_daily_chat_limit: int = 100
|
default_daily_chat_limit: int = 100
|
||||||
default_user_name_prefix: str = "用户"
|
default_user_name_prefix: str = "用户"
|
||||||
chat_max_active_requests: int = 2
|
chat_max_active_requests: int = 30
|
||||||
chat_max_queue_size: int = 20
|
chat_max_queue_size: int = 90
|
||||||
chat_queue_timeout_seconds: int = 60
|
chat_queue_timeout_seconds: int = 90
|
||||||
chat_active_lease_seconds: int = 900
|
chat_active_lease_seconds: int = 900
|
||||||
periodic_report_worker_enabled: bool = True
|
periodic_report_worker_enabled: bool = True
|
||||||
periodic_report_poll_seconds: int = 5
|
periodic_report_poll_seconds: int = 5
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from app.services.security_state_service import SecurityStateService
|
|||||||
from app.services.auth_service import AuthService
|
from app.services.auth_service import AuthService
|
||||||
from app.services.model_service import ModelClientService
|
from app.services.model_service import ModelClientService
|
||||||
from app.core.observability import RequestObservabilityMiddleware
|
from app.core.observability import RequestObservabilityMiddleware
|
||||||
|
from app.core.config import Settings
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -43,6 +44,14 @@ def test_secret_is_encrypted_and_masked():
|
|||||||
assert SecretService.masked(encrypted) == MASKED_SECRET
|
assert SecretService.masked(encrypted) == MASKED_SECRET
|
||||||
|
|
||||||
|
|
||||||
|
def test_production_chat_capacity_defaults_cover_expected_burst():
|
||||||
|
settings = Settings(_env_file=None)
|
||||||
|
|
||||||
|
assert settings.chat_max_active_requests == 30
|
||||||
|
assert settings.chat_max_queue_size == 90
|
||||||
|
assert settings.chat_queue_timeout_seconds == 90
|
||||||
|
|
||||||
|
|
||||||
def test_migration_revision_ids_fit_default_alembic_version_column():
|
def test_migration_revision_ids_fit_default_alembic_version_column():
|
||||||
versions_dir = Path(__file__).parents[1] / "alembic" / "versions"
|
versions_dir = Path(__file__).parents[1] / "alembic" / "versions"
|
||||||
revision_pattern = re.compile(r'^revision\s*=\s*["\']([^"\']+)', re.MULTILINE)
|
revision_pattern = re.compile(r'^revision\s*=\s*["\']([^"\']+)', re.MULTILINE)
|
||||||
|
|||||||
Reference in New Issue
Block a user