Learning
VOL. VII · NO. 97 · OTC Derivatives · 19 JUL 2026

Monitoring And Observability

OTC 衍生品 · 19 JUL 2026 · 9 min read · 1,472 words
· · ·

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-2020Log4j 1.xCATS(大宗商品模块)
2017-2022Log4j 2.xeds-web-app, hedging-as, edsWeb
2022-至今Logbackofareg, 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-webodyssey-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 中都没有 livenessProbereadinessProbe

# 当前 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、共享文档、团队约定)。

从代码中推测的响应方式

虽然没有正式文档,但从代码中可以推断出大致的响应流程:

  1. 用户报障:交易员发现系统异常 → 联系 IT 支持
  2. 值班查看日志:登录服务器查 applog/err/err.log
  3. 查看 Grafana:看 Prometheus 指标是否异常
  4. 回滚或 Hotfix:快速回滚到上一个版本,或提交修复
  5. 事后总结:在团队内讨论(无文档记录)

可观测性的缺失与影响

缺失影响业务后果
服务级 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→32s6
企业微信通知指数退避 1s→2s→4s3
BCT 提交结果同步固定间隔 5s3
分布式锁争用可配置间隔和次数自定义

数据目录

日志配置:
  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                 → 听云脚本注入