cloud-disk

cloud-disk 部署与运维手册

S3 兼容对象存储服务 —— 基于 Spring Cloud Alibaba 微服务架构的高性能对象存储平台。

Java 21 + Spring Boot 3.3.7 + Spring Cloud 2023.0.3 + Maven 多模块


目录

  1. 环境要求
  2. 快速部署
  3. 服务端口一览表
  4. 配置参考
  5. 健康检查
  6. 完整 API 接口文档
  7. 日志查看
  8. 常见问题排查

1. 环境要求

硬件要求

项目 最低配置 推荐配置
内存 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+ 克隆仓库

2. 快速部署

2.1 克隆仓库

git clone <repository-url>
cd cloud-disk

2.2 编译项目

# 配置 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

2.3 一键启动

docker compose up -d

首次启动需要拉取镜像(MySQL、Redis、MinIO 等),耗时取决于网络状况。 服务启动顺序已通过 depends_on + healthcheck 自动编排:MySQL → MySQL replication init + Nacos → Redis Cluster init → 业务服务。

2.4 等待服务就绪

启动后约 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"}'

2.5 启动 Admin UI(管理后台)

cd admin-ui
pnpm install
pnpm dev     # 启动开发服务器 http://localhost:5173
pnpm build   # 生产构建到 dist/

2.6 验证部署

访问以下管理控制台确认服务正常运行:

控制台 地址 默认凭证
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

2.7 停止与清理

# 推荐:快速停启(保留网络和 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

3. 服务端口一览表

所有端口在默认值基础上 偏移 +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> 请求头。


4. 配置参考

4.1 环境变量列表

以下是各模块 application.yml 中所有 ${...} 占位符变量及其默认值:

Gateway (gateway/src/main/resources/application.yml)

环境变量 默认值 说明
NACOS_SERVER localhost:38848 Nacos 服务端地址
SENTINEL_DASHBOARD localhost:38858 Sentinel 控制台地址

object-storage

环境变量 默认值 说明
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 执行器端口

object-metadata

环境变量 默认值 说明
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 执行器端口

4.2 如何修改密码

修改密码需要同时更新多处配置,以修改 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.ymlenvironment 字段统一传入环境变量,避免硬编码在 application.yml 中。

4.3 如何调整内存限制

方式一:调整 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    # 年轻代

4.4 如何修改 MinIO 配置

修改 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-keyminio.secret-key),修改 MinIO 密码后需同步更新此文件并重新编译构建。

4.5 如何配置多存储后端

创建 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_IDOSS_ACCESS_KEY_SECRET 环境变量,留空则禁用 OSS 后端。


5. 健康检查

5.1 各服务健康检查端点

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。

5.2 Docker 内置健康检查

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 完成 → 业务服务启动。

5.3 排查启动失败

步骤 1: 查看各容器状态

docker compose ps

步骤 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,进入”服务管理 > 服务列表”,确认 gatewayobject-storageobject-metadata 均已注册。


6. 完整 API 接口文档

基础地址:所有 API 通过 Gateway 转发,基础 URL 为 http://localhost:38080

认证方式:通过 Authorization: Bearer <token> 请求头传递 JWT Token(需部署鉴权模块)。

用户标识:通过 X-User-Id 请求头传递用户 ID(当前为可选参数)。

通用响应格式

{
  "code": 200,
  "message": "OK",
  "data": { ... }
}

6.1 Bucket 管理

6.1.1 创建 Bucket

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"
}

6.1.2 列举 Bucket

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
  }
}

6.1.3 删除 Bucket

仅允许删除空 Bucket(不含任何对象)。

DELETE /{bucket}

请求示例:

curl -X DELETE http://localhost:38080/my-bucket \
  -H "Authorization: Bearer <token>"

响应: 200 OK

6.1.4 检测 Bucket 是否存在

HEAD /{bucket}

请求示例:

curl -I http://localhost:38080/my-bucket \
  -H "Authorization: Bearer <token>"

响应:


6.2 对象操作

6.2.1 上传对象 (PutObject)

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。

6.2.2 下载对象 (GetObject)

GET /{bucket}/{*objectKey}

默认行为 — 302 跳转至预签名 URL:

Gateway 默认返回 302 FoundLocation 头指向 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() 方法,始终走代理。

6.2.3 获取对象元数据 (HeadObject)

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

6.2.4 删除对象 (DeleteObject)

DELETE /{bucket}/{*objectKey}

请求示例:

curl -X DELETE http://localhost:38080/my-bucket/hello.txt \
  -H "Authorization: Bearer <token>"

响应: 200 OK

删除操作为软删除,对象移入回收站(状态变为 RECYCLED)。


6.3 批量删除对象

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"]
  }
}

6.4 复制对象 (CopyObject)

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"
  }
}

6.5 Multipart Upload(分片上传)

分片上传分为以下步骤:

  1. 初始化 — 获取 uploadId
  2. 上传分片 — 上传每个分片,获取 ETag
  3. 完成 — 提交分片列表,合并文件
  4. (可选)取消 — 放弃未完成的上传
  5. (可选)列举分片 — 查看已上传的分片

6.5.1 初始化分片上传

POST /{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 格式,天然基于时间有序。

6.5.2 上传分片

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 需要记录下来,用于后续完成分片上传时的提交。

6.5.3 完成分片上传

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
  }
}

6.5.4 取消分片上传

DELETE /{bucket}/{*objectKey}?uploadId={uploadId}

请求示例:

curl -X DELETE "http://localhost:38080/my-bucket/large-file.iso?uploadId=0195f1a2-..." \
  -H "Authorization: Bearer <token>"

响应: 200 OK

6.5.5 列举已上传分片

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
    }
  ]
}

6.6 列举对象 (ListObjectsV2)

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>

IsTruncatedtrue 时,响应中包含 <NextContinuationToken>,可使用该值作为 continuation-token 参数请求下一页。


6.7 分享链接

6.7.1 创建分享链接

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
  }
}

6.7.2 验证分享链接

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
  }
}

6.7.3 我的分享列表

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
  }
}

6.7.4 撤销分享

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"
}

6.8 回收站管理

6.8.1 回收站列表

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
  }
}

6.8.2 从回收站恢复

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-Job recycleCleanupHandler 每天凌晨 3 点自动清理过期对象(标记 DELETED → 物理删除 MinIO 文件 → 删除 DB 记录)。

6.9 预签名 URL (Pre-signed URL)

为 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-gatewayextra_hosts)使容器内可解析宿主机地址。


7. 日志查看

7.1 Docker Compose 日志

# 查看所有服务日志
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

7.2 日志级别

各模块默认日志级别:

模块 日志级别
com.clouddisk DEBUG
io.minio WARN
org.springframework.cloud.gateway INFO

可通过环境变量或 application.yml 临时调整日志级别:

logging:
  level:
    com.clouddisk: TRACE    # 更详细的调试日志
    org.springframework: ERROR

7.3 各模块日志路径

应用运行在容器中,日志默认输出到容器标准输出(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

8. 常见问题排查

8.1 端口冲突

现象: 启动时提示 port is already allocated

原因: 宿主机上的其他服务占用了同一端口。

解决方案:

# 查找占用端口的进程
ss -tlnp | grep 38080

# 终止进程
kill <PID>

# 常见:残留 JMeter 进程占用端口
pkill -f ApacheJMeter.jar

# 或在 docker-compose.yml 中修改端口映射
services:
  gateway:
    ports:
      - "48080:38080"   # 改为其他可用端口

8.2 内存不足

现象: 容器启动后自动退出(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 或更高。

8.3 MinIO 连接失败

现象: object-storage 启动报错 MinIO endpoint unreachableConnection refused

排查步骤:

  1. 确认 MinIO 容器是否正常运行:
docker compose ps minio
  1. 查看 MinIO 日志:
docker compose logs minio
  1. 从容器内部测试连通性:
docker exec -it cloud-disk-object-storage curl -f http://minio:39000/minio/health/live
  1. 检查 MinIO 凭证是否匹配:

    • docker-compose.ymlMINIO_ROOT_USER / MINIO_ROOT_PASSWORD
    • application.ymlminio.access-key / minio.secret-key

8.4 MySQL 连接失败

现象: 业务服务启动时抛出 CommunicationsExceptionAccess denied

排查步骤:

  1. 确认 MySQL 容器状态:
docker compose ps mysql
  1. 查看 MySQL 日志:
docker compose logs mysql
  1. 从容器内部测试连接:
docker exec -it cloud-disk-object-storage mysql -h mysql -P 3306 -u root -proot123 -e "SELECT 1"
  1. 确认 XXL-Job 中的数据库连接参数与 MySQL 密码一致:
services:
  xxl-job:
    environment:
      PARAMS: "--spring.datasource.password=root123"
  1. 检查 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

8.4.1 MySQL 主从复制失效

现象:登录返回 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;"

8.5 Nacos 注册失败

现象: Gateway 启动后访问路由返回 503,提示 Service Unavailable

排查步骤:

  1. 确认 Nacos 容器正常运行:
docker compose ps nacos
  1. 访问 Nacos 控制台:http://localhost:38848/nacos
  2. 检查服务列表是否包含 object-storageobject-metadata
  3. 从服务容器内部测试 Nacos 连通性:
docker exec -it cloud-disk-object-storage curl -f http://nacos:8848/nacos/v1/ns/service/list

8.6 上传文件失败(超时/过大)

现象: 上传大文件时连接断开或超时。

解决方案:

在 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     # 不限制请求大小

S3 兼容性速查表

功能 支持 说明
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