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>
254 lines
12 KiB
Markdown
254 lines
12 KiB
Markdown
# 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.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` | `status` |
|
|
|
|
### 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. 参考资料
|
|
|
|
- [mark3labs/mcp-go GitHub](https://github.com/mark3labs/mcp-go) — MCP Server 库
|
|
- [Model Context Protocol 规范](https://modelcontextprotocol.io/) — MCP 2025-03+ 标准
|
|
- [YARN RM REST API](https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/ResourceManagerRest.html) — YARN RM 接口
|
|
- [Spark History Server REST API](https://spark.apache.org/docs/latest/monitoring.html#rest-api) — SHS 接口
|
|
- [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) — 纯 Go SQLite 驱动
|