优化架构K8s 副本和队列
This commit is contained in:
@@ -147,7 +147,7 @@ jobs:
|
|||||||
sed -i "s|airshelf.airlabs.art|${{ env.DOMAIN_WEB }}|g" k8s/ingress.yaml
|
sed -i "s|airshelf.airlabs.art|${{ env.DOMAIN_WEB }}|g" k8s/ingress.yaml
|
||||||
|
|
||||||
# ===== Core (real app) image + domain substitution =====
|
# ===== Core (real app) image + domain substitution =====
|
||||||
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-api:latest|${CR_IMAGE}/airshelf-core-api:${{ env.IMAGE_TAG }}|g" k8s/core/api-deployment.yaml k8s/core/worker-deployment.yaml
|
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-api:latest|${CR_IMAGE}/airshelf-core-api:${{ env.IMAGE_TAG }}|g" k8s/core/api-deployment.yaml k8s/core/worker-deployment.yaml k8s/core/worker-quick-deployment.yaml
|
||||||
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-web:latest|${CR_IMAGE}/airshelf-core-web:${{ env.IMAGE_TAG }}|g" k8s/core/web-deployment.yaml
|
sed -i "s|\${CI_REGISTRY_IMAGE}/airshelf-core-web:latest|${CR_IMAGE}/airshelf-core-web:${{ env.IMAGE_TAG }}|g" k8s/core/web-deployment.yaml
|
||||||
sed -i "s|airshelf-web.airlabs.art|${{ env.DOMAIN_CORE }}|g" k8s/core/ingress.yaml
|
sed -i "s|airshelf-web.airlabs.art|${{ env.DOMAIN_CORE }}|g" k8s/core/ingress.yaml
|
||||||
|
|
||||||
@@ -196,6 +196,7 @@ jobs:
|
|||||||
# Core real app (api + celery worker + web + ingress)
|
# Core real app (api + celery worker + web + ingress)
|
||||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/api-deployment.yaml
|
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/api-deployment.yaml
|
||||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-deployment.yaml
|
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-deployment.yaml
|
||||||
|
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/worker-quick-deployment.yaml
|
||||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/web-deployment.yaml
|
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/web-deployment.yaml
|
||||||
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/ingress.yaml
|
kubectl $KUBECTL_TIMEOUT apply -f k8s/core/ingress.yaml
|
||||||
|
|
||||||
@@ -205,6 +206,7 @@ jobs:
|
|||||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-web
|
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-web
|
||||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-api
|
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-api
|
||||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker
|
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker
|
||||||
|
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-worker-quick
|
||||||
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-web
|
kubectl $KUBECTL_TIMEOUT rollout restart deployment/airshelf-core-web
|
||||||
} 2>&1 | tee /tmp/deploy.log && { ok=1; break; }
|
} 2>&1 | tee /tmp/deploy.log && { ok=1; break; }
|
||||||
echo "Attempt $attempt failed, retrying in 30s..."
|
echo "Attempt $attempt failed, retrying in 30s..."
|
||||||
|
|||||||
@@ -204,6 +204,14 @@ CELERY_TASK_ACKS_LATE = True
|
|||||||
CELERY_TASK_REJECT_ON_WORKER_LOST = True
|
CELERY_TASK_REJECT_ON_WORKER_LOST = True
|
||||||
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
||||||
CELERY_TIMEZONE = TIME_ZONE
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
|
# 快活走 airshelf.quick(独立 worker-quick Pod);出图/出片仍默认 celery 队列。
|
||||||
|
CELERY_TASK_ROUTES = {
|
||||||
|
"apps.ai.tasks.extract_entities_task": {"queue": "airshelf.quick"},
|
||||||
|
"apps.ai.tasks.poll_free_video_task": {"queue": "airshelf.quick"},
|
||||||
|
"apps.ai.tasks.drain_asset_reviews_task": {"queue": "airshelf.quick"},
|
||||||
|
"apps.ops.tasks.ensure_team_notifications_task": {"queue": "airshelf.quick"},
|
||||||
|
"apps.projects.tasks.run_quick_script_task": {"queue": "airshelf.quick"},
|
||||||
|
}
|
||||||
|
|
||||||
REDIS_LOCK_URL = env("REDIS_LOCK_URL", "redis://127.0.0.1:6379/3")
|
REDIS_LOCK_URL = env("REDIS_LOCK_URL", "redis://127.0.0.1:6379/3")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from airshelf.celery import app
|
from airshelf.celery import app
|
||||||
|
|
||||||
|
QUICK_QUEUE = "airshelf.quick"
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True, max_retries=3)
|
@app.task(bind=True, max_retries=3)
|
||||||
def submit_ai_task(self, task_id: str) -> str:
|
def submit_ai_task(self, task_id: str) -> str:
|
||||||
@@ -21,7 +23,7 @@ def generate_standalone_image_task(self, task_id: str) -> str:
|
|||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True, max_retries=0)
|
@app.task(bind=True, max_retries=0, queue=QUICK_QUEUE)
|
||||||
def extract_entities_task(self, task_id: str) -> str:
|
def extract_entities_task(self, task_id: str) -> str:
|
||||||
"""实体提取的慢活(豆包思考模型流式,可达数十秒)在 worker 内跑,Web 层秒回不被占住、不再 502。
|
"""实体提取的慢活(豆包思考模型流式,可达数十秒)在 worker 内跑,Web 层秒回不被占住、不再 502。
|
||||||
幂等且失败自退费(见 run_extract_entities_task),故 max_retries=0,不向上抛重试。"""
|
幂等且失败自退费(见 run_extract_entities_task),故 max_retries=0,不向上抛重试。"""
|
||||||
@@ -83,7 +85,7 @@ def generate_model_triview_task(self, task_id: str) -> str:
|
|||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True, max_retries=0)
|
@app.task(bind=True, max_retries=0, queue=QUICK_QUEUE)
|
||||||
def poll_free_video_task(self, task_id: str, attempt: int = 0) -> str:
|
def poll_free_video_task(self, task_id: str, attempt: int = 0) -> str:
|
||||||
"""自由创作视频·worker 兜底轮询:每 30s 一次自重排(不依赖 celery beat),
|
"""自由创作视频·worker 兜底轮询:每 30s 一次自重排(不依赖 celery beat),
|
||||||
最多 60 次(约 30 分钟);用尽后只停止 Worker 兜底,不把业务任务判失败。
|
最多 60 次(约 30 分钟);用尽后只停止 Worker 兜底,不把业务任务判失败。
|
||||||
@@ -119,7 +121,7 @@ def poll_free_video_task(self, task_id: str, attempt: int = 0) -> str:
|
|||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True, max_retries=0)
|
@app.task(bind=True, max_retries=0, queue=QUICK_QUEUE)
|
||||||
def drain_asset_reviews_task(self) -> str:
|
def drain_asset_reviews_task(self) -> str:
|
||||||
"""全平台人脸审核兜底:未送审自动送火山、审核中自动拉绿/红盾。
|
"""全平台人脸审核兜底:未送审自动送火山、审核中自动拉绿/红盾。
|
||||||
自重排(无 celery beat);eager 下不自转,避免单测死循环。"""
|
自重排(无 celery beat);eager 下不自转,避免单测死循环。"""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from airshelf.celery import app
|
|||||||
from apps.accounts.models import Team
|
from apps.accounts.models import Team
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task(queue="airshelf.quick")
|
||||||
def ensure_team_notifications_task(team_id: str, user_id: str | None = None) -> None:
|
def ensure_team_notifications_task(team_id: str, user_id: str | None = None) -> None:
|
||||||
"""后台生成/补齐团队站内通知。
|
"""后台生成/补齐团队站内通知。
|
||||||
|
|
||||||
|
|||||||
@@ -453,7 +453,13 @@ export function App() {
|
|||||||
}
|
}
|
||||||
const active = detail.video_segments.filter((segment) => ["running", "queued"].includes(segment.status));
|
const active = detail.video_segments.filter((segment) => ["running", "queued"].includes(segment.status));
|
||||||
if (active.length === 0) return;
|
if (active.length === 0) return;
|
||||||
await Promise.all(active.map((segment) => api.pollVideo(activeProjectId, segment.id).catch(() => undefined)));
|
const polls = await Promise.all(active.map((segment) => api.pollVideo(activeProjectId, segment.id).catch(() => undefined)));
|
||||||
|
// 仍在 queued/running 就别拉整棵项目树(26KB);只有某段终态才回读详情。
|
||||||
|
const settled = polls.some((row) => {
|
||||||
|
const status = row && typeof row === "object" ? (row as { status?: string }).status : "";
|
||||||
|
return Boolean(status) && status !== "running" && status !== "queued";
|
||||||
|
}) || polls.some((row) => row && typeof row === "object" && "id" in (row as object));
|
||||||
|
if (!settled) return;
|
||||||
const next = await api.project(activeProjectId).catch(() => null);
|
const next = await api.project(activeProjectId).catch(() => null);
|
||||||
// 晚到的旧项目轮询结果不要冲掉已切换的当前项目详情
|
// 晚到的旧项目轮询结果不要冲掉已切换的当前项目详情
|
||||||
if (next && next.id === activeProjectIdRef.current) {
|
if (next && next.id === activeProjectIdRef.current) {
|
||||||
|
|||||||
@@ -2668,8 +2668,17 @@ export function PipelinePage(props: {
|
|||||||
const activeVideoCount = segments.filter((s) => ["running", "queued"].includes(s.status)).length;
|
const activeVideoCount = segments.filter((s) => ["running", "queued"].includes(s.status)).length;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeVideoCount === 0) return;
|
if (activeVideoCount === 0) return;
|
||||||
const timer = window.setInterval(() => { void onPollVideosQuiet(); }, 5000);
|
const tick = () => {
|
||||||
return () => window.clearInterval(timer);
|
if (document.hidden) return;
|
||||||
|
void onPollVideosQuiet();
|
||||||
|
};
|
||||||
|
const timer = window.setInterval(tick, 5000);
|
||||||
|
const onVis = () => { if (!document.hidden) void onPollVideosQuiet(); };
|
||||||
|
document.addEventListener("visibilitychange", onVis);
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timer);
|
||||||
|
document.removeEventListener("visibilitychange", onVis);
|
||||||
|
};
|
||||||
}, [activeVideoCount, onPollVideosQuiet]);
|
}, [activeVideoCount, onPollVideosQuiet]);
|
||||||
|
|
||||||
// 进入视频 / 拼接阶段时回填已有成片(此前合成过就直接给出播放/下载入口)
|
// 进入视频 / 拼接阶段时回填已有成片(此前合成过就直接给出播放/下载入口)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
app: airshelf-core-api
|
app: airshelf-core-api
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 2
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: airshelf-core-api
|
app: airshelf-core-api
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ spec:
|
|||||||
# Celery worker connects to the external (Volcano managed) Redis broker
|
# Celery worker connects to the external (Volcano managed) Redis broker
|
||||||
# configured via the airshelf-core-env secret. Uses `args` (not `command`)
|
# configured via the airshelf-core-env secret. Uses `args` (not `command`)
|
||||||
# so the image ENTRYPOINT still runs but skips migrate/collectstatic ($1=celery).
|
# so the image ENTRYPOINT still runs but skips migrate/collectstatic ($1=celery).
|
||||||
args: ["celery", "-A", "airshelf.celery:app", "worker", "-l", "info", "-Q", "celery,airshelf.quick", "--concurrency", "4"]
|
# 本 Pod 只吃默认 celery 队列(出图/出片等慢活)。提取/通知/脚本思考在 worker-quick。
|
||||||
|
args: ["celery", "-A", "airshelf.celery:app", "worker", "-l", "info", "-Q", "celery", "--concurrency", "4"]
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: airshelf-core-env
|
name: airshelf-core-env
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: airshelf-core-worker-quick
|
||||||
|
labels:
|
||||||
|
app: airshelf-core-worker-quick
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: airshelf-core-worker-quick
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: airshelf-core-worker-quick
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: cr-pull-secret
|
||||||
|
containers:
|
||||||
|
- name: airshelf-core-worker-quick
|
||||||
|
image: ${CI_REGISTRY_IMAGE}/airshelf-core-api:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
# 只消费 airshelf.quick:提取角色、通知补齐、脚本思考、视频状态轻轮询。
|
||||||
|
# 与出片 worker 隔离,Seedance 占满时提取仍能跑。
|
||||||
|
args: ["celery", "-A", "airshelf.celery:app", "worker", "-l", "info", "-Q", "airshelf.quick", "--concurrency", "4"]
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: airshelf-core-env
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["sh", "-c", "celery -A airshelf.celery:app inspect ping"]
|
||||||
|
initialDelaySeconds: 40
|
||||||
|
periodSeconds: 60
|
||||||
|
timeoutSeconds: 15
|
||||||
|
failureThreshold: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
Reference in New Issue
Block a user