因为“简单实现”旨在厘清数据流向、角色边界与失败点,而非替代go-fastdfs或minio等成熟方案;二者虽开箱即用、无中心、自动同步,但会掩盖文件id生成、元数据存储、节点下线兜底等关键设计决策。

为什么不用直接上 go-fastdfs 或 MinIO?
因为“简单实现”不是为了替代成熟方案,而是为了看清数据流向、角色边界和失败点。go-fastdfs 是开箱即用的生产级系统,但它的无中心设计、自动同步逻辑、分块上传策略会掩盖很多基础决策——比如:谁负责生成文件 ID?元数据存在哪?节点下线时请求怎么兜底?这些在自己写一遍 Gateway 和 StorageNode 时才会暴露。
Gateway 必须绕开文件体,只做路由和校验
网关层如果把整个文件读进内存再转发,就变成性能瓶颈和单点故障源。正确做法是让它只处理 HTTP 头和元信息:
-
POST /upload只接收filename、size、md5(可选),不读body - 校验通过后,调用
selectStorageNode()拿到健康节点,生成带签名的临时 URL:https://node2:8081/file/abc123?token=sha256... - 签名必须包含过期时间戳,否则
token被截获后可无限重放 - 返回给客户端的 JSON 中不要暴露节点真实地址,用反向代理或 DNS 隐藏内部拓扑
Storage Node 的 PUT 接口要独立校验,不能信任 Gateway
网关可能被绕过,或因网络问题发错请求。每个存储节点必须自己验证三件事:
- 检查
token是否有效且未过期(用相同 secret + 时间窗口比对) - 读取
Content-Length并和 URL 中的size参数比对,防止恶意放大攻击 - 若客户端传了
X-File-MD5header,保存前必须计算并校验,不匹配就返回400 Bad Request - 文件落地路径别硬编码
/tmp,用配置项BaseStorage,并确保目录有写权限
Registry 不需要 etcd,一个带 TTL 的 map 就够用
中小规模场景下,强一致注册中心反而增加运维负担。用 Go 原生 sync.Map + 定时清理更轻量:
- 每个节点启动后向 Registry 发
POST /register?node_id=node1&addr=10.0.1.5:8081 - Registry 用
map[string]struct{Addr string; LastSeen time.Time}存储,LastSeen每次心跳更新 - 后台 goroutine 每 30 秒扫描,删掉
LastSeen超过 90 秒的节点 - Gateway 查询节点列表时,只返回
LastSeen在 90 秒内的条目,并按磁盘使用率过滤
真正容易被忽略的,是 Storage Node 在写入失败后要不要主动通知 Registry 下线自己——不通知会导致流量继续打过来,形成雪崩。这个闭环必须手工补上,没有框架替你做。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











