主题
风险清单与改进建议
所有条目均有代码/配置行号实证,不写"可能存在问题"这类推断;优先级按「是否造成数据错误或服务不可用」判定。
优先级定义
| 级别 | 判定标准 | 处置时限 |
|---|---|---|
| P0 | 已造成或必然造成线上数据错误、服务不可用、凭据泄露 | 立即修复,禁止带病交付 |
| P1 | 特定部署形态/配置下造成可用性或一致性缺陷,且发现成本高 | 本迭代内修复或在交付文档强制约束 |
| P2 | 长期债:依赖膨胀、可维护性、攻击面扩大,不影响当前功能 | 排期治理 |
R-01 captcha-schedule 必须单副本,但无任何技术强制
现象:全模块任务都挂在 Spring @Scheduled 上,无分布式锁、无 leader 选举、produce worker 甚至没有进程内防重入;一旦部署 2 副本,图片会被重复生成、统计会被重复累加、删除任务会并发执行。
证据:
- 任务全部是
@Scheduled:PuzzleProduceWorker.java:66、VoiceProduceWorker.java:50、CommonProduceWorker.java:83、SpaceProduceWorker.java:47、SubVersionSwitchJob.java:125(均 60s);ImageCleanUpWorker.java:53(30s)、ImageStatusSyncJob.java:51/ImageDeleteSync.java:45(5s)、EsIndexCleanUpWorker.java:37(0 0 2 * * ?)、CaptchaRequestStatWork.java:60(0 0 1 * * ?)。 - 调度入口极简,无副本协调:
Main.java:11-15仅new ClassPathXmlApplicationContext("scheduler-application-context.xml");全模块检索tryLock|setnx|RedisLock|synchronized零命中。 - produce worker 连内存标记都没有:
PuzzleProduceWorker.java:68-73→:80-99,exec.submit(puzzleFillTask)前只判isNeedRefill()(查 DB/Redis 水位,不是锁)。
影响:图片重复生成吃满 CPU(OpenCV 生成为重计算);CaptchaRequestStatWork 与日统计按副本数翻倍污染报表;两个副本并发跑清理任务会删除同一目标(ES 删索引近似幂等,图片文件删除会报错并残留脏状态)。
建议:交付层用 replicas: 1 + strategy: Recreate(不要 RollingUpdate,否则升级窗口内会出现 1→2 的瞬时双副本重叠);代码层在 AbstractWorker 统一前置 Redis SET NX PX 租约;SubVersionSwitchJob 属全局生效操作,单独加锁。
优先级:P0
R-02 captcha-server / captcha-admin 的 @Scheduled 在多副本下重复执行
现象:两个 WAR 都带定时任务,语义是「聚合写统计」「发告警邮件」,副本数即重复倍数。
证据:
captcha-server6 处:DeviceStatHandler.java:82、LocationStatHandler.java:154、StatHourly.java:104(均cron=5 * * * * *)、HourlyIpService.java:131(cron=0 0 * * * ?)、ZhuoWangIpEnrichmentService.java:120、AbstractListManagementSync.java:155。captcha-admin:PrometheusMetricsJob.java:56(fixedDelay=5000)→:63-111拉 Prometheus → 超阈值sendCombinedAlertEmail(:369-389)真实发信。- 唯一副本保护是
is.backup,且只作用于 admin 的告警任务:PrometheusMetricsJob.java:58读baseProperties['is.backup'],而base.properties:58默认false,即默认全副本发邮件;server 的 6 个任务无任何is.backup判断。
影响:3 副本 = 3 倍统计值(请求量、设备、地域分布全错)+ 3 倍告警邮件(告警风暴);名单同步重复执行放大 DB 查询与 Redis 写入。
建议:统计类改为「按 pod 上报原始计数、查询侧求和」或加 Redis 日维度唯一键;告警任务必须加锁或外挂单副本 CronJob;把 is.backup 语义扩展为「定时任务归属开关」并在多副本部署时强制开启。
优先级:P1
R-03 License 双表双套代码,配一个就有一半功能不可用
现象:仓库里存在 nc_licence 与 nc_license 两张表、两套 Mapper/Service、两套 admin 控制器,各读各的,互不感知。
证据:
- 旧链(
nc_licence):captcha-base/src/main/java/com/netease/is/nc/base/licence/Licence.xml:26-113(SQL 全部指向nc_licence);LicenceServiceImpl.java:75-118每 5s 全量重载,licenceMapper.queryAll()后逐条rsaDecryptByPk(Hex.decodeHex(...))解licence_content,按 JSON 内e字段判过期;queryHaveLicence()见:121-129。消费者是captcha-server的/api/**准入拦截(LicenceCheckInterceptor.java:27-32)。 - 新链(
nc_license):captcha-base/src/main/java/com/netease/is/nc/base/license/License.xml:45-132;LicenseServiceImpl.java:133实现canUseCaptcha(captchaType, typeEnhance),看effective_start_time/end_time+captcha_type_specification+max_business_num;消费者是取图/发声能力校验(GetCaptchaService.java:103、GetControllerV2.java:207、SysConfLoader.java:690-702)与 admin 配置校验(AppController.java:276、FrequencyController.java:191)。 - 后台并列两个控制器目录:
captcha-admin/.../admin/licence/LicenceController.java与captcha-admin/.../admin/license/LicenseController.java。
影响:只写 nc_licence → /api/** 放行但取图报「license不支持验证码类型」;只写 nc_license → 取图正常但所有请求被 no valid licence 拒绝并返回 LICENCE_ERROR。两种都是一半可用,且报错文案被刻意隐藏(LicenceCheckInterceptor.java:29 注释「对外隐藏错误描述」),排障成本高。
建议:交付文档把「License 必须双写」写成硬前置并给验证清单(/api/v2/get 返回 200 且 check 不返回 LICENCE_ERROR);代码层合并为单一 LicenseService 兼容读,或在 SysConfLoader 启动时校验双表一致性并打 ERROR;长期删除旧链前需确认 nc_licence 存量迁移路径。
优先级:P0
R-04 -D / 环境变量无法覆盖 base.properties 已有 key
现象:运维用 JVM_EXTRA_CONFIG="-Ddb.url=..." 覆盖配置,实际不生效,服务仍连镜像内默认库。
证据:
- 配置源唯一:
base-application-context.xml:16-19的basePropertyConfigurer只挂classpath:base.properties。 - 该类未改
systemPropertiesMode:DecryptingPropertyPlaceholderConfigurer.java:16-24只重写convertPropertyValue做Enc(...)解密,其余继承父类默认值——即SYSTEM_PROPERTIES_MODE_FALLBACK,-D仅在 key 缺失时兜底。 base.properties:1-80里 DB、Redis、S3、SMTP 等 key 全部有非空默认值,故-D永不生效。
影响:最典型的「改了配置但没生效」故障——连错库、图片传到演示 S3、告警发到内部邮箱,日志无任何异常(因为配置本身"合法")。
建议:WAR 统一用「同名文件遮蔽」:ConfigMap + subPath 挂 /java-web-dir/webroot/WEB-INF/classes/base.properties(WEB-INF/classes 优先于 WEB-INF/lib);captcha-schedule 用 approot/conf/*.properties(entrypoint.sh:61-62 中 conf 在 classpath 最前);要支持 -D 覆盖需显式 setSystemPropertiesMode(SYSTEM_PROPERTIES_MODE_OVERRIDE),属行为变更需回归。
优先级:P1
R-05 限流走独立的第二套 Redis,且不可被 -D 覆盖
现象:业务缓存与限流计数在两个不同 Redis 集群;限流不可用不影响取图,但会静默失去限流保护。
证据:
captcha-server/src/main/resources/frequency.properties:1-13:redis.type=4(cluster)、redis.nodes=10.44.45.74:6001,6002,6003、redis.server.pwd=Yidun@2024(明文)、frequency.refresh.interval=1。- 与
base.properties:3-9(redis.redisType=1单机)完全独立,是两套 Redis 客户端与连接池;该文件被硬编码 classpath 读取,无外部化入口。
影响:只配了 base.properties 的 Redis → 限流连演示集群或连不上,高频统计整体降级。按 docs/frequency_redis.md 契约,StatResult.unavailable()(sum=-1)使 sum > threshold 恒 false——限流静默失效,不报错不告警。
建议:交付清单把 frequency.properties 与 base.properties 并列为主配置项;限流 Redis 连接失败应在启动期打 ERROR 并入 Prometheus;长期把两套 Redis 合并到 frequency.redis.* 命名空间。
优先级:P1
R-06 getconf.api.server / getconf.static.servers 为空导致 server 启动失败
现象:用 conf/base.properties.template 起服务,captcha-server 直接起不来。
证据:
- 模板两键均为空:
captcha-base/src/main/resources/conf/base.properties.template:40,42。 - 消费点是
@Value强注入:captcha-server/src/main/java/com/netease/is/nc/server/api/GetConfController.java:71,73。 - 可用参考值:
base.properties:24-26。
影响:部署第一道拦路虎,报错是占位符解析/使用期空指针,与"缺配置项"因果不直观。该键决定 getConf 下发的接口与静态资源域名,填错比启动失败更糟——全量客户端请求打到错误域名。
建议:模板改占位符并在注释标注「必填、不带协议」;加 @PostConstruct 判空校验;注意 getconf.api.server 下发的是带服务标识的完整前缀,代理层不得剥离或改写(docs/联通支付验证码代理方案.md:30)。
优先级:P1
R-07 Kafka / Dubbo / Redisson / Curator 依赖零使用
现象:根 pom 声明了消息与 RPC 生态版本,但无模块引用、无代码使用。
证据:
- 根
pom.xml:309-319(spring-kafka,已排除kafka-clients)、pom.xml:476-479(dubbo 2.8.4-nisp)、pom.xml:606-608(redisson)、pom.xml:486-518(zookeeper+curator-*)。 - 8 个子模块 pom 对上述 artifact 零引用;代码侧
kafka唯一命中是注释captcha-base/src/main/java/com/netease/is/nc/base/log/LogMessageObj.java:120。
影响:当前不影响运行,但误导架构判断(以为有消息链路)、拖慢依赖审计;一旦有人误引 spring-kafka 会因已排除 kafka-clients 而必然缺类。
建议:清理 dependencyManagement 中未使用的坐标,或注释标注「历史遗留,勿引」。
优先级:P2
R-08 明文密码、硬编码仓库凭据与固定路径
现象:仓库内多处明文敏感信息,且部分随镜像分发到客户环境。
证据:
| 位置 | 内容 |
|---|---|
captcha-base/src/main/resources/base.properties:16 | db.password=eHSkDv7c77*j(明文) |
base.properties:29-30 | s3.accessKey / s3.secretKey 明文 AWS 凭据 |
base.properties:48 | smtp.password 明文 |
base.properties:68 | sso.appSecret 明文 |
base.properties:78-79 | zhuowang.ip.app-key / app-secret 明文 |
captcha-server/src/main/resources/frequency.properties:7 | redis.server.pwd=Yidun@2024 明文 |
captcha-server/pom.xml:390-391 | 镜像仓库 username=bsd-harbor / password=BsdHarbor321 硬编码在 pom |
base.properties:12,20,24,39,54,65-66 | 内网 IP、测试域名、Grafana dashboard ID 等固定路径 |
影响:交付物即凭据泄露源;客户未逐项替换,等于把演示环境数据库/对象存储暴露在客户机房。项目本身已具备加密能力(PropertiesEncryptUtil + Enc(...) 前缀 + DecryptingPropertyPlaceholderConfigurer.java:19-23),只是默认配置没用起来。
建议:口令类全改 Enc(...),私钥经 -Dconfig.decrypt.key 注入;从 captcha-server/pom.xml 移除 registry 凭据,改走 CI 变量;交付前跑凭据扫描(S3 AK、口令、内网 IP),把 base.properties 收敛为全占位符模板。
优先级:P0
R-09 captcha-admin 的 /health/* 只有映射与放行规则,没有 handler
现象:按 web.xml 与安全配置推断 /health/online 可用,实际全部 404。
证据:
- Servlet 映射 4 个路径:
captcha-admin/src/main/webapp/WEB-INF/web.xml:82-85(/health/{online,check,status,offline})。 - 安全配置显式放行:
captcha-admin/src/main/resources/spring-security.xml:187,203(<intercept-url pattern="/health/**" access="permitAll"/>)。 - 全仓检索
/health只命中上述两文件,无任何 Controller 映射(admin 的api/包只有NormalController、PrometheusMetricsController)。
影响:K8s 探针若配 /health/check → 持续 404 → 判定不健康 → 反复重启 → 后台不可用。典型"配置看起来支持、实际没实现"陷阱。
建议:探针改用真实存在的 GET /login.html;要么补齐 HealthController(返回 200 + DB/Redis 连通性),要么删掉 web.xml 死映射。
优先级:P1
R-10 license.publicKey 为空不阻塞启动,但取图 100% 失败
现象:服务能起、/api/** 能过、日志无异常,但所有 get 请求报「license不支持验证码类型」。
证据:
- 默认值为空:
captcha-base/src/main/resources/base.properties:62(license.publicKey=,注释已说明「部署时请使用 License 签发方提供的实际公钥覆盖」)。 - 注入点:
captcha-base/src/main/java/com/netease/is/nc/base/license/LicenseServiceImpl.java:49-50(@Value("${license.publicKey}")),用于:268的RSAUtils.decryptByPublicKey。 - 强校验点:
captcha-server/src/main/java/com/netease/is/nc/server/service/GetCaptchaService.java:103,Validate.isTrue(...)不通过即抛业务异常。
影响:这是"启动成功但核心功能全废"的最坏形态——/metrics 健康、探针通过、流量正常进来,但用户拿不到任何验证码。空公钥只影响后台导入 License 时的解密;直接写库可绕过(见 R-03)。
建议:部署校验脚本加一条:license.publicKey 非空 或 nc_license 有未过期记录(二者之一),否则拒绝上线;两者都缺失时启动期打 WARN 并在 /metrics 暴露专用 gauge,让监控先于用户发现。
优先级:P0
R-11 image.upload.server 必须是 IP,默认配置却是域名
现象:纯私有化(无对象存储)模式下 captcha-schedule 向 captcha-image 逐节点推送素材;该键配成域名(负载均衡)时素材只落到 LB 后面某一个节点,其余节点图片 404。
证据:
- 架构约束明文:
.codemaker/codeindex/_architecture.md:51——「captcha-image为本地文件存储,集群下必须由captcha-schedule按image.upload.server(必须写 IP,不可写域名)逐节点上传」。 - 默认值却是域名:
base.properties:20(image.upload.server=http://nc-image-private.test.dun.163.com),同文件:18的static.server亦然。 - 落盘路径为容器本地目录(
captcha-image/src/main/resources/image.properties的upload_file_path→/java-web-dir/custom-image/)。
影响:多副本 captcha-image 且无 S3 时出现"部分图片加载失败、刷新又好",随机性故障难定位——DB 里记录是成功的。
建议:无对象存储时把该键写成多节点清单或逐 IP 推送;有 S3 时干脆不部署 captcha-image,走 S3UploadService;加启动校验:s3.bucketName 为空且该键为域名 → 打 ERROR。
优先级:P1
R-12 调度池 5 线程 + sleep 式遍历,任务相互挤占
现象:task:scheduler pool-size=5 被所有 @Scheduled 共用,produce worker 内还有 TimeUnit.SECONDS.sleep(10),图片集变多后单轮耗时成倍增长。
证据:base-application-context.xml:34-36(线程池声明,被 server-application-context.xml:17 与 scheduler-application-context.xml:16 双双 import,两个进程各持一个 5 线程池);PuzzleProduceWorker.java:88-97(每个图片集 sleep(10),且 catch (InterruptedException) 只打日志不恢复中断标志)。
影响:单轮耗时突破 60s 的 fixedDelay 后变成"永远在跑";中断标志丢失会让线程带着中断状态继续执行。建议:sleep 下沉到 fill task 限速;catch 里补 Thread.currentThread().interrupt();生产/统计/清理拆成独立 scheduler bean。优先级:P1
R-13 sys_conf 建表与初始化数据是硬前置,无 properties 回退
现象:只起服务不导 build/db_init/,应用能启动但大量功能按空配置运行。
证据:SysConfLoader.java:44(INTERVAL_SECOND=30)、:91-99(@PostConstruct 先 reload() 再 scheduleAtFixedRate),无 properties 回退分支;初始化资产在 build/db_init/(三方言)与 build/es/(模板 + ILM)。
影响:安全类配置(admin.security.safe.config、admin.special.char.check.conf)缺失时返回"最宽松"默认值(getSafeConfig() 注释「未配置时返回默认(不做任何限制)」,SysConfLoader.java:113-122)——安全策略静默降级为不限制。建议:build/db_init/、build/es/ 列为与镜像同级硬前置;安全类 key 启动期强校验;reload() 把「关键 key 缺失计数」暴露到 /metrics。优先级:P1
R-14 captcha-schedule 镜像把解密密钥随镜像分发
现象:lib.enc 加密只是让静态扫描器看不到明文 jar,密钥文件(伪装成 .app.runtime.dat)就在同一镜像内。
证据:captcha-schedule/Dockerfile:14-24(构建期 openssl rand -base64 32 > /out/.app.runtime.dat 后加密 lib)、:41-43(密文与密钥都拷进最终镜像);entrypoint.sh:5-7 自述「解密密钥随镜像分发」,:45-48 用 -pass file: 解密到 /java-app-dir/.runtime/applib。
影响:对「静态扫描识别组件版本」有效,对「拿到镜像的人逆向」无效——属混淆而非加密,不满足「密钥与密文分离」类合规要求。建议:交付文档如实描述强度;需真分离则改 K8s Secret 挂载;运行目录用 emptyDir{medium:Memory}(entrypoint.sh:18 已支持 RUNTIME_LIB_DIR)。优先级:P2
汇总表
| 编号 | 风险 | 关键证据 | 优先级 | 建议动作 |
|---|---|---|---|---|
| R-01 | captcha-schedule 无锁,必须单副本 | Main.java:11-15;schedule 全模块无 tryLock/setnx/synchronized | P0 | replicas: 1 + Recreate;代码加 Redis 租约 |
| R-02 | server/admin @Scheduled 多副本重复 | DeviceStatHandler.java:82 等 6 处;PrometheusMetricsJob.java:56,58 | P1 | 统计改聚合口径;告警任务加锁 |
| R-03 | License 双表双代码 | Licence.xml:26/License.xml:45;LicenceServiceImpl.java:121/LicenseServiceImpl.java:133 | P0 | 双写必检 + 一致性告警 |
| R-04 | -D 不能覆盖已有 key | base-application-context.xml:16-19;DecryptingPropertyPlaceholderConfigurer.java:16-24 | P1 | 用同名文件遮蔽,不用 -D |
| R-05 | 限流独立第二套 Redis | frequency.properties:1-13 | P1 | 并列为主配置项 + 连接失败告警 |
| R-06 | getconf.* 为空启动失败 | GetConfController.java:71,73;base.properties.template:40,42 | P1 | 启动期强校验 |
| R-07 | Kafka/Dubbo/Redisson 零使用 | pom.xml:309,476,606;模块 pom 零引用 | P2 | 清理版本声明 |
| R-08 | 明文口令与仓库凭据 | base.properties:16,29-30,48,68,78-79;frequency.properties:7;captcha-server/pom.xml:390-391 | P0 | 全量 Enc(...) + 凭据移出 pom |
| R-09 | admin /health/* 必 404 | captcha-admin/.../web.xml:82-85;spring-security.xml:187,203 | P1 | 探针改 /login.html 或补 Controller |
| R-10 | license.publicKey 空则取图全失败 | base.properties:62;GetCaptchaService.java:103 | P0 | 上线校验双表/公钥至少一项 |
| R-11 | image.upload.server 需 IP 却默认域名 | base.properties:20;_architecture.md:51 | P1 | 逐 IP 推送或改用 S3 |
| R-12 | 调度池 5 线程 + sleep 阻塞 | base-application-context.xml:34-36;PuzzleProduceWorker.java:88-97 | P1 | 拆分 scheduler + 恢复中断标志 |
| R-13 | sys_conf 建库硬前置,无回退 | SysConfLoader.java:91-99,113-122;build/db_init/ | P1 | 硬前置 + 安全 key 强校验 |
| R-14 | schedule 解密密钥随镜像分发 | captcha-schedule/Dockerfile:14-24,41-43;entrypoint.sh:5-18 | P2 | 改用 Secret 挂载 |
治理排序建议
先修「不可见故障」:R-03、R-10(启动成功但取图全废)、R-04、R-05(配置静默失效)——共同特征是没有异常日志,排障成本最高。再修部署形态:R-01、R-02、R-09、R-11,靠交付清单与校验闭环,不改代码。最后清债:R-08、R-12、R-13、R-07、R-14,其中 R-08 建议在下次版本升级窗口一次性做掉。
贯穿性建议:把可机器判定的条目(公钥非空、双表有记录、frequency.properties 可达、不用 /health、image.upload.server 为 IP、is.backup 与副本数匹配)做成一份启动前 preflight 脚本,比任何文档都有效。