Files
raisfast/docs/multi-instance-deployment.md
T
2026-05-13 10:38:14 +08:00

13 KiB
Raw Blame History

多实例部署方案:单机多租户

一台服务器运行多个 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 配置文件:

# /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"
# /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

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 文件管理所有实例:

# /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 常用命令

# 启动/停止单个实例
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

# /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. 路径前缀路由(省域名)

所有用户共享一个域名,通过路径前缀区分:

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 动态添加路由:

# 新增用户路由
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. 子域名路由

# /etc/nginx/conf.d/raisfast.conf

# 通配符证书需要手动配置(certbot 通配符需要 DNS 验证)
# certbot certonly --dns-cloudflare -d '*.api.example.com'

server {
    listen 443 ssl;
    server_name ~^(?<user>.+)\.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 优势:用正则 (?<user>.+) 从子域名提取用户名,一个 server 块匹配所有用户,不需要为每个用户单独写配置。

SSL 续签

# crontab -e
0 3 * * * certbot renew --quiet && systemctl reload nginx

B. 路径前缀路由

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:

#!/bin/bash
# nginx-add-user.sh <username>
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 新增用户

#!/bin/bash
# add-user.sh <username>

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 删除用户

#!/bin/bash
# remove-user.sh <username>

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 批量状态查看

#!/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 socketstmpfs,重启自动清理)
  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,每个实例需要降低缓存占用:

-- 每个实例启动时执行
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 隔离设计。