# 服务端接入层设计文档 ## 一、设计原则 ### 1.1 核心原则 - **高并发**:单节点支撑 1000+ QPS 事件上报 - **低延迟**:接口响应 < 50ms(先入队再处理) - **高可用**:进程崩溃不丢数据(本地持久化队列) - **可扩展**:支持横向扩展,无状态设计 - **兼容性**:完全兼容 Sentry 协议,SDK 可无缝切换 ### 1.2 性能指标 | 指标 | 目标值 | 说明 | |------|--------|------| | 单节点 QPS | 1000+ | 4C8G 服务器 | | P99 响应时间 | < 50ms | 99% 请求在 50ms 内返回 | | 数据丢失率 | < 0.01% | 异常情况下的数据丢失率 | | 单事件处理耗时 | < 1ms | 从入队到写入存储 | --- ## 二、API 接口设计 ### 2.1 接口列表 | 端点 | 方法 | 功能 | 优先级 | |------|------|------|--------| | `/api/{projectId}/envelope/` | POST | Envelope 批量上报 | 最高 | | `/api/{projectId}/store/` | POST | 单事件上报 | 高 | | `/api/{projectId}/nel/` | POST | NEL 网络错误上报 | 中 | | `/api/{projectId}/health` | GET | 健康检查 | - | | `/api/projects/` | GET/POST | 项目管理 | - | | `/api/projects/{id}` | GET/PUT/DELETE | 项目 CRUD | - | ### 2.2 Envelope 接口(主要上报接口) #### URL 格式 ``` POST /api/{projectId}/envelope/?sentry_key={publicKey}&sentry_version=7 ``` #### 请求头 | Header | 说明 | 必填 | |--------|------|------| | `Content-Type` | `application/x-sentry-envelope` 或 `application/json` | 是 | | `X-Sentry-Auth` | Sentry 认证头(备用方式) | 否 | #### 认证方式(二选一) **方式 1:URL 参数(推荐,兼容性好)** ``` ?sentry_key={publicKey}&sentry_version=7 ``` **方式 2:Header 方式** ``` X-Sentry-Auth: Sentry sentry_version=7, sentry_key={publicKey} ``` #### Envelope 格式解析 标准 Envelope 格式: ``` # 第 1 行:envelope header(JSON) {"event_id":"abc123","sent_at":"2024-01-01T00:00:00Z"} # 第 2 行:item header(JSON) {"type":"event","length":123} # 第 3 行:item payload(JSON,长度 = length) {"level":"error","message":"test",...} # 第 4 行:下一个 item header {"type":"transaction","length":456} # ... ``` 支持的 item 类型: | type | 说明 | 处理方式 | |------|------|----------| | `event` | 错误事件 | 完整处理 + 写入 Loki + 聚合 | | `transaction` | 性能事务 | 提取关键指标 + 聚合 | | `session` | 会话 | 计数 + 聚合 | | `attachment` | 附件 | 忽略(轻量版不支持) | | `profile` | 性能剖析 | 忽略 | | `statsd` | 客户端统计 | 忽略 | | `user_report` | 用户反馈 | 存储 + 计数 | #### 简化格式(自研 SDK 专用) 为了减少解析开销,自研 SDK 可使用简化格式: ```json { "events": [ { "type": "error", "data": { ... } }, { "type": "performance", "data": { ... } } ] } ``` Content-Type: `application/json` ### 2.3 Store 接口(兼容旧版 SDK) ``` POST /api/{projectId}/store/?sentry_key={publicKey} Content-Type: application/json { "event_id": "abc123", "level": "error", "message": "...", "exception": { ... } } ``` ### 2.4 NEL 接口(Network Error Logging) ``` POST /api/{projectId}/nel/ Content-Type: application/reports+json [ { "age": 123, "type": "network-error", "url": "https://example.com/", "body": { "sampling_fraction": 1.0, "server_ip": "1.2.3.4", "protocol": "h2", "method": "GET", "status_code": 0, "elapsed_time": 123, "type": "dns.failed" } } ] ``` --- ## 三、处理流程 ### 3.1 整体流程 ``` HTTP 请求到达 ↓ Nginx (限流 + CORS + 日志) ↓ 1. 认证中间件 ├─ 解析 projectId (URL) ├─ 解析 publicKey (query / header) └─ 校验项目是否存在 ↓ 2. 请求体解析 ├─ 根据 Content-Type 选择解析器 ├─ envelope 格式 → 逐行解析 └─ json 格式 → JSON.parse ↓ 3. 数据清洗 ├─ 字段校验 ├─ 敏感数据脱敏 ├─ 计算错误指纹 └─ 补充默认字段 ↓ 4. 写入内存队列(立即返回响应) ↓ 5. 异步批量消费 ├─ 写入 Loki(原始日志) ├─ 更新 MySQL 聚合表 └─ 触发告警检测 ``` ### 3.2 认证中间件 ```javascript async function sentryAuth(req, res, next) { // 1. 从 URL 中提取 projectId const projectId = req.params.projectId; // 2. 从 URL 参数或 Header 获取 publicKey const publicKey = req.query.sentry_key || parseSentryAuthHeader(req.headers['x-sentry-auth']); // 3. 校验项目是否存在 const project = projectStore.findByProjectId(projectId); if (!project) { return res.status(404).json({ detail: 'Project not found' }); } // 4. 校验 publicKey if (project.publicKey !== publicKey) { return res.status(401).json({ detail: 'Invalid public key' }); } // 5. 挂载到请求对象 req.project = project; next(); } ``` ### 3.3 中间件顺序(重要) ``` express.raw() ← Sentry 路由先挂载,避免 express.json() 干扰 ↓ sentryAuth ← DSN 认证 ↓ envelopeParser ← 解析 envelope 格式 ↓ rateLimit ← 限流 ↓ handler ← 业务处理 express.json() ← 其他 API 路由 ↓ 其他业务中间件 ``` **关键设计**:Sentry 上报接口必须在 `express.json()` 之前挂载,否则 raw body 会被解析成 JSON,导致 envelope 格式解析失败。 --- ## 四、内存队列设计 ### 4.1 队列结构 ``` ┌─────────────────────────────────────────┐ │ 内存队列 (Array) │ │ [ evt1, evt2, evt3, ..., evtN ] │ │ ↑ ↑ │ │ head(tail) maxSize │ └─────────────────────────────────────────┘ │ ▼ ┌─────────────────────┐ │ 批量消费定时器 │ │ 每 1s 或满 100 条 │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ 批量写入 Loki │ │ 批量更新 MySQL │ │ 告警检测 │ └─────────────────────┘ ``` ### 4.2 队列参数 | 参数 | 默认值 | 说明 | |------|--------|------| | `maxQueueSize` | 10000 | 队列最大长度 | | `flushInterval` | 1000ms | 定时刷间隔 | | `batchSize` | 100 | 每批处理数量 | | `persistInterval` | 5000ms | 持久化间隔(防止丢数据) | ### 4.3 队列满时的策略 1. **新事件丢弃最旧的**:FIFO,保证新事件优先 2. **采样降级**:队列 > 80% 时,非错误事件自动降采样 3. **快速失败**:队列 > 95% 时,直接返回 429 4. **本地持久化**:优雅关闭时刷入本地文件 ### 4.4 本地持久化(可选) 防止进程崩溃丢数据: - 使用 LevelDB / SQLite 作为持久化队列 - 入队时先写磁盘,再读内存 - 启动时从磁盘恢复未处理的事件 - 性能影响:QPS 从 1000+ 降到 ~500 --- ## 五、数据清洗与脱敏 ### 5.1 字段校验 | 字段 | 校验规则 | 不通过处理 | |------|----------|-----------| | `event_id` | 32 位 hex,可选 | 自动生成 | | `timestamp` | ISO 时间戳,可选 | 用服务器时间 | | `message` | 长度 < 8KB | 截断 | | `exception.stacktrace` | 深度 < 50 帧 | 截断 | | `breadcrumbs` | 数量 < 100 | 截断 | | `extra` | 大小 < 16KB | 截断 | | 总大小 | < 256KB | 拒绝 | ### 5.2 敏感数据脱敏 自动脱敏的字段: | 字段名(不区分大小写) | 脱敏方式 | |----------------------|----------| | `password`, `passwd`, `pwd` | `***` | | `token`, `access_token`, `refresh_token` | 前 4 位 + `***` | | `secret`, `api_key`, `apikey` | `***` | | `email` | `a***@b.com` | | `phone`, `mobile` | `138****1234` | | `id_card`, `idcard` | `110***********1234` | | `credit_card`, `card_no` | `6222**********1234` | | IP 地址 | 保留前两段(192.168.x.x) | ### 5.3 错误指纹计算 用于错误去重和聚合: ```javascript function computeFingerprint(event) { const { exception } = event; if (!exception) { return md5(event.message || 'unknown'); } // 提取关键信息 const type = exception.type || 'Error'; const message = normalizeMessage(exception.value); // 脱敏 + 去变量 const frames = exception.stacktrace?.frames || []; // 取前 3 个 in_app 栈帧 const keyFrames = frames .filter(f => f.in_app !== false) .slice(0, 3) .map(f => `${f.filename}:${f.lineno}`); return md5(`${type}:${message}:${keyFrames.join('|')}`); } ``` --- ## 六、限流设计 ### 6.1 限流维度 | 维度 | 默认限制 | 说明 | |------|----------|------| | 每项目每秒 | 100 条 | 项目级 QPS 限制 | | 每项目每天 | 100000 条 | 项目级日配额 | | 每 IP 每秒 | 50 条 | IP 级 QPS 限制 | | 全局限流 | 1000 QPS | 服务器总 QPS | ### 6.2 限流算法 - **令牌桶算法**:QPS 限流用令牌桶 - **滑动窗口**:日配额用滑动窗口 - **内存计数**:单节点足够,分布式需 Redis ### 6.3 超限处理 | 超出比例 | 处理方式 | |----------|----------| | < 80% | 正常处理 | | 80% - 100% | 非错误事件降采样 50% | | 100% - 150% | 只保留错误事件,其余丢弃 | | > 150% | 全部丢弃,返回 429 | --- ## 七、批量消费设计 ### 7.1 消费流程 ``` 批次事件 ↓ 按项目分组 ↓ 按事件类型分组 (error / performance / network / ...) ↓ 并行处理 ├─ error 类型 │ ├─ 写入 Loki(批量) │ ├─ 更新错误聚合表(按指纹分组计数) │ └─ 触发告警检测 ├─ performance 类型 │ ├─ 写入 Loki │ └─ 更新性能指标表(P50/P95/P99) └─ 其他类型 └─ 写入 Loki ``` ### 7.2 Loki 批量写入 使用 Loki 的 `/loki/api/v1/push` 接口,批量写入: ```json { "streams": [ { "stream": { "project_id": "1001", "level": "error", "type": "error" }, "values": [ ["", ""], ["", ""] ] } ] } ``` ### 7.3 MySQL 批量更新 - 使用 `INSERT ... ON DUPLICATE KEY UPDATE` - 按批次聚合后一次性写入 - 避免逐条更新,提升性能 --- ## 八、错误码设计 | HTTP 状态码 | 说明 | 场景 | |-------------|------|------| | 200 | 成功 | 正常接收 | | 204 | 成功无内容 | 同上,兼容不同 SDK | | 400 | 请求格式错误 | envelope 格式不对、body 为空 | | 401 | 认证失败 | publicKey 错误 | | 404 | 项目不存在 | projectId 无效 | | 413 | 请求体过大 | 超过 256KB | | 429 | 限流 | 超过配额 | | 500 | 服务器错误 | 内部异常 | --- ## 九、可观测性 ### 9.1 自身监控指标 | 指标 | 说明 | |------|------| | `events_received_total` | 接收事件总数 | | `events_received_per_second` | 每秒接收数 | | `events_dropped_total` | 丢弃事件总数 | | `events_processed_total` | 处理成功总数 | | `queue_size` | 当前队列长度 | | `process_duration_ms` | 处理耗时(P50/P95/P99) | | `loki_write_errors_total` | Loki 写入错误数 | | `mysql_write_errors_total` | MySQL 写入错误数 | ### 9.2 健康检查接口 ``` GET /health { "status": "ok", "timestamp": "2024-01-01T00:00:00Z", "uptime": 86400, "queue_size": 123, "events_processed": 1234567, "loki": "connected", "mysql": "connected" } ``` --- ## 十、横向扩展 ### 10.1 无状态设计 - API Server 完全无状态 - 项目配置缓存,启动时加载,定时刷新 - 队列在内存中,扩展时直接加机器 ### 10.2 负载均衡 - Nginx 层做负载均衡 - 按 projectId 一致性哈希路由 - 同一项目的事件打到同一台机器(有利于缓存和聚合) ### 10.3 队列扩展 - 小流量:内存队列足够 - 中流量:加 Redis 作为分布式队列 - 大流量:加 Kafka(不推荐,太重了)