状态:✅ 已完成 前置阅读:02-Mac本地部署实操.md(部署与踩坑实录)、03-技术架构与业务模块分析.md(模块与链路全景) 日期:2026-09-14 素材来源:JoySafety 仓库源码逐一核实(compose / Dockerfile / Java 源码),引用格式
path:line;与提纲素材不符处以实际代码为准(见 0.3 勘误表) 读者定位:已跑通开源版,想系统评估"离上生产还差什么、怎么补、补完怎么验"的读者
〇、先建全局认识:差距在哪、先补哪块
0.1 两条链路回顾(所有改造都落在这两条链路上)
配置链路(配置怎么流转):
| |
请求链路(请求怎么流转):
| |
0.2 差距清单总表
| # | 改造项 | 现状一句话 | 爆炸半径 | 优先级 |
|---|---|---|---|---|
| ① | 数据持久化 | MySQL/Redis/Vearch 全没挂数据卷 | down 一次全丢 | P0 |
| ② | 安全加固 | admin 8006 无任何鉴权且直接映射宿主机 | 拿到 8006 ≈ 关掉整条安全防线 | P0 |
| ③ | 启动依赖治理 | sleep 30/60 硬编码排序,healthcheck 被注释且路径写错 | 冷启动靠运气 | P1 |
| ④ | 配置热更新 | 60s 轮询能感知变更,但 OkHttp 静态缓存使超时类变更必须重启 | 变更滞后/需重启 | P1 |
| ⑤ | mq responseMode | 实现是空 stub,mq 模式实际等价 http 模式 | 业务方按 README 用会拿不到结果 | P1 |
| ⑥ | 可观测性 | 埋点调用遍布代码,但 MonitorAlarmService 方法体全空 | 出事故无指标可查 | P1 |
| ⑦ | 水平扩展 | free_taxi 分布式锁天然支持多实例,但锁实现有两处原子性瑕疵;skill 地址写死单点 | 会话锁死/双跑 | P2 |
| 附 | 技术栈升级 | Spring Boot 2.0.9 / Jedis 2.5.2 / fastjson 双栈混用 | 漏洞包袱+演进成本 | P2 |
0.3 素材坐标勘误(提纲 vs 实际代码)
写作前逐一核实,提纲有三处包路径偏差,本文一律以实际代码为准:
| 提纲写法 | 实际位置 |
|---|---|
service/impl/MessageQueueServiceImpl.java:32 | service/storage/MessageQueueServiceImpl.java(空方法 appendResponseData 恰好在 :32,行号仍对得上) |
engine/FunctionChainBuilderImpl.java | service/execute/FunctionChainBuilderImpl.java:37 |
engine/functions/ParallelFunction.java | service/execute/functions/ParallelFunction.java:41 |
其余坐标(DynamicConfigService.java:45 类声明、GroovyRouter.java:27、contoller/rpc/DefenseApiServiceImpl.java:25)均核实无误。
一、数据持久化(最紧急,P0)
现状:根目录 docker-compose.yaml 三个有状态组件都没挂数据卷——
- MySQL(:8-33):volumes 只有
my.cnf和 3 个初始化 SQL(:19-23,挂进/docker-entrypoint-initdb.d/),/var/lib/mysql留在容器可写层。 - Redis(:35-48):command 只有
redis-server --requirepass(:39),无 volume、未开 AOF。 - Vearch(:50-66):只挂了
config.toml(:58-59);而deploy/thirdparty/vearch/config.toml:7声明data = ["datas/","datas1/"]是相对路径,数据落在容器内工作目录(官方镜像 WORKDIR=/vearch)。 - 对照组:
deploy/docker-compose.yml:20其实给 MySQL 挂了./thirdparty/mysql/data:/var/lib/mysql——源码构建路径做了持久化,发布版反而没做(该文件里 Redis/Vearch 同样没挂)。
风险:docker compose down / 误删容器 → 策略、业务、敏感词、知识条目、知识向量全丢(02 篇 4.1 已实测,4.2 的恢复流程本质就是"认命重灌")。分层看爆炸半径:MySQL 与 Vearch 是硬损失(向量库重建还依赖 MySQL 知识表先保住);Redis 里全是短 TTL 易失数据(free_taxi 任务/结果队列 300s、识别结果缓存 900s——TaskExecutor.java:15、FunctionExecutorImpl.java:247),丢了只是在途检测任务和缓存命中率,Redis 持久化是锦上添花,不是生死线。
改法(compose diff 示意):
| |
两个注意点:① MySQL 的 /docker-entrypoint-initdb.d/ 脚本只在空数据目录首次启动时执行——挂上已有数据的卷后改 init SQL 不会重放;② 持久化 ≠ 备份,生产上另加定时 mysqldump 与 vearch 快照策略。
验证:docker compose down && docker compose --env-file .env up -d 后,admin 里策略/词库原样在(02 篇 4.2 的"重跑附录 A/B"步骤应可省掉);docker exec safety-redis ls /data 能看到 appendonlydir;删掉 vearch 容器重建后 curl http://localhost:8817 查 space 仍在。
二、启动依赖治理(P1)
现状:根 compose 用硬编码 sleep 做启动排序——admin sleep 30(:84)、api sleep 60(:118)、keywords sleep 30(:153)、example-init sleep 60(:228)。健康检查层面更尴尬:
- MySQL/Redis/Vearch/Knowledge 有 healthcheck(:29-33/:44-48/:62-66/:193-197,其中 vearch 的
nc检查是假警报,见 02 篇坑 3); - admin/api 的 healthcheck 被整段注释(:94-98/:128-132),且路径写的是
/health——实际是/basic/healthcheck(contoller/HealthCheckController.java:14-24,类级@RequestMapping("/basic"));该控制器故意注入ConfigService(:17-19),保证返回 ok 时配置已首载完成,语义很对; - admin 源码里根本没有 health 端点(全模块 grep
health无匹配); deploy/docker-compose.yml反面教材:给 api→admin、knowledge→vearch 配了condition: service_healthy(:137-138/:83-85),但被依赖方既无 compose healthcheck、镜像 Dockerfile 里也无HEALTHCHECK指令(safety-admin/Dockerfile、safety-api/Dockerfile 均无),compose v2 会因"依赖未声明 healthcheck"直接报错拒启动。
风险:sleep 是"按京东内网机器速度拍的数"——Mac 模拟 x86 场景 Java 启动更慢,60s 未必够;api 在配置首载完成前接流量就是 02 篇的 token错误/业务不存在 窗口;example-init 的 sleep 60 同理,快了白等、慢了初始化脚本直接打挂。
改法:① 给 admin 补一个同款 /basic/healthcheck 端点(或引 actuator);② 放开 api/admin/keywords 的 healthcheck 并把路径改成 /basic/healthcheck;③ depends_on 全部升级为 condition: service_healthy;④ 删掉四处 sleep;⑤ example-init 把 sleep 60 换成对 admin/api 健康端点的轮询循环(容器内 curl 重试到 200 再执行初始化)。
验证:time docker compose --env-file .env up -d 对比冷启动总时长;docker inspect --format '{{.State.Health.Status}}' safety-api 看到 healthy 后再跑 quickstart 四案例,不应再出现 token 错误窗口;故意 docker stop safety-mysql 再恢复,观察依赖级联行为符合预期。
三、配置热更新完善(P1)
现状:轮询机制本身是能自洽工作的,核心代码:
| |
判变更依据的时间戳在 admin 侧取三方最大值(DefenseConfigServiceImpl.java:274-291:business_info / execute_dag_conf / function_conf 的 updateTime),所以改 function 也能被 api 感知——感知后全量替换 volatile Map 并回调 listener 重建 DAG(FunctionChainBuilderImpl.java:47-51 注册 this::loadConf,:53-71 逐业务重建;Router 反射实例化在 :101-106,groovy 引擎在 GroovyRouter.java:76-82 → GroovyHelper.java:18-22 重建并 warmup 编译)。
真正的坑在资源缓存:
| |
各 HTTP Function 的 OkHttp client 在 doInit 时经此池 get-or-add(如 KeywordFunction.java:58-65),callTimeout 在首次创建时烤死。结论:改 url 有效(新 key 建新 client),同 url 只改 timeout 无效,必须重启 api——这就是 02 篇坑 5 的代码级根因。
风险:① 变更最长滞后 60s,且 listener 重建走 cached 线程池异步执行(DynamicConfigService.java:106-110、:195),实际生效还要再加一段不确定延迟;② 多实例各自轮询,配置切换不齐,灰度期间同业务不同实例行为可能不同;③ 单业务重建失败只记日志并保留旧 DAG(:63-68),其余业务照常替换——半新半旧状态只能靠人看日志发现;④ 长期频繁改 url 会让旧 client 在静态池里累积(连接池+线程不释放)。
改法(三档,按投入选):
| 档位 | 做法 | 解决什么 |
|---|---|---|
| L1 小修 | 轮询周期配置化;FunctionResourcePool 的 key 加 timeout(或重建前按业务 invalidate) | 重启才生效、滞后窗口 |
| L2 推送 | admin 的 online/upgrade 成功后 webhook 回调 api——api 需新增一个 HTTP 端点包装已有的 FunctionChainBuilderImpl.refresh(businessName)(:128-140,现无对外端点,已核实) | 秒级生效、多实例齐切 |
| L3 基建 | 变更事件走 MQ 广播(正好复用第五节 mq 基建)或引入 Nacos/Apollo | 彻底的事件驱动配置中心 |
验证:L1 后改 function 超时不重启,重跑 02 篇案例 3 观察耗时从 ~2000ms 截断变为正常命中;api 日志出现"配置更新成功";L2 后从 admin upgrade 到 api 行为变化端到端 < 数秒,且双实例日志时间戳一致。
四、mq responseMode 补实现(P1)
现状:接口实现是空 stub,全文就这么多(service/storage/MessageQueueServiceImpl.java:30-37):
唯一调用点在异步任务落结果之后(service/task/TaskExecutorImpl.java:222);而请求分派处 case mq: 直接落入 case http: break;(service/DefenseServiceImpl.java:171-173)。即 mq 模式今天 = http 模式 + 一次空调用,结果只进 Redis 结果队列(TTL 300s)。
风险:业务方按 README 选 mq 模式却收不到任何消息;不接 Redis 拉取接口的话结果 5 分钟后蒸发;检测数据无法进数仓做拦截率/误杀率分析(第六节也依赖它)。
改法(实现要点):
init()里建 producer(rocketmq-spring-boot 或 kafka-clients),topic 按safety-result-{group}规划、tag=accessKey;appendResponseData()里异步 send,message key=sessionId,body 即 response JSON;send 失败只记日志+计数,绝不阻塞/拖垮主检测链路;appendContextData()投递会话上下文(供多轮检测外部分析用);- 消费契约文档化:按 tag 过滤、按 key 归并同会话结果;
- 引依赖时留意与现有 fastjson 双栈、Spring Boot 2.0.9 的版本兼容(见第七节)。
验证:compose 加一个 namesrv+broker(或 kafka)单节点;发 mq 模式请求后消费端收到结构化结果;docker stop broker 期间主链路审核不受影响、错误日志有兜底记录。
五、高可用与水平扩展(P2)
现状:free_taxi 的多实例互斥是设计内自带的(service/task/TaskService.java:20-25 注释:“每个处理线程使用分布式锁独占会话,顺序处理”),锁实现:
| |
配套机制:抢锁只试 1 次(TaskExecutorImpl.java:73 TASK_WAIT_LOOP = 1)、抢到后每批处理前 expire 续约(:135)、finally 里 unlock(:156)即直接 del(:116-118)。容量侧两组线程池都偏小:并行节点池 200 线程/队列 1/CallerRunsPolicy(service/execute/functions/ParallelFunction.java:84-93,:86 留着官方 todo"根据实际情况调整"),异步任务池 200/队列 10(TaskExecutorImpl.java:56-63);限流是实例级 Guava RateLimiter 内存 Map(RateLimitServiceImpl.java:30);skill 地址写死在 function 配置的 url 里(如 KeywordFunction.java:48-50 校验 url 非空)。
风险:① setnx 与 expire 两条命令之间进程崩溃 → 锁无 TTL → 该会话永久无人处理(直到人工 del);② 锁到期被别的实例抢到后,原持有者 finally 里的 del 不校验 owner,会误删新锁 → 同会话双跑、结果重复 lpush;③ 队列 1 意味着几乎无缓冲,CallerRunsPolicy 把压力回压给调用线程(DAG 主线程/Tomcat worker),同步响应被拖长——注意 Tomcat 默认 maxThreads 也是 200,三层 200 要对齐着扩容;④ N 副本实际限流 = qpsLimit × N;⑤ skill 单点。
改法:锁原子化——set(key, val, NX, PX, ttl) 一条命令 + Lua 脚本校验 value 后再 del(标准分布式锁写法,顺手把 jedis 升到 4.x/5.x,见第七节);api 多副本前置 LB(nginx/网关按 URL 前缀转发即可,api 本身无状态);skill 多副本后把 function url 换成 LB 地址或 K8s Service 名;限流换 Redis Lua 令牌桶做全局限额,或按副本数拆分配额;线程池按"峰值并发 × 单请求占用 function 数"公式估算并配置化。
验证:双副本 api 下 kill -9 其一,另一副本应在锁 10s 过期后接管会话(日志按 lockId 对账无重复处理);压测观察 CallerRunsPolicy 触发点即容量水位;双副本下接口限流阈值实测确认。
六、可观测性(P1)
现状:最大的发现——埋点"管线"已铺遍关键路径,但汇集类是空壳:
| |
调用点却一个不少:节点异常计数(FunctionExecutorImpl.java:311)、并行子项失败(ParallelFunction.java:108/134/169)、异步任务全流程(TaskExecutorImpl.java:118/137/139/163/177/185)、DAG 构建失败(FunctionChainBuilderImpl.java:60/67)。耗时埋点也已就绪:AbstractFunction.run() 给每个结果 setCost 并 appendResult 进 ctx(:41-59)。日志侧 log4j2 配置(example/safety-api/conf/log4j2-example.xml):RollingRandomAccessFile 100MB×10 滚动、console 只打 ERROR、com.jd logger 重复声明两段(:35-40)、无 JSON 格式无 traceId。
风险:线上无 QPS/拦截率/错误率/function 耗时任何指标,事故定位全靠捞 app.log;队列积压、锁竞争、慢 skill 全部不可见。
改法:用 Micrometer(Spring Boot 2.x 自带)填 MonitorAlarmService 各方法——counter 类(riskIncr/taskIncr/requestIncr)+ timer 类(包装 appendResult 处的 cost);引 actuator 暴露 /actuator/prometheus(内网或网关后,别裸奔,见第八节);log4j2 改 JSON layout + MDC 塞 requestId/sessionId,与日志平台对接;Python skill 侧用 prometheus_client 暴露推理耗时;拦截/误杀回流分析借第四节 mq 把结果入仓。
验证:curl /actuator/prometheus | grep safety 出指标;构造一次 BERT 超时,对应 parallel 错误计数 +1;Grafana 出 QPS/耗时/拦截率三块面板。
七、技术栈升级风险评估(P2)
现状:版本底账(均已核实)——
| 依赖 | 版本 | 出处 | 备注 |
|---|---|---|---|
| Spring Boot | 2.0.9.RELEASE | safety-api/pom.xml:8 | 2018 年末,早已 EOL 无安全补丁 |
| JDK | 1.8 / temurin-8 | pom.xml:20-21、safety-api/Dockerfile:1 | |
| Jedis | 2.5.2 | pom.xml:54 | 2015 年代,无 SetParams、无集群/ssl 现代特性 |
| fastjson | com.alibaba:fastjson:2.0.32 | pom.xml:48 | 1.x 包名 API 的 2.x 兼容发行版,代码里 com.alibaba.fastjson.* import 共 19 处 |
| fastjson2 | com.alibaba.fastjson2:fastjson2:2.0.46 | safety-basic/pom.xml:90-93 | DynamicConfigService/RedisStorageService 等在用——两套 JSON 栈混用 |
| groovy-all | 4.0.21 | pom.xml:190 | |
| okhttp | 4.12.0 | pom.xml:184 | 反而很新 |
并发瓶颈在 groovy 求值串行化(core/engine/routers/GroovyRouter.java:42-46):
parallel 归约脚本同款 synchronized(ParallelFunction.java:155-161)。ScriptEngine 非线程安全才出此下策,后果是同一路由节点上所有请求的脚本求值串行,高并发下直接顶到 p99。
风险:EOL 组件无补丁;fastjson 双栈序列化行为差异(SerializerFeature 语义不同)埋数据类 bug;升级联动面广(Boot 3.x 要 javax→jakarta 全量替换,knife4j 4.5.0 也得换成 springdoc)。
改法:保守档——Boot 2.0.9→2.7.18(javax 不变,得补丁与新版 micrometer)、jedis 2.5.2→4.x(API 变动集中在 JedisPool 构造与 set 参数)、fastjson 统一收敛到 fastjson2 或 Jackson;激进档——Boot 3.x + JDK 17,按模块灰度。groovy 瓶颈:脚本预编译缓存(GroovyShell.parse 一次、运行期只跑 Script.run() 并池化 Binding),或把热点路由逻辑下沉成 Java Router 实现(ROUTER_MAP 本就支持新增类型)。
验证:升级后全量回归 quickstart 四案例 + admin 全接口 CRUD + free_taxi 链路;JMeter 对 groovy 路由节点压测对比升级前后 QPS/p99。
八、安全加固(P0)
现状:admin 全部管理接口零鉴权——safety-admin 源码 grep Interceptor|Filter|Authorization|auth 无任何匹配,pom 也无 spring-security/shiro;而根 compose 把 8006 直接映射宿主机(:87-88)。其余面:test 开头 accessKey 免签名是写死在代码里的特赦(DefenseApiV2Controller.java:74-75)而非配置项;演示 secretKey=123456、.env 默认口令 123456;vearch skip_auth = true(config.toml:16)、signkey = "secret"(:13);RPC 控制器 DefenseApiServiceImpl 整体被注释停用(:24-25 //todo + //@Service)——当前无 RPC 暴露面,但改造时切勿顺手放开注释而不加鉴权。
风险:拿到 8006 即可下线/篡改全部安全策略,等于从内部关掉整条防线——比任何单个绕过都危险;test 后门在生产可被直接免验签调用;弱口令 + 镜像未扫描。
改法:网络层先行(最便宜最有效):compose 不映射 8006 到宿主机、仅内网可达,前置 nginx 加 basic auth/IP 白名单;应用层跟上:admin 加统一签名校验过滤器(可直接复用 SignUtil 的 HMAC-SHA1),test 特赦改成配置开关且生产默认关;密钥轮换:business 增加"上一密钥"字段,双密钥并行验签一个窗口期再作废旧钥;容器加固:非 root 运行、只读根文件系统、镜像扫描进 CI。
验证:无凭证 curl 8006 各管理接口应一律 401;生产配置下 test accessKey 请求被拒;轮换演练:换钥后旧 key 在窗口内通过、窗口外 403;docker compose config 确认 8006 不再暴露宿主机。
九、改造路线图与坐标速查
9.1 建议实施波次
| 波次 | 内容 | 理由 |
|---|---|---|
| 第一波(本周) | ①数据持久化 + ②安全加固网络层 | 都是纯 compose/部署改动,不改一行代码,堵住两个 P0 |
| 第二波(两周) | ③启动治理 + ⑥可观测性 + ⑤mq | 建立"看得见、起得稳、流得出"的运维底座 |
| 第三波(排期) | ④热更新 L2/L3 + ⑦水平扩展 + 附·技术栈保守档 | 要动代码和压测,放在底座稳固之后 |
9.2 本文引用坐标速查
| 主题 | 位置(均相对仓库根目录) |
|---|---|
| 根 compose(无数据卷/sleep) | docker-compose.yaml:19-23,39,58-59,84,118,153,228 |
| 开发版 compose 对照 | deploy/docker-compose.yml:20,83-85,137-138 |
| 源码构建入口 | deploy/build_all.sh:11-33、safety/safety-api/build/build.sh:4-6、safety/safety-api/Dockerfile:1-27 |
| 配置轮询 | safety/safety-api/.../service/DynamicConfigService.java:65,106-110,113-151 |
| 变更时间戳(admin 侧) | safety/safety-admin/.../service/business/DefenseConfigServiceImpl.java:274-291 |
| DAG 重建 | safety/safety-api/.../service/execute/FunctionChainBuilderImpl.java:47-71,128-140 |
| OkHttp 静态缓存(坑 5 根因) | safety/safety-api/.../service/execute/functions/FunctionResourcePool.java:20-29 |
| mq 空 stub | safety/safety-api/.../service/storage/MessageQueueServiceImpl.java:30-37 |
| mq 分派落空 | safety/safety-api/.../service/DefenseServiceImpl.java:171-173 |
| free_taxi 锁 | safety/safety-api/.../service/storage/RedisStorageService.java:102-118、service/task/TaskExecutorImpl.java:73-188 |
| 线程池参数 | service/execute/functions/ParallelFunction.java:84-93、service/task/TaskExecutorImpl.java:56-63 |
| 监控空壳 | safety/safety-api/.../service/MonitorAlarmService.java:11-69 |
| 耗时埋点 | safety/safety-basic/.../core/engine/AbstractFunction.java:41-59 |
| groovy 串行求值 | safety/safety-basic/.../core/engine/routers/GroovyRouter.java:42-46 |
| 版本底账 | safety/safety-api/pom.xml:8,48,54,190、safety/safety-basic/pom.xml:90-93 |
| 健康端点 | safety/safety-api/.../contoller/HealthCheckController.java:14-24 |
| 停用的 RPC 控制器 | safety/safety-api/.../contoller/rpc/DefenseApiServiceImpl.java:24-25 |
与前几篇的联动:02 篇坑 4(两层超时)的完整答案在 03 篇 3.4;坑 5(改配置需重启)的代码级根因在本文第三节;4.2 的灾后重灌流程在本文第一节落地数据卷后可退役。
