Files
spark-mcp/findings.md
T
tao.chenandClaude a4e2472716 Phase 0+1+2 续: 项目骨架 + 配置 + 日志 + 存储层
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>
2026-07-10 12:12:53 +08:00

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_pathspark_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.name query param)。Kerberos 不在 v1(用户环境不需要)

用户中途修正了 "纯 RM/SHS 不支持提交" 决策,改为 "读+本地 spark-submit 提交":

  • 新增 spark_submit Tool,使用 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

防护策略:

  1. 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
  2. Hostname allowlist: 来自 cluster 配置的 rm_url / shs_url 的 host 走豁免
  3. DNS rebinding 防护: 解析 hostname 拿 IP,然后 dial 用 IP(不用 hostname 二次解析)
  4. 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) *MCPServer
  • mcp.NewTool(name string, opts ...ToolOption) Tool
  • mcp.WithString(name string, opts ...PropertyOption) ToolOption
  • mcp.WithArray(name string, opts ...PropertyOption) ToolOption (用于 spark_submit 的 args)
  • server.NewStreamableHTTPServer(s *MCPServer, opts ...StreamableHTTPOption) *StreamableHTTPServer
  • mcp.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. 参考资料