监控告警

完善的监控和告警是保证分布式 ID 系统稳定运行的关键。

监控指标

1. 核心指标

QPS(每秒查询数):

QPS = 每秒生成的 ID 数量

阈值:
- 正常:> 10,000
- 警告:< 10,000
- 严重:< 1,000

延迟(Latency):

P50:50% 的请求延迟
P99:99% 的请求延迟
P999:99.9% 的请求延迟

阈值:
- 优秀:P99 < 1ms
- 良好:P99 < 10ms
- 警告:P99 < 100ms
- 严重:P99 ≥ 100ms

错误率(Error Rate):

错误率 = 失败请求数 / 总请求数 × 100%

阈值:
- 优秀:< 0.01%
- 良好:< 0.1%
- 警告:< 1%
- 严重:≥ 1%

可用性(Availability):

可用性 = (总时间 - 停机时间) / 总时间 × 100%

阈值:
- 优秀:≥ 99.99%
- 良好:≥ 99.9%
- 警告:≥ 99%
- 严重:< 99%

2. 资源指标

CPU 使用率:

阈值:
- 正常:< 70%
- 警告:70-90%
- 严重:> 90%

内存使用率:

阈值:
- 正常:< 70%
- 警告:70-90%
- 严重:> 90%

网络带宽:

阈值:
- 正常:< 70%
- 警告:70-90%
- 严重:> 90%

3. 业务指标

Buffer 使用率:

Buffer 使用率 = 已分配 ID / Buffer 总容量 × 100%

阈值:
- 正常:< 50%
- 警告:50-80%
- 严重:> 80%

号段获取频率:

号段获取频率 = 每秒获取号段次数

阈值:
- 正常:< 10 次/秒
- 警告:10-50 次/秒
- 严重:> 50 次/秒

监控实现

1. 使用 Prometheus

配置:

配置要点

  • 配置表达的是环境差异和运行参数,不是业务规则本身。

Java 实现:


2. 使用 Micrometer

Micrometer 是 Spring 生态的指标门面:应用只面向 Micrometer 的 API 写指标,具体落到 Prometheus、Datadog 还是别的后端由配置决定。

配置:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    tags:
      application: id-generator
    distribution:
      percentiles-histogram:
        id.generate: true
      percentiles:
        id.generate: 0.5, 0.99, 0.999

Java 实现:

/**
 * 用 Micrometer 把发号过程的关键指标暴露出去。
 */
@Component
public class IdMetrics {

    private final Counter generatedCounter;
    private final Counter failedCounter;
    private final Timer generateTimer;

    /** 号段剩余量交给 Gauge 采集。 */
    private final AtomicLong segmentRemaining = new AtomicLong();

    public IdMetrics(MeterRegistry registry) {
        this.generatedCounter = Counter.builder("id.generated.total")
                .description("成功生成的 ID 数量")
                .register(registry);
        this.failedCounter = Counter.builder("id.failed.total")
                .description("生成失败的次数")
                .register(registry);
        this.generateTimer = Timer.builder("id.generate")
                .description("生成一个 ID 的耗时")
                .publishPercentiles(0.5, 0.99, 0.999)
                .register(registry);

        // 瞬时状态用 Gauge:采集时回调取值
        Gauge.builder("id.segment.remaining", segmentRemaining, AtomicLong::get)
                .description("本地号段剩余量")
                .register(registry);
    }

    /** 记录一次成功的发号。 */
    public void recordSuccess(long costNanos) {
        generatedCounter.increment();
        generateTimer.record(costNanos, TimeUnit.NANOSECONDS);
    }

    /** 记录一次失败。 */
    public void recordFailure() {
        failedCounter.increment();
    }

    /** 续期完成后更新号段剩余量。 */
    public void updateSegmentRemaining(long remaining) {
        segmentRemaining.set(remaining);
    }
}

指标要挑这几个:

指标类型说明
id.generated.totalCounter发号总量,除以时间就是 QPS
id.failed.totalCounter失败量,和总量一起算出错误率
id.generateTimer发号耗时,P99 对应监控告警里的延迟阈值
id.segment.remainingGauge本地号段剩余量,反映续期是否跟得上
id.degradedGauge当前生效的降级方案等级,0 表示主方案

号段剩余量、降级等级这类「瞬时状态」只能做成 Gauge,每次采集时回调取值;写成 Counter 就没法表达「现在还剩多少」。

指标名要稳定。指标名一旦上线就很难改,Prometheus 里的历史序列和告警规则都绑在上面,命名规范要在项目开始时定好。


告警策略

1. 告警级别

P0 - 严重告警:

- 系统完全不可用
- QPS = 0
- 错误率 > 10%

响应时间:立即处理(< 5分钟)

P1 - 高级告警:

- 系统部分故障
- QPS < 1,000
- 错误率 > 1%

响应时间:1小时内处理

P2 - 中级告警:

- 性能下降
- QPS < 10,000
- 延迟 P99 > 100ms

响应时间:24小时内处理

P3 - 低级告警:

- 资源使用率偏高
- Buffer 使用率 > 50%

响应时间:持续观察

2. 告警规则

Prometheus 告警规则:

配置要点

  • 配置表达的是环境差异和运行参数,不是业务规则本身。

告警通知

1. 通知方式

邮件:


短信:


钉钉/企业微信:


2. 告警聚合

聚合策略:

同一类型的告警在 5 分钟内只发送一次
严重告警立即发送
其他告警批量发送

日志追踪

1. 结构化日志

发号链路横跨接入层、服务层、数据层,出了问题要靠日志把一个请求的各段串起来。结构化日志就是每条日志都输出成机器可读的 JSON,字段固定,方便日志系统按字段检索和聚合。

输出格式:

{
  "timestamp": "2024-04-10 00:00:01",
  "level": "INFO",
  "service": "id-generator",
  "traceId": "9f1c2d3e4a5b6c7d",
  "bizType": "order",
  "segmentId": 1024,
  "costMs": 1,
  "message": "号段获取成功"
}

字段设计有三条原则:

  • 必备字段固定:时间、级别、服务名、链路 ID、业务类型。字段名不要随场景变化,否则日志系统里的字段索引会碎掉。
  • 上下文用 MDC 传递。traceId 这类每个请求都有的值,在入口写进 MDC,之后任何一行日志都能自动带上,不用一层层往下传。
  • 敏感信息不进日志。手机号、身份证、完整的请求体都不能进日志;ID 本身可以进,它的作用就是定位。

实现:

/**
 * 用 MDC 把链路上下文带进每一行日志。
 */
@Component
public class TraceIdFilter extends OncePerRequestFilter {

    private static final String TRACE_ID = "traceId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null || traceId.isEmpty()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }

        MDC.put(TRACE_ID, traceId);
        // 回写给下游服务,整条链路才能串起来
        response.setHeader("X-Trace-Id", traceId);
        try {
            filterChain.doFilter(request, response);
        } finally {
            // 线程来自线程池,清理必须放在 finally 里
            MDC.remove(TRACE_ID);
        }
    }
}

日志格式用 logback 的 JSON 编码器配置:

<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeMdcKeyName>traceId</includeMdcKeyName>
        <customFields>{"service":"id-generator"}</customFields>
    </encoder>
</appender>

清理必须放在 finally 里:线程来自线程池,不清理的话下一个请求会带上上一个请求的 traceId,看起来像是串行污染。


2. 分布式追踪

日志能告诉你「发生了什么」,分布式追踪能告诉你「在哪一步花的时间」。一个发号请求经过接入层、服务层、数据库三层,每一层都记一笔 span,串起来就是一条完整的时间线。

使用 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: 1.0
  zipkin:
    base-url: http://zipkin:9411

实现:

/**
 * 在发号的关键节点上打点,耗时和结果都会进入 Zipkin。
 */
@Service
public class TracedIdService {

    private final SegmentIdGenerator idGenerator;
    private final Tracer tracer;

    public TracedIdService(SegmentIdGenerator idGenerator, Tracer tracer) {
        this.idGenerator = idGenerator;
        this.tracer = tracer;
    }

    /** 生成一个 ID,并记录这次发号落在哪个 span 里。 */
    public long nextId() {
        Span span = tracer.nextSpan().name("id.generate").start();
        try (Tracer.SpanInScope scope = tracer.withSpan(span)) {
            long id = idGenerator.nextId();
            span.tag("id", Long.toString(id));
            return id;
        } catch (RuntimeException e) {
            span.error(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

接入之后,一个发号请求在 Zipkin 上会呈现成这样:

客户端 ── POST /api/id ──▶ id-generator ── SELECT id_segment ──▶ MySQL
   12ms                      10ms                8ms                2ms

注意事项:

  • 采样率要按流量调。probability 是 1.0 表示全采样,压测或者线上大流量时改成 0.1 甚至更低,否则追踪本身会成为负担。
  • Sleuth 在 Spring Boot 3 之后已经停止演进,新项目改用 Micrometer Tracing,API 不同但思路一致:上下文都放在 ThreadLocal 里,跨线程要显式传递。
  • span 的粒度要合适。给每一条 SQL 都建 span 会把链路图撑爆,发号这种关键路径上留 2 到 3 个 span 就够了。

监控面板

1. Grafana 面板

关键指标:

  • QPS 趋势图
  • 延迟分布图
  • 错误率趋势图
  • 资源使用率图
  • Buffer 使用率图

2. 自定义面板

示例:

配置要点

  • 配置表达的是环境差异和运行参数,不是业务规则本身。
  • 队列、缓存、存储和服务参数决定系统在高峰期的缓冲能力。

最佳实践

1. 完善的监控

✅ 监控所有核心指标
✅ 设置合理的阈值
✅ 实现多级告警
✅ 支持多种通知方式

2. 及时的告警

✅ P0 告警立即处理
✅ P1 告警 1 小时内处理
✅ P2 告警 24 小时内处理
✅ P3 告警持续观察

3. 完整的日志

✅ 结构化日志
✅ 分布式追踪
✅ 日志聚合
✅ 日志分析

总结

完善的监控和告警系统是保证分布式 ID 系统稳定运行的关键。


课程总结

恭喜你完成了《系统设计 - 分布式 ID 生成器》课程!

你学到了: ✅ 分布式 ID 的多种方案 ✅ 各方案的优缺点和适用场景 ✅ 生产级系统的设计原则 ✅ 完整的系统架构 ✅ 监控和告警策略

下一步

建议继续学习:

  • 系统设计其他主题
  • 分布式系统设计
  • 微服务架构
  • 高可用系统设计

感谢学习!🎉

发现内容有问题?

告诉我们哪里需要改进。