简简单单搭建一个oss服务
第一部分:什么是 OSS?
OSS 的全称是 对象存储服务。
你可以把它想象成一个巨大的、无限的、在互联网上的“网盘”,但它是专门为程序(而不是主要为人)来存储和读取文件而设计的。
为了理解它,我们通常把它和另外两种存储方式做对比:
块存储
- 比喻:就像电脑的硬盘(C盘、D盘)。它被格式化成一种文件系统(如NTFS, ext4),你可以创建文件夹、移动文件、直接修改文件的一部分。
- 特点:低延迟,常用于安装操作系统、数据库等需要直接读写磁盘块的场景。
- 例子:你电脑的硬盘、服务器的本地硬盘、SAN。
文件存储
- 比喻:就像一个公司内部的共享网络驱动器(Network Attached Storage - NAS)。你通过路径(如
\\NAS\部门\项目\报告.doc
)来访问文件。 - 特点:有目录层级,支持文件锁,多人可共享访问。
- 例子:Windows 网络共享、NFS。
- 比喻:就像一个公司内部的共享网络驱动器(Network Attached Storage - NAS)。你通过路径(如
对象存储(OSS)
- 比喻:一个巨大的仓库。每个文件都是一个独立的“对象”。你不需要关心这个对象放在仓库的哪个货架(路径)上,系统会给你一个唯一的“取件码”(Object Key 或 URL)。你通过这个“取件码”来存或取整个文件。
- 核心概念:
- 对象: 文件本身 + 文件的元数据(描述信息,如创建时间、类型) + 一个全局唯一的 Key。
- 桶: 存放对象的容器,你可以把它理解成仓库里的一个区域。桶名在全球必须是唯一的。
- 扁平结构: 没有真正的文件夹概念。虽然 Key 可以包含类似路径的前缀(如
images/2024/photo.jpg
),但这只是为了组织方便,在底层它依然是一个唯一的字符串 Key。
- 特点:
- 海量存储: 容量可以轻松扩展到 EB 级别。
- 高可靠耐用: 数据会自动在多个设备、多个机房备份,防止丢失。
- 高可用性: 随时可以通过互联网访问。
- RESTful API: 使用简单的 HTTP/HTTPS 请求(GET, PUT, DELETE)即可上传下载文件。
- 成本低: 通常比块存储和文件存储更便宜。
主流 OSS 服务商:阿里云 OSS、腾讯云 COS、AWS S3、微软 Azure Blob Storage 等。
第二部分:怎么做一个简单的 OSS?
搭建一个生产级别、高可用的 OSS(像阿里云那样)极其复杂,涉及分布式系统、集群管理、数据一致性等高级话题。但我们可以搭建一个简化版的、单机的 OSS 服务来理解其核心原理。
我们的目标是:创建一个可以通过 HTTP API 上传、下载、删除文件的服务。
技术栈选择:
- 后端语言: Python(简单易学,库丰富)
- Web 框架: FastAPI(现代,高性能,自动生成 API 文档)
- 文件存储: 直接使用服务器的本地文件系统
- 元数据存储: 使用 SQLite 数据库(轻量级)来记录文件的元信息(Key, 文件名, 大小等)
实现步骤:
第 1 步:项目初始化
# 创建项目目录
mkdir my_simple_oss
cd my_simple_oss# 创建虚拟环境(可选但推荐)
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate.bat # Windows# 安装依赖
pip install fastapi uvicorn sqlalchemy python-multipart
第 2 步:编写代码 (main.py
)
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import FileResponse
import sqlite3
import os
import shutil
from pathlib import Path
from pydantic import BaseModel# 初始化 FastAPI 应用
app = FastAPI(title="我的简易 OSS")# 配置
STORAGE_PATH = "./storage" # 文件存储的根目录
DB_PATH = "./metadata.db" # 元数据库路径# 确保存储目录存在
Path(STORAGE_PATH).mkdir(exist_ok=True)# 初始化数据库
def init_db():conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()cursor.execute('''CREATE TABLE IF NOT EXISTS objects (key TEXT PRIMARY KEY,original_filename TEXT,size INTEGER,content_type TEXT,uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')conn.commit()conn.close()# 启动时初始化数据库
init_db()class ObjectItem(BaseModel):key: stroriginal_filename: strsize: intcontent_type: str# 1. 上传文件(创建对象)
@app.post("/objects/{key}")
async def upload_object(key: str, file: UploadFile = File(...)):# 防止路径遍历攻击,确保 key 是安全的if ".." in key or "/" in key:raise HTTPException(status_code=400, detail="Invalid key")file_path = os.path.join(STORAGE_PATH, key)# 保存文件到本地存储with open(file_path, "wb") as buffer:# 读取文件内容并写入,避免大内存占用contents = await file.read()buffer.write(contents)# 将文件元信息保存到数据库conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()try:cursor.execute("INSERT INTO objects (key, original_filename, size, content_type) VALUES (?, ?, ?, ?)",(key, file.filename, len(contents), file.content_type))conn.commit()except sqlite3.IntegrityError:# 如果 key 已存在,我们覆盖它(实际生产环境可能更复杂)os.remove(file_path)raise HTTPException(status_code=409, detail=f"Object with key '{key}' already exists")finally:conn.close()return {"message": "File uploaded successfully", "key": key}# 2. 下载文件(获取对象)
@app.get("/objects/{key}")
async def download_object(key: str):file_path = os.path.join(STORAGE_PATH, key)# 检查文件是否存在if not os.path.isfile(file_path):raise HTTPException(status_code=404, detail="Object not found")# 从数据库获取元信息(例如文件名)conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()cursor.execute("SELECT original_filename, content_type FROM objects WHERE key=?", (key,))row = cursor.fetchone()conn.close()if row is None:raise HTTPException(status_code=404, detail="Object metadata not found")original_filename, content_type = row# 使用 FileResponse 返回文件,并设置正确的下载文件名return FileResponse(path=file_path,filename=original_filename,media_type=content_type)# 3. 列出所有对象(可选,简单实现)
@app.get("/objects/")
async def list_objects():conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()cursor.execute("SELECT key, original_filename, size, content_type FROM objects")rows = cursor.fetchall()conn.close()objects = []for row in rows:key, original_filename, size, content_type = rowobjects.append(ObjectItem(key=key,original_filename=original_filename,size=size,content_type=content_type))return objects# 4. 删除对象
@app.delete("/objects/{key}")
async def delete_object(key: str):file_path = os.path.join(STORAGE_PATH, key)# 删除文件if os.path.isfile(file_path):os.remove(file_path)# 删除元数据conn = sqlite3.connect(DB_PATH)cursor = conn.cursor()cursor.execute("DELETE FROM objects WHERE key=?", (key,))conn.commit()conn.close()return {"message": f"Object '{key}' deleted successfully"}if __name__ == "__main__":import uvicornuvicorn.run(app, host="0.0.0.0", port=8000)
第 3 步:运行服务
python main.py
服务会运行在 http://127.0.0.1:8000
。访问 http://127.0.0.1:8000/docs
可以看到自动生成的 API 文档界面,你可以在那里直接测试接口。
测试你的简易 OSS
使用 curl
或 Postman 测试:
上传文件:
curl -X POST -F "file=@/path/to/your/local/image.jpg" http://127.0.0.1:8000/objects/my-first-image
下载文件:
直接浏览器打开http://127.0.0.1:8000/objects/my-first-image
。列出文件:
浏览器打开http://127.0.0.1:8000/objects/
。
从“简易 OSS”到“生产级 OSS”的差距
我们这个简易 OSS 只是一个教学模型,离真正的云服务相差甚远,主要差距在:
- 分布式与高可用: 单点故障!服务器挂了服务就没了。生产级 OSS 数据会在多个节点、多个机房冗余存储。
- 性能: 单机磁盘 I/O 是瓶颈。生产级使用分片上传、CDN 加速、负载均衡。
- 安全性: 缺少身份认证(AK/SK)、权限控制(Bucket Policy)、加密、防盗链等。
- 数据一致性: 确保在分布式环境下数据读写的一致性非常复杂。
- 生命周期管理: 自动将旧数据转移到更便宜的归档存储。
- 版本控制: 防止文件被覆盖或误删。
总结
- OSS 是什么: 一个通过 HTTP API 存取海量非结构化数据的服务。
- 如何做一个:
- 使用云服务:对于绝大多数应用,直接使用阿里云 OSS、AWS S3 等是最好、最经济的选择。
- 自建开源方案:如果需要私有化部署,可以考虑 MinIO 或 Ceph,它们提供了生产级别的对象存储能力,你可以基于它们搭建自己的 OSS。
- 自己从头开发:如上所示,可以做一个原型理解概念,但要达到生产级别,需要巨大的工程投入。