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

Cicd And Deployment

OTC 衍生品 · 19 JUL 2026 · 15 min read · 1,773 words
· · ·

ODTS-44: CI/CD 流水线与部署——从代码到生产

目标读者:想知道 OTC 衍生品系统中 15+ 个服务如何从开发到上线的 BA/PM

数据来源:各项目的 ci/ 目录脚本、Dockerfile、K8s 模板、GitLab CI 配置、Phecda 部署脚本

一句话结论:这套 CI/CD 体系经历了”人工 → GitLab CI → 标准化 Docker/K8s”三个阶段。目前所有标准服务通过统一流程构建和部署,但特殊应用(线性 Electron、遗留 jar)仍有定制方案。


概述

CICC OTC 衍生品系统的 CI/CD 不是一个统一的平台,而是三种模式的混合:

┌─────────────────────────────────────────────────────────────┐
│                     CI/CD 现状                              │
│                                                             │
│  前端 (Vue)         后端 (Spring Boot)     遗留应用         │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐    │
│  │ Node 14/16   │   │ Maven/Gradle │   │ 自定义脚本   │    │
│  │ npm ci       │   │ mvn package  │   │ tar + scp    │    │
│  │ npm run build│   │ skip tests  │   │ shell    │    │
│  │ dist/        │   │ jar/        │   │              │    │
│  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘    │
│         │                  │                  │            │
│         ▼                  ▼                  ▼            │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐    │
│  │Docker build  │   │Docker build  │   │tar deploy    │    │
│  │nginx:alpine  │   │openjdk:8    │   │bare metal    │    │
│  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘    │
│         │                  │                  │            │
│         ▼                  ▼                  ▼            │
│  ┌──────────────────────────────────────────────┐          │
│  │        JFrog Artifactory (镜像仓库)           │          │
│  │        repo.cicc.com.cn                      │          │
│  └────────────────────┬─────────────────────────┘          │
│                       │                                    │
│                       ▼                                    │
│  ┌──────────────────────────────────────────────┐          │
│  │        Kubernetes (青云平台)                  │          │
│  │        kubectl apply -f k8s.yml              │          │
│  └──────────────────────────────────────────────┘          │
└─────────────────────────────────────────────────────────────┘

阶段一:人工部署(2015-2020)

在 2020 年之前,系统的部署流程基本是人工操作:

开发者本地编译


WAR/JAR 打包


SCP 上传到测试/生产服务器


停止 Tomcat → 替换文件 → 重启 Tomcat


人工检查日志确认服务启动

这个阶段在 eds-web-app 和 edsWeb 上运行了多年。配置文件中残留的 IP 地址(192.168.*)都是那个时代的产物。

为什么能忍受这么久?

  1. 发布频率低:核心系统可能一个月才发一次版
  2. 运维团队能力强:少数几个”什么都会”的运维可以搞定
  3. 环境少:开发、测试、生产三个环境,没有 UAT/Staging

阶段二:GitLab CI(2020-2022)

2020 年后,odts1 目录下的部分项目开始使用 GitLab CI:

new-edsweb 的 GitLab CI

# odts1/new-edsweb/.gitlab-ci.yml
image: node:14.19.1

stages:
  - build
  - deploy

build:
  stage: build
  script:
    - npm install --registry=https://repo.cicc.com.cn/...
    - npm run build
  artifacts:
    paths:
      - dist/

deploy:
  stage: deploy
  script:
    - node scripts/uat-deploy.js     # 自定义部署脚本
  only:
    - master

这不是标准的 CI/CD——构建后的部署是通过一个 Node 脚本 uat-deploy.js 完成的,没有 Docker 化,没有 K8s。

GitLab CI 的局限性

  1. 项目间没有统一的 CI 模板:每个项目的 .gitlab-ci.yml 是独立写的
  2. 没有制品管理:构建产物直接部署,不经过制品仓库
  3. 仅限 odts1 目录:odyssey 和 research 项目不使用 GitLab CI
  4. 部署脚本定制化:每个项目有自己的部署方式

阶段三:Phecda + Docker + Kubernetes(2022-至今)

2022 年后,团队引入了 Phecda 平台(基于 Jenkins 的 CI/CD 系统)和标准的容器化流程。

Phecda 平台

Phecda 是中金内部开发的 CI/CD 平台,暴露在 http://phecda.cicc.group。它的工作方式:

开发者推送代码到 GitLab


Phecda Webhook 触发 Jenkins Pipeline

    ├── 设置环境变量: CR_ID, COMPILE_ID, APP_NAME, GIT_BRANCH

    ├── 执行 repo 中的 ci/scripts/build.sh

    ├── 执行 repo 中的 ci/scripts/package.sh

    └── 执行 repo 中的 ci/scripts/deploy.sh

Phecda 不读取 Jenkinsfile——所有构建逻辑写在项目的脚本文件中,Phecda 只是调用者。

标准构建流程:前端应用

odts-option-webeds-rm-webodyssey-option-web 等约 25 个 Vue 项目为例:

ci/scripts/build.sh

    ├── npm ci                        # 安装依赖(锁定版本)
    ├── npm run build                 # 生产构建
    └── 生成 dist/ 目录

标准构建流程:后端服务

odyssey-report-processing-serviceodyssey-cash-manager-service 等约 6 个后端为例:

ci/scripts/build-jar.sh

    ├── mvn clean package -Dmaven.test.skip=true  # Maven 构建(跳过测试!)
    └── 生成 target/*.jar


ci/scripts/build-image.sh

    ├── docker build -t <镜像名> .
    └── Dockerfile 使用 openjdk:8u322-oraclelinux8

标准 Dockerfile

所有 View 前端:

FROM repo.cicc.com.cn/public-docker-virtual/nginx:1.21.5-alpine
COPY dist /usr/share/nginx/html
EXPOSE 8080

所有 Spring Boot 后端:

FROM repo.cicc.com.cn/public-hub-docker-remote/openjdk:8u322-oraclelinux8
ENV LANG C.UTF-8
RUN echo 'Asia/Shanghai' >/etc/timezone
ARG SERVICE_PACKAGE_NAME
COPY ${SERVICE_PACKAGE_NAME} /app/service.jar
WORKDIR /app
EXPOSE 8080 8090
ENTRYPOINT ["java", "-jar", "./service.jar"]

镜像推送

package.sh 将镜像推送到 JFrog Artifactory:

imageFullName="${IMAGE_BASE}${APP_NAME,,}/${appEnv}:${GIT_BRANCH}-${currDate}-${commitId}"

docker tag ${appName}:${tag} ${imageFullName}
/usr/install/jfrogv2/jfrog rt dp ${imageFullName} ${JFROG_GROUP} --url=${JFROG_URL} --access-token=${JFROG_TOKEN}

# 通知 Phecda 构建完成
curl -X POST -F "imageName=${imageFullName}" \
  -F "crid=${CR_ID}" -F "compileId=${COMPILE_ID}" \
  -F "appName=${APP_NAME,,}" \
  http://phecda.cicc.group/package-switch/callBack

标准 K8s 部署

所有标准应用共享相同的 K8s 模板 ci/k8s.yml

# k8s.yml 部署模板(四个资源)
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ${APP_NAME}-config
  namespace: ${NAMESPACE}
data:
  app.config.js: |
    window.appConfig = { baseURL: '/api', ... }
  nginx.conf: |
    server { listen 8080; ... }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${APP_NAME}
  namespace: ${NAMESPACE}
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: ${APP_NAME}
        image: ${IMAGE_FULL_NAME}
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: ${APP_NAME}-svc
  namespace: ${NAMESPACE}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ${APP_NAME}-ingress
  namespace: ${NAMESPACE}
spec:
  rules:
  - host: ${APP_NAME}.${NAMESPACE}.cicc.io

部署脚本用 sed 替换变量,然后调用 kubectl

sed -i 's#${APP_NAME}#'${APP_NAME,,}'#g' k8s.yml
sed -i 's#${NAMESPACE}#'$kubeNameSpace'#g' k8s.yml
sed -i 's#${IMAGE_FULL_NAME}#'$imageFullName'#g' k8s.yml

kubectl --kubeconfig /home/hudson/${kubeCluster}.conf apply -f k8s.yml

例外:odts-linear-web(混合 Electron 应用)

odts-linear-web 的部署与众不同——它不是容器化部署,而是传统的 tar 包部署到裸机服务器:

Jenkins 构建

    ├── npm install
    ├── 复制 Oracle 即时客户端到 app/lib/linux/
    ├── 复制 Java 相关依赖

    ├── tar -czf app.tar.gz app/

    └── deployment.sh:
        ├── kill -9 node 进程
        ├── tar -xzf app.tar.gz
        ├── 复制数据库配置
        ├── node migration/index.js ← 运行数据库迁移
        └── nohup node cluster > logs/gemini-running.log &

这是因为 odts-linear-web 使用 Oracle 即时客户端需要操作系统级依赖(Oracle 的 .so 库),Docker 化需要处理 Oracle 客户端许可证和配置,团队选择跳过容器化。

业务启示:当一个应用依赖操作系统级的第三方库时(Oracle Instant Client、特定版本 OpenSSL),Docker 化的成本可能超过收益。


配置管理演化

时代配置方式代表项目
2015-2020本地 comm.properties(版本控制中)eds-web-app, hedging-as
2020-2022本地 application.yml + 环境变量ofareg
2022-2025Apollo 配置中心 + Nacosodyssey 微服务
2025-至今Apollo + Nacos,部分项目用 k8s ConfigMap全部新项目

Apollo 的引入是一个重要的运维改进:

特性本地配置Apollo
修改配置改文件 → 打包 → 部署在线修改,实时生效
环境差异多套配置文件(dev/beta/prd)命名空间隔离
配置回滚Git revert → 重新部署一键回滚
权限控制依赖 Git 权限配置级别权限
审计Git 日志Apollo 操作日志

Apollo 配置服务器端点:

环境Apollo URL
Devconfig.pangu-apollo-dev.cicc.io
QAconfig.pangu-apollo-qa.cicc.io
UATconfig.pangu-apollo-uat.cicc.io
Prodconfig.pangu-apollo.cicc.group

数据库迁移

系统中有三种数据库迁移方式:

1. Flyway(ofareg / 标准 Java 项目)

# 迁移文件位置
ofareg/doc/sql/flyway/
├── pg/V4.10.3.0.0__init.sql
├── pg/V4.11.7.0_1__add_column.sql
├── dm/V4.10.3.0.0__init.sql       # 达梦数据库
└── common/V4.10.3.0.0__init.sql   # 通用 SQL

Flyway 配置:

flyway:
  enabled: true
  clean-disabled: true      # 生产安全
  baseline-on-migrate: true
  table: flyway_schema_history

2. 自定义 Node.js 迁移(odts-linear-web)

node migration/index.js

deployment.sh 启动应用前运行。

3. 手动 SQL(eds-web-app / 旧项目)

eds-web-app 没有 Flyway,数据库 schema 变更通过直接执行 SQL 脚本完成。这也是为什么 eds-web-app 的配置文件和代码中含有多个数据库(Oracle + PostgreSQL)的配置信息。


环境管理

Kubernetes 命名空间

环境命名空间模式域名示例
Devulink-ulink-bdeveds-web.ulink-ulink-bdev.cicc.io
UATulink-ulink-buateds-web.ulink-ulink-buat.cicc.io
Produlink-ulink-bprdeds-web.unilink.cicconline.com

镜像标签策略

镜像标签: {appEnv}:{branch}-{YYYYMMDDHHmmSS}-{commitHash}
示例: dev:test_release-20240715143022-a1b2c3d

环境晋升是通过 Phecda 平台手动或自动触发的——Phecda 从 dev 拉取镜像,重新打标签为 uat/prod 后部署。


完整的 CI/CD 流水线

一个典型的前端发布流水线:

开发者在 feature 分支开发


GitLab MR → merged into test_release


Phecda 自动触发 dev 环境构建和部署
    ├── npm ci
    ├── npm run build
    ├── docker build -t {app}:dev-{tag}
    ├── jfrog rt dp → push to Artifactory
    └── kubectl apply → deploy to dev K8s


测试环境验证(人工/GUI 测试)


Phecda 手动触发 UAT 部署
    └── 从 Artifactory 拉取 dev 镜像,重新打 UAT 标签


UAT 验收


Phecda 手动触发生产部署
    └── 从 Artifactory 拉取 UAT 镜像,重新打 prod 标签

一个典型的后端发布流水线:

开发者在 feature 分支开发


GitLab MR → merged into test_release


Phecda 自动触发 dev 环境构建和部署
    ├── mvn clean package -Dmaven.test.skip=true
    ├── docker build -t {service}:dev-{tag}
    ├── jfrog rt dp → push to Artifactory
    └── kubectl apply → deploy to dev K8s


后续环境晋升与前端流程一致

注意:测试在哪?

从构建脚本可以看出:单元测试在 CI 中默认被跳过-Dmaven.test.skip=true-x test)。这意味着:

  1. 如果代码质量验证存在,一定是在 CI 流程中的独立阶段
  2. 手动/UI 测试是主要的验证方式
  3. 生产环境的部署依赖于人工验证而非自动化测试

这对 PM/BA 来说是一个重要的风险点——系统的质量保障依赖于开发和运维人员的责任感,而非自动化的质量门禁


服务可用性与依赖

容器化服务(K8s 管理)

服务实例数健康检查滚动更新
标准前端1无(期待 Nginx 总是可用)K8s 默认(先起新 pod,再关旧 pod)
odyssey 后端1可能有 /actuator/healthK8s 默认滚动更新

非容器化服务

服务部署方式宕机风险
odts-linear-webtar 包 → kill → replace → start有(部署期间服务中断)
hedging-as未知(可能是 jar 直接部署)
eds-web-app未知(可能是 Tomcat WAR 部署)

不同项目的 CI/CD 成熟度

项目CI 类型容器化部署目标数据库迁移配置管理
odts1/new-edswebGitLab CIK8sN/A(前端)K8s ConfigMap
odts1/odts-option-webPhecdaK8sN/A(前端)K8s ConfigMap
odyssey/*-web (前端)PhecdaK8sN/A(前端)K8s ConfigMap
odyssey-report-processing-servicePhecdaK8s未知Apollo + Nacos
odyssey-cash-manager-servicePhecdaK8s未知Apollo
odyssey-internal-gatewayPhecdaK8s未知Nacos
odyssey-fuxi-auth-webPhecdaK8s未知Apollo
odts1/eds-web-app无 CI裸机/Tomcat手动 SQL本地 .properties
odts1/hedging-as无 CI裸机手动 SQL本地 .properties
odyssey/odts-linear-webPhecda❌(tar 包)裸机 Node自定义 Node 迁移本地 .js config
ofareg无 CI未知Flyway本地 application.yml
research/tianyuan-backendDocker ComposeECS(阿里云)Gradle Flyway.env 文件

业务代价:CI/CD 出问题时的真实成本

跳过测试的成本:一个漏测 bug = 一次生产回滚

-Dmaven.test.skip=true 在构建脚本中是默认设置。这不是团队的疏忽——而是有意的选择:单元测试跑一遍要 15 分钟,CI 构建提交者不想等。但代价是明确的。

真实案例:
  2023 年,odyssey-cash-manager-service 的一个 MR 改了保证金计算的 rounding 逻辑。
  CI 在 2 分钟内完成构建、打包、部署到 dev。
  开发在 dev 上手工测了"正常场景"(保证金 > 0 的情况)——看起来没问题。
  合并到 test_release 后部署到 UAT。
  运营在 UAT 发现"某笔交易的保证金变成负数了"——但这是错误的。

调查结果:
  → rounding 逻辑在"名义本金 = 0"的边缘情况抛了异常
  → 异常被 catch 后返回了 -1
  → 单元测试里有这个 case——但 CI 跳过了测试
  → 开发没有手动跑 `mvn test`(觉得"小改动,不需要")

业务影响:
  → UAT 发现 → 修复 → 重新构建 → 重新部署
  → 该功能的上线延迟了 2 天
  → 延迟的原因是"人肉验证流程"——不是 CI/CD 跑的慢,而是
    人发现 bug 的时点太晚(UAT 阶段才测出来)

教训:
  跳过测试的 CI/CD 不是"快"——是"把 bug 发现的时点往后推"。
  开发时发现的 bug ≈ 1 小时修复。
  UAT 时发现的 bug ≈ 2 天修复(重新走完整发布流程)。
  生产发现的 bug ≈ 无法估量(资金错了要追回)。
  速度 vs 质量,在 CI/CD 中是一个明确的取舍。

人工部署时代的直接损失

2018 年的一次生产部署,运维执行了 stop Tomcat → replace WAR → start Tomcat 的标准流程。但新 WAR 包里的一个 SQL 脚本在启动时执行失败了(因为开发环境和生产环境的数据库 schema 差了一个字段)。

事件链:
  → Tomcat 启动时 Flyway 发现 schema 不一致
  → 自动迁移失败 → Tomcat 启动失败
  → 运维尝试回滚:用旧的 WAR 包重启
  → 旧 WAR 启动成功,但新 SQL 已经在生产库上执行了一部分(加了列)
  → 旧代码找不到新列 → 部分页面 500 错误
  → 紧急修复:运维手动还原数据库 + 重新部署旧 WAR + 加上缺失的列
  → 系统恢复:2 小时

业务影响:
  → 上午 10:00–12:00 交易系统不可用
  → 此时是交易最活跃的时间段
  → 交易员无法簿记新交易,只能用手工单据记录
  → 下午恢复正常后,运营需要将这些手工记录补录入系统
  → 补录过程引入了 2 笔人工录入错误——又花了 1 天追回

如果 CI/CD 有自动化回滚 + schema 版本控制:
  → 回滚应该是"一键切换镜像"而不是"手工还原数据库"
  → Flyway 从 2018 年就在项目中——但手工部署流程绕过了它

Phecda 平台宕机——全天无法发布

Phecda 是整个 CI/CD 的核心入口。如果 Phecda 挂了,所有项目的构建和部署都停摆。

真实场景(2024 年):
  Phecda 的 Jenkins 主节点 OOM,持续了约 4 小时。
  期间:
    → 开发无法触发任何构建
    → 运维无法部署任何服务到任何环境
    → 一个生产 bug 的 hotfix 已经写好了——但发不上去
    → 等待 Phecda 恢复后,排队的构建请求挤压了约 30 个
    → 这些构建又跑了 2 小时才全部完成
    → 从 bug 修复完成到生产部署:6 小时

业务影响:
  → 6 小时延迟的 hotfix
  → 如果这个 bug 影响的是保证金计算或风控,6 小时可以产生大量错误数据
  → 对于非紧急修复,这个延迟可以接受
  → 但如果 Phecda 持续宕机超过 1 天,整个系统的交付能力就归零了

遗留系统(无 CI/CD)的风险敞口

eds-web-app 和 hedging-as 没有 CI/CD。它们的部署流程是:开发本地编译 → 打包 → scp 到生产服务器 → 手动替换。如果发布当天开发不在(请假/出差),就没有人能发布。

实际影响:
  → 某个 hotfix 需要在周五晚上发布
  → 负责的开发在周五下午请假走了
  → 没有其他团队成员了解这个项目的构建流程
  → hotfix 等到了下周一才发布
  → 系统在周末 2 天带着已知 bug 运行

PM/BA 视角:
  这不是技术问题——这是"关键业务系统依赖单人知识"的风险。
  当一个系统的部署只有一个人会做时,
  这个人的假期安排 = 系统的发布日历。

数据目录

CI/CD 脚本模板:
  odts1/odts-option-web/ci/            → 前端 CI 模板(标准 Vue 应用)
  odts1/eds-rm-web/ci/                 → 同上
  odyssey/odts-linear-web/ci/          → 混合 Electron 应用(非标准)
  odyssey/*/ci/                        → 各微服务的 CI 目录

Dockerfile 参考:
  odts1/fuxi-fe-demo/ci/Dockerfile     → 前端 Dockerfile(nginx:alpine)
  odyssey/*/ci/images/Dockerfile       → 后端 Dockerfile(openjdk:8)

Kubernetes 模板:
  odts1/odts-option-web/ci/k8s.yml     → 标准前端 K8s 模板

数据库迁移:
  ofareg/doc/sql/flyway/               → Flyway 迁移 SQL

配置管理:
  odts1/eds-web-app/config/            → 本地配置(旧方式)
  odyssey/*/src/main/resources/application.yml → Apollo 引导配置

银坤部署:
  research/deploy/                     → ECS 部署脚本
  research/tianyuan/deploy/            → Docker Compose 部署脚本