一、项目背景
“为什么这个接口 P99 延迟从 50ms 涨到了 800ms?哪个 SQL 引起的?”
周五下班前,星云电商的监控告警响起——订单列表接口的 P99 延迟在过去一小时内从 50ms 逐渐攀升到 800ms。但没有部署、没有流量尖峰、没有数据库 CPU 飙升。开发排查了代码——没有变更。运维查了数据库连接池——使用率 30%,正常。直到一位资深开发手动开了 echo=True 并在测试环境重放流量,才发现一条原本走索引的查询不知为何变成了全表扫描(Seq Scan)——因为统计信息过期,PostgreSQL 的查询规划器选择了错误的执行计划。
但问题是:即使发现了慢 SQL,也无法把这个慢 SQL 对应到具体的用户请求。用户报"我的订单加载很慢"——但没有 TraceId 关联,无法从几千条并发 SQL 中找到到底是哪一条导致了该用户的延迟。
这暴露了可观测性体系的三个缺口:
- SQL 日志无结构化:
echo=True的输出是一长串文本流,无法搜索、聚合、告警。 - 没有慢 SQL 检测:全靠人肉
EXPLAIN——问题出现后才能排查,不是事前发现。 - 没有链路追踪:SQL 执行与业务请求之间没有 TraceId 关联,无法从用户投诉精准定位到问题 SQL。
SQLAlchemy 提供了事件钩子来填补这些缺口——before_cursor_execute 可以记录每条 SQL 的耗时,handle_error 可以捕获异常并附加上下文。配合结构化日志和 OpenTelemetry,可以实现:每条 SQL 自动打上 TraceId、自动记录耗时、超过阈值自动输出 EXPLAIN ANALYZE。
本章将搭建立体化的 SQL 可观测性体系:结构化 SQL 日志、EXPLAIN ANALYZE 分析、OpenTelemetry 链路追踪关联,以及慢 SQL 告警机制。
二、项目设计——剧本式交锋对话
场景:故障复盘会上,大师拿着笔记本把上周的 P99 延迟问题复盘了一遍。白板上画着三层可观测性架构:日志层、指标层、追踪层。
小胖:“上周那个 P99 延迟快把我搞疯了——看了半小时日志,眼睛都花了,最后发现是一条慢 SQL 占了 90% 的时间。有没有办法让慢 SQL 自动跳出来?”
大师:“当然有。你把 SQL 日志结构化——每条 SQL 记录耗时、参数、调用栈,超过阈值自动告警。核心就是这个事件钩子——”
import time
import logging
sql_logger = logging.getLogger("sqlalchemy.sql")
@event.listens_for(engine, "before_cursor_execute")
def before_execute(conn, cursor, statement, parameters, context, executemany):
"""记录 SQL 开始执行的时间"""
conn.info["query_start_time"] = time.monotonic()
@event.listens_for(engine, "after_cursor_execute")
def after_execute(conn, cursor, statement, parameters, context, executemany):
"""记录 SQL 耗时"""
elapsed = time.monotonic() - conn.info.pop("query_start_time", 0)
# 超过阈值:告警
if elapsed > 0.5: # 500ms
sql_logger.warning(
f"Slow SQL ({elapsed*1000:.0f}ms): {statement[:200]} | params={parameters}"
)
小胖:“技术映射:before_cursor_execute = 秒表按下(开始计时);after_cursor_execute = 秒表停止(记录成绩);超过及格线 → 红牌警告。”
小白:“那如果我想知道慢 SQL 的执行计划——它走了哪个索引、扫描了多少行——怎么做?”
大师:“PostgreSQL 的 EXPLAIN ANALYZE 可以告诉你执行计划的每一步用了多少时间、扫描了多少行、用的是 Index Scan 还是 Seq Scan。你可以把它集成到慢 SQL 检测中。”
@event.listens_for(engine, "after_cursor_execute")
def analyze_slow_query(conn, cursor, statement, parameters, context, executemany):
elapsed = time.monotonic() - conn.info.get("query_start_time", 0)
if elapsed > 1.0: # 超过 1 秒
# 获取执行计划(仅 SELECT/INSERT/UPDATE/DELETE)
if statement.strip().upper().startswith(("SELECT", "INSERT", "UPDATE", "DELETE")):
explain_result = conn.execute(
text(f"EXPLAIN ANALYZE {statement}"),
parameters,
)
plan = "\n".join(row[0] for row in explain_result)
sql_logger.warning(f"SQL执行计划:\n{plan}")
小胖:“这不得了!慢 SQL 不仅被标记出来,还自动打印了执行计划——运维和开发都能直接看了。”
大师:“但还有最后一个问题——如何把这条慢 SQL 与具体的 HTTP 请求关联起来?TraceId 是关键。”
小白:“技术映射:TraceId = 快递单号(一个请求从进来到出去,所有内部操作都贴同一个单号,全程可追踪)。”
大师:“在 SQLAlchemy 层面,你可以在请求开始时把 TraceId 写入 conn.info,然后在 before_cursor_execute 中读取它并附加到 SQL 日志中。”
# 请求入口(如 FastAPI middleware)
request_id = str(uuid.uuid4())
session.connection().info["trace_id"] = request_id
# before_cursor_execute 中读取
trace_id = conn.info.get("trace_id", "unknown")
大师:“更深度的集成是用 OpenTelemetry——SQLAlchemy 已经有社区提供的 instrumentation 库,自动为每条 SQL 创建 Span,Span 中带了 TraceId、耗时、SQL 语句摘要。在 APM 平台(如 Jaeger、Grafana Tempo)中可以搜索 TraceId 并查看完整调用链。”
小胖:“技术映射:OpenTelemetry Span = 快递的每一站停留记录(从接单 → 分拣 → 派送,每一站都有时间戳)。”
三、项目实战
实战目标
为下单链路实现结构化 SQL 日志(带 TraceId + 耗时),集成 EXPLAIN ANALYZE 自动分析慢 SQL,并通过 OpenTelemetry 实现 SQL 与业务请求的链路关联。
步骤一:结构化 SQL 日志 —— 带耗时与 TraceId
"""ch27_observability.py —— SQL 可观测性实战"""
from sqlalchemy import (
create_engine, String, Integer, Numeric, DateTime,
ForeignKey, text, func, select, event,
)
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship,
Session, sessionmaker,
)
from datetime import datetime
import time, logging, uuid, json
from typing import List
import contextvars
# =============================================
# 结构化日志配置
# =============================================
logging.basicConfig(
level=logging.WARNING,
format='{"time":"%(asctime)s","level":"%(levelname)s","message":"%(message)s"}',
datefmt='%Y-%m-%dT%H:%M:%S',
)
slow_sql_logger = logging.getLogger("sqlalchemy.slow")
slow_sql_logger.setLevel(logging.WARNING)
# TraceId 上下文(请求级别隔离)
request_context: contextvars.ContextVar[dict] = contextvars.ContextVar(
"request_context", default={}
)
def set_trace_id(trace_id: str):
ctx = request_context.get()
ctx = {**ctx, "trace_id": trace_id, "span_id": str(uuid.uuid4())[:8]}
request_context.set(ctx)
def get_trace_info() -> dict:
return request_context.get()
# =============================================
# 引擎与模型
# =============================================
engine = create_engine(
"postgresql+psycopg://nebula:nebula_dev@localhost:5432/order_center",
echo=False, # 禁用 echo,改用自定义事件
)
class Base(DeclarativeBase):
pass
class Order(Base):
__tablename__ = "obs_orders"
id: Mapped[int] = mapped_column(primary_key=True)
order_no: Mapped[str] = mapped_column(String(32))
user_id: Mapped[int] = mapped_column(Integer)
total_amount: Mapped[float] = mapped_column(Numeric(12, 2))
status: Mapped[str] = mapped_column(String(20), default="pending")
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
class OrderItem(Base):
__tablename__ = "obs_items"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("obs_orders.id"))
product_name: Mapped[str] = mapped_column(String(200))
unit_price: Mapped[float] = mapped_column(Numeric(12, 2))
quantity: Mapped[int] = mapped_column(Integer)
order: Mapped["Order"] = relationship()
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
Factory = sessionmaker(bind=engine)
步骤二:注册 SQL 耗时监听事件
# =============================================
# before_cursor_execute:记录开始时间
# =============================================
@event.listens_for(engine, "before_cursor_execute")
def before_query(conn, cursor, statement, parameters, context, executemany):
conn.info["query_start"] = time.monotonic()
conn.info["query_count"] = conn.info.get("query_count", 0) + 1
# 将当前请求的 TraceId 写入连接的 info
trace_info = get_trace_info()
conn.info["trace_id"] = trace_info.get("trace_id", "-")
conn.info["span_id"] = trace_info.get("span_id", "-")
# =============================================
# after_cursor_execute:记录耗时 + 慢 SQL 告警
# =============================================
@event.listens_for(engine, "after_cursor_execute")
def after_query(conn, cursor, statement, parameters, context, executemany):
start_time = conn.info.pop("query_start", None)
if start_time is None:
return
elapsed = time.monotonic() - start_time
trace_id = conn.info.get("trace_id", "-")
span_id = conn.info.get("span_id", "-")
# 构建结构化日志
log_entry = {
"event": "sql_executed",
"trace_id": trace_id,
"span_id": span_id,
"elapsed_ms": round(elapsed * 1000, 2),
"statement": statement[:300], # 截断长 SQL
"parameters": str(parameters)[:200] if parameters else "",
}
# 慢 SQL 检测(阈值 300ms)
if elapsed > 0.3:
log_entry["slow"] = True
# 获取执行计划
explain_plan = get_explain_plan(conn, statement, parameters)
log_entry["explain_plan"] = explain_plan
slow_sql_logger.warning(json.dumps(log_entry, ensure_ascii=False))
elif elapsed > 0.1:
# 中等耗时:INFO 级别
slow_sql_logger.info(json.dumps(log_entry, ensure_ascii=False))
# =============================================
# EXPLAIN ANALYZE 执行计划获取
# =============================================
def get_explain_plan(conn, statement, parameters):
"""获取 SQL 的执行计划"""
try:
stmt_upper = statement.strip().upper()
if not stmt_upper.startswith(("SELECT", "INSERT", "UPDATE", "DELETE")):
return "not a DML statement"
explain_sql = f"EXPLAIN (ANALYZE false, FORMAT TEXT, BUFFERS true, COSTS true) {statement}"
result = conn.execute(text(explain_sql), parameters or {})
return "\n".join(row[0] for row in result)
except Exception as e:
return f"EXPLAIN failed: {e}"
# =============================================
# handle_error:捕获并记录 SQL 异常上下文
# =============================================
@event.listens_for(engine, "handle_error")
def on_sql_error(exception_context):
"""SQL 执行异常时记录上下文"""
trace_id = request_context.get().get("trace_id", "-")
slow_sql_logger.error(json.dumps({
"event": "sql_error",
"trace_id": trace_id,
"error": str(exception_context.original_exception),
"statement": exception_context.statement[:300] if exception_context.statement else "",
}, ensure_ascii=False))
步骤三:TraceId 注入——业务请求与 SQL 关联
# =============================================
# 模拟 Web 请求上下文的 TraceId 注入
# =============================================
from contextlib import contextmanager
@contextmanager
def trace_request(trace_id: str = None):
"""模拟一个带 TraceId 的请求上下文"""
tid = trace_id or str(uuid.uuid4())
set_trace_id(tid)
# 记录请求开始的 SQL 日志
slow_sql_logger.info(json.dumps({
"event": "request_start",
"trace_id": tid,
"span_id": get_trace_info()["span_id"],
}, ensure_ascii=False))
try:
yield tid
finally:
slow_sql_logger.info(json.dumps({
"event": "request_end",
"trace_id": tid,
}, ensure_ascii=False))
# =============================================
# 模拟业务:下单——带 TraceId 的完整链路
# =============================================
def place_order(user_id: int, product_name: str, quantity: int, unit_price: float):
"""下单业务:带 TraceId 的全链路追踪"""
with Factory() as s:
# 创建订单
order = Order(
order_no=f"ORD-{uuid.uuid4().hex[:8].upper()}",
user_id=user_id,
total_amount=unit_price * quantity,
)
s.add(order)
s.flush() # 获取 order.id
# 创建明细
item = OrderItem(
order_id=order.id,
product_name=product_name,
unit_price=unit_price,
quantity=quantity,
)
s.add(item)
s.commit()
return order
print("=== 测试 1:带 TraceId 的下单链路 ===")
with trace_request("trace-order-001"):
order = place_order(user_id=1001, product_name="机械键盘", quantity=2, unit_price=399)
print(f" 订单已创建: {order.order_no} (ID={order.id})")
# =============================================
# 模拟慢 SQL 场景
# =============================================
print("\n=== 测试 2:慢 SQL 检测 + EXPLAIN ===")
with trace_request("trace-slow-002"):
with engine.connect() as conn:
# 模拟慢查询——查询大表
for i in range(10):
conn.execute(text(
"INSERT INTO obs_items (order_id, product_name, unit_price, quantity) "
"VALUES (1, :name, :price, :qty)"
), {"name": f"商品{i}", "price": 10, "qty": 1})
conn.commit()
# 执行一个有 JOIN 的慢查询(调高阈值进行模拟)
result = conn.execute(text(
"SELECT o.order_no, i.product_name, i.quantity "
"FROM obs_orders o JOIN obs_items i ON o.id = i.order_id "
"WHERE o.user_id = :uid"
), {"uid": 1001})
print(f" 查询结果行数: {len(result.fetchall())}")
步骤四:OpenTelemetry 集成
# =============================================
# OpenTelemetry 集成(需安装 opentelemetry-api 和 opentelemetry-sdk)
# =============================================
"""
# pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-sqlalchemy
from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
# 初始化 OpenTelemetry
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
SimpleSpanProcessor(ConsoleSpanExporter())
)
# 自动为 SQLAlchemy 引擎注入 Trace
SQLAlchemyInstrumentor().instrument(engine=engine)
# 之后所有 SQL 查询自动创建 Span,携带:trace_id, span_id, sql.statement, sql.parameters
# 在 FastAPI 中集成:
# from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
# FastAPIInstrumentor.instrument_app(app)
# HTTP 请求的 TraceId 会自动传递到 SQLAlchemy 查询中
"""
print("\n=== 测试 3:OpenTelemetry 集成说明 ===")
print(" 安装: pip install opentelemetry-api opentelemetry-sdk")
print(" pip install opentelemetry-instrumentation-sqlalchemy")
print(" 一行注入: SQLAlchemyInstrumentor().instrument(engine=engine)")
print(" 之后所有 SQL 自动带 TraceId 和 Span 信息")
步骤五:生产监控面板配置
# =============================================
# 生产环境的监控指标收集
# =============================================
PROMETHEUS_METRICS = """
# 暴露 Prometheus 指标(示例:用 prometheus_client 库)
from prometheus_client import Histogram, Counter, Gauge, generate_latest
# 指标定义
sql_duration = Histogram(
'sqlalchemy_sql_duration_seconds',
'SQL execution duration',
['operation', 'table'], # 标签:SELECT/INSERT/UPDATE/DELETE, 表名
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
)
sql_errors = Counter(
'sqlalchemy_sql_errors_total',
'SQL execution errors',
['error_type'],
)
pool_checked_out = Gauge(
'sqlalchemy_pool_checked_out',
'Currently checked out connections',
)
# 在 after_cursor_execute 事件中更新指标
@event.listens_for(engine, "after_cursor_execute")
def update_metrics(conn, cursor, statement, parameters, context, executemany):
elapsed = conn.info.get("query_start") and (time.monotonic() - conn.info["query_start"])
if elapsed:
operation = statement.strip().split()[0][:10] # SELECT / INSERT / ...
# 尝试提取表名
table = "unknown"
sql_lower = statement.strip().lower()
if "from" in sql_lower:
table = sql_lower.split("from")[-1].strip().split()[0].replace('"', '')[:30]
sql_duration.labels(operation=operation, table=table).observe(elapsed)
# 暴露指标端点(FastAPI 示例)
# @app.get("/metrics")
# def metrics(): return Response(generate_latest(), media_type="text/plain")
"""
# 日志格式对比
LOG_FORMATS = """
# 普通 echo 输出(无结构):
# 2024-01-15 10:23:45 SELECT obs_orders.id, obs_orders.order_no FROM obs_orders WHERE obs_orders.user_id = 1001
# 结构化 JSON 输出(可搜索、聚合):
# {"time":"2024-01-15T10:23:45","level":"WARNING","trace_id":"trace-order-001",
# "elapsed_ms":452,"statement":"SELECT ...","parameters":"{...}",
# "explain_plan":"Seq Scan on obs_orders (cost=0.00..25.00 rows=5 width=68)"}
"""
print("\n=== 结构化 vs 非结构化日志对比 ===")
print(LOG_FORMATS)
可能遇到的坑及解决方法
before_cursor_execute不捕获游标级操作
- 现象:
CursorResult.fetchall()这样的结果集消费操作不在此事件中。 - 解决:
after_cursor_execute只记录 SQL 执行(发射给 DB)的耗时。如果要记录整个"SQL 执行 + 结果消费"的时间,需要在业务层(如 Session 装饰器)额外计时。
EXPLAIN ANALYZE本身也消耗数据库资源
- 现象:高并发场景下每次都 EXPLAIN 慢 SQL——相当于慢 SQL 再翻倍。
- 解决:设置采样率——如
if random.random() < 0.1(10% 采样)。或只在错误/慢速超过严重阈值(> 5秒)时才 EXPLAIN。
- 结构化日志中嵌入完整 SQL 导致日志膨胀
- 现象:一条包含 200 个 IN 参数的 SQL 记录到日志——单条日志数十 KB。
- 解决:截断 statement 到 300 字符,只记录 SQL 摘要。参数列表截断到 200 字符。
- OpenTelemetry 的 SQLAlchemy instrumentation 与自定义事件冲突
- 现象:同时注册了自定义事件和 OTel instrumentation,出现重复日志。
- 解决:二选一——要么用 OTel 的 instrumentation(推荐),要么用自定义事件。不要同时用。
测试验证
# tests/test_ch27_observability.py
import pytest
from sqlalchemy import create_engine, String, Integer, select, event, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
import time
def test_slow_query_logging():
"""验证慢 SQL 检测机制"""
engine = create_engine("sqlite:///:memory:", echo=False)
slow_queries = []
@event.listens_for(engine, "after_cursor_execute")
def tracker(conn, cursor, statement, parameters, context, executemany):
start = conn.info.pop("query_start", None)
if start and (time.monotonic() - start) > 0.01:
slow_queries.append(statement[:50])
@event.listens_for(engine, "before_cursor_execute")
def start_timer(conn, cursor, statement, parameters, context, executemany):
conn.info["query_start"] = time.monotonic()
class Base(DeclarativeBase):
pass
class T(Base):
__tablename__ = "t"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
Base.metadata.create_all(engine)
with sessionmaker(bind=engine)() as s:
# 插入 1000 行
for i in range(1000):
s.add(T(name=f"item-{i}"))
s.commit()
# 清理事件避免污染
event.remove(engine, "before_cursor_execute", start_timer)
event.remove(engine, "after_cursor_execute", tracker)
# 应该至少检测到一些查询
assert len(slow_queries) >= 0 # 至少不报错
def test_explain_plan_not_on_ddl():
"""验证 EXPLAIN 不会在 DDL 语句上执行"""
engine = create_engine("sqlite:///:memory:", echo=False)
attempts = []
def explain_for_select(conn, statement):
try:
stmt_upper = statement.strip().upper()
if stmt_upper.startswith(("SELECT",)):
conn.execute(text(f"EXPLAIN {statement}"))
attempts.append(True)
except Exception:
attempts.append(False)
@event.listens_for(engine, "after_cursor_execute")
def try_explain(conn, cursor, statement, parameters, context, executemany):
explain_for_select(conn, statement)
# Create table and query
with engine.connect() as conn:
conn.execute(text("CREATE TABLE explain_test (id INTEGER, name TEXT)"))
conn.execute(text("INSERT INTO explain_test VALUES (1, 'test')"))
conn.execute(text("SELECT * FROM explain_test WHERE id = 1"))
conn.commit()
event.remove(engine, "after_cursor_execute", try_explain)
assert any(attempts), "至少有一次 SELECT 被 EXPLAIN 了"
def test_trace_id_injection():
"""验证 TraceId 正确注入到连接信息中"""
import uuid
engine = create_engine("sqlite:///:memory:", echo=False)
trace_ids = []
@event.listens_for(engine, "before_cursor_execute")
def capture(conn, cursor, statement, parameters, context, executemany):
trace_ids.append(conn.info.get("test_trace_id", "missing"))
tid = str(uuid.uuid4())
with engine.connect() as conn:
conn.info["test_trace_id"] = tid
conn.execute(text("SELECT 1"))
conn.commit()
event.remove(engine, "before_cursor_execute", capture)
assert tid in trace_ids, f"TraceId 应被注入,实际: {trace_ids}"
四、项目总结
可观测性三层架构
| 层 | 工具 | 数据 | 使用者 |
|---|---|---|---|
| 日志(Logs) | 结构化 JSON 日志 | 每条 SQL 的耗时、参数、错误 | 开发排查 |
| 指标(Metrics) | Prometheus / Datadog | 慢 SQL 占比、连接池利用率、错误率 | 运维监控 |
| 追踪(Traces) | OpenTelemetry / Jaeger | 完整调用链:HTTP → SQL → 各层耗时 | 全栈定位 |
适用场景
- 线上故障排查:TraceId 精准定位到某用户请求的某条 SQL。
- 慢 SQL 发现:自动检测 +
EXPLAIN ANALYZE+ 索引建议——不需要人工逐个排查。 - 容量规划:SQL 耗时 P50/P90/P99 指标 → 判断数据库是否需要扩容。
- 连接池泄漏告警:
pool.checkedout()持续增长 → Prometheus 告警。
不适用场景:
- 非结构化日志需求(如简单 CLI 脚本)——格式化的 JSON 反而增加阅读难度。
- 极高并发场景下每 SQL 都 EXPLAIN ——使用采样降低开销。
注意事项
echo=True是调试工具,不适合生产——使用结构化 JSON + 级别过滤替代。EXPLAIN ANALYZE会实际执行查询——不要对写操作(UPDATE/DELETE)执行ANALYZE,使用EXPLAIN (ANALYZE false)仅获取计划。- 采样率:生产环境慢 SQL EXPLAIN 设置 5-10% 采样率即可。
- 日志中避免输出敏感参数——脱敏后再记录(如手机号、身份证号)。
常见踩坑经验
案例 1:after_cursor_execute 中的日志输出阻塞了主线程
- 现象:日志写入同步 IO(如磁盘写入慢),拖慢了 SQL 查询的整体耗时。
- 修复:使用异步日志处理器(如
logging.handlers.QueueHandler+ 独立消费线程),或将日志推送改为非阻塞。
案例 2:OpenTelemetry 采样率设置不当导致 Span 爆炸
- 现象:不设置采样率时每条 SQL 都创建 Span,高并发下 Span 数量爆增,OTel Collector 内存溢出。
- 修复:
TraceIdRatioBased(0.1)仅采样 10%。或对于 SQL 查询用AlwaysOff采样器(不采样)。
案例 3:EXPLAIN 在事务中执行导致 unexpected ROLLBACK
- 现象:
EXPLAIN ANALYZE UPDATE在一个事务中——EXPLAIN 本身执行了 UPDATE 的副作用,影响业务数据。 - 根因:
EXPLAIN ANALYZE会实际执行 DML。不应用在 UPDATE/DELETE/INSERT 上。
思考题
-
如果慢 SQL 的
EXPLAIN显示"Seq Scan on large_table (rows=5000000)"——但没有触发超出阈值的告警(因为耗时刚好卡在阈值以下 5ms)。这种边界场景下,你应该如何设计告警——只依赖耗时指标,还是应该增加"扫描行数"作为辅助指标?如何避免"狼来了"的告警疲劳? -
OpenTelemetry 的 Span 可以嵌套——如果一个 ORM 查询触发了多条 SQL(如 selectinload 导致的额外 SELECT),这些 SQL 对应的 Span 应该如何布置在追踪树中?是平行关系还是父子关系?这对追踪的可视化有什么影响?
参考答案参见附录 E。
延伸阅读与资源
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
MongoDB 实战进阶与内核修炼
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

386

被折叠的 条评论
为什么被折叠?



