Phase 0 (项目骨架):
- main.go 改为最小骨架 (config + logging + gin + /healthz + 优雅退出)
- internal/{config,logging,storage,cluster,...}/ 目录占位
Phase 1 (配置 + 日志):
- internal/config: env 解析 + 必填校验 + token 隐藏的 String()
- internal/logging: slog multi-handler 双输出 (终端 text + 主文件 JSON)
+ StartToolCall per-tool 独立文件 (0600), tools 目录 0700
Phase 2 续 (存储层):
- internal/cluster: 16 字段 Cluster struct (AuthPassword json:"-")
- internal/storage: modernc.org/sqlite 接入, WAL 模式, schema 自动迁移
- ClusterRepo CRUD: Create/Get/List/Update/Delete + ErrNotFound
+ AuthPassword 空字符串 = 保留旧密码 (核心约定)
- 7 个单元测试全绿 (:memory: DB)
- created_at/updated_at 改纳秒精度, 消除 List 测试的 sleep 特殊 case
Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
12 KiB
Findings & Research
调研发现、风险分析、决策依据
1. 仓库现状 (2026-07-10 调研)
提交历史: 1dfef0f init (单提交)
Go 版本: 1.25.5
模块名: spark-mcp-go
1.1 文件清单
| 文件 | 状态 | 备注 |
|---|---|---|
.gitignore |
存在 | 包含 Python 模板 + data/ 目录(Spark 相关) |
.idea/ |
gitignored | JetBrains IDE 配置 |
go.mod |
存在 | 所有依赖 // indirect 状态 |
go.sum |
存在 | — |
main.go |
GoLand 模板 | "Hello gopher",非业务代码 |
CLAUDE.md |
已创建 | 项目元信息 + 待确认决策 |
task_plan.md |
v0.3 | 7 阶段计划 |
findings.md |
(本文件) | 调研与决策依据 |
progress.md |
(会话日志) | — |
1.2 关键发现
go.mod里所有依赖都是// indirect— 还没有任何代码 import 它们mongo-driver/v2已 pin 但用户决定改用 SQLite → Phase 0.1 需要移除.gitignore里的data/注释"persisted Spark connections / pending jobs"暗示原意是用作本地状态存储 → 现统一改用 SQLite- 没有任何 README、文档、测试 → 从零开始
2. 架构评审结论
2.1 用户原始方案的关键问题 (v0.1 评审)
用户在初版架构文档里提出的 5 个 Tool + Gin + MCP + MongoDB 方案,经过 Linus 视角评审,发现以下问题:
问题 1: 存储选型自我矛盾
- 文档说 "SQLite or GORM"
- go.mod pin 了
mongo-driver/v2 - .gitignore 保留
data/目录 - 结论: 三选一 → 选 SQLite(用户已确认),
data/目录保留作为上传文件存储
问题 2: "元工具" 归属不清
- 文档提到
fetch_url/exec_shell/upload_file是"底层元工具" - 结论: 用户确认只暴露
fetch_url+upload_file(不暴露exec_shell)fetch_url是 LLM 的 HTTP 出入口upload_file是 LLM 的本地文件存储入口
问题 3: Tool 粒度问题
- 5 个 Tool 里 3 个本质都是
GET <URL> - 结论: v0.2 收敛到 4 个 Tool;v0.3 因加入 spark_submit 回到 5 个
问题 4: 示例代码 API 不存在
server.NewStreamableHTTPServer等在 mcp-go v0.56.0 不存在- 结论: Phase 6 实施时先查 mcp-go 真实 API 再写代码
2.2 v0.3.x 范围变更汇总
| 版本 | 变更 | 触发原因 |
|---|---|---|
| v0.3 | 加 spark_submit Tool,upload_file 改本地存储,Cluster 加 binary 路径 |
用户从"纯读"改"读+提交" |
| v0.3.1 | 合并 spark_submit_path + spark2_submit_path → spark_submit_execute_bin |
消除 special case |
| v0.3.2 | list_clusters 标为强制入口,Cluster 加 is_active 字段 |
LLM 必须能拿到 RM/SHS 地址 |
| v0.3.3 | 加回 fetch_spark_metrics + fetch_cluster_env (高层 Tool,内部调 fetch_url) |
业务端点封装,LLM 不必拼 URL |
| v0.3.4 | 加回 analyze_spark_log + 服务端启发式过滤 (data_skew / gc_pressure / bottleneck) + LLM-Ready Prompt |
用户原 spec 的 [Log Analyzer + Regex Rules + LLM-Ready Prompt] 组件。规则是 first-pass filter,LLM 拿 findings 后做最终判断 |
| v0.3.5 | 拆 yarn_job_manage → 4 个 RM Tool (list_applications / get_application_status / get_application_logs / kill_application) |
用户参考实现是 4 个独立 Python 入口,1 Tool = 1 端点 |
| v0.3.5 | 加鉴权层 (none / basic / kerberos) + SSL + manual redirect | Spark on YARN + Hadoop 2.7,生产环境需要 SPNEGO;get_application_logs RM→NM 跨主机 307 必须保留 Authorization |
| v0.3.5 | analyze_spark_log 日志改走 RM 降级链 |
SHS logs 端点不稳定,RM aggregated-logs + amContainerLogs + NM 是生产链路 |
| v0.3.6 | 鉴权层简化成 none / simple / basic |
用户环境 simple auth,kerberos 推到 v2 |
| v0.3.6 | Hadoop 2.7 兼容性"已验证" | 用户实测 endpoint 通,风险解除 |
| v0.3.7 | 加 per-cluster default_submit_args + rate_limit_per_min |
减 LLM 负担 + 防刷 |
| v0.3.7 | 删 applications 表,加 audit_log 表 |
纯读模式不需要 app 缓存,audit 必需 |
| v0.3.7 | 多 AdminTokens + analyzer 阈值 env 可配 |
多用户 + 集群特定阈值 |
| v0.3.8 | 用 log/slog 替换默认 log,加双输出 + per-Tool 独立日志 |
用户要求结构化日志 + 调试 LLM 行为 |
v0.3.3 设计模式确立:
- 通用 HTTP 原语 (
fetch_url): LLM 自主拼任意 URL - 业务高层封装 (
fetch_spark_metrics,fetch_cluster_env): 内部调fetch_url,处理 URL 拼装、cluster 校验 - 两者并存,LLM 按场景选:常用端点用高层,长尾查询用原语
2.3 待确认项
- (已清空 — v0.3.7 用户接受所有 6 项建议,无未决项)
- 环境约束 (用户告知 + 验证): Spark 脚本, YARN-on-Spark, Hadoop 2.7 (RM/SHS endpoint 已验证工作) → 无需额外兼容代码
- v1 鉴权范围:
none/simple/basic(YARN SimpleAuth 用user.namequery param)。Kerberos 不在 v1(用户环境不需要)
用户中途修正了 "纯 RM/SHS 不支持提交" 决策,改为 "读+本地 spark-submit 提交":
- 新增
spark_submitTool,使用 Server 本地exec.Command(binary, args...) - 重新定义
upload_file:从"通用 HTTP 上传"改为"本地文件存储" (LLM 把脚本内容传给 Server,Server 写到./data/) - Cluster 配置新增 单一
spark_submit_execute_bin字段 (v0.3.1 进一步合并spark_submit_path+spark2_submit_path)
重新引入的风险:命令注入 — 通过 exec.Command 切片传参 + Tool 参数 schema 强约束来缓解
3. 技术选型依据
3.1 SQLite 驱动对比
| 驱动 | 优点 | 缺点 |
|---|---|---|
modernc.org/sqlite |
纯 Go,无 CGO,跨平台编译简单 | 性能略低于原生 |
mattn/go-sqlite3 |
CGO 绑定,性能最优 | 需要 C 编译器,跨平台编译复杂 |
选择: modernc.org/sqlite (本项目数据量小,QPS 极低,纯 Go 部署友好)
3.2 MCP 传输对比
| 传输 | 标准 | 优点 | 缺点 |
|---|---|---|---|
| stdio | MCP 基础 | 简单,本地 Agent 首选 | 不能与 HTTP 框架共存 |
| HTTP + SSE | MCP legacy | 经典 HTTP 传输 | 仅 server → client 单向 |
| Streamable HTTP | MCP 2025-03+ | POST + 可选 SSE,现代标准 | 实现稍复杂 |
选择: Streamable HTTP(用户已确认),与 Gin 路由共存
3.3 HTTP 框架: Gin
- 用户原始方案已选
- 适合: admin API + 挂载 MCP HTTP handler
- 生态成熟,中间件丰富
- 与
mark3labs/mcp-go的 Streamable HTTP handler 通过gin.WrapH集成
3.4 共享 HTTP 客户端
为什么独立包:
- SSRF 防护集中实现,所有 Tool 复用
- Timeout / Size Limit 统一配置
- DNS rebinding 防护只在 client 层做一次
4. 安全考量
4.1 SSRF 防护 (核心)
威胁: fetch_url / upload_file 被恶意利用访问内网服务或元数据 endpoint
防护策略:
- IP 黑名单:
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10 - Hostname allowlist: 来自 cluster 配置的
rm_url/shs_url的 host 走豁免 - DNS rebinding 防护: 解析 hostname 拿 IP,然后 dial 用 IP(不用 hostname 二次解析)
- Scheme 校验: 仅允许
http/https
4.2 鉴权
| 端点 | Token | 来源 |
|---|---|---|
/admin/* |
ADMIN_TOKEN |
环境变量,必填 |
/mcp |
AGENT_TOKEN |
环境变量,必填 |
/healthz |
公开 | 探活用 |
实现: Authorization: Bearer <token> Header,中间件统一校验
未来演进: 多租户 / OAuth / mTLS(当前计划不包含)
4.3 输入校验
| 输入 | 校验 |
|---|---|
cluster.rm_url / shs_url |
必须 http:// 或 https:// 开头 |
cluster.id |
非空,长度限制 |
upload_file.file_path |
必须在 ./data/ 允许目录内 |
fetch_url.url |
必须绝对 URL,scheme 校验 |
yarn_job_manage.action |
enum: kill |
4.4 资源限制
| 资源 | 限制 | 默认值 |
|---|---|---|
| HTTP response body | MaxResponseBytes |
1 MB |
| HTTP request timeout | HTTPClientTimeout |
30s |
| Token 大小 | 启动时校验 | < 1 KB |
| 集群数量 | 业务上无限制,DB 层建议 1000 | — |
5. 已知不确定项
5.1 mcp-go v0.56.0 真实 API (Phase 6.1 需要查)
server.NewMCPServer(name, version string, opts ...ServerOption) *MCPServermcp.NewTool(name string, opts ...ToolOption) Toolmcp.WithString(name string, opts ...PropertyOption) ToolOptionmcp.WithArray(name string, opts ...PropertyOption) ToolOption(用于 spark_submit 的 args)server.NewStreamableHTTPServer(s *MCPServer, opts ...StreamableHTTPOption) *StreamableHTTPServermcp.NewServer(...)/mcp.Server/mcp.NewTool是 alias?
行动: Phase 6 实施时先看 GitHub tag 源码或官方 docs
5.2 upload_file 的具体业务场景 (已解决)
- 确认: 本地文件存储,供 spark_submit 引用
- 输入: 文本或 base64 编码的 content
- 输出: 相对
data_dir的路径
5.3 applications 表是否需要
- 当前为"后续阶段"预留(缓存 RM/SHS 返回的 app 状态)
- spark_submit 返回的 app_id 是否要入库?本阶段暂不入库,LLM 自行跟踪
- 行动: Phase 2 末根据实际需求决定保留或删除
5.4 Gin 与 mcp-go Streamable HTTP 的集成方式
gin.WrapH(mcpHandler)把 http.Handler 适配为 gin.HandlerFunc- 需要确认: mcp-go 的 handler 是否能直接 Wrap,还是需要中间件处理 CORS / Content-Type
- 行动: Phase 6.4 实施时验证
5.5 spark-submit 的 binary 路径在不同环境下的差异
- 开发环境:
/usr/local/bin/spark-submit - 生产环境:
/opt/cloudera/parcels/SPARK2/bin/spark-submit或/opt/spark/bin/spark-submit - 由 admin 在 cluster 配置中指定 — 已经处理
5.6 app_id 解析的 YARN 输出格式
- YARN 提交成功时输出:
Submitted application application_12345_0001 - 不同 YARN 版本可能略有差异(如 Spark Standalone 模式输出不同)
- 方案: 用 regex
application_\d+_\d+提取,失败则 app_id 为空字符串 - 保留 regex 可配置(可选,本阶段不强制)
6. 风险矩阵
| 风险 | 概率 | 影响 | 缓解 |
|---|---|---|---|
| mcp-go v0.56.0 API 与文档不一致 | 中 | 中 | Phase 6.1 先查源码 |
| 命令注入 (spark_submit 引入) | 中 | 高 | exec.Command(name, args...) 切片形式 + submitter enum + path 校验 |
| 私网 IP 漏判 (IPv6 段不全) | 低 | 高 | 写测试覆盖 IPv6 段 |
| DNS rebinding TOCTOU | 中 | 高 | 解析时锁定 IP,后续 dial 用 IP |
| upload_file 路径逃逸 | 中 | 高 | filepath.Clean + 必须在 data_dir 之下 |
| Token 通过 env 泄露到日志 | 低 | 高 | 启动时打印配置时隐藏 token 字段 |
| spark-submit binary 被替换 | 低 | 高 | 启动时 + runtime 都校验 binary 存在 + 可执行 + 绝对路径 |
| SQLite 并发写冲突 | 低 | 中 | 配置 journal_mode=WAL, 单写多读 |
| YARN 输出格式变化导致 app_id 解析失败 | 中 | 低 | regex 解析失败则 app_id 为空,不影响主流程 |
| 内存爆炸 (大响应/大 stdout) | 中 | 中 | HTTP body 1MB 截断,spark stdout 10MB 截断 |
7. 参考资料
- mark3labs/mcp-go GitHub — MCP Server 库
- Model Context Protocol 规范 — MCP 2025-03+ 标准
- YARN RM REST API — YARN RM 接口
- Spark History Server REST API — SHS 接口
- modernc.org/sqlite — 纯 Go SQLite 驱动