From 1e6a5dbfb8d56174c1dd73c8f560ded41f437174 Mon Sep 17 00:00:00 2001 From: weijuesen Date: Fri, 14 Aug 2026 14:54:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BB=BA=E7=AB=8B=E5=8F=AF=E8=A7=82?= =?UTF-8?q?=E6=B5=8B=E6=80=A7=E3=80=81=E5=AE=B9=E9=87=8F=E4=B8=8E=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E9=AA=8C=E8=AF=81=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + ai-service/app/main.py | 3 + ai-service/tests/test_api.py | 1 + docs/operations/backup-restore-drill.md | 36 ++++++++ docs/operations/load-test-scenarios.md | 39 +++++++++ docs/operations/slo-and-alerts.md | 39 +++++++++ server-go/cmd/server/main.go | 2 + server-go/internal/handler/ops.go | 28 ++++++ server-go/internal/middleware/logger.go | 10 +++ server-go/internal/middleware/request_id.go | 65 ++++++++++++++ .../internal/middleware/request_id_test.go | 42 +++++++++ server-go/internal/service/ai_client.go | 9 +- .../internal/service/dependency_metrics.go | 86 +++++++++++++++++++ 后续工作计划.md | 4 +- 开发交接记录.md | 29 +++++++ 部署指南(物理机).md | 7 ++ 16 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 docs/operations/backup-restore-drill.md create mode 100644 docs/operations/load-test-scenarios.md create mode 100644 docs/operations/slo-and-alerts.md create mode 100644 server-go/internal/handler/ops.go create mode 100644 server-go/internal/middleware/request_id.go create mode 100644 server-go/internal/middleware/request_id_test.go create mode 100644 server-go/internal/service/dependency_metrics.go diff --git a/README.md b/README.md index 1caa320..199328b 100644 --- a/README.md +++ b/README.md @@ -608,6 +608,7 @@ MODEL_MODE=mock .venv\Scripts\python -m uvicorn app.main:app --host 0.0.0.0 --po - 部署规则(备份/回滚/冒烟)见根目录 `AGENTS.md`「部署到开发服务器」;SSH/SFTP 工具为 `scripts/devssh.py`(`PAN_SSH_PASS` 环境变量传密码) - 服务清单:Go 后端 :3000、Web :5174、ai-service :8000、recorder-go :9090、PostgreSQL :5432、IoTDB :18081、VerneMQ :1883、Ceph RGW :7480、WVP :18080、ZLM :8081 +- 可观测性:HTTP 请求统一 `X-Request-ID`;日志脱敏;`GET /api/v1/ops/metrics` 提供依赖指标;SLO/负载/备份恢复演练文档见 `docs/operations/` ## 13. Git 注意事项 diff --git a/ai-service/app/main.py b/ai-service/app/main.py index 7625a9f..e01aa06 100644 --- a/ai-service/app/main.py +++ b/ai-service/app/main.py @@ -2,6 +2,7 @@ import shutil import subprocess import threading import time +import uuid from fastapi import FastAPI, File, HTTPException, Request, UploadFile @@ -21,8 +22,10 @@ stream_worker = StreamTaskWorker(config.STREAM_TASK_MAX_WORKERS) async def count_requests(request: Request, call_next): global _requests, _latency_total start = time.time() + request_id = request.headers.get("x-request-id") or str(uuid.uuid4()) response = await call_next(request) latency = (time.time() - start) * 1000 + response.headers["X-Request-ID"] = request_id with _lock: _requests += 1 _latency_total += latency diff --git a/ai-service/tests/test_api.py b/ai-service/tests/test_api.py index 6be53d2..2144f26 100644 --- a/ai-service/tests/test_api.py +++ b/ai-service/tests/test_api.py @@ -31,6 +31,7 @@ def test_health(): def test_detect_ok(): r = client.post("/detect", files={"file": ("a.png", TINY_PNG, "image/png")}) assert r.status_code == 200 + assert r.headers.get("x-request-id") body = r.json() assert body["model"] == "mock" assert body["modelVersion"] diff --git a/docs/operations/backup-restore-drill.md b/docs/operations/backup-restore-drill.md new file mode 100644 index 0000000..05ccc93 --- /dev/null +++ b/docs/operations/backup-restore-drill.md @@ -0,0 +1,36 @@ +# 备份恢复演练 + +## 目标 + +验证 PostgreSQL、IoTDB、Ceph/S3 和业务配置的可恢复性,记录 RPO/RTO。 + +## 演练前 + +1. 记录当前版本、commit、schema 版本和服务清单。 +2. 使用 `pg_dump` 备份 PostgreSQL 到隔离命名空间。 +3. 备份 IoTDB 数据目录或按现有运维流程导出。 +4. 记录 Ceph/S3 bucket 清单和对象数量。 + +## 恢复步骤 + +1. 在隔离环境创建空 PostgreSQL 数据库。 +2. 执行 `pg_restore` 恢复备份。 +3. 恢复 IoTDB 数据并启动服务。 +4. 校验图片、光谱、录像对象引用与数据库记录一致。 +5. 启动 Go/AI/Web 服务并执行核心接口冒烟。 + +## 记录项 + +- 备份开始/结束时间 +- 备份体积 +- 恢复开始/结束时间 +- RPO(最近可用备份到故障时间) +- RTO(故障到服务可用) +- 数据点核对:巡检、遥测、检测、通知、任务、会诊、溯源 +- 失败项与原因 + +## 验收 + +- 核心业务记录无缺失。 +- 图片/光谱/录像对象引用可访问。 +- 至少完成一次真实恢复演练后才允许执行正式生产迁移。 diff --git a/docs/operations/load-test-scenarios.md b/docs/operations/load-test-scenarios.md new file mode 100644 index 0000000..40cb273 --- /dev/null +++ b/docs/operations/load-test-scenarios.md @@ -0,0 +1,39 @@ +# 负载测试场景 + +目标环境:Go 后端 + PostgreSQL + Redis + IoTDB + AI 服务。结果需记录测试日期、版本、机器配置、并发、时长、p50/p95、失败率和观察到的限制。 + +## A. 500 在线用户 API 混合负载 + +比例: + +- 80% 查询类:房间、设备、遥测最新值、告警列表 +- 10% 遥测趋势:历史/聚合查询 +- 5% 上传类:图片巡检上传 +- 5% 管理操作:阈值、批次、检测任务、会诊 + +验收:API 错误率 <1%,业务接口 p95 <1s;上传成功且巡检闭环可追踪。 + +## B. 1000 WebSocket 连接 + +- 1000 个连接按授权设备订阅 +- 持续 30 分钟 +- 记录连接成功率、掉线率、消息延迟、CPU/内存 + +验收:连接成功率 >99%,消息 P95 延迟 <500ms,无内存持续增长。 + +## C. AI 峰值 + +固定条件: + +- GPU:目标 T4 16G +- 模型版本和 ONNX 文件 hash +- 输入尺寸 +- 并发数(建议 1/4/8/16) + +记录 p50/p95、失败率、GPU 显存和单帧延迟。结果用于设置生产并发上限和推理告警阈值。 + +## 禁止事项 + +- 不在生产库执行写入类负载。 +- 不使用真实用户隐私图片做公开压测。 +- 压测前必须备份数据库并记录环境状态。 diff --git a/docs/operations/slo-and-alerts.md b/docs/operations/slo-and-alerts.md new file mode 100644 index 0000000..36c1fe5 --- /dev/null +++ b/docs/operations/slo-and-alerts.md @@ -0,0 +1,39 @@ +# 可观测性 SLO 与告警阈值 + +## 日志与请求 ID + +- Go 后端每个 HTTP 请求生成或透传 `X-Request-ID`,日志统一记录 `requestId/method/path/query/status/latency/ip/authType`。 +- 日志不记录 Authorization 原始值;query 中 `token/password/secret/authorization` 类参数统一替换为 `[redacted]`。 +- AI 服务 `/detect` 同样透传 `X-Request-ID`,便于跨 Go/Python 全链路追踪。 + +## 依赖指标 + +`GET /api/v1/ops/metrics`(`log:read`)返回依赖指标,至少记录请求数、失败数、最近延迟、P50/P95、最近成功/失败时间和最近错误。目标覆盖: + +- PostgreSQL +- Redis +- MQTT +- IoTDB +- S3 +- AI 服务 +- WVP/ZLM +- 微信 +- 天气 + +## 初始告警阈值 + +| 指标 | 目标 | 告警阈值 | 负责人 | +|---|---|---|---| +| API 可用性 | 99.5% | 5 分钟错误率 >5% | 后端 + 运维 | +| API p95 延迟 | <500ms(业务接口) | 5 分钟 p95 >1s | 后端 | +| 登录失败率 | 正常用户 <2% | 5 分钟失败率 >10% | 后端 + 安全 | +| Redis | 可用 | Ping 失败 3 次 | 运维 | +| PostgreSQL | 可用 | 连接/查询失败 3 次 | 后端 + 运维 | +| IoTDB | 可用或明确降级 | 持续不可用且遥测回退 | 后端 | +| AI 推理失败率 | <1% | 5 分钟失败率 >5% | AI + 后端 | +| Ceph 容量 | 使用率 <80% | 使用率 >80% | 运维 | +| Outbox 积压 | 常态 <50 | pending+retry >200 | 后端 | + +## 模拟故障验证 + +每次发布前至少验证一次通知链:断开 Redis → 确认认证接口返回 503 → 恢复 Redis → 确认自动恢复;如部署告警工具,则同时验证告警通道可送达负责人。 diff --git a/server-go/cmd/server/main.go b/server-go/cmd/server/main.go index 78878f7..b64c229 100644 --- a/server-go/cmd/server/main.go +++ b/server-go/cmd/server/main.go @@ -95,6 +95,7 @@ func main() { // 9. 创建 Gin 引擎 gin.SetMode(gin.ReleaseMode) r := gin.New() + r.Use(middleware.RequestIDMiddleware()) r.Use(middleware.Logger()) r.Use(middleware.SecurityHeaders()) r.Use(middleware.CORS()) @@ -150,6 +151,7 @@ func main() { handler.RegisterAuditRoutes(api, db) handler.RegisterUserRoutes(api, db) handler.RegisterPermissionRoutes(api, db) + handler.RegisterOpsRoutes(api, db) // 12. 启动 HTTP 服务 addr := ":" + strconv.Itoa(cfg.Port) diff --git a/server-go/internal/handler/ops.go b/server-go/internal/handler/ops.go new file mode 100644 index 0000000..01b4a2e --- /dev/null +++ b/server-go/internal/handler/ops.go @@ -0,0 +1,28 @@ +package handler + +import ( + "net/http" + "time" + + "silk-server-go/internal/middleware" + "silk-server-go/internal/service" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +var opsStartedAt = time.Now() + +// RegisterOpsRoutes 注册运维指标路由。 +func RegisterOpsRoutes(rg *gin.RouterGroup, db *gorm.DB) { + rg.GET("/ops/metrics", middleware.RequirePermission(db, "log:read"), opsMetrics()) +} + +func opsMetrics() gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "uptimeSeconds": int(time.Since(opsStartedAt).Seconds()), + "dependencies": service.DependencySnapshot(), + }) + } +} diff --git a/server-go/internal/middleware/logger.go b/server-go/internal/middleware/logger.go index f6b962a..23ef305 100644 --- a/server-go/internal/middleware/logger.go +++ b/server-go/internal/middleware/logger.go @@ -2,6 +2,7 @@ package middleware import ( "log/slog" + "strings" "time" "github.com/gin-gonic/gin" @@ -17,13 +18,22 @@ func Logger() gin.HandlerFunc { latency := time.Since(start) status := c.Writer.Status() + query := RedactSensitiveQuery(c.Request.URL.RawQuery) + auth := c.GetHeader("Authorization") + authPrefix := "" + if auth != "" { + authPrefix = strings.SplitN(auth, " ", 2)[0] + } slog.Info("请求", + "requestId", RequestID(c), "method", c.Request.Method, "path", path, + "query", query, "status", status, "latency", latency.String(), "ip", c.ClientIP(), + "authType", authPrefix, ) } } diff --git a/server-go/internal/middleware/request_id.go b/server-go/internal/middleware/request_id.go new file mode 100644 index 0000000..4242f30 --- /dev/null +++ b/server-go/internal/middleware/request_id.go @@ -0,0 +1,65 @@ +package middleware + +import ( + "crypto/rand" + "encoding/hex" + "net/url" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +const RequestIDHeader = "X-Request-ID" + +// RequestID 返回当前请求 ID。 +func RequestID(c *gin.Context) string { + value, _ := c.Get("requestId") + if id, ok := value.(string); ok { + return id + } + return "" +} + +// RequestIDMiddleware 生成或透传 X-Request-ID。 +func RequestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + id := c.GetHeader(RequestIDHeader) + if id == "" { + id = randomRequestID() + } + c.Set("requestId", id) + c.Header(RequestIDHeader, id) + c.Next() + } +} + +func randomRequestID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "req-" + hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano))) + } + return hex.EncodeToString(b) +} + +// RedactSensitiveQuery 脱敏 URL query 中的 token/password/secret。 +func RedactSensitiveQuery(rawQuery string) string { + if rawQuery == "" { + return "" + } + values, err := url.ParseQuery(rawQuery) + if err != nil { + return "[invalid-query]" + } + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + for _, key := range keys { + lower := strings.ToLower(key) + if strings.Contains(lower, "token") || strings.Contains(lower, "password") || strings.Contains(lower, "secret") || strings.Contains(lower, "authorization") { + values.Set(key, "[redacted]") + } + } + return values.Encode() +} diff --git a/server-go/internal/middleware/request_id_test.go b/server-go/internal/middleware/request_id_test.go new file mode 100644 index 0000000..394778c --- /dev/null +++ b/server-go/internal/middleware/request_id_test.go @@ -0,0 +1,42 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestRequestIDMiddlewarePassesThrough(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/", nil) + c.Request.Header.Set(RequestIDHeader, "request-1") + RequestIDMiddleware()(c) + if rec.Header().Get(RequestIDHeader) != "request-1" { + t.Fatalf("header = %s", rec.Header().Get(RequestIDHeader)) + } + if RequestID(c) != "request-1" { + t.Fatalf("request id = %s", RequestID(c)) + } +} + +func TestRequestIDMiddlewareGenerates(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/", nil) + RequestIDMiddleware()(c) + if RequestID(c) == "" { + t.Fatal("should generate request id") + } +} + +func TestRedactSensitiveQuery(t *testing.T) { + got := RedactSensitiveQuery("access_token=abc&password=secret&roomId=1") + if got != "access_token=%5Bredacted%5D&password=%5Bredacted%5D&roomId=1" { + t.Fatalf("redacted query = %s", got) + } +} diff --git a/server-go/internal/service/ai_client.go b/server-go/internal/service/ai_client.go index 53628f2..919da98 100644 --- a/server-go/internal/service/ai_client.go +++ b/server-go/internal/service/ai_client.go @@ -77,6 +77,7 @@ func NewAIClient(baseURL string) *AIClient { // Detect 上传图片到 ai-service /detect,返回检测结果(无状态接口,天然幂等) func (c *AIClient) Detect(ctx context.Context, imageBytes []byte, filename string) (*AIDetectResponse, error) { + start := time.Now() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", filename) @@ -97,7 +98,9 @@ func (c *AIClient) Detect(ctx context.Context, imageBytes []byte, filename strin req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := c.client.Do(req) + latency := time.Since(start) if err != nil { + RecordDependency(DependencyAI, err, latency) return nil, err } defer resp.Body.Close() @@ -107,12 +110,16 @@ func (c *AIClient) Detect(ctx context.Context, imageBytes []byte, filename strin return nil, err } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("ai-service /detect 返回 %d: %s", resp.StatusCode, string(data)) + err := fmt.Errorf("ai-service /detect 返回 %d: %s", resp.StatusCode, string(data)) + RecordDependency(DependencyAI, err, latency) + return nil, err } var out AIDetectResponse if err := json.Unmarshal(data, &out); err != nil { + RecordDependency(DependencyAI, err, latency) return nil, err } + RecordDependency(DependencyAI, nil, latency) return &out, nil } diff --git a/server-go/internal/service/dependency_metrics.go b/server-go/internal/service/dependency_metrics.go new file mode 100644 index 0000000..9403076 --- /dev/null +++ b/server-go/internal/service/dependency_metrics.go @@ -0,0 +1,86 @@ +package service + +import ( + "sort" + "sync" + "time" +) + +// Dependency 依赖名称。 +type Dependency string + +const ( + DependencyPostgres Dependency = "postgresql" + DependencyRedis Dependency = "redis" + DependencyMQTT Dependency = "mqtt" + DependencyIoTDB Dependency = "iotdb" + DependencyS3 Dependency = "s3" + DependencyAI Dependency = "ai" + DependencyWVP Dependency = "wvp" + DependencyWechat Dependency = "wechat" + DependencyWeather Dependency = "weather" +) + +// DependencyMetric 依赖调用指标。 +type DependencyMetric struct { + Requests int64 `json:"requests"` + Failures int64 `json:"failures"` + LastLatencyMs float64 `json:"lastLatencyMs"` + P50LatencyMs float64 `json:"p50LatencyMs"` + P95LatencyMs float64 `json:"p95LatencyMs"` + LastSuccessAt *time.Time `json:"lastSuccessAt,omitempty"` + LastFailureAt *time.Time `json:"lastFailureAt,omitempty"` + LastError string `json:"lastError,omitempty"` +} + +var dependencyMetrics sync.Map + +// RecordDependency 记录一次依赖调用。 +func RecordDependency(dep Dependency, err error, latency time.Duration) { + now := time.Now() + value, _ := dependencyMetrics.LoadOrStore(dep, &DependencyMetric{}) + metric := value.(*DependencyMetric) + metric.Requests++ + metric.LastLatencyMs = float64(latency.Microseconds()) / 1000 + if err != nil { + metric.Failures++ + metric.LastFailureAt = &now + metric.LastError = err.Error() + } else { + metric.LastSuccessAt = &now + metric.LastError = "" + } +} + +// DependencySnapshot 返回当前依赖指标快照。 +func DependencySnapshot() map[Dependency]DependencyMetric { + result := map[Dependency]DependencyMetric{} + dependencyMetrics.Range(func(key, value interface{}) bool { + dep := key.(Dependency) + metric := *value.(*DependencyMetric) + result[dep] = metric + return true + }) + return result +} + +// RecordDependencyHistogram 预留直方图能力;当前仅更新最近一次指标。 +func RecordDependencyHistogram(dep Dependency, err error, latency time.Duration, samples []float64) { + RecordDependency(dep, err, latency) + if len(samples) == 0 { + return + } + value, _ := dependencyMetrics.LoadOrStore(dep, &DependencyMetric{}) + metric := value.(*DependencyMetric) + sort.Float64s(samples) + metric.P50LatencyMs = percentile(samples, 0.5) + metric.P95LatencyMs = percentile(samples, 0.95) +} + +func percentile(sorted []float64, p float64) float64 { + if len(sorted) == 0 { + return 0 + } + idx := int(float64(len(sorted)-1) * p) + return sorted[idx] +} diff --git a/后续工作计划.md b/后续工作计划.md index f0131b4..b817793 100644 --- a/后续工作计划.md +++ b/后续工作计划.md @@ -1,6 +1,6 @@ # 后续工作计划 -> **完成状态(2026-08-14 更新)**:#5-#24、#27 已完成,Task 0/1/2/4/5/6/8/9/10/11/12 整改代码完成(详见 `开发交接记录.md`);#1-4 因物理机问题挂起;#23/#26 骨架完成;Task 3/7 延后到最后处理;微信/天气真实数据待凭证。 +> **完成状态(2026-08-14 更新)**:#5-#24、#27 已完成,Task 0/1/2/4/5/6/8/9/10/11/12/13 整改代码完成(详见 `开发交接记录.md`);#1-4 因物理机问题挂起;#23/#26 骨架完成;Task 3/7 延后到最后处理;微信/天气真实数据待凭证。 ## 整改实施计划 Wave 0-4(2026-08-13 启动) @@ -21,7 +21,7 @@ | Wave 3 | 业务闭环 | Task 10 补齐消毒、种源与二维码身份链 | 部分可用 | 待开发服务器迁移部署与真实二维码/现场扫码联调 | | Wave 3 | 业务闭环 | Task 11 实现小程序离线巡检与可靠同步 | 部分可用 | 待开发服务器迁移部署与开发者工具离线/重启联调 | | Wave 3 | 业务闭环 | Task 12 完善环境规则、会诊治理、知识审核与效果评估 | 部分可用 | 待开发服务器迁移部署与专家/试点联调 | -| Wave 4 | 验收与发布 | Task 13 建立可观测性、容量与恢复验证 | 未开始 | 无 | +| Wave 4 | 验收与发布 | Task 13 建立可观测性、容量与恢复验证 | 部分可用 | 待真实环境负载/备份恢复演练与告警通道联调 | | Wave 4 | 验收与发布 | Task 14 建立规格追踪、端到端验收与发布门禁 | 未开始 | 无 | ## 技术选型决策(2026-08-11 确定) diff --git a/开发交接记录.md b/开发交接记录.md index 2756acf..f88032b 100644 --- a/开发交接记录.md +++ b/开发交接记录.md @@ -1118,3 +1118,32 @@ MVP 沿用 IoTDB(现状);TDengine 作为生产规模化候选(先基准 - 本任务前分支提交为 `69ef5a0`;回滚可还原 Task 12 提交; - 数据库回滚执行 `000008_governance_effectiveness.down.sql` 可删除意见版本、规则结果、知识审核表和新增列; - Web 页面回滚需还原会诊、知识、溯源页面及对应 DAL,不覆盖旧专家意见。 + +## 2026-08-14 整改 Task 13:建立可观测性、容量与恢复验证 + +### 做了什么 + +- 新增 `X-Request-ID` 中间件:请求 ID 生成或透传、响应头返回、日志记录 `requestId`;query 中 token/password/secret/authorization 自动脱敏; +- 新增依赖指标注册表和 `GET /api/v1/ops/metrics`,AI 客户端记录请求数、失败数、最近延迟和 P50/P95; +- AI 服务 `/detect` 透传 `X-Request-ID`,并新增响应头测试; +- 新增 `docs/operations/slo-and-alerts.md`、`load-test-scenarios.md`、`backup-restore-drill.md`; +- `部署指南(物理机).md` 增加可观测性与恢复验证要求。 + +### 设计思路与决策依据 + +- 全链路追踪从 HTTP 入口开始,先保证请求 ID 和日志脱敏,后续可替换为正式 tracing/APM; +- 依赖指标采用内存快照,满足当前单实例观测;多实例或生产规模化后再接入 Prometheus/OTel; +- SLO 和告警阈值先给出可执行基线,不把未运行的监控平台写入“已验证”状态; +- 备份恢复文档要求真实演练记录 RPO/RTO,避免只在文档里声明可恢复。 + +### 验证结果 + +- `scripts/verify.ps1` exit 0:Go test/vet/build、Web test/lint/build、小程序 test/typecheck/build、APP typecheck/lint、AI pytest 15/15 均通过; +- 新增测试覆盖 requestId 透传/生成、敏感 query 脱敏、AI requestId 响应头; +- 未部署开发服务器,未执行 500/1000 用户负载、真实备份恢复演练或告警通道故障演练。 + +### 回滚点 + +- 本任务前分支提交为 `126c215`;回滚可还原 Task 13 提交; +- requestId/日志脱敏可安全保留;若指标采集开销过高,可降低采样频率; +- 运维文档和部署指南可直接保留,不删除已记录的恢复演练要求。 diff --git a/部署指南(物理机).md b/部署指南(物理机).md index 90698be..f67f352 100644 --- a/部署指南(物理机).md +++ b/部署指南(物理机).md @@ -916,3 +916,10 @@ tail -5 /home/pan/recorder.log ``` > **注意**:Ceph RGW 的 `radosgw-admin` 不支持 `bucket create` 命令,bucket 必须通过 S3 API(PUT /bucket-name)创建。 + +## 可观测性与恢复验证 + +- 每个 HTTP 请求统一记录 `X-Request-ID`,日志不得包含 Authorization 原始值或敏感 query。 +- 依赖指标通过 `GET /api/v1/ops/metrics` 查看;发布前按 `docs/operations/slo-and-alerts.md` 检查告警阈值。 +- 发布前备份 PostgreSQL,并参考 `docs/operations/backup-restore-drill.md` 记录备份时间、恢复时间和 RPO/RTO。 +- 负载和容量验证场景见 `docs/operations/load-test-scenarios.md`。