SaaS平台的可观测性体系:租户维度的指标、追踪与告警设计
SaaS平台的可观测性体系租户维度的指标、追踪与告警设计多租户SaaS的可观测性挑战在于整体看起来一切正常但某个租户可能正经历严重问题。传统的全局监控粒度太粗无法感知租户级别的异常。本文将复盘一套租户粒度的可观测性体系设计涵盖Metrics指标、Tracing追踪、Logging日志三大支柱以及租户级成本归因和SLA仪表盘。一、多租户可观测性的独特挑战在单租户系统中可观测性关注的是我的系统是否健康。在多租户SaaS中问题变成了每个租户是否都得到了公平且符合SLA的体验。核心问题拆解挑战传统方案的不足租户可观测性方案问题定位QPS下降了20%租户A的QPS下降了80%其他租户正常SLA保障全局P99延迟每个租户维度的P99延迟成本归因按实例计费按租户的资源实际消耗计费故障隔离全部告警某租户超限只告警该租户的CSM容量规划按全局趋势按Top租户的增长趋势分别规划二、租户粒度的指标监控2.1 租户维度指标设计Configuration public class TenantMetricsConfig { private final MeterRegistry meterRegistry; /** * 注册租户维度的核心指标 */ PostConstruct public void registerTenantMetrics() { // 1. 租户级QPS this.tenantQpsCounter Counter.builder(saas.tenant.requests) .tag(metric_type, throughput) .description(租户请求计数) .register(meterRegistry); // 2. 租户级延迟分布 this.tenantLatencyTimer Timer.builder(saas.tenant.latency) .tag(metric_type, latency) .description(租户请求延迟) .publishPercentiles(0.5, 0.95, 0.99, 0.999) .publishPercentileHistogram() .register(meterRegistry); // 3. 租户级错误率 this.tenantErrorCounter Counter.builder(saas.tenant.errors) .tag(metric_type, errors) .description(租户错误计数) .register(meterRegistry); // 4. 租户级并发连接数 this.tenantConcurrency Gauge.builder(saas.tenant.concurrency, tenantConcurrencyMap, Map::size) .description(租户当前并发数) .register(meterRegistry); } } // 在拦截器中自动采集 Component public class TenantMetricsInterceptor implements HandlerInterceptor { private final MeterRegistry meterRegistry; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId TenantContext.getTenantId(); String api extractApi(request.getRequestURI()); // 记录开始时间 request.setAttribute(metric.start, System.nanoTime()); request.setAttribute(metric.tenant, tenantId); request.setAttribute(metric.api, api); // 并发计数 1 tenantConcurrencyMap.merge(tenantId, 1, Integer::sum); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { String tenantId (String) request.getAttribute(metric.tenant); String api (String) request.getAttribute(metric.api); long startNanos (Long) request.getAttribute(metric.start); long duration System.nanoTime() - startNanos; Tags tags Tags.of( tenant_id, tenantId, api, api, status, String.valueOf(response.getStatus()), tier, resolveTenantTier(tenantId) ); // 记录QPS meterRegistry.counter(saas.tenant.requests, tags).increment(); // 记录延迟 meterRegistry.timer(saas.tenant.latency, tags) .record(duration, TimeUnit.NANOSECONDS); // 记录错误 if (response.getStatus() 500 || ex ! null) { Tags errorTags tags.and(error_type, ex ! null ? ex.getClass().getSimpleName() : HTTP_ response.getStatus()); meterRegistry.counter(saas.tenant.errors, errorTags).increment(); } // 并发计数 -1 tenantConcurrencyMap.computeIfPresent(tenantId, (k, v) - v 1 ? v - 1 : null); } }2.2 Prometheus查询示例# 租户A的P99延迟最近5分钟 histogram_quantile(0.99, rate(saas_tenant_latency_seconds_bucket{tenant_idtenant_a}[5m])) # Top-10 错误率最高的租户 topk(10, rate(saas_tenant_errors_total[5m]) / rate(saas_tenant_requests_total[5m]) ) # 某租户QPS突增告警 rate(saas_tenant_requests_total{tenant_idtenant_a}[5m]) rate(saas_tenant_requests_total{tenant_idtenant_a}[1h]) * 3三、分布式追踪的租户上下文传递3.1 跨服务传递租户ID在多服务调用链中确保租户ID贯穿整个调用链是关键Component public class TenantTraceInjector { private final Tracer tracer; /** * 在Span中注入租户上下文 */ public Span injectTenantContext(Span currentSpan, String tenantId) { return currentSpan.setAttribute(tenant.id, tenantId) .setAttribute(tenant.tier, resolveTier(tenantId)) .setAttribute(tenant.plan, resolvePlan(tenantId)); } } // Feign调用时自动传递租户ID Component public class TenantTraceFeignInterceptor implements RequestInterceptor { Override public void apply(RequestTemplate template) { String tenantId TenantContext.getTenantId(); String traceId Span.current().getSpanContext().getTraceId(); String spanId Span.current().getSpanContext().getSpanId(); // 传递租户上下文到下游服务 template.header(X-Tenant-Id, tenantId); template.header(X-Trace-Id, traceId); template.header(X-Span-Id, spanId); // B3 Propagation 格式兼容Zipkin/Jaeger template.header(b3, traceId - spanId -1); } } // 消息队列中也传递租户上下文 Component public class TenantTraceKafkaInterceptor implements ProducerInterceptorString, Object { Override public ProducerRecordString, Object onSend(ProducerRecordString, Object record) { String tenantId TenantContext.getTenantId(); Span currentSpan Span.current(); record.headers().add(tenant_id, tenantId.getBytes(StandardCharsets.UTF_8)); record.headers().add(trace_id, currentSpan.getSpanContext().getTraceId().getBytes()); return record; } }3.2 基于Trace的租户级问题定位Service public class TenantTraceAnalyzer { private final JaegerQueryClient jaegerClient; /** * 查询某租户在特定时间段的慢请求 */ public ListTraceSummary findSlowTraces(String tenantId, Duration threshold, Instant start, Instant end) { String query String.format( tenant.id\%s\ AND duration %dms, tenantId, threshold.toMillis() ); return jaegerClient.searchTraces(query, start, end) .stream() .map(trace - TraceSummary.builder() .traceId(trace.getTraceId()) .duration(trace.getDuration()) .spanCount(trace.getSpans().size()) .services(trace.getServices()) .operations(trace.getOperations()) .slowestSpan(findSlowestSpan(trace)) .build()) .sorted(Comparator.comparing(TraceSummary::getDuration).reversed()) .limit(20) .toList(); } /** * 对比两个租户的调用链差异A正常/B异常 → 定位差异点 */ public DiffResult diffTenantTraces(String tenantA, String tenantB, String operation, Instant window) { ListSpan tracesA findSpans(tenantA, operation, window); ListSpan tracesB findSpans(tenantB, operation, window); // 对比每个下游调用的P99延迟 MapString, Duration latencyA calcP99ByDownstream(tracesA); MapString, Duration latencyB calcP99ByDownstream(tracesB); ListDiff diffs new ArrayList(); for (String downstream : latencyB.keySet()) { Duration a latencyA.getOrDefault(downstream, Duration.ZERO); Duration b latencyB.getOrDefault(downstream, Duration.ZERO); if (b.minus(a).compareTo(Duration.ofMillis(50)) 0) { diffs.add(new Diff(downstream, a, b, 租户B的 downstream 延迟显著偏高)); } } return new DiffResult(tenantA, tenantB, diffs); } }四、租户级成本归因与SLA仪表盘4.1 成本归因模型Service public class TenantCostAttributionService { /** * 按租户维度统计资源消耗 */ public TenantCostReport generateCostReport(String tenantId, YearMonth month) { // 1. 计算资源消耗 double computeCost calcComputeCost(tenantId, month); double storageCost calcStorageCost(tenantId, month); double networkCost calcNetworkCost(tenantId, month); double aiTokenCost calcAITokenCost(tenantId, month); // 2. 分摊共享资源成本 double sharedCost calcSharedCostAllocation(tenantId, month); // 3. 资源利用率分析 double utilization calcUtilization(tenantId, month); return TenantCostReport.builder() .tenantId(tenantId) .period(month) .compute(computeCost) .storage(storageCost) .network(networkCost) .aiTokens(aiTokenCost) .sharedAllocation(sharedCost) .totalCost(computeCost storageCost networkCost aiTokenCost sharedCost) .resourceUtilization(utilization) .costPerActiveUser(computeCost storageCost networkCost aiTokenCost sharedCost / getActiveUsers(tenantId, month)) .optimizationSuggestions(generateSuggestions(utilization)) .build(); } /** * 共享资源按实际使用量比例分摊 */ private double calcSharedCostAllocation(String tenantId, YearMonth month) { double totalSharedCost getTotalSharedCost(month); double tenantUsage getTenantUsage(tenantId, month); double totalUsage getTotalUsage(month); return totalSharedCost * (tenantUsage / Math.max(totalUsage, 1)); } }4.2 SLA仪表盘实现RestController RequestMapping(/api/dashboard/sla) public class SLADashboardController { private final MeterRegistry meterRegistry; private final TenantSlaConfig slaConfig; GetMapping(/{tenantId}) public SLADashboard getTenantSLA(PathVariable String tenantId) { // 计算当前SLA达成情况 double uptime calcUptime(tenantId); double p99Latency calcP99(tenantId); double errorRate calcErrorRate(tenantId); TenantSla target slaConfig.getSla(tenantId); return SLADashboard.builder() .tenantId(tenantId) .period(current_month) .metrics(Map.of( availability, SLAMetric.builder() .current(round(100 - errorRate, 4)) .target(target.getAvailability()) .status(100 - errorRate target.getAvailability() ? OK : BREACHED) .build(), p99_latency_ms, SLAMetric.builder() .current(round(p99Latency, 2)) .target(target.getP99LatencyMs()) .status(p99Latency target.getP99LatencyMs() ? OK : BREACHED) .build(), uptime_pct, SLAMetric.builder() .current(round(uptime, 4)) .target(target.getUptime()) .status(uptime target.getUptime() ? OK : BREACHED) .build() )) .slaComplianceScore(calcComplianceScore(tenantId)) .historicalBreaches(getBreaches(tenantId, 90)) .build(); } }五、总结多租户可观测性体系建设的四个核心原则租户是第一维度。所有指标QPS、延迟、错误率都必须携带tenant_id标签没有租户标签的指标在SaaS场景下价值有限。追踪上下文必须贯穿全链路。不仅HTTP调用消息队列、定时任务、异步线程都需要传递tenant_id和trace_id。告警要租户化。整体错误率上升的告警不如租户A错误率突增至15%有行动力——后者CSM可以直接联系客户。成本归因是可观测性的商业闭环。让客户看到每一分钱的资源消耗明细不仅提升信任感也是成本优化的数据基础。全链路落地后线上问题定位时间从平均45分钟缩短到8分钟——不是因为工具更高级而是因为租户维度的观测数据让排查路径从大海捞针变成了靶向定位。