Monitoring And Observability
ODTS-48: 监控与可观测性——当系统出问题时谁在看着
目标读者:想知道系统出问题时团队如何感知、如何排查、如何恢复的 BA/PM
数据来源:各项目的 logback.xml、log4j2.xml、健康检查接口、告警工具类、Eva-health 监控项目
一句话结论:日志体系完备,但可观测性集中在”看日志”层面——没有分布式追踪、没有 K8s 健康探针、没有集中的服务日志聚合、没有事故复盘文档。
监控体系全景
用户层 听云 (Tingyun APM)
└─ 前端页面性能监控
│
应用层 日志 (本地文件)
├── ~/data/logCenter/ (Odyssey 微服务)
└── ./applog/ (odts1 各模块)
│
业务层 Eva-health (Prometheus Pushgateway)
├── 交易数据统计
├── 对手方/账户数
├── 估值完成率
└── Nginx 访问分析
│
基础设施层 Kubernetes (无健康探针)
├── 无 livenessProbe
└── 无 readinessProbe
日志体系
三代日志框架
| 时代 | 框架 | 代表项目 |
|---|---|---|
| 2015-2020 | Log4j 1.x | CATS(大宗商品模块) |
| 2017-2022 | Log4j 2.x | eds-web-app, hedging-as, edsWeb |
| 2022-至今 | Logback | ofareg, odyssey 微服务, research |
日志分类方式
旧版系统(odts1)按功能模块分文件:
applog/
├── all/all.log ← 全部日志
├── err/err.log ← 错误日志
├── db/sql.log ← SQL 执行
├── pricing/pricing.log ← 定价过程
├── quote/quote.log ← 行情数据
├── risk/risk.log ← 风控计算
├── margin/margin.log ← 保证金
├── session/session.log ← 会话管理
├── msg/msg.log ← 消息通信
└── contractFile/ ← 合同文件
新系统(odyssey)按应用分文件:
~/data/logCenter/
├── odyssey-report-processing-service/
│ ├── report-service.log
│ └── report-service_error.log
├── odyssey-cash-manager-service/
│ ├── cash-manager.log
│ └── cash-manager_error.log
└── ... (每个服务一个目录)
每个服务两个文件:一个全部日志(INFO+),一个只记 ERROR。
日志留存
配置中保留 30 天(MAX_HISTORY=30)。没有发现日志归档到对象存储的配置。
业务监控:Eva-health
odts1/eva-health/ 是一个 Python 写的自定义监控代理,推送业务指标到 Prometheus Pushgateway:
# eva-health/monitor/pushgateway.py
prometheus.Pushgateway(gateway, gatewayport).push(
registry, job_name='odts_business_metrics'
)
跟踪的指标
| 指标类别 | 具体内容 | 业务意义 |
|---|---|---|
| 交易规模 | 名义本金(线性 PB / 非线性 EDS) | 每日交易量 |
| 交易规模 | 存量名义本金、成交额 | 持仓规模 |
| 交易规模 | 前 5 大客户、前 5 大标的 | 集中度风险 |
| 对手方 | 对手方数量、交易账户数、小型合约数 | 业务覆盖面 |
| EOD 批处理 | 首期/二期清算完成状态 | 批处理是否正常结束 |
| 估值表 | 待生成/已完成 | 估值日进度 |
| 事件处理 | 业务事件处理状态 | 自动化流程健康度 |
| Nginx 访问 | 热门 URL、来源 IP、状态码、响应时间 | 系统使用情况 |
| Nginx 访问 | 失败次数占比、流水趋势 | 异常检测 |
| 服务健康 | 各服务 /health/check 结果 | 服务可用性 |
| 开发活动 | Git 提交数(团队/周) | 开发效率 |
Eva-health 同时有一个 Flask 端点在端口 5000 暴露 /metrics,支持 Prometheus 拉取模式。
Grafana
Grafana 在 beta 环境中确认存在(研究笔记中提及 Grafana 日志),但Dashboard 配置不在代码仓库中。
前端 APM:听云 (TingYun)
多个前端的 K8s 部署通过 Nginx sub_filter 注入听云的 JavaScript 监控脚本:
# k8s.yml 中通过 ConfigMap 注入
sub_filter '</body>' '<script>tingyun_rum_xxx</script></body>';
sub_filter_once on;
听云监控的是:
- 页面加载性能
- 接口响应时间
- JavaScript 错误
- 用户会话
但并非所有前端都接入了听云——odyssey-swap-web 和 odyssey-rm-web 等配置中被注释掉了。
健康检查
后端健康接口
最古老的健康检查(eds-web-app):
// HealthyController.java
// 注释: "安全起见,没有用Acurator(Actuator), 自己写一个"
@RequestMapping("/health/check")
public Result health() {
// 只检查数据库连通性
return query("select 1 from dual") ? UP : DOWN;
}
这段注释本身就有信息量——开发者知道 Spring Boot Actuator 的存在,但选择自己写一个。原因可能是 Actuator 暴露的信息太多(环境变量、配置、Bean 列表等),他们想要一个更安全的实现。
Actuator 端点
新服务(Odyssey)在测试配置中启用了 Actuator:
management:
endpoints:
web:
base-path: /actuator
exposure:
include: health,metrics,prometheus
endpoint:
health:
probes:
enabled: true
但这是测试配置。生产环境中是否启用、是否暴露在公网,从代码中无法确认。
缺失:K8s 健康探针
所有 ci/k8s.yml 中都没有 livenessProbe 或 readinessProbe:
# 当前 k8s.yml —— 没有探针
containers:
- name: my-app
image: my-image
ports:
- containerPort: 8080
这意味着:
- K8s 无法自动检测服务是否死锁或僵死
- 无法在服务启动完成前阻止流量进入
- 无法自动重启无响应的 Pod
分布式追踪
单服务追踪(有)
ofareg 有自定义的 TraceIdUtils:
// 每个请求生成一个 traceId
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put("traceId", traceId);
// 每个 API 响应都带上 traceId
public class CommonResult<T> {
private String traceId; // 自动填充
}
日志中也会包含这个 traceId:
2026-07-18 14:30:22 [http-nio-8080] INFO - [42][a1b2c3d4e5f6...] com.xx.Service : 处理完成
跨服务追踪(无)
sequenceDiagram
participant Gateway as Internal Gateway
participant ServiceA as 服务 A
participant ServiceB as 服务 B
Gateway->>ServiceA: 请求 (traceId: A)
Note over ServiceA: 日志有 traceId A
ServiceA->>ServiceB: 调用
Note over ServiceB: 生成自己的 traceId B
ServiceB-->>ServiceA: 返回
Note over Gateway,ServiceB: 无法关联 A 和 B 的日志
每个服务生成自己的 traceId,没有传递机制:没有 Sleuth、没有 OpenTelemetry、没有 HTTP Header 传递。排查跨服务的慢请求只能靠人工对时间戳。
告警
企业微信
odyssey-report-processing-service 是企业微信告警的唯一使用者:
// 期权估值上传失败时
WechatUtil.sendGroupMessage(
"option-valuation-alert", // 群组 key
"186****1234", // @某个员工
"估值文件上传失败,请检查 FTP 连接"
);
带有指数退避的重试机制:
// 重试策略:1秒 → 2秒 → 4秒(最多 3 次)
retryTemplate.execute(context -> {
wechatClient.send(message);
return null;
});
短信告警
odyssey-cash-manager-service 集成了短信通知(Jums 第三方服务),但不清楚实际使用的场景。
生产告警的现状
┌─────────────────────────────────────────────────────┐
│ 告警覆盖情况 │
├─────────────────────────────────────────────────────┤
│ 有告警的: │
│ ├── 期权估值上传失败 (WeChat) │
│ └── 对手方数量异常 (Prometheus + Grafana) │
├─────────────────────────────────────────────────────┤
│ 没有告警的: │
│ ├── 服务宕机 (无 K8s 探针) │
│ ├── 数据库连接池耗尽 (Actuator 只在测试配置) │
│ ├── 磁盘空间不足 │
│ ├── 批处理超时 │
│ ├── 消息队列积压 │
│ └── 交易量异常下降 │
└─────────────────────────────────────────────────────┘
事故响应
没有找到事故文档
代码库中没有任何:
- 事故复盘记录(post-mortem)
- 应急预案(runbook)
- 故障分级定义(P0/P1/P2)
- 值班联系方式
- 回滚流程文档
这并不意味着团队没有这些流程——很可能它们存在于代码库之外(Wiki、共享文档、团队约定)。
从代码中推测的响应方式
虽然没有正式文档,但从代码中可以推断出大致的响应流程:
- 用户报障:交易员发现系统异常 → 联系 IT 支持
- 值班查看日志:登录服务器查
applog/err/err.log - 查看 Grafana:看 Prometheus 指标是否异常
- 回滚或 Hotfix:快速回滚到上一个版本,或提交修复
- 事后总结:在团队内讨论(无文档记录)
可观测性的缺失与影响
| 缺失 | 影响 | 业务后果 |
|---|---|---|
| 服务级 APM | 无法定位”慢在哪” | 排查问题耗时从分钟级变小时级 |
| 分布式追踪 | 跨服务请求无法关联 | 微服务架构下排查一个交易失败需要查 3-5 个服务的日志 |
| K8s 健康探针 | Pod 僵死不会被重启 | 服务无声宕机直到用户报障 |
| 集中日志 | 日志分散在每台机器上 | 排查需要登录多台服务器 |
| 事故文档 | 同类问题重复发生 | 每次都是”新”问题 |
| 全量告警 | 大部分异常没人知道 | 异常直到用户发现才被修复 |
错误处理
全局异常处理器
ofareg 的 GlobalExceptionHandler 是系统中最完善的异常处理机制:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ApiException.class)
public CommonResult handleApiException(ApiException e) {
return CommonResult.failed(e.getErrorCode());
}
@ExceptionHandler(BusinessException.class)
public CommonResult handleBusinessException(BusinessException e) {
return CommonResult.failed(e.getErrorCode(), e.getData());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult handleValidation(MethodArgumentNotValidException e) {
return CommonResult.validateFailed(e.getBindingResult());
}
@ExceptionHandler(Exception.class)
public CommonResult handleUnknown(Exception e) {
return CommonResult.failed(ErrorCode.UNKNOWN);
}
}
错误码体系
ofareg 定义了 60+ 个错误码,按模块分区:
10000-19999: 登录与认证
30000-39999: 请求参数错误
40000-49999: 数据库操作错误
60000-69999: 配置错误
70000-79999: 模板错误
80000-89999: 数据转换错误
90000-99999: 文件生成错误
100000-109999: 审批流程错误
130000-139999: 角色权限
160000-169999: 交易所/BCT 对接
170000-179999: 数据源
180000-189999: 未实现
190000-199999: 实时申报
统一返回格式
{
"success": true,
"errorCode": 200,
"errorMessage": "操作成功",
"traceId": "a1b2c3d4e5f6...",
"data": {}
}
重试机制
| 场景 | 重试策略 | 最大次数 |
|---|---|---|
| 期权估值上传 | 指数退避 1s→2s→4s→8s→16s→32s | 6 |
| 企业微信通知 | 指数退避 1s→2s→4s | 3 |
| BCT 提交结果同步 | 固定间隔 5s | 3 |
| 分布式锁争用 | 可配置间隔和次数 | 自定义 |
数据目录
日志配置:
odts1/hedging-as/src/main/resources/log4j2.xml → 按模块分类日志(15+ appender)
odts1/eds-web-app/config/log4j2.xml → ODTS v1 日志配置
odyssey/*/src/main/resources/logback-spring.xml → 各微服务 Logback 配置
odyssey/ofareg/common/src/main/resources/logback-spring.xml → SiftingAppender
监控:
odts1/eva-health/ → Prometheus 业务监控代理
odts1/eva-health/monitor/pushgateway.py → 推送到 Pushgateway
odts1/eva-health/health.py → 服务健康检查轮询
健康检查:
odts1/eds-web-app/src/.../HealthyController.java → 自定义 DB 健康检查
odyssey/*/src/test/resources/application.yml → Actuator 测试配置
告警:
odyssey/odyssey-report-processing-service/.../WechatUtil.java → 企业微信告警
odyssey/odyssey-cash-manager-service/.../notification/ → 短信通知
错误处理:
odyssey/ofareg/common/src/main/java/.../GlobalExceptionHandler.java → 全局异常处理
odyssey/ofareg/data-api/src/main/java/.../ErrorEnum.java → 60+ 错误码
odyssey/ofareg/common/src/main/java/.../CommonResult.java → 统一返回格式
odyssey/ofareg/common/src/main/java/.../TraceIdUtils.java → 自定义 traceId
听云 APM:
odyssey/odyssey-main-web/ci/k8s.yml → 听云脚本注入