主题
构建、部署与运维
五个模块四类镜像形态:WAR 解包进 Tomcat(server/admin/image/demo)、JAR 加密后由 entrypoint 解密启动(schedule);差异全在 Dockerfile 与启动脚本里。
1. 构建产物
| 模块 | 打包 | 产物路径 | 镜像基础层 |
|---|---|---|---|
captcha-server | WAR | captcha-server/target/captcha-server*.war | library/captcha-base:20260202 |
captcha-admin | WAR | captcha-admin/target/captcha-admin*.war | 同上 |
captcha-image | WAR | captcha-image/target/captcha-image*.war | 同上 |
captcha-demo | WAR | captcha-demo/target/captcha-demo*.war | 同上 |
captcha-schedule | JAR(依赖外置 target/lib) | captcha-schedule/target/captcha-schedule*.jar + target/lib/ | library/captcha-base:schedule |
captcha-base / captcha-common | JAR | 不单独出镜像,被上面五个依赖 | — |
常用命令:
bash
# 全量
mvn clean package -DskipTests
# 单模块(含依赖模块)
mvn clean package -pl captcha-server -am -DskipTests
# 换版本号
mvn versions:set -DnewVersion=1.3.0-SNAPSHOT -DprocessAllModules=true -DallowSnapshots=true-DskipTests 不是可选项而是事实标准:模块内 src/test 有大量 Spring Mock 测试(见《测试体系》),依赖真实中间件,无法在构建机上跑通。
2. 五个 Dockerfile 逐行解析
2.1 server / admin / image / demo(WAR 系)
四个文件结构几乎相同(captcha-server/Dockerfile 全 31 行):
| 行号 | 内容 | 作用 |
|---|---|---|
:2 | FROM private-registry.yidun.internal/library/captcha-base:20260202 | 基础镜像,内含 JDK8 + Tomcat 9.0.115 + tomcat start 包装脚本 |
:7-16 | ENV XMX_CONFIG=1024 XMS_CONFIG=1024 META_CONFIG=256 XDEBUG_ADDRESS=... JVM_EXTRA_CONFIG="" APPNAME_CONFIG=... NAGENT_MONITOR=1 NAGENT_CLUSTER_NAME="" | 全部可在 K8s 侧覆盖;JVM_EXTRA_CONFIG 是注入 -D 的唯一通道 |
:19 | ADD ${module}/target/${module}*.war /java-web-dir/webroot | WAR 落到 /java-web-dir/webroot |
:20 | COPY start.sh /java-web-dir/start.sh | 启动脚本 |
:26 | RUN unzip ${module}*.war | 镜像内解包,这是 WEB-INF/classes 遮蔽方案成立的前提 |
:30 | CMD ["/bin/bash","-c","/java-web-dir/start.sh"] | 前台启动 |
差异点:
captcha-image:XMX/XMS只给 512(:7-8),目录注释里列出icons/space/imgs三个可烤入镜像的自定义素材目录(:20-22,当前全注释)。captcha-admin:多一个CONTEXT_PATH=""(:18),用于子路径部署。captcha-demo:多CAPTCHA_STATIC_SERVER/CAPTCHA_API_SERVER(:19-20),并发写一个 Tomcat context 描述符把应用固定挂到/mgt/decision-captcha/v2/demo(:31-33);启动脚本是docker-entrypoint.sh而非start.sh(:37-40)。
start.sh(captcha-server/start.sh)的 34 行做三件事:拼 CATALINA_OPTS(:18-26,含 G1GC、HeapDumpOnOutOfMemoryError、GC 日志轮转 10×50M、-Dappname、${JVM_EXTRA_CONFIG})→ 写 bin/setenv.sh(:17-27)→ tomcat start && tail -f /dev/null(:34)。
2.2 schedule(JAR 系,两阶段加密构建)
captcha-schedule/Dockerfile 67 行,是本仓库唯一多阶段构建:
| 行号 | 内容 | 作用 |
|---|---|---|
:4 | FROM ...:schedule AS builder | 构建阶段用带工具的镜像 |
:9-10 | 拷入 target/lib 与 JAR 到 /tmp/_stage/lib | 明文 jar 只存在于 builder 层 |
:17-24 | openssl rand -base64 32 > .app.runtime.dat + tar czf - | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 | 一次一密,产出 lib.enc |
:42-43 | COPY --from=builder /out/lib.enc、.app.runtime.dat 到 /java-app-dir/approot/ | 最终镜像只有密文 + 伪装成数据文件的密钥 |
:44-49 | 中文字体包、entrypoint、清理脚本、静态 redis-cli | /java-app-dir/ 为 JAR 系根目录 |
:52-63 | 安装中文字体并 fc-cache,清 /tmp、/root/.cache | 语音/文字题面渲染依赖中文字体 |
:67 | ENTRYPOINT ["/java-app-dir/entrypoint.sh"] | 不用 CMD,保证信号直达 |
entrypoint.sh 关键行:
| 行号 | 逻辑 |
|---|---|
:12-14 | APP_HOME=/java-app-dir/approot、ENC_FILE=lib.enc、KEY_FILE=.app.runtime.dat |
:18 | RUNTIME_LIB_DIR 默认 /java-app-dir/.runtime/applib(注释说明 /dev/shm 仅 64MB 会撑爆) |
:27-34 | 密文/密钥缺失即 exit 1,避免半启动 |
:40-42 | 预清理解压目录并 chmod 700 |
:45-48 | openssl enc -d … | tar -xzf -,全程不落明文中间文件 |
:51-55 | trap cleanup EXIT INT TERM,退出即删解压目录 |
:61-62 | CLASSPATH="${CONF_DIR}:${APP_HOME}:${RUNTIME_LIB_DIR}/*",conf 最优先 |
:69-75 | JAVA_OPTS:堆、Metaspace、G1GC、OOM dump、GC 日志轮转 |
:83 | exec java … Main,java 接管 PID 1 |
运维含义:schedule 镜像里没有明文 jar,kubectl exec 也 ls 不到业务 jar;排查类冲突要去 .runtime/applib。镜像体积与启动耗时都增加约 1~3 秒(每次启动解密+解压)。
3. 两种构建脚本
| 脚本 | 适用 | 交互 | 关键行为 |
|---|---|---|---|
maven_build.sh | 本地/跳板机手工构建 | 全交互(删目录、克隆、切分支、拉代码、选模块、是否推送) | 克隆固定仓库 ssh://git@g.hz.netease.com:22222/yidun/captcha/netease-captcha-custom.git(:13),把 $module/Dockerfile 复制到根目录再 docker build .(:117,127),推 latest + 时间戳双标签(:124,136-138) |
build_image.sh | CI / 批量出镜像 | 无交互,bash build_image.sh <branch> <module...> | 见下 |
build_image.sh 六步(:47-181):docker login → 克隆/校验目录 → git fetch/checkout/pull → mvn clean package -pl <modules> -am -DskipTests(:91,可选 -s settings_wy.xml,:92-94)→ 逐模块校验产物存在再 --no-cache 构建并推送(:124-168)→ 汇总到 built_images_<时间戳>.txt。
镜像 tag 规则:<branch>-<shortHash>-<timestamp>(:77)。不要用 latest 回滚,回滚应指定完整 tag。
两个脚本共同的前提:$module/Dockerfile 里的 ADD ${module}/target/... 是相对仓库根目录的路径,所以 Dockerfile 必须复制到根目录构建;直接在模块目录 docker build 会失败。
4. 目录约定
| 路径 | 归属 | 是否持久化 | 内容 |
|---|---|---|---|
/java-web-dir/webroot | 四个 WAR | 否(镜像内) | 解包后的 WAR,含 WEB-INF/classes、WEB-INF/lib |
/java-web-dir/work/target/logs | 四个 WAR | emptyDir 即可 | 应用日志、GC 日志、heap dump |
/java-web-dir/custom-image/ | captcha-image | 是,必须 | 上传的底图/素材,RWX 卷 |
/java-app-dir/approot | schedule | 否 | lib.enc、.app.runtime.dat、conf/、logs/ |
/java-app-dir/.runtime/applib | schedule | 否(每次启动重建) | 解密后的 jar,建议 emptyDir{medium:Memory} |
/java-app-dir/entrypoint.sh、clean_voice_redis.sh | schedule | 否 | 运维脚本,clean_voice_redis.sh 用于清理语音 Redis 残留 |
5. K8s 交付清单
5.1 副本与策略约束
| 工作负载 | 副本数 | 原因 |
|---|---|---|
captcha-schedule | 必须 1,且 strategy: Recreate | 全部 @Scheduled,无分布式锁;produce worker 连进程内防重入都没有,多副本会重复生成图片、重复写统计 |
captcha-server | 可多副本 | 但含统计类 @Scheduled,多副本会重复累加(可接受但需知悉) |
captcha-admin | 建议 1~2 | 含 Prometheus 告警邮件任务,多副本会重复发信 |
captcha-image | 可多副本 | 必须 RWX 持久卷,否则各 Pod 看到不同图片 |
captcha-demo | 任意 | 纯演示页 |
captcha-schedule 的 @Scheduled 任务清单(captcha-schedule/src/main/java/.../):
| 任务 | 周期 |
|---|---|
PuzzleProduceWorker.produce | fixedDelay=60000(:66) |
CommonProduceWorker.produce | fixedDelay=60000(:83) |
SpaceProduceWorker.produce | fixedDelay=60000(:47) |
VoiceProduceWorker.produce | fixedDelay=60000(:50) |
ImageCleanUpWorker | fixedDelay=30000(:53) |
ImageDeleteSync / ImageStatusSyncJob | fixedDelay=5000(:45 / :51) |
SubVersionSwitchJob | fixedDelay=60000(:125) |
CaptchaRequestStatWork | cron=0 0 1 * * ?(:60) |
EsIndexCleanUpWorker | cron=0 0 2 * * ?(:37) |
5.2 探针
| 服务 | 探针路径 | 说明 |
|---|---|---|
captcha-server | GET /metrics | Prometheus 指标端点,进程存活即可返回 200 |
captcha-admin | GET /login.html | 唯一可靠的免鉴权页面 |
captcha-image | GET / | 首页/目录页 |
captcha-schedule | exec kill -0 1 或 ls /java-app-dir/approot/lib.enc | 无 HTTP 端口,只能 exec 探针 |
不要用 admin /health/**:spring-security.xml:187 只声明放行,没有对应 handler,必然 404,会导致 CrashLoop。
5.3 部署 YAML 片段
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: captcha-schedule
spec:
replicas: 1 # 硬约束,不可扩
strategy:
type: Recreate # 避免新旧 Pod 同时跑定时任务
template:
spec:
containers:
- name: captcha-schedule
image: private-registry.yidun.internal/neteaseis/yidun_bgp_test2/bsd-pub/captcha-schedule:<branch>-<hash>-<ts>
env:
- name: XMX_CONFIG
value: "1024"
- name: JVM_EXTRA_CONFIG
value: "-Dconfig.decrypt.key=$(SM2_PRIVATE_KEY)"
volumeMounts:
- name: cfg
mountPath: /java-app-dir/approot/conf/base.properties
subPath: base.properties
- name: runtime
mountPath: /java-app-dir/.runtime
readinessProbe:
exec:
command: ["/bin/sh", "-c", "[ -d /java-app-dir/.runtime/applib ]"]
initialDelaySeconds: 20
volumes:
- name: cfg
configMap:
name: captcha-config
- name: runtime
emptyDir:
medium: MemorysubPath 挂单文件是关键:直接挂目录到 approot/conf 会把镜像内已有文件“盖空”(ConfigMap 卷是独立的空目录)。
5.4 必需中间件矩阵
| 中间件 | 版本/形态 | 配置位置 | 是否强依赖 |
|---|---|---|---|
| MySQL 8 / PanWeiDB / Oracle | 单库多表 | base.properties:11-16 | 是(启动即连) |
| Redis(业务) | 单机/sentinel/sharding/cluster/ncr | base.properties:2-9 | 是 |
| Redis(限流高频) | 建议 cluster | frequency.properties:3-13 | 是(server 启动即连) |
| Elasticsearch | 7.17,索引 captcha_info_yyyy-MM-dd | elasticsearch.properties | 否(日志降级) |
| S3 兼容对象存储 | bucket + AK/SK | base.properties:29-33 | 是(取图) |
| Prometheus | 供 admin 5s 拉取 | base.properties:39 | 否(仅告警) |
| SMTP | smtps/465 | base.properties:44-52 + sys_conf.smtp.conf | 否(仅邮件) |
| Grafana | 仪表盘跳转 | base.properties:54 | 否 |
| Kafka | pom 有依赖,代码零使用 | — | 不需要部署 |
6. 上线检查清单
6.1 部署前
build/db_init/mysql/mysql.sql(或对应方言)已执行;45 张表齐全(PanWeiDB 脚本缺account_operate_info,需手工补)。init-data.sql已导入;SELECT COUNT(*) FROM sys_conf= 48、sys_user有admin、nc_app10 条。nc_licence与nc_license两张都写,且effective_end_time覆盖服务期。base.properties的getconf.api.server/getconf.static.servers已填(空格即启动失败)。frequency.properties指向正确的限流 Redis。- S3 bucket 可写、
/java-web-dir/custom-image/卷可写。 CONTEXT_PATH与网关 PATH 完全一致(仅 admin 子路径部署)。- 镜像 tag 用完整
<branch>-<hash>-<ts>,不用latest。
6.2 部署中
- schedule
replicas=1+Recreate已生效。 - 首次启动观察日志
[entrypoint] 解密并解压 lib与启动 Java 进程:main=com.netease.is.nc.scheduler.Main。 - server 日志出现
apiServer=..., staticServers=[...],说明配置解析成功。 - 预热等待:schedule 需跑完一轮 produce(
fixedDelay=60000),nc_image_output中status=1行数达到预期再放流量。
6.3 部署后
GET /api/v2/getconf?id=<captchaId>返回 200 且含staticServers。GET /api/v3/get?id=<captchaId>返回题面与图片 URL;图片 URL 可直接 200。GET /api/v3/check用真实轨迹跑一次,返回validate;再POST /api/v2/verify返回 success。GET /metrics有http_server_requests类指标。- admin
GET /login.html200,admin能登录且菜单不全 403。 - ES 有当日索引
captcha_info_<今天>且doc_count增长。
7. 故障排查手册
| # | 症状 | 排查命令/日志关键字 | 处理 |
|---|---|---|---|
| 1 | server 容器起不来,日志刷 apiServer conf is empty | kubectl logs;关键字 apiServer conf is empty(GetConfController.java:85) | 补 getconf.api.server / getconf.static.servers,用 ConfigMap subPath 遮蔽 |
| 2 | 取图 500 / error=201(缺少配置) | SELECT * FROM nc_app WHERE captcha_id=?;SELECT * FROM nc_license | 检查 nc_license 行是否在有效期、captcha_type_specification 是否含该类型 |
| 3 | 取图 100% 失败,/api/** 全被拦 | 日志关键字 licence、LicenceCheckInterceptor | nc_licence 的 licence_content 解密后 JSON 的 e 是否过期;license.publicKey 是否为空导致后台导入失败 |
| 4 | 后台登录后全部 403 | SELECT permission FROM sys_permission;对比 spring-security.xml 的 hasAuthority 字符串 | 补 sys_permission / sys_role_permission;新接口忘了加 XML 行也会 403 |
| 5 | 客户端拿不到题面,error=202(缺少图片集) | SELECT COUNT(*) FROM nc_image WHERE set_id=?;SELECT COUNT(*) FROM nc_image_output WHERE status=1 | 素材未导入或 schedule 未完成预热;检查 nc_image_set 的 3 个默认图集 |
| 6 | schedule 反复重启 | kubectl describe pod 看 Last State;日志 解密并解压 lib 后中断 | 检查 lib.enc/.app.runtime.dat 是否存在、/java-app-dir/.runtime 是否可写(只读根文件系统会失败) |
| 7 | 图片上传成功但客户端 404 | kubectl exec captcha-image -- ls /java-web-dir/custom-image | 多副本未用 RWX 卷,各 Pod 目录不一致;或 image.download.server 与网关不一致 |
| 8 | 高频策略“配了没用” | SELECT value FROM sys_conf WHERE key_name='frequency.enabled';redis-cli -c -h <freq-redis> keys 'freq*' | frequency.enabled 为 false,或 frequency.properties 指向了错误 Redis |
| 9 | 统计数值翻倍 | kubectl get deploy captcha-server -o jsonpath='{.spec.replicas}' | 统计类 @Scheduled 多副本重复累加;收敛副本或改造为分布式锁 |
| 10 | 后台告警邮件不发 | SELECT value FROM sys_conf WHERE key_name='smtp.conf' | 收件人列表在 sys_conf 不在 properties;SMTP 账号看 base.properties:44-52 |
| 11 | ES 索引不增长 | 日志关键字 Elasticsearch、captcha_info;curl <es>/_cat/indices/captcha_info_* | ES 不可用只降级日志,不报错;查集群地址与账号 |
| 12 | Pod OOMKilled | kubectl describe pod 的 Last State: OOMKilled;ls /java-web-dir/work/target/logs/heapdump | 提 XMX_CONFIG;schedule 解密后 classpath 在 emptyDir{Memory} 时会计入内存,注意 resources.limits |
| 13 | admin 探针 404 CrashLoop | kubectl logs 无业务异常但 Probe failed | 探针写成 /health 了,改 GET /login.html |
| 14 | 配置改了不生效 | grep key base.properties | 用了 -D 覆盖(FALLBACK 模式无效);或改的是 jar 内文件,需 ConfigMap subPath 遮蔽/approot/conf |
8. 升级与回滚
| 项 | 做法 | 兼容注意 |
|---|---|---|
| 镜像回滚 | 指定完整 <branch>-<hash>-<ts> tag 回滚,禁用 latest | schedule 每次构建密钥不同,回滚必须整体回滚镜像,不能混用 lib.enc 与 .appRuntime.dat |
| 版本号升级 | mvn versions:set 后重新构建;父子 pom 需 -DprocessAllModules=true -DallowSnapshots=true | captcha-test 的 artifactId 是 captcha-custom-test,与目录名不同,脚本按目录名找产物时取 captcha-custom-test*.jar |
| DB 升级 | 只做增量 ALTER,禁止删列 | init-data.sql 里 nc_app/nc_product 的 secret_key 是运行期凭证,重导会覆盖客户配置 |
| 配置升级 | ConfigMap 全量替换后逐 Pod 重启 | WAR 侧配置只在启动期解析,热更新无效;sys_conf 可热改 |
| 中间件升级 | ES 需在升级前确认 captcha_info_template 与 ILM 策略已 reindex 或兼容 | 模板里 dynamic_templates 把新字符串字段默认映射成 keyword,升级后新增字段做分词需重建索引 |
| 灰度 | server 可多副本灰度;admin 同理 | schedule 不能灰度,单副本且定时任务不可分割 |