可观测与合规
这一课你将能做到
- 为 API 接入完整的可观测体系:指标(Metrics)、日志(Logging)、链路追踪(Tracing)
- 实现多维度限流策略:按用户、按 IP、按接口、按租户
- 理解 GDPR 等合规要求,在 API 设计中落实数据保护
为什么要学
API 上线只是开始。流量突增时系统会不会崩?某个接口突然变慢怎么定位?用户数据被泄露怎么追溯?可观测让你「看见」系统运行状态,合规让你「合法」处理用户数据。两者都是生产环境的生死线。
先建立图像
可观测三支柱:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Metrics │ │ Logging │ │ Tracing │
│ 指标 │ │ 日志 │ │ 链路追踪 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ QPS、延迟 │ │ 错误日志、 │ │ 请求跨服务 │
│ 错误率、CPU │ │ 业务日志、 │ │ 的完整路径 │
│ 内存、磁盘 │ │ 审计日志 │ │ 和耗时分布 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────┼────────────────┘
▼
┌─────────────────┐
│ 可观测平台 │
│ Prometheus + │
│ Grafana + ELK │
│ + Jaeger/SkyWalking│
└─────────────────┘
跟着做
步骤 1:Spring Boot Actuator + Micrometer 指标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
tags:
application: codeforge-api
// 自定义业务指标
@Component
public class LessonMetrics {
private final Counter lessonCreateCounter;
private final Timer lessonQueryTimer;
public LessonMetrics(MeterRegistry registry) {
this.lessonCreateCounter = Counter.builder("lesson.create.total")
.description("课程创建总数")
.register(registry);
this.lessonQueryTimer = Timer.builder("lesson.query.duration")
.description("课程查询耗时")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
}
public void recordCreate() {
lessonCreateCounter.increment();
}
public void recordQuery(Runnable action) {
lessonQueryTimer.record(action);
}
}
Prometheus 抓取配置:
# prometheus.yml
scrape_configs:
- job_name: 'codeforge-api'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
步骤 2:结构化日志
// 使用 SLF4J + Logback + JSON 格式
// logback-spring.xml
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</configuration>
// 业务日志
@Service
public class LessonService {
private static final Logger log = LoggerFactory.getLogger(LessonService.class);
public Lesson create(LessonRequest request, Long userId) {
MDC.put("userId", userId.toString());
MDC.put("operation", "createLesson");
log.info("开始创建课程, title={}", request.getTitle());
try {
Lesson lesson = doCreate(request, userId);
log.info("课程创建成功, lessonId={}", lesson.getId());
return lesson;
} catch (Exception e) {
log.error("课程创建失败, title={}", request.getTitle(), e);
throw e;
} finally {
MDC.clear();
}
}
}
{
"timestamp": "2026-10-07T10:30:00.000+08:00",
"level": "INFO",
"logger": "com.codeforge.LessonService",
"message": "课程创建成功, lessonId=123",
"traceId": "abc123def456",
"userId": "1001",
"operation": "createLesson"
}
步骤 3:分布式追踪——Sleuth + Zipkin
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
spring:
sleuth:
sampler:
probability: 0.1 # 采样率 10%,生产环境不要 100%
zipkin:
base-url: http://zipkin:9411
GET /api/lessons/123
├── lesson-service (50ms)
│ ├── lessonRepository.findById() (30ms)
│ └── instructorServiceClient.get() (15ms)
│ └── user-service (10ms)
└── 返回
Zipkin 界面能看到每个 Span 的耗时、标签、日志。
步骤 4:多维度限流
@Service
public class RateLimitService {
private final RedisTemplate<String, String> redisTemplate;
// 固定窗口限流
public boolean fixedWindow(String key, int limit, int seconds) {
String redisKey = "rate:fixed:" + key;
String current = redisTemplate.opsForValue().get(redisKey);
if (current == null) {
redisTemplate.opsForValue().set(redisKey, "1", Duration.ofSeconds(seconds));
return true;
}
int count = Integer.parseInt(current);
if (count >= limit) {
return false;
}
redisTemplate.opsForValue().increment(redisKey);
return true;
}
// 滑动窗口限流(更精确)
public boolean slidingWindow(String key, int limit, int seconds) {
long now = System.currentTimeMillis();
long windowStart = now - seconds * 1000L;
String redisKey = "rate:sliding:" + key;
// 删除窗口外的记录
redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
// 统计当前窗口请求数
Long count = redisTemplate.opsForZSet().count(redisKey, windowStart, now);
if (count != null && count >= limit) {
return false;
}
// 记录本次请求
redisTemplate.opsForZSet().add(redisKey, String.valueOf(now), now);
redisTemplate.expire(redisKey, Duration.ofSeconds(seconds));
return true;
}
// 令牌桶(应对突发流量)
public boolean tokenBucket(String key, int capacity, int ratePerSecond) {
// 用 Redis + Lua 实现原子操作
String luaScript = """
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local key = KEYS[1]
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- 计算应补充的令牌
local elapsed = math.max(0, now - last_refill)
local refill = elapsed * rate
tokens = math.min(capacity, tokens + refill)
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 1
else
return 0
end
""";
String redisKey = "rate:bucket:" + key;
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(redisKey),
String.valueOf(capacity),
String.valueOf(ratePerSecond),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1;
}
}
// 使用示例
@GetMapping("/api/lessons")
public ApiResponse<List<Lesson>> list(HttpServletRequest request,
@AuthenticationPrincipal User user) {
// 按 IP 限流:100 次/分钟
String ip = getClientIp(request);
if (!rateLimitService.slidingWindow("ip:" + ip, 100, 60)) {
throw new BusinessException(429, "请求过于频繁,请稍后再试");
}
// 按用户限流:1000 次/小时
if (user != null && !rateLimitService.slidingWindow("user:" + user.getId(), 1000, 3600)) {
throw new BusinessException(429, "用户请求超限");
}
return ApiResponse.success(lessonService.findAll());
}
步骤 5:GDPR 合规要点
// 数据主体权利响应
@RestController
@RequestMapping("/api/gdpr")
public class GdprController {
// 数据可携权:导出用户所有数据
@GetMapping("/export")
public ApiResponse<UserDataExport> exportData(@AuthenticationPrincipal User user) {
UserDataExport export = gdprService.exportUserData(user.getId());
return ApiResponse.success(export);
}
// 被遗忘权:删除用户所有数据
@DeleteMapping("/erase")
public ApiResponse<Void> eraseData(@AuthenticationPrincipal User user) {
gdprService.eraseUserData(user.getId());
return ApiResponse.success(null);
}
}
- 数据最小化:只收集必要数据,不过度采集
- 用户同意:明确告知数据用途,获取同意
- 数据可携:用户有权导出自己的数据
- 被遗忘:用户有权要求删除数据
- 数据保护:加密存储、访问控制、泄露通知(72 小时内)
讲清楚要点
- 可观测不是可选的:没有 Metrics 你不知道系统健不健康,没有 Logging 你排查问题靠猜,没有 Tracing 你定位慢请求靠运气。三支柱缺一不可。
- 指标要分层次:系统指标(CPU、内存、磁盘)、应用指标(JVM GC、线程池)、业务指标(订单量、注册数、课程创建数)。业务指标最能反映系统价值。
- 日志要结构化:JSON 格式比纯文本更容易检索。带 traceId 的日志能串联整个请求链路,排查问题时「一键定位」。
- 限流算法有取舍:固定窗口简单但有临界问题;滑动窗口精确但内存消耗大;令牌桶应对突发但实现复杂。按场景选择,或组合使用。
- GDPR 是法律,不是建议:违反 GDPR 最高罚款全球营收 4%。欧盟用户的数据必须合法处理,数据本地化、跨境传输都有严格规定。
你可能会懵的地方
-
Prometheus 和 InfluxDB 怎么选?
Prometheus:拉模式、生态丰富、K8s 原生,适合云原生;InfluxDB:推模式、写入性能高、SQL-like 查询,适合 IoT、传感器。监控用 Prometheus,时序数据用 InfluxDB。
-
日志量太大怎么办?
采样(如错误日志 100%,普通日志 1%)、分级(ERROR 存长期,DEBUG 存短期)、压缩、冷热分离(热数据 Elasticsearch,冷数据 S3)。
-
限流阈值怎么定?
根据系统容量测试得出:CPU 70%、内存 80%、RT P99 < 500ms 时的最大 QPS,再留 30% 余量。逐步调整,不要拍脑袋。
-
分布式追踪采样率多少合适?
核心链路(如支付、下单)100%;普通查询 1-10%。采样率太高影响性能,太低丢失关键信息。
-
中国用户需要 GDPR 吗?
不需要 GDPR,但需要《个人信息保护法》(PIPL)。要求类似:知情同意、最小必要、数据本地化、跨境安全评估。出海业务两者都要考虑。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 只监控系统指标,不监控业务指标 | 系统正常但业务下跌发现不了 | 业务指标(订单量、注册数)同样告警 |
| 日志打印敏感信息(密码、Token) | 日志泄露 = 数据泄露 | 脱敏后打印,或完全不打印敏感字段 |
| 所有接口一个限流阈值 | 登录接口被刷爆,正常用户也登录不了 | 按接口、按用户、按 IP 多维度限流 |
| 生产环境日志级别 DEBUG | 磁盘打满,性能下降 | 生产环境 INFO 以上,按需临时开 DEBUG |
| 用户数据随意跨境传输 | 违反 PIPL/GDPR,面临罚款 | 数据本地化,跨境前做安全评估 |
卡点急救
- Prometheus 采集不到指标:检查
management.endpoints.web.exposure.include 是否包含 prometheus,防火墙是否放行端口。
- Zipkin 看不到链路:检查 Sleuth 版本与 Spring Boot 版本兼容性(Spring Boot 3.x 用 Micrometer Tracing),采样率是否为 0。
- Redis 限流在集群环境下不准:Redis Cluster 有主从延迟,用 Lua 脚本保证原子性,或改用本地缓存 + 分布式协调。
面试常问
-
可观测三支柱是什么?怎么落地?
Metrics(Prometheus + Grafana)、Logging(ELK/Loki)、Tracing(Jaeger/SkyWalking)。能讲出每个支柱的工具选型和数据流。
-
怎么设计一个限流系统?
算法(固定窗口、滑动窗口、令牌桶、漏桶)、维度(IP、用户、接口、租户)、存储(Redis、本地缓存)、降级策略(限流后返回缓存或排队)。
-
GDPR 的核心要求?怎么在技术层面落实?
知情同意(Cookie 弹窗、隐私政策)、数据最小化(只收集必要字段)、数据可携(导出接口)、被遗忘(删除接口)、数据保护(加密、访问控制、泄露通知)。
自测三问
- 如果 API 响应时间突然从 100ms 涨到 2s,你会怎么排查?
- 设计一个「秒杀」场景的限流方案,预期 QPS 10 万。
- 用户要求删除所有个人数据,你的系统需要怎么处理?
小结一句话
可观测让你「看见」系统,合规让你「合法」运营;指标、日志、追踪是排障三剑客,限流、加密、审计是防护三盾牌。
延伸