S3 兼容对象存储服务 —— 基于 Spring Cloud Alibaba 微服务架构的高性能对象存储平台。
Java 21 + Spring Boot 3.3.7 + Spring Cloud 2023.0.3 + Maven 多模块
| 项目 | 最低配置 | 推荐配置 |
|---|---|---|
| 内存 | 8GB | 16GB |
| 磁盘 | 20GB | 50GB+ |
| CPU | 2 核 | 4 核 |
所有组件通过 Docker Compose 编排,各容器内存限制如下:
| 服务 | 内存限制 |
|---|---|
| MySQL (master) | 512M |
| MySQL (slave) | 512M |
| Redis Cluster | 192M ×6 |
| RabbitMQ | 320M |
| MinIO | 320M |
| Nacos | 448M |
| Sentinel | 256M |
| XXL-Job | 320M |
| Gateway | 768M |
| object-storage | 512M |
| object-metadata | 512M |
| 合计 | ~5.6GB |
容器总限制约 5.6GB,宿主机还需为操作系统、Docker 运行时、日志缓存等预留空间,故推荐 16GB 内存。
| 软件 | 版本要求 | 说明 |
|---|---|---|
| Docker | 24.0+ | 容器运行时 |
| Docker Compose | 2.20+ | 容器编排工具 |
| Java | 21+ | 仅本地编译需要,运行时由容器提供 |
| Maven | 3.9+ | 仅本地编译需要 |
| Git | 2.30+ | 克隆仓库 |
git clone <repository-url>
cd cloud-disk
# 配置 JAVA_HOME(写入 .env 文件,Makefile 自动加载)
echo "JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64" > .env
# 自动检测本机 IP 并写入 .env(MinIO 预签名 URL 需要外部可达地址)
make config
# 全量编译,跳过测试以加速
make package-fast
编译成功后,每个模块的 target/ 目录下会生成对应的 JAR 包:
gateway/target/gateway-1.0.0-SNAPSHOT.jar
object-storage/target/object-storage-1.0.0-SNAPSHOT.jar
object-metadata/target/object-metadata-1.0.0-SNAPSHOT.jar
docker compose up -d
首次启动需要拉取镜像(MySQL、Redis、MinIO 等),耗时取决于网络状况。 服务启动顺序已通过
depends_on+healthcheck自动编排:MySQL → MySQL replication init + Nacos → Redis Cluster init → 业务服务。
启动后约 2~3 分钟 所有服务完成初始化。可通过以下方式确认:
# 查看所有容器状态(全部 Up 且基础设施为 healthy)
docker compose ps
# 验证网关可用
curl -s -w "\nHTTP:%{http_code}" -X POST http://localhost:38080/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
cd admin-ui
pnpm install
pnpm dev # 启动开发服务器 http://localhost:5173
pnpm build # 生产构建到 dist/
访问以下管理控制台确认服务正常运行:
| 控制台 | 地址 | 默认凭证 |
|---|---|---|
| Admin UI | http://localhost:5173 | admin/123456 |
| Nacos 注册中心 | http://localhost:38848/nacos | nacos / nacos |
| MinIO 管理控制台 | http://localhost:39001 | minioadmin / minioadmin |
| RabbitMQ 管理界面 | http://localhost:45672 | admin / admin123 |
| Sentinel 仪表盘 | http://localhost:38858 | sentinel / sentinel |
| XXL-Job 调度中心 | http://localhost:39080/xxl-job-admin | admin / 123456 |
# 推荐:快速停启(保留网络和 volumes,Redis Cluster 不受影响)
docker compose stop
docker compose start
# 完整停止(删除网络)
docker compose down
# ⚠️ 如果执行了 docker compose down 后重新 up
# 必须先清除 Redis Cluster 数据卷!否则子网变化导致 cluster 不可用:
docker volume rm cloud-disk_redis_7000_data cloud-disk_redis_7001_data \
cloud-disk_redis_7002_data cloud-disk_redis_7003_data \
cloud-disk_redis_7004_data cloud-disk_redis_7005_data
# 停止并删除所有数据卷(⚠️ 会清空 MySQL、MinIO、Redis 等持久化数据)
docker compose down -v
所有端口在默认值基础上 偏移 +30000 以避免与本地已有服务冲突。
| 服务 | 映射端口 | 容器内端口 | 说明 |
|---|---|---|---|
| Admin UI(管理后台) | 5173 | — | React 开发服务器 (dev) |
| Gateway(API 网关) | 38080 | 38080 | 统一入口,路由转发 + CORS |
| object-storage(数据面) | 38081 | 38081 | 上传/下载/分片/MinIO 操作 |
| object-metadata(控制面) | 38082 | 38082 | Bucket/元数据/回收站/分享 |
| MySQL (master) | 33306 | 3306 | 元数据库(读写) |
| MySQL (slave) | 33307 | 3306 | 元数据库(只读) |
| Redis Cluster (6 节点) | 37000-37005 | 6379 | 缓存/秒传/分片状态 |
| RabbitMQ (AMQP) | 35672 | 5672 | 消息代理 |
| RabbitMQ (管理界面) | 45672 | 15672 | 管理控制台 |
| MinIO (API) | 39000 | 9000 | S3 兼容对象存储引擎 |
| MinIO (控制台) | 39001 | 9001 | MinIO 管理界面 |
| Nacos (服务发现) | 38848 | 8848 | 注册配置中心 |
| Nacos (gRPC) | 39848 | 9848 | Nacos gRPC 通信端口 |
| Sentinel Dashboard | 38858 | 8858 | 流控熔断仪表盘 |
| XXL-Job 调度中心 | 39080 | 39080 | 分布式定时任务调度中心 |
Gateway (38080) 根据请求路径和方法自动路由到后端服务:
| 路由规则 | 鉴权 | 目标服务 |
|---|---|---|
POST /login |
公开 | object-metadata |
POST /logout, GET /me |
JWT | object-metadata |
GET / (listBuckets) |
JWT | object-metadata |
/api/** |
JWT | object-metadata |
/admin/** |
JWT | object-metadata |
/share/** |
公开 | object-metadata |
PUT/DELETE/HEAD /{bucket} |
JWT | object-metadata |
GET /{bucket}?list-type=2 |
JWT | object-metadata |
含 uploads/uploadId/partNumber 参数的对象路径 |
JWT | object-storage |
GET/HEAD /{bucket}/{*objectKey} |
JWT | object-storage |
PUT/DELETE/POST /{bucket}/{*objectKey} |
JWT | object-storage |
鉴权说明:Gateway AuthFilter 放行
/login、/register、/logout、/health、/share/**、/actuator/**。其余路径需携带Authorization: Bearer <token>请求头。
以下是各模块 application.yml 中所有 ${...} 占位符变量及其默认值:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
NACOS_SERVER |
localhost:38848 |
Nacos 服务端地址 |
SENTINEL_DASHBOARD |
localhost:38858 |
Sentinel 控制台地址 |
| 环境变量 | 默认值 | 说明 |
|---|---|---|
NACOS_SERVER |
localhost:38848 |
Nacos 服务端地址 |
SENTINEL_DASHBOARD |
localhost:38858 |
Sentinel 控制台地址 |
MYSQL_HOST |
localhost |
MySQL 主库主机地址 |
MYSQL_PORT |
33306 |
MySQL 主库端口 |
MYSQL_SLAVE_HOST |
localhost |
MySQL 从库主机地址 |
MYSQL_SLAVE_PORT |
33307 |
MySQL 从库端口 |
MYSQL_USER |
root |
MySQL 用户名 |
MYSQL_PASSWORD |
root123 |
MySQL 密码 |
RABBITMQ_HOST |
localhost |
RabbitMQ 主机地址 |
RABBITMQ_PORT |
35672 |
RabbitMQ AMQP 端口 |
RABBITMQ_USER |
admin |
RabbitMQ 用户名 |
RABBITMQ_PASSWORD |
admin123 |
RabbitMQ 密码 |
REDIS_CLUSTER_NODES |
localhost:37000,...,37005 |
Redis Cluster 6 节点地址 |
MINIO_ENDPOINT |
http://localhost:39000 |
MinIO 服务端点地址 |
MINIO_PUBLIC_ENDPOINT |
http://localhost:39000 |
预签名 URL 公开端点(需客户端可达) |
STORAGE_LOCAL_PATH |
./data |
本地存储后端基础路径 |
OSS_ENDPOINT |
oss-cn-hangzhou.aliyuncs.com |
OSS 端点地址 |
OSS_ACCESS_KEY_ID |
— | OSS AccessKey ID(留空禁用) |
OSS_ACCESS_KEY_SECRET |
— | OSS AccessKey Secret(留空禁用) |
XXL_JOB_ADMIN |
http://localhost:39080/xxl-job-admin |
XXL-Job Admin 地址 |
XXL_JOB_TOKEN |
cloud-disk-token |
XXL-Job 访问 Token |
XXL_JOB_APPNAME |
object-storage |
XXL-Job 执行器名称 |
XXL_JOB_EXECUTOR_PORT |
38091 |
XXL-Job 执行器端口 |
| 环境变量 | 默认值 | 说明 |
|---|---|---|
NACOS_SERVER |
localhost:38848 |
Nacos 服务端地址 |
SENTINEL_DASHBOARD |
localhost:38858 |
Sentinel 控制台地址 |
MYSQL_HOST |
localhost |
MySQL 主库主机地址 |
MYSQL_PORT |
33306 |
MySQL 主库端口 |
MYSQL_SLAVE_HOST |
localhost |
MySQL 从库主机地址 |
MYSQL_SLAVE_PORT |
33307 |
MySQL 从库端口 |
MYSQL_USER |
root |
MySQL 用户名 |
MYSQL_PASSWORD |
root123 |
MySQL 密码 |
RABBITMQ_HOST |
localhost |
RabbitMQ 主机地址 |
RABBITMQ_PORT |
35672 |
RabbitMQ AMQP 端口 |
RABBITMQ_USER |
admin |
RabbitMQ 用户名 |
RABBITMQ_PASSWORD |
admin123 |
RabbitMQ 密码 |
REDIS_CLUSTER_NODES |
localhost:37000,...,37005 |
Redis Cluster 6 节点地址 |
XXL_JOB_ADMIN |
http://localhost:39080/xxl-job-admin |
XXL-Job Admin 地址 |
XXL_JOB_TOKEN |
cloud-disk-token |
XXL-Job 访问 Token |
XXL_JOB_APPNAME |
object-metadata |
XXL-Job 执行器名称 |
XXL_JOB_EXECUTOR_PORT |
38092 |
XXL-Job 执行器端口 |
修改密码需要同时更新多处配置,以修改 MySQL 密码为例:
步骤 1: 修改 docker-compose.yml 中的环境变量
services:
mysql:
environment:
MYSQL_ROOT_PASSWORD: my-new-password # 改为新密码
步骤 2: 修改业务服务的 application.yml 中 ShardingSphere 数据源密码默认值
# object-storage/src/main/resources/application.yml 和 object-metadata/src/main/resources/application.yml
spring:
shardingsphere:
datasource:
ds_master:
password: ${MYSQL_PASSWORD:my-new-password}
ds_slave:
password: ${MYSQL_PASSWORD:my-new-password}
# ... 其他数据源同理
步骤 3: 如果 XXL-Job 也需要连接 MySQL,同步修改其环境变量中的密码。
各服务之间共享密码的场景,建议通过
docker-compose.yml的environment字段统一传入环境变量,避免硬编码在 application.yml 中。
方式一:调整 Docker 容器级别内存限制
在 docker-compose.yml 中修改 mem_limit:
services:
object-storage:
mem_limit: 512M # 从 350M 改为 512M
方式二:调整 JVM 堆内存
通过 JVM_OPTS 环境变量控制:
services:
object-storage:
build:
args:
JVM_OPTS: "-Xms256m -Xmx512m"
environment:
JVM_OPTS: "-Xms256m -Xmx512m"
Nacos 和 Sentinel 使用独立的 JVM 环境变量:
services:
nacos:
environment:
JVM_XMS: 256m # 初始堆
JVM_XMX: 384m # 最大堆
JVM_XMN: 128m # 年轻代
修改 MinIO 根凭证:
services:
minio:
environment:
MINIO_ROOT_USER: my-new-user # 默认 minioadmin
MINIO_ROOT_PASSWORD: my-new-pass # 默认 minioadmin
同时同步修改业务服务的 MinIO 连接配置:
services:
object-storage:
environment:
MINIO_ENDPOINT: http://minio:39000
# 注意:minio access-key 和 secret-key 在 application.yml 中硬编码,
# 修改后需要同步更新 object-storage/src/main/resources/application.yml
当前 MinIO 的 access-key 和 secret-key 在
object-storage/src/main/resources/application.yml中为硬编码(minio.access-key和minio.secret-key),修改 MinIO 密码后需同步更新此文件并重新编译构建。
创建 Bucket 时通过 X-Cloud-Storage-Type 请求头指定存储类型:
# MinIO(默认,无需指定)
curl -X PUT http://localhost:38080/my-bucket -H "Authorization: Bearer <token>"
# 本地文件系统
curl -X PUT http://localhost:38080/my-local-bucket \
-H "Authorization: Bearer <token>" \
-H "X-Cloud-Storage-Type: LOCAL"
# 阿里云 OSS(需先在 application.yml 中配置凭证)
curl -X PUT http://localhost:38080/my-oss-bucket \
-H "Authorization: Bearer <token>" \
-H "X-Cloud-Storage-Type: OSS"
本地存储后端的文件默认保存在 ./data 目录,可通过 STORAGE_LOCAL_PATH 环境变量修改。
OSS 后端需配置 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET 环境变量,留空则禁用 OSS 后端。
object-storage 和 object-metadata 提供 Spring Boot Actuator 健康端点。Gateway 未引入 Actuator,使用登录接口验证。
| 服务 | 健康检查地址 |
|---|---|
| Gateway | POST http://localhost:38080/login (验证可用) |
| object-storage | http://localhost:38081/actuator/health |
| object-metadata | http://localhost:38082/actuator/health |
Gateway 验证示例:
curl -s -w "\nHTTP:%{http_code}" -X POST http://localhost:38080/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
预期响应:HTTP 200 + JSON 含 token。
docker-compose.yml 已为基础设施服务配置内置健康检查:
| 服务 | 检查命令 | 间隔 | 超时 | 重试 |
|---|---|---|---|---|
| MySQL | mysqladmin ping -h localhost |
10s | 5s | 5 |
| Redis | redis-cli -p 6379 ping |
10s | 5s | 5 |
| RabbitMQ | rabbitmq-diagnostics ping |
10s | 5s | 5 |
| MinIO | curl -f http://localhost:9000/minio/health/live |
10s | 5s | 5 |
| Nacos | curl -f http://localhost:8848/nacos/v1/console/health/readiness |
10s | 5s | 5 |
depends_on 条件确保启动顺序:MySQL healthy → MySQL replication init 完成 + Nacos healthy + Redis Cluster init 完成 → 业务服务启动。
步骤 1: 查看各容器状态
docker compose ps
Up 表示运行正常Exit 或 Restarting 表示异常步骤 2: 查看异常容器日志
docker compose logs <service-name>
# 例如:
docker compose logs object-storage
docker compose logs mysql
步骤 3: 检查依赖链
启动顺序依赖关系(由 docker-compose.yml 的 depends_on 强制保证):
MySQL (healthy) → MySQL replication init
→ XXL-Job
Nacos (healthy) → Gateway / object-storage / object-metadata
Redis 6节点 (healthy) → Redis Cluster init (exit 0) → Gateway / object-storage / object-metadata
先确保 MySQL、Nacos 和 Redis Cluster 正常运行,再排查业务服务。如果 Redis Cluster 初始化失败,业务服务将不会启动。
步骤 4: Nacos 注册确认
访问 http://localhost:38848/nacos,进入”服务管理 > 服务列表”,确认 gateway、object-storage、object-metadata 均已注册。
基础地址:所有 API 通过 Gateway 转发,基础 URL 为
http://localhost:38080。认证方式:通过
Authorization: Bearer <token>请求头传递 JWT Token(需部署鉴权模块)。用户标识:通过
X-User-Id请求头传递用户 ID(当前为可选参数)。通用响应格式:
{
"code": 200,
"message": "OK",
"data": { ... }
}
PUT /{bucket}
请求示例:
curl -X PUT http://localhost:38080/my-bucket \
-H "Authorization: Bearer <token>" \
-H "X-User-Id: 1"
响应: 201 Created
{
"code": 200,
"message": "OK"
}
GET /
请求示例:
curl http://localhost:38080/ \
-H "Authorization: Bearer <token>" \
-H "X-User-Id: 1"
查询参数:
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
pageNum |
否 | 1 |
页码 |
pageSize |
否 | 20 |
每页条数 |
响应:
{
"code": 200,
"message": "OK",
"data": {
"list": [
{
"Name": "my-bucket",
"CreationDate": "2025-05-13T12:00:00"
}
],
"total": 1,
"pageNum": 1,
"pageSize": 20,
"totalPages": 1
}
}
仅允许删除空 Bucket(不含任何对象)。
DELETE /{bucket}
请求示例:
curl -X DELETE http://localhost:38080/my-bucket \
-H "Authorization: Bearer <token>"
响应: 200 OK
HEAD /{bucket}
请求示例:
curl -I http://localhost:38080/my-bucket \
-H "Authorization: Bearer <token>"
响应:
200 OK — Bucket 存在404 Not Found — Bucket 不存在PUT /{bucket}/{*objectKey}
请求头:
| 请求头 | 必填 | 说明 |
|---|---|---|
Content-Type |
否 | 对象 MIME 类型(默认 application/octet-stream) |
Content-Length |
是 | 对象大小(字节) |
x-cloud-disk-md5 |
否 | 文件 MD5 值,用于秒传判断 |
X-User-Id |
否 | 用户 ID |
请求示例——普通上传:
curl -X PUT http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer <token>" \
-H "Content-Type: text/plain" \
-H "Content-Length: 12" \
-d "Hello World!"
请求示例——秒传(携带 MD5):
MD5=$(md5sum large-file.bin | cut -d' ' -f1)
curl -X PUT http://localhost:38080/my-bucket/large-file.bin \
-H "Authorization: Bearer <token>" \
-H "x-cloud-disk-md5: $MD5" \
-T large-file.bin
响应:
{
"code": 200,
"message": "OK",
"data": {
"objectKey": "hello.txt",
"etag": "d41d8cd98f00b204e9800998ecf8427e",
"instant": false
}
}
instant: true表示秒传命中(文件已存在,无需实际上传);instant: false表示实际写入 MinIO。
GET /{bucket}/{*objectKey}
默认行为 — 302 跳转至预签名 URL:
Gateway 默认返回 302 Found,Location 头指向 MinIO 预签名下载 URL。客户端跟随重定向后直连存储后端下载,不经过 Gateway 转发,显著减轻 Gateway 带宽压力。
# 跟随 302 重定向下载(-L 参数)
curl -L http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer <token>" \
-o hello.txt
# 仅获取 302 响应(查看 Location 头)
curl -I http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer <token>"
# HTTP/1.1 302 Found
# Location: http://localhost:39000/my-bucket/hello.txt?X-Amz-Algorithm=...
代理模式 — 数据流经 Gateway:
对于不支持预签名的存储后端(如 Local),系统自动回退到代理模式。也可显式通过 ?proxy=true 强制代理:
# 强制代理模式(数据流经 Gateway,响应 200 + 流式 body)
curl http://localhost:38080/my-bucket/hello.txt?proxy=true \
-H "Authorization: Bearer <token>" \
-o hello.txt
响应头(代理模式):
Content-Type: text/plain
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Content-Length: 12
设计说明:302 跳转由
ObjectController.getObject()实现,先调用StorageBackend.presignGetObject()生成预签名 URL;若后端不支持(抛出UnsupportedOperationException),自动捕获并回退到getObjectWithMeta()代理下载。?proxy=true参数映射到getObjectProxy()方法,始终走代理。
HEAD /{bucket}/{*objectKey}
请求示例:
curl -I http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer <token>"
响应头:
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Content-Length: 12
Content-Type: text/plain
Last-Modified: 2025-05-13T12:00:00
DELETE /{bucket}/{*objectKey}
请求示例:
curl -X DELETE http://localhost:38080/my-bucket/hello.txt \
-H "Authorization: Bearer <token>"
响应: 200 OK
删除操作为软删除,对象移入回收站(状态变为 RECYCLED)。
POST /{bucket}?delete
请求体(XML 格式):
<Delete>
<Object>
<Key>file1.txt</Key>
</Object>
<Object>
<Key>file2.txt</Key>
</Object>
<Quiet>false</Quiet>
</Delete>
请求示例:
curl -X POST "http://localhost:38080/my-bucket?delete" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<Delete>
<Object><Key>file1.txt</Key></Object>
<Object><Key>file2.txt</Key></Object>
<Quiet>false</Quiet>
</Delete>'
响应:
{
"code": 200,
"message": "OK",
"data": {
"Deleted": ["file1.txt", "file2.txt"]
}
}
PUT /{bucket}/{destKey}
Header: x-copy-source: /{srcBucket}/{srcKey}
仅在同一个 Bucket 内复制对象。
请求示例:
curl -X PUT http://localhost:38080/my-bucket/copy-of-hello.txt \
-H "Authorization: Bearer <token>" \
-H "x-copy-source: /my-bucket/hello.txt"
响应:
{
"code": 200,
"message": "OK",
"data": {
"etag": "d41d8cd98f00b204e9800998ecf8427e",
"objectKey": "copy-of-hello.txt"
}
}
分片上传分为以下步骤:
uploadIdETagPOST /{bucket}/{*objectKey}?uploads
请求示例:
curl -X POST "http://localhost:38080/my-bucket/large-file.iso?uploads" \
-H "Authorization: Bearer <token>" \
-H "X-User-Id: 1"
响应: 201 Created
{
"code": 200,
"message": "OK",
"data": {
"uploadId": "0195f1a2-b3c4-5d6e-7f80-9a1b2c3d4e5f",
"bucket": "my-bucket",
"key": "large-file.iso"
}
}
uploadId使用 UUID v7 格式,天然基于时间有序。
PUT /{bucket}/{*objectKey}?partNumber={N}&uploadId={uploadId}
| 参数 | 说明 |
|---|---|
partNumber |
分片编号(从 1 开始) |
uploadId |
初始化时返回的上传 ID |
请求示例:
# 分片 1
curl -X PUT "http://localhost:38080/my-bucket/large-file.iso?partNumber=1&uploadId=0195f1a2-..." \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @part1.bin
# 分片 2
curl -X PUT "http://localhost:38080/my-bucket/large-file.iso?partNumber=2&uploadId=0195f1a2-..." \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @part2.bin
响应头:
ETag: "5a8dd3ad0756a93d72b9c4a1b1e5a5b1"
每个分片的 ETag 需要记录下来,用于后续完成分片上传时的提交。
POST /{bucket}/{*objectKey}?uploadId={uploadId}
请求体(XML 格式):
<CompleteMultipartUpload>
<Part>
<PartNumber>1</PartNumber>
<ETag>"5a8dd3ad0756a93d72b9c4a1b1e5a5b1"</ETag>
</Part>
<Part>
<PartNumber>2</PartNumber>
<ETag>"b9c4a1b1e5a5b15a8dd3ad0756a93d72"</ETag>
</Part>
</CompleteMultipartUpload>
请求示例:
curl -X POST "http://localhost:38080/my-bucket/large-file.iso?uploadId=0195f1a2-..." \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<CompleteMultipartUpload>
<Part>
<PartNumber>1</PartNumber>
<ETag>"5a8dd3ad0756a93d72b9c4a1b1e5a5b1"</ETag>
</Part>
<Part>
<PartNumber>2</PartNumber>
<ETag>"b9c4a1b1e5a5b15a8dd3ad0756a93d72"</ETag>
</Part>
</CompleteMultipartUpload>'
响应:
{
"code": 200,
"message": "OK",
"data": {
"objectKey": "large-file.iso",
"etag": "7a8dd3ad0756a93d72b9c4a1b1e5a5c3",
"instant": false
}
}
DELETE /{bucket}/{*objectKey}?uploadId={uploadId}
请求示例:
curl -X DELETE "http://localhost:38080/my-bucket/large-file.iso?uploadId=0195f1a2-..." \
-H "Authorization: Bearer <token>"
响应: 200 OK
GET /{bucket}/{*objectKey}?uploadId={uploadId}
请求示例:
curl "http://localhost:38080/my-bucket/large-file.iso?uploadId=0195f1a2-..." \
-H "Authorization: Bearer <token>"
响应:
{
"code": 200,
"message": "OK",
"data": [
{
"partNumber": 1,
"etag": "5a8dd3ad0756a93d72b9c4a1b1e5a5b1",
"size": 5242880
},
{
"partNumber": 2,
"etag": "b9c4a1b1e5a5b15a8dd3ad0756a93d72",
"size": 5242880
}
]
}
GET /{bucket}?list-type=2
查询参数:
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
list-type |
是 | — | 必须为 2 |
prefix |
否 | — | 对象键前缀过滤 |
delimiter |
否 | — | 分隔符,用于模拟目录层次 |
max-keys |
否 | 1000 |
最大返回对象数 |
continuation-token |
否 | — | 分页续传 Token |
请求示例——列举所有对象:
curl "http://localhost:38080/my-bucket?list-type=2" \
-H "Authorization: Bearer <token>"
请求示例——按前缀过滤:
curl "http://localhost:38080/my-bucket?list-type=2&prefix=images/" \
-H "Authorization: Bearer <token>"
请求示例——使用分隔符模拟目录:
curl "http://localhost:38080/my-bucket?list-type=2&prefix=docs/&delimiter=/" \
-H "Authorization: Bearer <token>"
响应(XML 格式,兼容 S3):
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>my-bucket</Name>
<Prefix>images/</Prefix>
<MaxKeys>1000</MaxKeys>
<IsTruncated>false</IsTruncated>
<KeyCount>2</KeyCount>
<Contents>
<Key>images/photo1.jpg</Key>
<LastModified>2025-05-13T12:00:00</LastModified>
<ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag>
<Size>1024000</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>images/photo2.jpg</Key>
<LastModified>2025-05-13T12:00:00</LastModified>
<ETag>"5a8dd3ad0756a93d72b9c4a1b1e5a5b1"</ETag>
<Size>2048000</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>
当
IsTruncated为true时,响应中包含<NextContinuationToken>,可使用该值作为continuation-token参数请求下一页。
POST /admin/share
请求体:
{
"bucketName": "my-bucket",
"objectKeys": ["file1.pdf", "file2.pdf"],
"password": "optional-password",
"expireMinutes": 1440,
"maxDownloads": 10
}
| 字段 | 必填 | 说明 |
|---|---|---|
bucketName |
是 | Bucket 名称 |
objectKeys |
是 | 分享的对象键列表 |
password |
否 | 访问密码(留空则无密码) |
expireMinutes |
否 | 有效期(分钟),默认 1440(1 天) |
maxDownloads |
否 | 最大下载次数,默认无限制 |
请求示例:
curl -X POST http://localhost:38080/admin/share \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "X-User-Id: 1" \
-d '{
"bucketName": "my-bucket",
"objectKeys": ["report.pdf"],
"password": "share123",
"expireMinutes": 60,
"maxDownloads": 5
}'
响应:
{
"code": 200,
"message": "OK",
"data": {
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"expireAt": "2025-05-13T13:00:00",
"maxDownloads": 5
}
}
POST /share/{token}/verify
请求体:
{
"password": "share123"
}
| 字段 | 必填 | 说明 |
|---|---|---|
password |
否 | 创建时设置的口令,无密码可不传 |
请求示例:
curl -X POST http://localhost:38080/share/a1b2c3d4-e5f6-7890-abcd-ef1234567890/verify \
-H "Content-Type: application/json" \
-d '{"password": "share123"}'
响应:
{
"code": 200,
"message": "OK",
"data": {
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"objectKeys": ["report.pdf"],
"remaining": 5
}
}
GET /admin/shares?pageNum={pageNum}&pageSize={pageSize}
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
pageNum |
否 | 1 |
页码 |
pageSize |
否 | 20 |
每页条数 |
请求示例:
curl http://localhost:38080/admin/shares \
-H "Authorization: Bearer <token>"
响应:
{
"code": 200,
"message": "OK",
"data": {
"list": [
{
"id": 1,
"token": "a1b2c3d4",
"objectKeys": ["report.pdf"],
"status": "ACTIVE",
"expireAt": "2025-05-14T12:00:00",
"currentDownloads": 3,
"maxDownloads": 50,
"createdAt": "2025-05-13T12:00:00"
}
],
"total": 1,
"pageNum": 1,
"pageSize": 20,
"totalPages": 1
}
}
POST /admin/share/{id}/revoke
请求示例:
curl -X POST http://localhost:38080/admin/share/1/revoke \
-H "Authorization: Bearer <token>"
响应: 200 OK
{
"code": 200,
"message": "OK"
}
GET /admin/recycle?bucketId={bucketId}&pageNum={pageNum}&pageSize={pageSize}
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
bucketId |
否 | — | 按 Bucket 过滤(留空查询所有) |
pageNum |
否 | 1 |
页码 |
pageSize |
否 | 20 |
每页条数 |
请求示例:
curl http://localhost:38080/admin/recycle \
-H "Authorization: Bearer <token>" \
-H "X-User-Id: 1"
响应:
{
"code": 200,
"message": "OK",
"data": {
"list": [
{
"id": 42,
"objectKey": "deleted-file.txt",
"size": 1024000,
"updatedAt": "2025-05-13T12:30:00"
}
],
"total": 1,
"pageNum": 1,
"pageSize": 20,
"totalPages": 1
}
}
POST /admin/recycle/{objectId}/restore
请求示例:
curl -X POST http://localhost:38080/admin/recycle/42/restore \
-H "Authorization: Bearer <token>"
响应:
{
"code": 200,
"message": "OK"
}
回收站中的对象默认保存 30 天,
RecycleServiceImpl.cleanExpiredObjects()通过 XXL-JobrecycleCleanupHandler每天凌晨 3 点自动清理过期对象(标记 DELETED → 物理删除 MinIO 文件 → 删除 DB 记录)。
为 MinIO 存储后端的对象生成有时效的预签名 URL,客户端可用该 URL 直接访问 MinIO,无需 JWT 认证。
注意:预签名 URL 指向 MinIO 公开端点(
MINIO_PUBLIC_ENDPOINT),客户端需能直接访问该地址。本地存储和 OSS 后端暂不支持。
POST /{bucket}/{*objectKey}?presigned-url
查询参数:
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
method |
否 | GET |
HTTP 方法:GET(下载)或 PUT(上传) |
expiry |
否 | 3600 |
有效期(秒),范围 1–604800(7 天) |
请求示例——生成下载链接:
curl -X POST "http://localhost:38080/my-bucket/report.pdf?presigned-url" \
-H "Authorization: Bearer <token>"
响应:
{
"code": 200,
"message": "OK",
"data": {
"url": "http://host.docker.internal:39000/my-bucket/report.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"expiresIn": 3600,
"method": "GET",
"bucket": "my-bucket",
"objectKey": "report.pdf"
}
}
请求示例——生成上传链接:
curl -X POST "http://localhost:38080/my-bucket/upload.bin?presigned-url&method=PUT&expiry=1800" \
-H "Authorization: Bearer <token>"
使用预签名 URL 下载(无需 JWT):
# 直接访问 MinIO,绕过 Gateway
curl "http://host.docker.internal:39000/my-bucket/report.pdf?X-Amz-Algorithm=..."
错误响应:
| 场景 | HTTP 状态码 |
|---|---|
| method 不是 GET 或 PUT | 400 |
| expiry 不在 1–604800 范围 | 400 |
| 本地存储或 OSS 后端 | 501 |
实现原理:使用独立的
presignMinioClient(配置为minio.public-endpoint)生成预签名 URL,确保签名中的 Host 头与客户端请求的 Host 一致。Docker 环境中通过host.docker.internal:host-gateway(extra_hosts)使容器内可解析宿主机地址。
# 查看所有服务日志
docker compose logs -f
# 查看指定服务日志
docker compose logs -f gateway
docker compose logs -f object-storage
docker compose logs -f object-metadata
docker compose logs -f mysql
docker compose logs -f redis
docker compose logs -f minio
docker compose logs -f nacos
# 查看最后 N 行日志
docker compose logs --tail=100 -f object-storage
# 将日志保存到文件
docker compose logs object-storage > logs/object-storage.log
各模块默认日志级别:
| 模块 | 日志级别 |
|---|---|
com.clouddisk |
DEBUG |
io.minio |
WARN |
org.springframework.cloud.gateway |
INFO |
可通过环境变量或 application.yml 临时调整日志级别:
logging:
level:
com.clouddisk: TRACE # 更详细的调试日志
org.springframework: ERROR
应用运行在容器中,日志默认输出到容器标准输出(stdout/stdin),通过 docker compose logs 查看。
需要持久化日志时,可在 docker-compose.yml 中挂载卷:
services:
object-storage:
volumes:
- ./logs/object-storage:/app/logs
并在 application.yml 中配置日志文件输出:
logging:
file:
name: /app/logs/object-storage.log
现象: 启动时提示 port is already allocated。
原因: 宿主机上的其他服务占用了同一端口。
解决方案:
# 查找占用端口的进程
ss -tlnp | grep 38080
# 终止进程
kill <PID>
# 常见:残留 JMeter 进程占用端口
pkill -f ApacheJMeter.jar
# 或在 docker-compose.yml 中修改端口映射
services:
gateway:
ports:
- "48080:38080" # 改为其他可用端口
现象: 容器启动后自动退出(Exit 137),日志中出现 Killed。
原因: Docker 容器内存不足被 OOM Killer 终止。
解决方案:
方案一: 减少 JVM 堆内存(注意 mem_limit 须为 JVM 堆的 2~3 倍以容纳堆外内存)
services:
object-storage:
environment:
JVM_OPTS: "-Xms64m -Xmx128m"
mem_limit: 320M # 最低 320M,低于此值容器会被 OOM Kill
方案二: 关闭不必要的服务
services:
# 注释掉非必要的服务
# sentinel:
# ...
# xxl-job:
# ...
方案三: 增加 Docker 资源限制
Docker Desktop > Settings > Resources > Memory,将内存从 4GB 增加到 8GB 或更高。
现象: object-storage 启动报错 MinIO endpoint unreachable 或 Connection refused。
排查步骤:
docker compose ps minio
docker compose logs minio
docker exec -it cloud-disk-object-storage curl -f http://minio:39000/minio/health/live
检查 MinIO 凭证是否匹配:
docker-compose.yml 中 MINIO_ROOT_USER / MINIO_ROOT_PASSWORDapplication.yml 中 minio.access-key / minio.secret-key现象: 业务服务启动时抛出 CommunicationsException 或 Access denied。
排查步骤:
docker compose ps mysql
docker compose logs mysql
docker exec -it cloud-disk-object-storage mysql -h mysql -P 3306 -u root -proot123 -e "SELECT 1"
services:
xxl-job:
environment:
PARAMS: "--spring.datasource.password=root123"
sql/init.sql 是否正确初始化数据库。如需手动初始化:# 先将 SQL 文件复制到容器
docker cp sql/init.sql cloud-disk-mysql:/tmp/init.sql
# 进入容器执行
docker exec -it cloud-disk-mysql mysql -u root -proot123 cloud_disk < /tmp/init.sql
现象:登录返回 200、创建 bucket 返回 201,但 list/head bucket 返回 404 或空列表。直接查 master 的 bucket_info 有数据、slave 无数据。
原因:ShardingSphere 读写分离(写 master + 读 slave round_robin),复制未运行导致读操作从空 slave 返回空结果。
诊断:
# 检查复制状态(期望 IO/SQL 都为 Yes)
docker exec cloud-disk-mysql-slave mysql -uroot -proot123 \
-e "SHOW REPLICA STATUS\G" 2>&1 | grep -E "Running|Error"
# 比较 master/slave 数据量
docker exec cloud-disk-mysql mysql -uroot -proot123 \
-e "SELECT COUNT(*) FROM cloud_disk.bucket_info"
docker exec cloud-disk-mysql-slave mysql -uroot -proot123 \
-e "SELECT COUNT(*) FROM cloud_disk.bucket_info"
修复:
# 复制从未配置(SHOW REPLICA STATUS 返回空)
docker compose run --rm mysql-replication-init
# IO 线程报 Authentication requires secure connection
docker exec cloud-disk-mysql mysql -uroot -proot123 -e "
DROP USER IF EXISTS 'repl'@'%';
CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'repl123';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
"
docker exec cloud-disk-mysql-slave mysql -uroot -proot123 \
-e "STOP REPLICA; START REPLICA;"
# slave 数据落后(IO/SQL 都 Yes 但行数不一致)→ 全量同步
docker exec cloud-disk-mysql mysqldump -uroot -proot123 \
--databases cloud_disk cloud_disk_0 cloud_disk_1 \
--master-data=2 --single-transaction --no-create-info --replace 2>/dev/null | \
docker exec -i cloud-disk-mysql-slave mysql -uroot -proot123
docker exec cloud-disk-mysql-slave mysql -uroot -proot123 \
-e "STOP REPLICA; START REPLICA;"
现象: Gateway 启动后访问路由返回 503,提示 Service Unavailable。
排查步骤:
docker compose ps nacos
object-storage 和 object-metadata。docker exec -it cloud-disk-object-storage curl -f http://nacos:8848/nacos/v1/ns/service/list
现象: 上传大文件时连接断开或超时。
解决方案:
在 Gateway 的 application.yml 中增加请求大小限制和超时配置:
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
codec:
max-in-memory-size: 10MB
在业务服务的 application.yml 中:
spring:
servlet:
multipart:
max-file-size: -1 # 不限制文件大小
max-request-size: -1 # 不限制请求大小
| 功能 | 支持 | 说明 |
|---|---|---|
| PUT Bucket | Y | 创建 Bucket |
| DELETE Bucket | Y | 删除空 Bucket |
| HEAD Bucket | Y | 检测 Bucket 是否存在 |
| GET Bucket (ListObjectsV2) | Y | 列举对象,XML 格式 |
| PUT Object | Y | 上传对象,支持秒传 |
| GET Object | Y | 默认 302 跳转预签名 URL;不支持时回退代理 |
| HEAD Object | Y | 获取对象元数据 |
| DELETE Object | Y | 软删除(移入回收站) |
| DELETE Multiple Objects | Y | 批量删除(XML 格式请求体) |
| CopyObject | Y | 同 Bucket 内复制 |
| Multipart Upload (Init) | Y | 初始化分片上传 |
| Multipart Upload (Part) | Y | 上传分片 |
| Multipart Upload (Complete) | Y | 完成分片上传 |
| Multipart Upload (Abort) | Y | 取消分片上传 |
| List Parts | Y | 列举已上传分片 |
| Pre-signed URL | Y | 通过 MinIO SDK 原生支持 |
| Bucket Policy | N | 暂不支持 |
| Object Lock / WORM | N | 暂不支持 |
| Versioning | N | 暂不支持(使用软删除替代) |
| Server-Side Encryption | N | 暂不支持 |
| CORS | Y | Gateway 层配置 |
更多架构细节参见 docs/architecture.md。