第二十章:日志与调试:结构化日志、分布式追踪与核心转储分析
做量化交易系统,尤其是做市商系统,最怕什么?
怕半夜被电话叫醒,说系统出问题了。更怕的是,你连问题出在哪都不知道。
日志和调试,就是你的「黑匣子」。今天我们就聊聊,怎么把这个黑匣子做得靠谱。
结构化日志:别再用 print 了
我见过不少团队,日志还是用 print 或者简单的字符串拼接。说实话,这在开发阶段凑合能用,一上生产就完蛋。
为什么?因为字符串日志没法被机器高效解析。你想查某个订单的所有日志,得用 grep 去搜,搜出来一堆乱七八糟的东西。
核心思路:用 JSON 格式输出日志。每条日志都是一个结构化的数据对象。
举个例子,这是传统的日志:
2024-01-15 10:30:45.123 INFO Order placed: id=12345, symbol=BTC-USDT, price=50000, qty=0.1
这是结构化日志:
{
"timestamp": "2024-01-15T10:30:45.123Z",
"level": "INFO",
"logger": "OrderManager",
"message": "Order placed",
"order_id": "12345",
"symbol": "BTC-USDT",
"price": 50000,
"quantity": 0.1,
"side": "BUY",
"strategy": "market_making_v2"
}
你想想看,后者可以直接扔进 Elasticsearch 或者 ClickHouse,想怎么查就怎么查。按 order_id 聚合、按 symbol 过滤、按时间范围检索,都特别方便。
我个人习惯用 Python 的 structlog 库。它比标准 logging 好用得多:
import structlog
logger = structlog.get_logger()
# 绑定上下文
log = logger.bind(order_id="12345", symbol="BTC-USDT")
# 输出结构化日志
log.info("order.placed", price=50000, qty=0.1, side="BUY")
# 自动带上时间戳、调用位置等信息
# 输出:{"event": "order.placed", "order_id": "12345", "symbol": "BTC-USDT", "price": 50000, ...}
小技巧:在日志里加一个 request_id 字段。这样一次交易请求的所有日志,都能串起来。我习惯在网关层生成这个 ID,然后透传到所有下游服务。
分布式追踪:OpenTelemetry 实战
做市商系统通常不是单体的。订单管理、风控、行情、策略执行,可能分布在不同的服务里。
问题来了:一个订单从发起到成交,经过了哪些服务?每个环节花了多少时间?
这就是分布式追踪要解决的问题。
我用 OpenTelemetry 比较多。它是 CNCF 的项目,生态好,支持的语言也多。
先看一个简单的例子,在 Python 里埋点:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# 初始化
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# 在关键路径埋点
with tracer.start_as_current_span("place_order") as span:
span.set_attribute("order_id", "12345")
span.set_attribute("symbol", "BTC-USDT")
# 调用风控服务
with tracer.start_as_current_span("risk_check") as child_span:
child_span.set_attribute("risk_result", "PASS")
# ... 实际调用逻辑
# 调用交易所接口
with tracer.start_as_current_span("exchange_submit") as child_span:
child_span.set_attribute("exchange", "BINANCE")
# ... 实际调用逻辑
这段代码会在每个 span 里记录开始时间、结束时间、属性。最后统一发送到 Jaeger 或 Zipkin 这样的后端。
我在项目中遇到过:有一次系统延迟突然飙升,查了半天找不到原因。后来用 Jaeger 看追踪数据,发现是某个 Redis 操作超时了。没有分布式追踪,这种问题根本没法定位。
下面这张图展示了分布式追踪的核心逻辑:
注意:不要每个函数都埋点。只埋关键路径:订单生命周期、风控检查、交易所交互、资金划转。埋点太多会影响性能,而且数据量太大,存储成本也高。
核心转储分析:当程序崩溃时
结构化日志和分布式追踪能解决大部分问题。但有一种情况它们帮不上忙:程序直接崩溃了。
比如 C++ 写的行情引擎,突然 Segmentation Fault。日志还没来得及写,进程就没了。
这时候就需要核心转储(Core Dump)。
核心转储是进程崩溃时,操作系统把进程的内存映像保存下来。你可以用 GDB 去分析它,看看崩溃时程序在干什么。
先配置系统允许生成 core dump:
# 查看当前限制
ulimit -c
# 设置无限制
ulimit -c unlimited
# 设置 core 文件路径和命名格式
echo "/tmp/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
然后模拟一个崩溃场景:
// crash_demo.cpp
#include <iostream>
#include <csignal>
void handle_signal(int sig) {
std::cerr << "Caught signal: " << sig << std::endl;
// 这里可以做一些清理工作
exit(1);
}
int main() {
// 注册信号处理
signal(SIGSEGV, handle_signal);
int* p = nullptr;
*p = 42; // 故意崩溃
return 0;
}
编译并运行:
g++ -g -o crash_demo crash_demo.cpp
./crash_demo
# 会在 /tmp 下生成 core 文件
用 GDB 分析:
gdb ./crash_demo /tmp/core.crash_demo.12345.1705312345
# 在 GDB 中
(gdb) bt # 查看调用栈
(gdb) info registers # 查看寄存器
(gdb) frame 0 # 切换到崩溃帧
(gdb) list # 查看源代码
(gdb) print p # 打印变量值
我曾经遇到过一个内存越界问题,程序运行几小时才崩溃。用 core dump 分析后发现,是一个 vector 的迭代器失效了。没有 core dump,这种间歇性崩溃根本没法复现。
这里有个表格,总结三种调试手段的适用场景:
| 手段 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 结构化日志 | 日常监控、问题排查 | 可搜索、可聚合、可告警 | 需要提前埋点 |
| 分布式追踪 | 跨服务调用链路分析 | 可视化、定位瓶颈 | 有一定性能开销 |
| 核心转储 | 程序崩溃、内存问题 | 完整的内存快照 | 文件大、分析门槛高 |
嗯,这里要注意一点:生产环境要不要开 core dump?
我的建议是:开,但要有限制。比如只保留最近 3 个 core 文件,或者按大小限制。不然一个 core 文件可能几个 GB,磁盘很快就满了。
另外,core dump 里可能包含敏感数据(比如内存中的密钥)。生产环境要做好访问控制,别让所有人都能看。
好了,日志和调试这块就聊这么多。记住一句话:没有日志的系统,等于没有监控。没有调试手段的系统,等于没有安全网。
无相订单流研究社 微信Lucian808555