38 KiB
raisfast 内置 Proxy 模块设计
替代 nginx/caddy,实现 raisfast 单二进制多租户部署,零外部依赖。
1. 目标
| 目标 | 说明 |
|---|---|
| 零依赖 | 不需要 nginx/caddy/证书工具,raisfast 一个二进制搞定 |
| 多租户路由 | 根据域名(Host 头)或路径前缀分发到后端实例 |
| 自动 HTTPS | 集成 ACME (Let's Encrypt),自动申请/续签证书 |
| 热更新 | 新增/删除租户不需要重启 proxy |
| 轻量 | proxy 进程自身内存 < 10MB |
| 可选 | proxy 是独立模式,不影响单实例直接部署 |
2. 架构
raisfast 二进制
┌────────────────────────────────────────────────────────────────┐
│ │
│ 模式 A:单实例(现有行为) │
│ ┌──────────────────────────────────────────┐ │
│ │ TCP :9898 → axum Router → 业务逻辑 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 模式 B:Proxy + 多实例(新模式) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Proxy 进程 (TCP :80/:443) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ TLS 终结 (rustls) │ │ │
│ │ │ Host/Path 匹配 → 路由表查找 │ │ │
│ │ │ HTTP/1.1 反向代理 → Unix Socket / TCP │ │ │
│ │ │ ACME 证书管理 (Let's Encrypt) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
关键决策:Proxy 是独立进程,不是嵌入业务进程的中间件。
原因:
- Proxy 必须在 80/443 端口上运行,后端实例在内网端口/Unix socket
- Proxy 挂了不应影响已有的长连接
- 可以单独升级/重启 proxy 而不影响业务实例
3. 启动方式
3.1 CLI 命令
# 单实例模式(现有行为,不变)
raisfast server start
# Proxy 模式(新增)
raisfast proxy start --config /etc/raisfast/proxy.toml
# 两者可以同时运行在同一台机器上
3.2 Proxy 配置文件
# /etc/raisfast/proxy.toml
[proxy]
listen_http = "0.0.0.0:80"
listen_https = "0.0.0.0:443"
# 证书存储目录(ACME 自动管理)
acme_dir = "/var/lib/raisfast/acme"
# ACME 邮箱
acme_email = "admin@example.com"
# ACME 目录(默认 Let's Encrypt)
acme_directory = "https://acme-v02.api.letsencrypt.org/directory"
# 测试环境
# acme_directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
# 是否自动 HTTP→HTTPS 重定向
redirect_http_to_https = true
# 管理 API(用于动态增删租户)
admin_listen = "127.0.0.1:9876"
admin_secret = "a-secret-for-admin-api"
# 租户配置文件目录(watch 模式)
tenants_dir = "/etc/raisfast/tenants"
# 健康检查间隔
health_check_interval_secs = 30
# 日志
log_dir = "/var/lib/raisfast/proxy/logs"
3.3 租户配置
每个租户一个 TOML 文件,放在 tenants_dir 下:
# /etc/raisfast/tenants/user1.toml
[tenant]
name = "user1"
# 路由匹配方式(二选一)
host = "user1.api.example.com"
# prefix = "/user1"
# 后端地址(支持 Unix socket 和 TCP)
backend = "unix:/run/raisfast/user1.sock"
# backend = "127.0.0.1:9901"
# TLS(可选,默认走通配符证书)
# 自定义证书路径
# tls_cert = "/etc/ssl/user1.pem"
# tls_key = "/etc/ssl/user1.key"
# 超时
connect_timeout_ms = 5000
read_timeout_ms = 30000
# 是否启用(可临时禁用)
enabled = true
4. 模块结构
src/proxy/
├── mod.rs # 模块入口,模式判断
├── config.rs # proxy 配置加载(proxy.toml + tenants/*.toml)
├── router.rs # 路由表(Host/Prefix → Backend 映射)
├── proxy.rs # HTTP 反向代理核心(hyper 实现)
├── tls.rs # TLS 终结 + SNI 路由 + 证书管理
├── acme.rs # ACME 自动证书申请/续签
├── admin.rs # 管理 API(动态增删租户)
├── health.rs # 后端健康检查
└── watcher.rs # 配置文件热加载(notify crate)
4.1 模块职责
config.rs — 配置加载
struct ProxyConfig {
listen_http: SocketAddr,
listen_https: SocketAddr,
acme_dir: PathBuf,
acme_email: String,
acme_directory: String,
redirect_http_to_https: bool,
admin_listen: SocketAddr,
admin_secret: String,
tenants_dir: PathBuf,
health_check_interval_secs: u64,
log_dir: PathBuf,
}
struct TenantConfig {
name: String,
host: Option<String>, // 子域名匹配
prefix: Option<String>, // 路径前缀匹配
backend: String, // "unix:/path" 或 "tcp:host:port"
tls_cert: Option<PathBuf>, // 自定义证书
tls_key: Option<PathBuf>,
connect_timeout_ms: u64,
read_timeout_ms: u64,
enabled: bool,
}
加载优先级:
tenants_dir/*.toml— 文件配置- 管理 API 动态注册 — 运行时增删
- 两者合并到路由表
router.rs — 路由表
use dashmap::DashMap;
struct RouterTable {
by_host: DashMap<String, Backend>, // host → backend
by_prefix: DashMap<String, Backend>, // prefix → backend
}
struct Backend {
name: String,
addr: BackendAddr,
healthy: Arc<AtomicBool>,
connect_timeout: Duration,
read_timeout: Duration,
}
enum BackendAddr {
UnixSocket(PathBuf),
Tcp(SocketAddr),
}
路由匹配优先级:
- 精确 Host 匹配
- 路径前缀匹配(最长前缀优先)
- 默认后端(可选)
proxy.rs — 反向代理核心
/// 核心代理函数
async fn proxy_request(
req: Request<IncomingBody>,
backend: &Backend,
) -> Result<Response<Body>, ProxyError> {
// 1. 根据 BackendAddr 拨号连接
// 2. 转发请求(headers + body)
// 3. 流式转发响应
// 4. 支持连接复用(keep-alive)
}
实现要点:
- 使用
hyper直接做 HTTP/1.1 代理(不经过 axum,减少开销) - Unix socket 用
tokio::net::UnixStream→hyper::client::conn::http1::handshake - TCP 用
tokio::net::TcpStream→ 同上 - 流式转发 body,不缓冲到内存(支持大文件上传)
- 透传
X-Forwarded-For/X-Forwarded-Proto/X-Forwarded-Host - 支持 WebSocket 升级(
Connection: Upgrade)
tls.rs — TLS 终结
use rustls::ServerConfig;
struct TlsManager {
cert_resolver: Arc<dyn ResolvesServerCert>,
acme: Arc<AcmeManager>,
}
/// 基于 SNI 的证书分发
/// - 通配符域名:*.api.example.com → 通配符证书
/// - 自定义证书:用户指定了 tls_cert/tls_key
/// - 自动 ACME:按需申请
实现要点:
- 使用
tokio-rustls做 TLS 终结 rustls::server::ResolvesServerCerttrait 实现动态证书选择- 支持 SNI 路由(TLS 握手时就知道目标域名)
- 通配符证书一个就够了,无需每租户单独申请
acme.rs — 自动 HTTPS
struct AcmeManager {
dir: PathBuf, // 证书存储目录
email: String,
directory: String, // ACME directory URL
account: OnceCell<Account>, // ACME 账号
}
impl AcmeManager {
/// 获取指定域名的证书(缓存优先,过期自动续签)
async fn get_certificate(&self, domain: &str) -> Result<Arc<CertifiedKey>>;
/// 通配符证书申请(DNS-01 验证,需要 DNS provider)
async fn request_wildcard(&self, domain: &str) -> Result<()>;
/// 单域名证书申请(HTTP-01 验证,proxy 自身处理 /.well-known/)
async fn request_single(&self, domain: &str) -> Result<()>;
}
实现方案:
- 不引入重量级
rustls-acmecrate,自己实现轻量 ACME 客户端 - 使用
reqwest调 ACME API +ring做签名/验证 - HTTP-01 验证:proxy 自身在
/.well-known/acme-challenge/路径返回验证文件 - DNS-01 验证:可选,通配符证书需要,需要集成 DNS provider API
- 证书缓存:
acme_dir/{domain}/fullchain.pem+privkey.pem - 续签:后台定时检查,过期前 30 天自动续签
admin.rs — 管理 API
// POST /admin/tenants 创建租户
// DELETE /admin/tenants/{name} 删除租户
// GET /admin/tenants 列出所有租户
// GET /admin/tenants/{name} 查看租户详情
// POST /admin/reload 重新加载配置文件
// GET /admin/stats 代理统计
认证:Authorization: Bearer {admin_secret}
health.rs — 健康检查
struct HealthChecker {
backends: Vec<(String, BackendAddr)>,
interval: Duration,
}
impl HealthChecker {
/// 定时检查所有后端,更新 healthy 状态
/// TCP 后端:尝试 TCP connect
/// Unix socket 后端:尝试 connect Unix socket
/// 也可发 HTTP GET /health
}
watcher.rs — 配置热加载
/// 监听 tenants_dir 目录变化
/// 新增 .toml → 添加到路由表
/// 修改 .toml → 更新路由表
/// 删除 .toml → 从路由表移除
fn watch_tenants_dir(dir: &Path, router: Arc<RouterTable>) -> JoinHandle<()>;
使用 notify crate 监听文件系统事件。
5. 依赖
# Cargo.toml 新增(在 proxy feature 下)
[features]
proxy = [
"hyper-util", # HTTP 客户端/服务端工具
"tower", # Service trait
"dashmap", # 并发路由表
"notify", # 文件系统监听
"rcgen", # 自签名证书生成
]
# 已有依赖(复用)
# hyper — 已在依赖中(axum 底层)
# tokio — 已在依赖中
# rustls — 已在 tls feature 中
# tokio-rustls — 已在 tls feature 中
# reqwest — 已在依赖中(ACME HTTP 请求)
# serde/toml — 已在依赖中
# tracing — 已在依赖中
不需要新增大型依赖,大部分可复用现有 crate。
6. 核心流程
6.1 请求处理流程
客户端请求 → TCP accept → TLS 握手(SNI → 选证书)
→ HTTP 解析 → 提取 Host 头
→ 路由表查找(by_host)
→ 未命中 → 路径前缀匹配(by_prefix)
→ 未命中 → 502 Bad Gateway
→ 命中 → 检查 healthy 状态
→ 不健康 → 503 Service Unavailable
→ 健康 → 拨号后端(Unix socket / TCP)
→ 流式转发请求
→ 流式转发响应
→ 记录 metrics
6.2 证书获取流程
新域名首次请求
→ cert_resolver 查询缓存 → miss
→ ACME 申请(HTTP-01)
→ 保存到 acme_dir
→ 返回证书
→ TLS 握手完成
后续请求
→ cert_resolver 查询缓存 → hit
→ 检查过期时间
→ 即将过期 → 后台续签
→ 返回证书
6.3 租户生命周期
1. 创建租户
方式 A:写入 /etc/raisfast/tenants/user1.toml → watcher 自动加载
方式 B:POST /admin/tenants → API 动态注册
2. 运行中
- 健康检查定时运行
- 证书自动续签
- metrics 收集
3. 删除租户
方式 A:删除 .toml 文件 → watcher 自动移除
方式 B:DELETE /admin/tenants/user1
7. CLI 集成
// src/cli.rs 新增
#[derive(Subcommand)]
enum Commands {
// ... 现有命令 ...
/// Proxy management (multi-tenant reverse proxy)
Proxy {
#[command(subcommand)]
action: ProxyAction,
},
}
#[derive(Subcommand)]
enum ProxyAction {
/// Start the proxy server
Start {
/// Path to proxy config file
#[arg(short, long, default_value = "/etc/raisfast/proxy.toml")]
config: String,
},
/// Validate proxy configuration
Check {
/// Path to proxy config file
#[arg(short, long, default_value = "/etc/raisfast/proxy.toml")]
config: String,
},
/// List all registered tenants
Tenants {
/// Proxy admin API address
#[arg(short, long, default_value = "127.0.0.1:9876")]
addr: String,
},
/// Add a new tenant
AddTenant {
/// Tenant name
name: String,
/// Host domain (e.g., user1.api.example.com)
#[arg(short, long)]
host: Option<String>,
/// Path prefix (e.g., /user1)
#[arg(short, long)]
prefix: Option<String>,
/// Backend address (e.g., unix:/run/raisfast/user1.sock)
#[arg(short, long)]
backend: String,
},
/// Remove a tenant
RemoveTenant {
/// Tenant name
name: String,
},
}
8. 数据流
HTTP 反向代理核心代码(简化版)
use hyper::body::Incoming;
use hyper::Request;
use hyper::Response;
use hyper::body::Body;
async fn handle_request(
req: Request<Incoming>,
router: Arc<RouterTable>,
) -> Result<Response<Body>, ProxyError> {
// 1. 提取 Host
let host = req.headers()
.get("host")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.split(':')
.next()
.unwrap_or("");
// 2. 查找后端
let backend = router.find_by_host(host)
.or_else(|| router.find_by_prefix(req.uri().path()))
.ok_or(ProxyError::NoBackend)?;
// 3. 健康检查
if !backend.healthy.load(Ordering::Relaxed) {
return Ok(Response::builder()
.status(503)
.body("service unavailable".into())
.unwrap());
}
// 4. 拨号连接
let stream = match &backend.addr {
BackendAddr::UnixSocket(path) => {
let s = UnixStream::connect(path).await?;
TokioIo::new(s)
}
BackendAddr::Tcp(addr) => {
let s = TcpStream::connect(addr).await?;
TokioIo::new(s)
}
};
// 5. 构建代理请求
let (mut sender, conn) = hyper::client::conn::http1::handshake(stream).await?;
tokio::spawn(async move { let _ = conn.await; });
// 6. 注入转发头
let mut proxy_req = Request::new(req.into_body());
*proxy_req.method_mut() = req.method().clone();
*proxy_req.uri_mut() = req.uri().clone();
*proxy_req.headers_mut() = req.headers().clone();
proxy_req.headers_mut().insert(
"x-forwarded-for",
"peer-ip".parse().unwrap(),
);
proxy_req.headers_mut().insert(
"x-forwarded-proto",
"https".parse().unwrap(),
);
// 7. 发送并返回响应
let response = sender.send_request(proxy_req).await?;
Ok(response.map(|b| Body::wrap_stream(b)))
}
9. 性能预期
| 指标 | 目标 | 说明 |
|---|---|---|
| 代理延迟增加 | < 0.05ms | Unix socket 本地转发 |
| 代理吞吐 | > 100k req/s | hyper 直连,无 axum 开销 |
| 内存占用 | < 10MB | proxy 进程本身 |
| 并发连接 | > 10k | tokio 异步 |
| TLS 握手 | < 5ms | rustls 会话复用 |
10. 安全
| 安全项 | 措施 |
|---|---|
| 管理 API | Bearer token 认证,仅监听 127.0.0.1 |
| 后端隔离 | 每租户独立 Unix socket,进程级隔离 |
| TLS | 仅支持 TLS 1.2+,禁用弱密码套件 |
| 请求头 | 注入 X-Forwarded-* 头,后端可验证 |
| 速率限制 | proxy 层全局速率限制(可选) |
| 路径穿越 | 阻止 ../ 等路径穿越攻击 |
11. 实施阶段
Phase 1:最小可用(1 周)
src/proxy/mod.rs+config.rs— 配置加载src/proxy/router.rs— 路由表src/proxy/proxy.rs— HTTP 反向代理(仅 Unix socket + TCP)- CLI 集成
raisfast proxy start - 测试:手动配置 2 个租户,curl 验证路由分发
产出:能用,但没有 TLS,没有自动证书,手动配路由。
Phase 2:TLS + 自动证书(1 周)
src/proxy/tls.rs— TLS 终结 + SNI 路由src/proxy/acme.rs— ACME HTTP-01 自动证书- HTTP→HTTPS 自动重定向
- 证书缓存 + 自动续签
- 测试:Let's Encrypt 真实证书申请
产出:自动 HTTPS,不需要 certbot。
Phase 3:生产就绪(1 周)
src/proxy/admin.rs— 管理 APIsrc/proxy/health.rs— 健康检查src/proxy/watcher.rs— 配置文件热加载src/proxy/access_log.rs— 访问日志(见第 15 节详细设计)- WebSocket 代理支持
- metrics(请求数/延迟/错误率)
- 优雅关闭
- 压力测试
产出:生产可用的内置反向代理。
Phase 4:高级特性(可选)
- 通配符证书(DNS-01 验证)
- HTTP/2 代理
- 请求/响应缓冲控制
- 限流(per-tenant rate limit)
- 负载均衡(一个租户多后端)
12. 与现有代码的关系
| 文件 | 变更 |
|---|---|
src/main.rs |
无变更 |
src/cli.rs |
新增 Proxy 子命令 |
src/cli/proxy_cmd.rs |
新增 |
src/proxy/ |
新增整个模块 |
src/server.rs |
无变更 |
src/config/app.rs |
无变更(proxy 有自己的配置) |
Cargo.toml |
新增 proxy feature |
Proxy 模块完全独立于现有 server 模块,零侵入。
13. 运维命令速查
# 启动 proxy
raisfast proxy start
raisfast proxy start --config /etc/raisfast/proxy.toml
# 检查配置
raisfast proxy check
# 列出租户
raisfast proxy tenants
# 添加租户
raisfast proxy add-tenant user1 --host user1.api.example.com --backend unix:/run/raisfast/user1.sock
# 删除租户
raisfast proxy remove-tenant user1
# 管理 API(curl)
curl -H "Authorization: Bearer secret" http://127.0.0.1:9876/admin/tenants
curl -X POST -H "Authorization: Bearer secret" http://127.0.0.1:9876/admin/tenants \
-d '{"name":"user2","host":"user2.api.example.com","backend":"unix:/run/raisfast/user2.sock"}'
curl -X DELETE -H "Authorization: Bearer secret" http://127.0.0.1:9876/admin/tenants/user2
14. 与 nginx/caddy 的功能对比
| 功能 | nginx | caddy | raisfast proxy |
|---|---|---|---|
| HTTP 反向代理 | ✅ | ✅ | ✅ |
| Unix socket 后端 | ✅ | ✅ | ✅ |
| 自动 HTTPS | ❌ (需 certbot) | ✅ | ✅ |
| TLS SNI 路由 | ✅ | ✅ | ✅ |
| 管理 API | ❌ | ✅ | ✅ |
| 配置热加载 | ✅ (reload) | ✅ (API) | ✅ (API + fs watch) |
| WebSocket | ✅ | ✅ | ✅ |
| Access Log | ✅ | ✅ | ✅ (见第 15 节) |
| HTTP/3 | 需模块 | ✅ | ❌ (Phase 4+) |
| 负载均衡 | ✅ | ✅ | ❌ (Phase 4+) |
| HTTP/2 proxy | ✅ | ✅ | ❌ (Phase 4+) |
| 内置业务逻辑 | ❌ | ❌ | ✅ (同二进制) |
| 部署依赖 | 需安装 | 需安装 | 零依赖 |
15. Access Log 详细设计
15.1 设计目标
对标 nginx log_format + access_log,但更现代:
- 结构化 JSON 输出,可直接对接 ELK / Loki / Datadog
- 每租户独立日志文件,方便多租户隔离
- 异步写入,不阻塞请求处理
- 可配置格式(JSON / Common Log Format / 自定义)
- 自动日志轮转,避免磁盘爆满
15.2 日志格式
JSON 格式(默认,推荐)
{
"timestamp": "2026-05-14T10:23:45.123Z",
"tenant": "user1",
"client_ip": "203.0.113.50",
"method": "GET",
"path": "/api/v1/posts",
"query": "?page=1&limit=10",
"protocol": "HTTP/1.1",
"status": 200,
"bytes_sent": 4523,
"bytes_received": 0,
"latency_ms": 12,
"upstream": "unix:/run/raisfast/user1.sock",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"referer": "https://example.com/blog",
"request_id": "req-a1b2c3d4",
"tls_version": "TLSv1.3",
"tls_cipher": "TLS_AES_256_GCM_SHA384"
}
Common Log Format(兼容模式,对标 nginx)
203.0.113.50 - - [14/May/2026:10:23:45 +0000] "GET /api/v1/posts?page=1&limit=10 HTTP/1.1" 200 4523 "https://example.com/blog" "Mozilla/5.0 ..." "user1" 12ms
15.3 配置
# proxy.toml 新增
[access_log]
# 是否启用(默认 true)
enabled = true
# 日志格式:"json" | "clf" | "combined"
format = "json"
# 日志输出:"file" | "stdout" | "both"
output = "file"
# 日志文件目录
dir = "/var/lib/raisfast/proxy/logs"
# 是否按租户分文件
per_tenant = true
# 是否记录请求体大小(默认 true)
log_bytes_received = true
# 是否记录 TLS 信息(默认 true)
log_tls_info = true
# 排除的路径(不记录日志,如健康检查)
exclude_paths = ["/health", "/healthz", "/readyz", "/metrics"]
# 日志轮转
[access_log.rotation]
# 最大单文件大小(MB)
max_size_mb = 100
# 保留文件数
max_files = 30
# 压缩旧日志
compress = true
15.4 文件布局
/var/lib/raisfast/proxy/logs/
├── access.log # 全局日志(per_tenant=false 时)
├── access.2026-05-13.log.gz # 轮转后的压缩日志
├── tenants/
│ ├── user1.access.log # user1 的独立日志
│ ├── user1.access.2026-05-13.log.gz
│ ├── user2.access.log # user2 的独立日志
│ └── ...
15.5 核心实现
src/proxy/access_log.rs
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::mpsc;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct AccessLogEntry {
pub timestamp: String,
pub tenant: String,
pub client_ip: String,
pub method: String,
pub path: String,
pub query: Option<String>,
pub protocol: String,
pub status: u16,
pub bytes_sent: u64,
pub bytes_received: u64,
pub latency_ms: u64,
pub upstream: String,
pub user_agent: Option<String>,
pub referer: Option<String>,
pub request_id: Option<String>,
pub tls_version: Option<String>,
pub tls_cipher: Option<String>,
}
#[derive(Debug, Clone)]
pub enum LogFormat {
Json,
Clf,
Combined,
}
#[derive(Debug, Clone)]
pub enum LogOutput {
File,
Stdout,
Both,
}
pub struct AccessLogConfig {
pub enabled: bool,
pub format: LogFormat,
pub output: LogOutput,
pub dir: PathBuf,
pub per_tenant: bool,
pub log_bytes_received: bool,
pub log_tls_info: bool,
pub exclude_paths: Vec<String>,
pub max_size_mb: u64,
pub max_files: usize,
pub compress: bool,
}
/// 异步 access log 写入器
///
/// 通过 mpsc channel 接收日志条目,后台线程批量写入文件。
/// 不阻塞请求处理线程。
pub struct AccessLogger {
tx: mpsc::Sender<AccessLogEntry>,
config: Arc<AccessLogConfig>,
}
impl AccessLogger {
pub fn new(config: AccessLogConfig) -> Self {
let (tx, rx) = mpsc::channel::<AccessLogEntry>(4096);
let config = Arc::new(config);
// 启动后台写入协程
tokio::spawn(Self::writer_loop(rx, config.clone()));
Self { tx, config }
}
/// 记录一条 access log(非阻塞,发送到 channel)
pub async fn log(&self, entry: AccessLogEntry) {
// 排除路径检查
if self.should_exclude(&entry.path) {
return;
}
// channel 满时丢弃,不阻塞请求
let _ = self.tx.try_send(entry);
}
/// 后台写入循环
async fn writer_loop(
mut rx: mpsc::Receiver<AccessLogEntry>,
config: Arc<AccessLogConfig>,
) {
// 打开文件句柄(全局 + per-tenant)
// 批量缓冲,定期 flush
// 检查文件大小,触发轮转
// 轮转后压缩旧文件
loop {
match rx.recv().await {
Some(entry) => {
let line = match config.format {
LogFormat::Json => serde_json::to_string(&entry).unwrap_or_default(),
LogFormat::Clf => format_clf(&entry),
LogFormat::Combined => format_combined(&entry),
};
if matches!(config.output, LogOutput::Stdout | LogOutput::Both) {
tracing::info!("{}", line);
}
if matches!(config.output, LogOutput::File | LogOutput::Both) {
Self::write_to_file(&config, &entry.tenant, &line).await;
}
}
None => break,
}
}
}
fn should_exclude(&self, path: &str) -> bool {
self.config.exclude_paths.iter().any(|p| path == p)
}
}
在 proxy.rs 中集成
async fn handle_request(
req: Request<Incoming>,
router: Arc<RouterTable>,
logger: Arc<AccessLogger>,
) -> Result<Response<Body>, ProxyError> {
let start = std::time::Instant::now();
let path = req.uri().path().to_string();
let method = req.method().clone();
let client_ip = extract_client_ip(&req);
let user_agent = req.headers()
.get("user-agent")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let referer = req.headers()
.get("referer")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
// ... 路由查找 + 代理转发 ...
let latency = start.elapsed();
let status = response.status().as_u16();
// 异步记录 access log
logger.log(AccessLogEntry {
timestamp: crate::utils::tz::now_str(),
tenant: backend.name.clone(),
client_ip,
method: method.to_string(),
path,
query: req.uri().query().map(|s| s.to_string()),
protocol: "HTTP/1.1".to_string(),
status,
bytes_sent: 0, // 从 response header 读取
bytes_received: 0, // 从 request header 读取
latency_ms: latency.as_millis() as u64,
upstream: backend.addr.to_string(),
user_agent,
referer,
request_id: None,
tls_version: None,
tls_cipher: None,
}).await;
Ok(response)
}
15.6 日志轮转
impl AccessLogger {
async fn write_to_file(config: &AccessLogConfig, tenant: &str, line: &str) {
let path = if config.per_tenant {
config.dir.join("tenants").join(format!("{tenant}.access.log"))
} else {
config.dir.join("access.log")
};
// 追加写入
use tokio::io::AsyncWriteExt;
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await
.unwrap_or_else(|_| {
// 目录不存在则创建
let _ = std::fs::create_dir_all(path.parent().unwrap());
tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await
.ok()
.unwrap()
});
let _ = file.write_all(format!("{line}\n").as_bytes()).await;
// 检查文件大小,触发轮转
if let Ok(metadata) = file.metadata().await {
let size_mb = metadata.len() / (1024 * 1024);
if size_mb >= config.max_size_mb {
Self::rotate(&path, config.max_files, config.compress).await;
}
}
}
async fn rotate(path: &Path, max_files: usize, compress: bool) {
// 1. 关闭当前文件句柄
// 2. 重命名 access.log → access.YYYY-MM-DD.log
// 3. 如果 compress=true,gzip 压缩
// 4. 删除超过 max_files 的旧文件
}
}
15.7 管理 API 集成
# 查看日志配置
GET /admin/access-log/config
# 动态修改日志配置(热更新)
PUT /admin/access-log/config
{
"format": "clf",
"exclude_paths": ["/health", "/metrics"]
}
# 查看指定租户的日志(尾部)
GET /admin/tenants/user1/logs?lines=100
# 全局日志统计
GET /admin/stats
{
"total_requests": 125430,
"by_tenant": { "user1": 50000, "user2": 75430 },
"by_status": { "200": 120000, "404": 3000, "500": 430 },
"avg_latency_ms": 15.3,
"p99_latency_ms": 120
}
15.8 与现有日志系统的关系
| raisfast 业务日志 (tracing) | proxy access log | |
|---|---|---|
| 目的 | 记录应用内部行为(错误、调试信息) | 记录每个 HTTP 请求的元数据 |
| 格式 | tracing 结构化日志 | JSON / CLF |
| 存放 | {log_dir}/app.log |
{access_log.dir}/access.log |
| 消费者 | 开发者调试 | 运维监控、审计、计费 |
| 生成方 | 业务进程 | proxy 进程 |
两者独立,互不影响。
16. 待完善功能清单
16.1 P0 — 性能与稳定性(必须做)
16.1.1 后端连接池复用
当前设计每次请求新建连接(UnixStream::connect / TcpStream::connect),虽然有 keep-alive 但没有跨请求复用。
目标:维护一个 per-backend 连接池,避免重复握手。
use dashmap::DashMap;
struct ConnectionPool {
pools: DashMap<String, mpsc::Sender<PooledConnection>>,
max_idle_per_host: usize, // 默认 8
idle_timeout: Duration, // 默认 90s
}
struct PooledConnection {
sender: hyper::client::conn::http1::SendRequest<Body>,
created_at: Instant,
}
效果:Unix socket 连接建立虽然快(~0.01ms),但池化后省掉 syscall 开销,高并发下差异明显。
16.1.2 Per-Tenant 限流
防止单个租户占用全部带宽,影响其他租户。
# tenant 配置
[tenant]
# 每秒最大请求数
rate_limit_rps = 100
# 最大并发连接数
max_concurrent_connections = 50
# 每月流量配额(GB)
monthly_bandwidth_gb = 100
实现:token bucket 算法(已有 RateLimiterSet 模式可复用)。
16.1.3 全局连接数限制
防止连接数爆增导致 OOM。
# proxy.toml
[proxy]
# 最大并发连接数(超出排队)
max_connections = 10000
# 排队超时(超出返回 503)
queue_timeout_ms = 5000
# 单 IP 最大连接数
max_connections_per_ip = 100
16.1.4 请求超时
防止慢后端占住 proxy 连接。
# proxy.toml 全局默认
[proxy]
# 连接后端超时
connect_timeout_ms = 5000
# 读取后端响应超时
read_timeout_ms = 30000
# 总请求超时(含 body 传输)
request_timeout_ms = 60000
# per-tenant 覆盖
[tenant]
connect_timeout_ms = 3000
read_timeout_ms = 10000
16.2 P1 — 运维便利(应该做)
16.2.1 自定义错误页
502/503/504 时展示品牌化的错误页面,而非裸 HTTP 状态码。
# proxy.toml
[error_pages]
# 错误页 HTML 目录
dir = "/etc/raisfast/error-pages"
# 默认错误页(按状态码命名:502.html, 503.html, 504.html)
# 缺失时使用内置简约页面
内置默认错误页(编译进二进制,零配置也可用):
<!-- 内置 503 页面 -->
<html>
<head><title>Service Temporarily Unavailable</title></head>
<body>
<h1>Site Under Maintenance</h1>
<p>We'll be back shortly.</p>
<hr>
<small>Powered by raisfast</small>
</body>
</html>
16.2.2 IP 黑白名单
防爬虫 / DDoS / 恶意请求。
# proxy.toml
[ip_filter]
# 黑名单(优先级高于白名单)
blacklist = ["1.2.3.0/24", "5.6.7.8"]
# 白名单(设置后仅允许这些 IP)
# whitelist = ["10.0.0.0/8", "172.16.0.0/12"]
# 自动封禁:60 秒内 404 超过 100 次的 IP 自动封禁 1 小时
auto_ban_threshold = 100
auto_ban_window_secs = 60
auto_ban_duration_secs = 3600
实现:CIDR 匹配用 ipnet crate(轻量),封禁列表存 DashMap<IpAddr, BanInfo>。
16.2.3 响应压缩(gzip / brotli)
减少出口带宽,特别是 JSON API 响应。
# proxy.toml
[compression]
enabled = true
# 压缩算法优先级
algorithms = ["brotli", "gzip"]
# 最小压缩阈值(字节)
min_size = 1024
# 压缩级别(1-9)
level = 4
# 不压缩的 Content-Type
exclude_types = ["image/", "video/", "application/zip"]
实现:async-compression crate(tokio + brotli + gzip)。响应体流式压缩,不缓冲到内存。
16.2.4 响应缓存
静态资源 + GET 请求短期缓存,减少后端压力。
# proxy.toml
[cache]
enabled = true
# 缓存存储:"memory" | "disk"
storage = "memory"
# 最大缓存大小(MB)
max_size_mb = 256
# 默认 TTL(秒)
default_ttl = 60
# 缓存条件:仅缓存 GET + 200 响应
methods = ["GET"]
statuses = [200, 301, 302]
# 缓存键:host + path + query
# 不缓存带 Authorization 头的请求
skip_auth_requests = true
缓存键:{host}:{path}?{query} → SHA256 hash
缓存淘汰:LRU(moka crate 已在依赖中)
缓存失效:通过管理 API 手动清除 POST /admin/cache/purge?tenant=user1
16.2.5 实时指标(Prometheus 格式)
在 access log 基础上,提供实时聚合指标。
GET /metrics # 已有 metrics endpoint,扩展 proxy 维度
新增指标维度:
| 指标 | 类型 | 标签 |
|---|---|---|
proxy_requests_total |
Counter | tenant, status, method |
proxy_request_duration_seconds |
Histogram | tenant |
proxy_active_connections |
Gauge | tenant |
proxy_backend_health |
Gauge | tenant, backend |
proxy_tls_handshake_seconds |
Histogram | tls_version |
proxy_bytes_sent |
Counter | tenant |
proxy_bytes_received |
Counter | tenant |
proxy_acme_cert_expiry_timestamp |
Gauge | domain |
16.2.6 请求 ID 透传
全链路追踪,proxy → backend → 日志 全程同一个 request_id。
客户端请求 → proxy 生成 X-Request-ID: req-a1b2c3d4
→ 转发时透传 X-Request-ID
→ 后端 raisfast 读取 X-Request-ID(已有 request_id 中间件)
→ access log 记录 request_id
→ 业务 log 记录 request_id
实现:proxy 入口检查 X-Request-ID,缺失则生成 UUID v7,透传给后端。
16.3 P2 — 高级特性(按需做)
16.3.1 慢请求日志
超阈值的请求单独记录,用于性能排查。
# proxy.toml
[slow_log]
enabled = true
# 慢请求阈值(ms)
threshold_ms = 3000
# 独立文件
file = "/var/lib/raisfast/proxy/logs/slow.log"
16.3.2 灰度/蓝绿部署
按比例将流量切换到新版后端,零风险上线。
# tenant 配置
[tenant]
# 稳定版后端
backend = "unix:/run/raisfast/user1.sock"
# 灰度版后端
canary_backend = "unix:/run/raisfast/user1-canary.sock"
# 灰度流量比例(0-100)
canary_weight = 10
实现:加权随机(rand::thread_rng() < canary_weight / 100),逐步调大 weight。
16.3.3 DNS 缓存
TCP 后端场景下,避免每次请求都做 DNS 解析。
# proxy.toml
[dns]
# DNS 缓存 TTL(秒)
cache_ttl = 300
# DNS 服务器(默认系统配置)
# servers = ["8.8.8.8", "1.1.1.1"]
实现:DashMap<String, (SocketAddr, Instant)> 简单缓存,过期重新解析。
16.3.4 负载均衡(一个租户多后端)
单租户扩展到多实例,proxy 内置负载均衡。
# tenant 配置
[[tenant.backends]]
addr = "unix:/run/raisfast/user1-a.sock"
weight = 50
[[tenant.backends]]
addr = "unix:/run/raisfast/user1-b.sock"
weight = 50
策略:
round-robin— 轮询(默认)weighted— 加权随机least-connections— 最少连接数
16.4 完善后的模块结构
src/proxy/
├── mod.rs # 模块入口
├── config.rs # 配置加载
├── router.rs # 路由表
├── proxy.rs # HTTP 反向代理核心
├── pool.rs # 后端连接池 ← 新增
├── limiter.rs # 限流 + 连接数限制 ← 新增
├── ip_filter.rs # IP 黑白名单 + 自动封禁 ← 新增
├── compression.rs # 响应压缩 ← 新增
├── cache.rs # 响应缓存 ← 新增
├── error_page.rs # 自定义错误页 ← 新增
├── tls.rs # TLS 终结
├── acme.rs # ACME 自动证书
├── admin.rs # 管理 API
├── health.rs # 健康检查
├── access_log.rs # 访问日志
├── slow_log.rs # 慢请求日志 ← 新增
├── watcher.rs # 配置热加载
└── metrics.rs # Prometheus 指标 ← 新增
16.5 实施优先级
| 阶段 | 功能 | 预估工作量 |
|---|---|---|
| Phase 3(与现有合并) | 连接池复用 + 超时 + 连接数限制 + 请求 ID 透传 | 3 天 |
| Phase 3(与现有合并) | 自定义错误页 | 0.5 天 |
| Phase 4 | Per-tenant 限流 + IP 黑白名单 | 2 天 |
| Phase 4 | 响应压缩 | 1 天 |
| Phase 4 | 响应缓存 | 2 天 |
| Phase 4 | 实时指标 | 1 天 |
| Phase 5 | 慢请求日志 | 0.5 天 |
| Phase 5 | 灰度/蓝绿部署 | 1 天 |
| Phase 5 | 负载均衡 | 2 天 |
| Phase 5 | DNS 缓存 | 0.5 天 |