493 lines
12 KiB
Markdown
493 lines
12 KiB
Markdown
# 服务端接入层设计文档
|
||
|
||
## 一、设计原则
|
||
|
||
### 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": [
|
||
["<ts_nano>", "<json_line>"],
|
||
["<ts_nano>", "<json_line>"]
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 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(不推荐,太重了)
|