# 多实例部署方案:单机多租户 > 一台服务器运行多个 raisfast 进程,每个用户一个实例、一个 SQLite 文件,通过 Unix Socket + Caddy 反向代理实现隔离与路由。 --- ## 1. 架构概览 ``` 用户浏览器 │ ▼ ┌─────────────────────┐ │ Caddy 反向代理 │ ← 自动 HTTPS (Let's Encrypt) │ *.api.example.com │ └───┬─────┬─────┬─────┘ │ │ │ Unix Socket ▼ ▼ ▼ user1 user2 user3 ← 独立进程、独立 DB、独立配置 .sock .sock .sock ``` **核心思路**: - 同一个二进制文件,在不同配置下启动多次,每次是一个独立 OS 进程 - Linux 内核通过 mmap 共享只读代码段,60MB 二进制启动 30 个实例代码只占 ~50MB(一份) - 每个实例只额外消耗私有内存(heap/stack/连接池等,约 25-35MB/实例) --- ## 2. 资源估算 ### 2.1 单实例内存拆解 | 项目 | 大小 | |------|------| | 进程基础开销 | ~5 MB | | Tokio runtime | ~3 MB | | SQLite 连接池 + 缓存(cache_size=8MB) | ~10-20 MB | | 请求处理 heap | ~5-10 MB | | **合计** | **~25-35 MB** | ### 2.2 不同规格的容量 | 服务器 | 可用内存(去掉 OS) | 实例数(保守 30MB/个) | |--------|---------------------|----------------------| | 2 GB | ~1748 MB | ~56 个 | | 4 GB | ~3748 MB | ~120 个 | | 8 GB | ~7748 MB | ~250 个 | ### 2.3 真实瓶颈 | 瓶颈 | 说明 | 建议上限 | |------|------|---------| | 文件描述符 | 每实例 ~50-100 fd | `ulimit -n 65535` 后基本够 | | CPU | 1-2 核上下文切换 | 单机 20-30 个活跃实例 | | 磁盘 I/O | 多个 SQLite 同时写 WAL | 视磁盘性能而定 | | 连接池 | 每实例独立,不能跨进程共享 | 每实例 `max_connections = 3` | --- ## 3. 配置文件 每个用户一个 TOML 配置文件: ```toml # /etc/raisfast/users/user1.toml database_url = "sqlite:/var/lib/raisfast/user1/data/raisfast.db" listen = "unix:/run/raisfast/user1.sock" data_dir = "/var/lib/raisfast/user1/data" jwt_secret = "random-secret-for-user1" ``` ```toml # /etc/raisfast/users/user2.toml database_url = "sqlite:/var/lib/raisfast/user2/data/raisfast.db" listen = "unix:/run/raisfast/user2.sock" data_dir = "/var/lib/raisfast/user2/data" jwt_secret = "random-secret-for-user2" ``` **隔离项**: | 配置项 | 说明 | |--------|------| | `database_url` | 每用户独立 SQLite 文件 | | `listen` | 每用户独立 Unix socket | | `data_dir` | 每用户独立数据目录 | | `jwt_secret` | 每用户独立密钥 | --- ## 4. Rust 端监听 Unix Socket ```rust use tokio::net::UnixListener; use axum::Router; async fn start_server(app: Router, listen: &str) -> anyhow::Result<()> { if let Some(path) = listen.strip_prefix("unix:") { // 清理旧 socket 文件,否则 bind 会失败 let _ = std::fs::remove_file(path); let listener = UnixListener::bind(path)?; axum::serve(listener, app).await?; } else { // TCP fallback let listener = tokio::net::TcpListener::bind(listen).await?; axum::serve(listener, app).await?; } Ok(()) } ``` **注意**:启动时必须先 `remove_file` 清理旧 socket 文件,否则上一次非正常退出后 bind 会报 "Address already in use"。 --- ## 5. systemd 管理多实例 使用 systemd **模板单元**(`@` 语法),一个 service 文件管理所有实例: ```ini # /etc/systemd/system/raisfast@.service [Unit] Description=raisfast instance %i After=network.target [Service] Type=simple User=raisfast Group=raisfast ExecStart=/usr/local/bin/raisfast --config /etc/raisfast/users/%i.toml RuntimeDirectory=raisfast Restart=on-failure LimitNOFILE=65535 [Install] WantedBy=multi-user.target ``` ### 5.1 常用命令 ```bash # 启动/停止单个实例 systemctl start raisfast@user1 systemctl stop raisfast@user2 systemctl restart raisfast@user3 # 查看状态 systemctl status raisfast@user1 journalctl -u raisfast@user1 -f # 批量操作 systemctl start 'raisfast@*' systemctl stop 'raisfast@*' systemctl restart 'raisfast@*' # 开机自启 systemctl enable raisfast@user1 ``` --- ## 6. 反向代理 ### Caddy vs Nginx 对比 | | Caddy | Nginx | |---|---|---| | **自动 HTTPS** | 内置 Let's Encrypt,零配置 | 需要额外装 certbot + cron 续签 | | **配置语法** | 简洁,接近自然语言 | 繁琐,指令多 | | **动态路由** | 有 HTTP API,可运行时增删 | 需 reload,无原生 API | | **性能** | 很好(略低于 Nginx) | 极致,行业标准 | | **生态/成熟度** | 较新(2015) | 20+ 年历史,资料丰富 | | **国内使用** | 知名度低 | 几乎所有运维都会 | **选择建议**:快速迭代 / SaaS 自动化选 Caddy(自动 HTTPS + 动态 API);国内客户 / 企业部署选 Nginx(运维熟悉 + 性能极致)。 --- ### 6.1 Caddy 方案 #### A. 子域名路由(推荐) 每个用户一个子域名,如 `user1.api.example.com`: ```caddyfile # /etc/caddy/Caddyfile *.api.example.com { @user1 host user1.api.example.com @user2 host user2.api.example.com @user3 host user3.api.example.com handle @user1 { reverse_proxy unix//run/raisfast/user1.sock } handle @user2 { reverse_proxy unix//run/raisfast/user2.sock } handle @user3 { reverse_proxy unix//run/raisfast/user3.sock } } ``` **DNS 要求**:需要通配符 DNS 记录 `*.api.example.com` 指向服务器 IP。 **Caddy 语法**:`unix//run/...` 是双斜杠,不是拼写错误。 #### B. 路径前缀路由(省域名) 所有用户共享一个域名,通过路径前缀区分: ```caddyfile api.example.com { handle /user1/* { uri strip_prefix /user1 reverse_proxy unix//run/raisfast/user1.sock } handle /user2/* { uri strip_prefix /user2 reverse_proxy unix//run/raisfast/user2.sock } handle /user3/* { uri strip_prefix /user3 reverse_proxy unix//run/raisfast/user3.sock } } ``` 访问路径示例:`https://api.example.com/user1/api/v1/posts` **注意**:`uri strip_prefix` 会去掉前缀再转发给后端,raisfast 收到的还是 `/api/v1/posts`,不需要适配路径。 #### C. 动态配置(Caddy API) 实例多时手写 Caddyfile 不现实,通过 Caddy API 动态添加路由: ```bash # 新增用户路由 curl -X POST localhost:2019/config/apps/http/servers/raisfast/routes \ -H "Content-Type: application/json" \ -d '{ "@id": "user1", "match": [{"host": ["user1.api.example.com"]}], "handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "unix//run/raisfast/user1.sock"}]}] }' # 删除用户路由 curl -X DELETE localhost:2019/config/apps/http/servers/raisfast/routes/user1 ``` --- ### 6.2 Nginx 方案 #### A. 子域名路由 ```nginx # /etc/nginx/conf.d/raisfast.conf # 通配符证书需要手动配置(certbot 通配符需要 DNS 验证) # certbot certonly --dns-cloudflare -d '*.api.example.com' server { listen 443 ssl; server_name ~^(?.+)\.api\.example\.com$; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; location / { proxy_pass http://unix:/run/raisfast/$user.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` **Nginx 优势**:用正则 `(?.+)` 从子域名提取用户名,**一个 server 块匹配所有用户**,不需要为每个用户单独写配置。 **SSL 续签**: ```bash # crontab -e 0 3 * * * certbot renew --quiet && systemctl reload nginx ``` #### B. 路径前缀路由 ```nginx server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; location ~ ^/user1(/.*)$ { proxy_pass http://unix:/run/raisfast/user1.sock:$1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location ~ ^/user2(/.*)$ { proxy_pass http://unix:/run/raisfast/user2.sock:$1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` **注意**:Nginx 路径前缀路由需要为每个用户写一个 location 块,无法像子域名那样用正则动态匹配。实例多时建议用子域名方式。 #### C. 动态配置(脚本生成) Nginx 没有 API,但可以用脚本生成配置后 reload: ```bash #!/bin/bash # nginx-add-user.sh USER=$1 cat >> /etc/nginx/conf.d/raisfast-users.conf << EOF location ~ ^/${USER}(/.*)$ { proxy_pass http://unix:/run/raisfast/${USER}.sock:\$1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } EOF nginx -t && systemctl reload nginx ``` 或者用子域名方式,一个 server 块自动匹配所有用户,**完全不需要动态生成配置**。 --- ## 7. 自动化脚本 ### 7.1 新增用户 ```bash #!/bin/bash # add-user.sh set -e USER=$1 BASE=/var/lib/raisfast/$USER SECRET=$(openssl rand -hex 32) # 创建数据目录 mkdir -p $BASE/data # 从模板复制空数据库 cp /var/lib/raisfast/template.db $BASE/data/raisfast.db # 生成配置文件 cat > /etc/raisfast/users/$USER.toml << EOF database_url = "sqlite:$BASE/data/raisfast.db" listen = "unix:/run/raisfast/$USER.sock" data_dir = "$BASE/data" jwt_secret = "$SECRET" EOF # 设置权限 chown -R raisfast:raisfast $BASE chmod 600 /etc/raisfast/users/$USER.toml # 启动实例 systemctl enable --now raisfast@$USER # 重载反向代理(二选一) systemctl reload caddy # Caddy systemctl reload nginx # Nginx echo "用户 $USER 已创建并启动" echo "API 地址: https://$USER.api.example.com" ``` ### 7.2 删除用户 ```bash #!/bin/bash # remove-user.sh set -e USER=$1 systemctl stop raisfast@$USER systemctl disable raisfast@$USER rm /etc/raisfast/users/$USER.toml rm -rf /var/lib/raisfast/$USER rm -f /run/raisfast/$USER.sock # 重载反向代理(二选一) systemctl reload caddy # Caddy systemctl reload nginx # Nginx echo "用户 $USER 已删除" ``` ### 7.3 批量状态查看 ```bash #!/bin/bash # list-users.sh echo "用户名 状态 PID 内存(MB)" echo "──────────────────────────────────────────" for config in /etc/raisfast/users/*.toml; do USER=$(basename "$config" .toml) STATUS=$(systemctl is-active "raisfast@$USER" 2>/dev/null) if [ "$STATUS" = "active" ]; then PID=$(systemctl show "raisfast@$USER" -p MainPID --value) MEM=$(ps -p $PID -o rss= 2>/dev/null | awk '{printf "%.0f", $1/1024}') else PID="-" MEM="-" fi printf "%-14s%-11s%-10s%s\n" "$USER" "$STATUS" "$PID" "$MEM" done ``` --- ## 8. 目录结构总览 ``` /usr/local/bin/raisfast # 二进制文件 /etc/raisfast/ users/ user1.toml # 用户配置 user2.toml user3.toml /var/lib/raisfast/ template.db # 空数据库模板 user1/ data/ raisfast.db # 用户数据 user2/ data/ raisfast.db /run/raisfast/ # Unix sockets(tmpfs,重启自动清理) user1.sock user2.sock user3.sock /etc/systemd/system/ raisfast@.service # 模板单元 /etc/caddy/ Caddyfile # Caddy 反向代理配置 /etc/nginx/conf.d/ raisfast.conf # Nginx 反向代理配置 /etc/letsencrypt/live/ # SSL 证书(Nginx 需要) ``` --- ## 9. SQLite 调优 多实例共享磁盘 I/O,每个实例需要降低缓存占用: ```sql -- 每个实例启动时执行 PRAGMA journal_mode = WAL; -- 写前日志,提高并发读 PRAGMA cache_size = -8000; -- 页缓存 8MB(默认约 2MB,单实例可给更多) PRAGMA busy_timeout = 5000; -- 写锁等待 5 秒 PRAGMA synchronous = NORMAL; -- WAL 模式下安全且快速 ``` **连接池**:每个实例 `max_connections = 3`(不要开太大,多实例场景下 fd 和内存都有限)。 --- ## 10. 扩展路径 当单机 2GB 不够时: | 阶段 | 方案 | 容量 | |------|------|------| | 初期 | 单机 2GB + SQLite 多实例 | ~50 用户 | | 增长 | 单机 8GB + SQLite 多实例 | ~200 用户 | | 规模化 | 切 PostgreSQL 多租户模式(所有用户共享一个进程,tenant_id 隔离) | 数千用户 | | 大规模 | PostgreSQL + 多节点 + 负载均衡 | 无上限 | PostgreSQL 多租户模式是规模化的关键转折点:一个进程服务所有用户,内存从 O(用户数×30MB) 降为 O(1),但需要提前在架构层做好 `tenant_id` 隔离设计。