feat: 初始化零工后端代码
This commit is contained in:
@@ -19,6 +19,13 @@ services:
|
||||
volumes:
|
||||
- qdrant_prod_data:/qdrant/storage
|
||||
|
||||
redis:
|
||||
image: docker.m.daocloud.io/library/redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis_prod_data:/data
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
@@ -33,9 +40,28 @@ services:
|
||||
LLM_BASE_URL: ${LLM_BASE_URL:-}
|
||||
LLM_API_KEY: ${LLM_API_KEY:-}
|
||||
LLM_MODEL: ${LLM_MODEL:-gpt-5.4}
|
||||
CACHE_BACKEND: ${CACHE_BACKEND:-redis}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||
INGEST_ASYNC_ENABLED: ${INGEST_ASYNC_ENABLED:-true}
|
||||
MATCH_ASYNC_ENABLED: ${MATCH_ASYNC_ENABLED:-true}
|
||||
MATCH_CACHE_ENABLED: ${MATCH_CACHE_ENABLED:-true}
|
||||
MATCH_CACHE_TTL_SECONDS: ${MATCH_CACHE_TTL_SECONDS:-30}
|
||||
QUERY_CACHE_ENABLED: ${QUERY_CACHE_ENABLED:-true}
|
||||
QUERY_CACHE_TTL_SECONDS: ${QUERY_CACHE_TTL_SECONDS:-20}
|
||||
APP_RATE_LIMIT_PER_MINUTE: ${APP_RATE_LIMIT_PER_MINUTE:-1200}
|
||||
APP_CIRCUIT_BREAKER_ERROR_RATE: ${APP_CIRCUIT_BREAKER_ERROR_RATE:-0.5}
|
||||
APP_CIRCUIT_BREAKER_MIN_REQUESTS: ${APP_CIRCUIT_BREAKER_MIN_REQUESTS:-50}
|
||||
APP_CIRCUIT_BREAKER_WINDOW_SECONDS: ${APP_CIRCUIT_BREAKER_WINDOW_SECONDS:-60}
|
||||
APP_CIRCUIT_BREAKER_COOLDOWN_SECONDS: ${APP_CIRCUIT_BREAKER_COOLDOWN_SECONDS:-30}
|
||||
DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-20}
|
||||
DATABASE_MAX_OVERFLOW: ${DATABASE_MAX_OVERFLOW:-30}
|
||||
DATABASE_POOL_TIMEOUT: ${DATABASE_POOL_TIMEOUT:-30}
|
||||
depends_on:
|
||||
- postgres
|
||||
- qdrant
|
||||
- redis
|
||||
ports:
|
||||
- "${API_PORT:-8000}:8000"
|
||||
|
||||
web:
|
||||
build:
|
||||
@@ -50,3 +76,4 @@ services:
|
||||
volumes:
|
||||
postgres_prod_data:
|
||||
qdrant_prod_data:
|
||||
redis_prod_data:
|
||||
|
||||
@@ -26,6 +26,14 @@ services:
|
||||
ports:
|
||||
- "6333:6333"
|
||||
|
||||
redis:
|
||||
image: docker.m.daocloud.io/library/redis:7-alpine
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
@@ -36,11 +44,29 @@ services:
|
||||
QDRANT_URL: http://qdrant:6333
|
||||
LOG_LEVEL: INFO
|
||||
LLM_ENABLED: "false"
|
||||
CACHE_BACKEND: "redis"
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
INGEST_ASYNC_ENABLED: "true"
|
||||
MATCH_ASYNC_ENABLED: "true"
|
||||
MATCH_CACHE_ENABLED: "true"
|
||||
MATCH_CACHE_TTL_SECONDS: "30"
|
||||
QUERY_CACHE_ENABLED: "true"
|
||||
QUERY_CACHE_TTL_SECONDS: "20"
|
||||
APP_RATE_LIMIT_PER_MINUTE: "1200"
|
||||
APP_CIRCUIT_BREAKER_ERROR_RATE: "0.5"
|
||||
APP_CIRCUIT_BREAKER_MIN_REQUESTS: "50"
|
||||
APP_CIRCUIT_BREAKER_WINDOW_SECONDS: "60"
|
||||
APP_CIRCUIT_BREAKER_COOLDOWN_SECONDS: "30"
|
||||
DATABASE_POOL_SIZE: "20"
|
||||
DATABASE_MAX_OVERFLOW: "30"
|
||||
DATABASE_POOL_TIMEOUT: "30"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
qdrant:
|
||||
condition: service_started
|
||||
redis:
|
||||
condition: service_started
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
@@ -57,3 +83,4 @@ services:
|
||||
volumes:
|
||||
postgres_data:
|
||||
qdrant_data:
|
||||
redis_data:
|
||||
|
||||
90
gig-poc/infrastructure/k8s/api.yaml
Normal file
90
gig-poc/infrastructure/k8s/api.yaml
Normal file
@@ -0,0 +1,90 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gig-poc-api
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gig-poc-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gig-poc-api
|
||||
spec:
|
||||
containers:
|
||||
- name: api
|
||||
image: gig-poc-api:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
env:
|
||||
- name: APP_ENV
|
||||
value: production
|
||||
- name: CACHE_BACKEND
|
||||
value: redis
|
||||
- name: REDIS_URL
|
||||
value: redis://gig-poc-redis:6379/0
|
||||
- name: INGEST_ASYNC_ENABLED
|
||||
value: "true"
|
||||
- name: MATCH_ASYNC_ENABLED
|
||||
value: "true"
|
||||
- name: MATCH_CACHE_ENABLED
|
||||
value: "true"
|
||||
- name: QUERY_CACHE_ENABLED
|
||||
value: "true"
|
||||
- name: APP_RATE_LIMIT_PER_MINUTE
|
||||
value: "3000"
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "2Gi"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gig-poc-api
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
selector:
|
||||
app: gig-poc-api
|
||||
ports:
|
||||
- name: http
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: gig-poc-api-hpa
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: gig-poc-api
|
||||
minReplicas: 3
|
||||
maxReplicas: 20
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
24
gig-poc/infrastructure/k8s/ingress.yaml
Normal file
24
gig-poc/infrastructure/k8s/ingress.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: gig-poc-ingress
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
rules:
|
||||
- host: gig-poc.local
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gig-poc-api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gig-poc-web
|
||||
port:
|
||||
number: 80
|
||||
9
gig-poc/infrastructure/k8s/kustomization.yaml
Normal file
9
gig-poc/infrastructure/k8s/kustomization.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: gig-poc
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- redis.yaml
|
||||
- api.yaml
|
||||
- web.yaml
|
||||
- ingress.yaml
|
||||
4
gig-poc/infrastructure/k8s/namespace.yaml
Normal file
4
gig-poc/infrastructure/k8s/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gig-poc
|
||||
41
gig-poc/infrastructure/k8s/redis.yaml
Normal file
41
gig-poc/infrastructure/k8s/redis.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gig-poc-redis
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gig-poc-redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gig-poc-redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
args: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gig-poc-redis
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
selector:
|
||||
app: gig-poc-redis
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
61
gig-poc/infrastructure/k8s/web.yaml
Normal file
61
gig-poc/infrastructure/k8s/web.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gig-poc-web
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gig-poc-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gig-poc-web
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: gig-poc-web:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: "200m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "1Gi"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gig-poc-web
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
selector:
|
||||
app: gig-poc-web
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: gig-poc-web-hpa
|
||||
namespace: gig-poc
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: gig-poc-web
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
114
gig-poc/infrastructure/scripts/acceptance-e2e.sh
Executable file
114
gig-poc/infrastructure/scripts/acceptance-e2e.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
API_BASE="${API_BASE:-http://127.0.0.1:8000}"
|
||||
BOOTSTRAP_ON_RUN="${BOOTSTRAP_ON_RUN:-true}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
step() {
|
||||
echo "[ACCEPTANCE] $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "[ACCEPTANCE][FAIL] $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
parse_json() {
|
||||
PY_EXPR="$1"
|
||||
INPUT_FILE="$2"
|
||||
python3 - "$PY_EXPR" "$INPUT_FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
expr = sys.argv[1]
|
||||
path = sys.argv[2]
|
||||
data = json.loads(open(path, "r", encoding="utf-8").read())
|
||||
safe_builtins = {"bool": bool, "len": len, "str": str}
|
||||
value = eval(expr, {"__builtins__": safe_builtins}, {"data": data})
|
||||
if isinstance(value, (dict, list)):
|
||||
print(json.dumps(value, ensure_ascii=False))
|
||||
elif value is None:
|
||||
print("")
|
||||
else:
|
||||
print(str(value))
|
||||
PY
|
||||
}
|
||||
|
||||
step "健康检查"
|
||||
curl -fsS "${API_BASE}/health" >"$TMP_DIR/health.json" || fail "health 接口不可用"
|
||||
|
||||
if [ "$BOOTSTRAP_ON_RUN" = "true" ]; then
|
||||
step "执行 bootstrap"
|
||||
curl -fsS -X POST "${API_BASE}/poc/ingest/bootstrap" >"$TMP_DIR/bootstrap.json" || fail "bootstrap 失败"
|
||||
fi
|
||||
|
||||
step "抽取岗位"
|
||||
curl -fsS -X POST "${API_BASE}/poc/extract/job" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text":"明天下午南山会展中心需要2个签到协助,5小时,150/人,女生优先"}' \
|
||||
>"$TMP_DIR/extract_job.json" || fail "岗位抽取调用失败"
|
||||
[ "$(parse_json "bool(data.get('success'))" "$TMP_DIR/extract_job.json")" = "True" ] || fail "岗位抽取失败"
|
||||
parse_json "data.get('data')" "$TMP_DIR/extract_job.json" >"$TMP_DIR/job.json"
|
||||
JOB_ID="$(parse_json "data.get('data', {}).get('job_id')" "$TMP_DIR/extract_job.json")"
|
||||
[ -n "$JOB_ID" ] || fail "岗位抽取缺少 job_id"
|
||||
|
||||
step "岗位入库"
|
||||
python3 - "$TMP_DIR/job.json" >"$TMP_DIR/ingest_job_payload.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
job = json.loads(open(sys.argv[1], "r", encoding="utf-8").read())
|
||||
print(json.dumps({"job": job}, ensure_ascii=False))
|
||||
PY
|
||||
curl -fsS -X POST "${API_BASE}/poc/ingest/job" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data @"$TMP_DIR/ingest_job_payload.json" \
|
||||
>"$TMP_DIR/ingest_job.json" || fail "岗位入库失败"
|
||||
|
||||
step "岗位匹配工人"
|
||||
curl -fsS -X POST "${API_BASE}/poc/match/workers" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"job_id\":\"${JOB_ID}\",\"top_n\":3}" \
|
||||
>"$TMP_DIR/match_workers.json" || fail "岗位匹配工人失败"
|
||||
MATCH_ID_1="$(parse_json "((data.get('items') or [{}])[0]).get('match_id')" "$TMP_DIR/match_workers.json")"
|
||||
[ -n "$MATCH_ID_1" ] || fail "岗位匹配工人未返回 match_id"
|
||||
|
||||
step "解释匹配(岗位->工人)"
|
||||
curl -fsS "${API_BASE}/poc/match/explain/${MATCH_ID_1}" >"$TMP_DIR/explain_1.json" || fail "匹配解释失败(岗位->工人)"
|
||||
|
||||
step "抽取工人"
|
||||
curl -fsS -X POST "${API_BASE}/poc/extract/worker" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text":"我做过商场促销和活动签到,也能做登记和引导,周末都能接,福田南山都方便。"}' \
|
||||
>"$TMP_DIR/extract_worker.json" || fail "工人抽取调用失败"
|
||||
[ "$(parse_json "bool(data.get('success'))" "$TMP_DIR/extract_worker.json")" = "True" ] || fail "工人抽取失败"
|
||||
parse_json "data.get('data')" "$TMP_DIR/extract_worker.json" >"$TMP_DIR/worker.json"
|
||||
WORKER_ID="$(parse_json "data.get('data', {}).get('worker_id')" "$TMP_DIR/extract_worker.json")"
|
||||
[ -n "$WORKER_ID" ] || fail "工人抽取缺少 worker_id"
|
||||
|
||||
step "工人入库"
|
||||
python3 - "$TMP_DIR/worker.json" >"$TMP_DIR/ingest_worker_payload.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
worker = json.loads(open(sys.argv[1], "r", encoding="utf-8").read())
|
||||
print(json.dumps({"worker": worker}, ensure_ascii=False))
|
||||
PY
|
||||
curl -fsS -X POST "${API_BASE}/poc/ingest/worker" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data @"$TMP_DIR/ingest_worker_payload.json" \
|
||||
>"$TMP_DIR/ingest_worker.json" || fail "工人入库失败"
|
||||
|
||||
step "工人匹配岗位"
|
||||
curl -fsS -X POST "${API_BASE}/poc/match/jobs" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"worker_id\":\"${WORKER_ID}\",\"top_n\":3}" \
|
||||
>"$TMP_DIR/match_jobs.json" || fail "工人匹配岗位失败"
|
||||
MATCH_ID_2="$(parse_json "((data.get('items') or [{}])[0]).get('match_id')" "$TMP_DIR/match_jobs.json")"
|
||||
[ -n "$MATCH_ID_2" ] || fail "工人匹配岗位未返回 match_id"
|
||||
|
||||
step "解释匹配(工人->岗位)"
|
||||
curl -fsS "${API_BASE}/poc/match/explain/${MATCH_ID_2}" >"$TMP_DIR/explain_2.json" || fail "匹配解释失败(工人->岗位)"
|
||||
|
||||
step "链路验收通过:抽取 -> 入库 -> 匹配 -> 解释"
|
||||
@@ -3,6 +3,10 @@ set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
INFRA_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
PROJECT_DIR=$(CDPATH= cd -- "$INFRA_DIR/.." && pwd)
|
||||
RUN_ACCEPTANCE_ON_UP="${RUN_ACCEPTANCE_ON_UP:-true}"
|
||||
EXPORT_OPENAPI_ON_UP="${EXPORT_OPENAPI_ON_UP:-true}"
|
||||
RUN_BASELINE_ON_UP="${RUN_BASELINE_ON_UP:-false}"
|
||||
|
||||
cd "$INFRA_DIR"
|
||||
docker compose -f docker-compose.yml up --build -d
|
||||
@@ -13,4 +17,20 @@ done
|
||||
until curl -fsS -X POST http://127.0.0.1:8000/poc/ingest/bootstrap >/dev/null 2>&1; do
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [ "$RUN_ACCEPTANCE_ON_UP" = "true" ]; then
|
||||
echo "执行一键闭环验收脚本..."
|
||||
sh "$SCRIPT_DIR/acceptance-e2e.sh"
|
||||
fi
|
||||
|
||||
if [ "$EXPORT_OPENAPI_ON_UP" = "true" ]; then
|
||||
echo "导出 OpenAPI 固化产物到 docs/openapi.json ..."
|
||||
sh "$SCRIPT_DIR/export-openapi.sh" "$PROJECT_DIR/docs/openapi.json"
|
||||
fi
|
||||
|
||||
if [ "$RUN_BASELINE_ON_UP" = "true" ]; then
|
||||
echo "执行容量基线压测..."
|
||||
sh "$SCRIPT_DIR/load-baseline.sh" "$PROJECT_DIR/docs/CAPACITY_BASELINE.md"
|
||||
fi
|
||||
|
||||
echo "本地环境已启动。Web: http://127.0.0.1:5173 API: http://127.0.0.1:8000/docs"
|
||||
|
||||
11
gig-poc/infrastructure/scripts/export-openapi.sh
Executable file
11
gig-poc/infrastructure/scripts/export-openapi.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
PROJECT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
|
||||
API_PORT="${API_PORT:-8000}"
|
||||
OUTPUT_PATH="${1:-$PROJECT_DIR/docs/openapi.json}"
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
||||
curl -fsS "http://127.0.0.1:${API_PORT}/openapi.json" -o "$OUTPUT_PATH"
|
||||
echo "OpenAPI 已导出到: $OUTPUT_PATH"
|
||||
37
gig-poc/infrastructure/scripts/freeze-openapi.sh
Executable file
37
gig-poc/infrastructure/scripts/freeze-openapi.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
PROJECT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
|
||||
OUTPUT_PATH="${1:-$PROJECT_DIR/docs/openapi.json}"
|
||||
API_PORT="${API_PORT:-8000}"
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
||||
|
||||
if PYTHONPATH="$PROJECT_DIR/apps/api" python3 - "$OUTPUT_PATH" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from app.main import app
|
||||
|
||||
output = sys.argv[1]
|
||||
spec = app.openapi()
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
json.dump(spec, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
print(f"OpenAPI 已固化到: {output}")
|
||||
PY
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "本机缺少 API 依赖,尝试从已运行 API 导出..."
|
||||
if curl -fsS "http://127.0.0.1:${API_PORT}/openapi.json" -o "$OUTPUT_PATH"; then
|
||||
echo "OpenAPI 已固化到: $OUTPUT_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "本机 API 端口不可用,尝试通过 Docker 运行 API 镜像离线导出..."
|
||||
docker compose -f "$PROJECT_DIR/infrastructure/docker-compose.yml" run --rm api \
|
||||
python -c "import json; from app.main import app; print(json.dumps(app.openapi(), ensure_ascii=False, indent=2))" \
|
||||
> "$OUTPUT_PATH"
|
||||
echo "OpenAPI 已固化到: $OUTPUT_PATH"
|
||||
137
gig-poc/infrastructure/scripts/load-baseline.sh
Executable file
137
gig-poc/infrastructure/scripts/load-baseline.sh
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
API_BASE="${API_BASE:-http://127.0.0.1:8000}"
|
||||
TOTAL_REQUESTS="${TOTAL_REQUESTS:-400}"
|
||||
CONCURRENCY="${CONCURRENCY:-40}"
|
||||
OUTPUT_PATH="${1:-$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)/docs/CAPACITY_BASELINE.md}"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
echo "[BASELINE] health check"
|
||||
curl -fsS "$API_BASE/health" >/dev/null
|
||||
|
||||
echo "[BASELINE] ensure bootstrap data"
|
||||
curl -fsS -X POST "$API_BASE/poc/ingest/bootstrap" >/dev/null
|
||||
|
||||
JOB_ID="$(curl -fsS "$API_BASE/poc/jobs" | python3 -c 'import json,sys; data=json.load(sys.stdin); print((data.get("items") or [{}])[0].get("job_id",""))')"
|
||||
WORKER_ID="$(curl -fsS "$API_BASE/poc/workers" | python3 -c 'import json,sys; data=json.load(sys.stdin); print((data.get("items") or [{}])[0].get("worker_id",""))')"
|
||||
|
||||
[ -n "$JOB_ID" ] || { echo "no job id found"; exit 1; }
|
||||
[ -n "$WORKER_ID" ] || { echo "no worker id found"; exit 1; }
|
||||
|
||||
run_case() {
|
||||
NAME="$1"
|
||||
METHOD="$2"
|
||||
URL="$3"
|
||||
BODY_FILE="$4"
|
||||
OUT_FILE="$5"
|
||||
python3 - "$METHOD" "$URL" "$BODY_FILE" "$TOTAL_REQUESTS" "$CONCURRENCY" "$OUT_FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
method, url, body_file, total, concurrency, out_file = sys.argv[1:]
|
||||
total = int(total)
|
||||
concurrency = int(concurrency)
|
||||
payload = None
|
||||
if body_file != "-":
|
||||
payload = open(body_file, "rb").read()
|
||||
|
||||
durations = []
|
||||
success = 0
|
||||
fail = 0
|
||||
|
||||
def once():
|
||||
start = time.perf_counter()
|
||||
req = urllib.request.Request(url=url, method=method)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
if payload is None:
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
code = resp.getcode()
|
||||
else:
|
||||
with urllib.request.urlopen(req, data=payload, timeout=20) as resp:
|
||||
code = resp.getcode()
|
||||
ok = 200 <= code < 400
|
||||
except Exception:
|
||||
ok = False
|
||||
ms = (time.perf_counter() - start) * 1000
|
||||
return ok, ms
|
||||
|
||||
bench_start = time.perf_counter()
|
||||
with ThreadPoolExecutor(max_workers=concurrency) as ex:
|
||||
futures = [ex.submit(once) for _ in range(total)]
|
||||
for f in as_completed(futures):
|
||||
ok, ms = f.result()
|
||||
durations.append(ms)
|
||||
if ok:
|
||||
success += 1
|
||||
else:
|
||||
fail += 1
|
||||
elapsed = time.perf_counter() - bench_start
|
||||
durations.sort()
|
||||
def pct(p):
|
||||
if not durations:
|
||||
return 0.0
|
||||
idx = min(len(durations) - 1, int(len(durations) * p))
|
||||
return round(durations[idx], 2)
|
||||
result = {
|
||||
"total": total,
|
||||
"success": success,
|
||||
"fail": fail,
|
||||
"success_rate": round(success / total, 4) if total else 0.0,
|
||||
"rps": round(total / elapsed, 2) if elapsed > 0 else 0.0,
|
||||
"latency_ms_avg": round(sum(durations) / len(durations), 2) if durations else 0.0,
|
||||
"latency_ms_p95": pct(0.95),
|
||||
"latency_ms_p99": pct(0.99),
|
||||
}
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
PY
|
||||
echo "[BASELINE] done $NAME"
|
||||
}
|
||||
|
||||
printf '{"job_id":"%s","top_n":10}\n' "$JOB_ID" >"$TMP_DIR/match_workers.json"
|
||||
printf '{"worker_id":"%s","top_n":10}\n' "$WORKER_ID" >"$TMP_DIR/match_jobs.json"
|
||||
|
||||
run_case "health" "GET" "$API_BASE/health" "-" "$TMP_DIR/health.result.json"
|
||||
run_case "jobs_list" "GET" "$API_BASE/poc/jobs" "-" "$TMP_DIR/jobs.result.json"
|
||||
run_case "match_workers" "POST" "$API_BASE/poc/match/workers" "$TMP_DIR/match_workers.json" "$TMP_DIR/match_workers.result.json"
|
||||
run_case "match_jobs" "POST" "$API_BASE/poc/match/jobs" "$TMP_DIR/match_jobs.json" "$TMP_DIR/match_jobs.result.json"
|
||||
run_case "match_workers_cached" "POST" "$API_BASE/poc/match/workers" "$TMP_DIR/match_workers.json" "$TMP_DIR/match_workers_cached.result.json"
|
||||
run_case "match_jobs_cached" "POST" "$API_BASE/poc/match/jobs" "$TMP_DIR/match_jobs.json" "$TMP_DIR/match_jobs_cached.result.json"
|
||||
run_case "match_workers_async_enqueue" "POST" "$API_BASE/poc/match/workers/async" "$TMP_DIR/match_workers.json" "$TMP_DIR/match_workers_async.result.json"
|
||||
run_case "match_jobs_async_enqueue" "POST" "$API_BASE/poc/match/jobs/async" "$TMP_DIR/match_jobs.json" "$TMP_DIR/match_jobs_async.result.json"
|
||||
|
||||
NOW="$(date '+%Y-%m-%d %H:%M:%S %z')"
|
||||
mkdir -p "$(dirname "$OUTPUT_PATH")"
|
||||
|
||||
{
|
||||
echo "# 容量基线(自动生成)"
|
||||
echo
|
||||
echo "- 生成时间: $NOW"
|
||||
echo "- API_BASE: $API_BASE"
|
||||
echo "- TOTAL_REQUESTS: $TOTAL_REQUESTS"
|
||||
echo "- CONCURRENCY: $CONCURRENCY"
|
||||
echo
|
||||
echo "| 场景 | 成功率 | RPS | 平均延迟(ms) | P95(ms) | P99(ms) |"
|
||||
echo "| --- | --- | --- | --- | --- | --- |"
|
||||
for case in health jobs match_workers match_jobs match_workers_cached match_jobs_cached match_workers_async match_jobs_async; do
|
||||
FILE="$TMP_DIR/${case}.result.json"
|
||||
python3 - "$case" "$FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
case, path = sys.argv[1], sys.argv[2]
|
||||
data = json.loads(open(path, "r", encoding="utf-8").read())
|
||||
print(f"| {case} | {data['success_rate']} | {data['rps']} | {data['latency_ms_avg']} | {data['latency_ms_p95']} | {data['latency_ms_p99']} |")
|
||||
PY
|
||||
done
|
||||
echo
|
||||
echo "> 建议:该基线仅代表当前单机/当前数据量下表现,发布前请在目标环境按 2x/5x 峰值复测。"
|
||||
} >"$OUTPUT_PATH"
|
||||
|
||||
echo "[BASELINE] report generated at $OUTPUT_PATH"
|
||||
8
gig-poc/infrastructure/scripts/prod-down.sh
Executable file
8
gig-poc/infrastructure/scripts/prod-down.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
INFRA_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
|
||||
cd "$INFRA_DIR"
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
@@ -3,7 +3,21 @@ set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
INFRA_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
API_PORT="${API_PORT:-8000}"
|
||||
BOOTSTRAP_ON_UP="${BOOTSTRAP_ON_UP:-true}"
|
||||
|
||||
cd "$INFRA_DIR"
|
||||
docker compose -f docker-compose.prod.yml up --build -d
|
||||
echo "生产部署容器已启动。请按实际域名或端口访问 Web。"
|
||||
echo "等待生产 API 健康检查..."
|
||||
until curl -fsS "http://127.0.0.1:${API_PORT}/health" >/dev/null 2>&1; do
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [ "$BOOTSTRAP_ON_UP" = "true" ]; then
|
||||
echo "执行 bootstrap 样本初始化..."
|
||||
until curl -fsS -X POST "http://127.0.0.1:${API_PORT}/poc/ingest/bootstrap" >/dev/null 2>&1; do
|
||||
sleep 3
|
||||
done
|
||||
fi
|
||||
|
||||
echo "生产环境已启动。Web: http://127.0.0.1:${WEB_PORT:-80} API: http://127.0.0.1:${API_PORT}/docs"
|
||||
|
||||
Reference in New Issue
Block a user