Files
raisfast/docs/proxy-design.md
T
2026-05-14 17:43:12 +08:00

1391 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 命令
```bash
# 单实例模式(现有行为,不变)
raisfast server start
# Proxy 模式(新增)
raisfast proxy start --config /etc/raisfast/proxy.toml
# 两者可以同时运行在同一台机器上
```
### 3.2 Proxy 配置文件
```toml
# /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` 下:
```toml
# /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` — 配置加载
```rust
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,
}
```
加载优先级:
1. `tenants_dir/*.toml` — 文件配置
2. 管理 API 动态注册 — 运行时增删
3. 两者合并到路由表
#### `router.rs` — 路由表
```rust
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),
}
```
路由匹配优先级:
1. 精确 Host 匹配
2. 路径前缀匹配(最长前缀优先)
3. 默认后端(可选)
#### `proxy.rs` — 反向代理核心
```rust
/// 核心代理函数
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 终结
```rust
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::ResolvesServerCert` trait 实现动态证书选择
- 支持 SNI 路由(TLS 握手时就知道目标域名)
- 通配符证书一个就够了,无需每租户单独申请
#### `acme.rs` — 自动 HTTPS
```rust
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-acme` crate,自己实现轻量 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
```rust
// 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` — 健康检查
```rust
struct HealthChecker {
backends: Vec<(String, BackendAddr)>,
interval: Duration,
}
impl HealthChecker {
/// 定时检查所有后端,更新 healthy 状态
/// TCP 后端:尝试 TCP connect
/// Unix socket 后端:尝试 connect Unix socket
/// 也可发 HTTP GET /health
}
```
#### `watcher.rs` — 配置热加载
```rust
/// 监听 tenants_dir 目录变化
/// 新增 .toml → 添加到路由表
/// 修改 .toml → 更新路由表
/// 删除 .toml → 从路由表移除
fn watch_tenants_dir(dir: &Path, router: Arc<RouterTable>) -> JoinHandle<()>;
```
使用 `notify` crate 监听文件系统事件。
---
## 5. 依赖
```toml
# 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 自动加载
方式 BPOST /admin/tenants → API 动态注册
2. 运行中
- 健康检查定时运行
- 证书自动续签
- metrics 收集
3. 删除租户
方式 A:删除 .toml 文件 → watcher 自动移除
方式 BDELETE /admin/tenants/user1
```
---
## 7. CLI 集成
```rust
// 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 反向代理核心代码(简化版)
```rust
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 2TLS + 自动证书(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` — 管理 API
- [ ] `src/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. 运维命令速查
```bash
# 启动 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
# 管理 APIcurl
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 格式(默认,推荐)
```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 配置
```toml
# 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`
```rust
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` 中集成
```rust
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 日志轮转
```rust
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=truegzip 压缩
// 4. 删除超过 max_files 的旧文件
}
}
```
### 15.7 管理 API 集成
```bash
# 查看日志配置
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 连接池,避免重复握手。
```rust
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 限流
防止单个租户占用全部带宽,影响其他租户。
```toml
# tenant 配置
[tenant]
# 每秒最大请求数
rate_limit_rps = 100
# 最大并发连接数
max_concurrent_connections = 50
# 每月流量配额(GB
monthly_bandwidth_gb = 100
```
**实现**token bucket 算法(已有 `RateLimiterSet` 模式可复用)。
---
#### 16.1.3 全局连接数限制
防止连接数爆增导致 OOM。
```toml
# proxy.toml
[proxy]
# 最大并发连接数(超出排队)
max_connections = 10000
# 排队超时(超出返回 503
queue_timeout_ms = 5000
# 单 IP 最大连接数
max_connections_per_ip = 100
```
---
#### 16.1.4 请求超时
防止慢后端占住 proxy 连接。
```toml
# 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 状态码。
```toml
# proxy.toml
[error_pages]
# 错误页 HTML 目录
dir = "/etc/raisfast/error-pages"
# 默认错误页(按状态码命名:502.html, 503.html, 504.html
# 缺失时使用内置简约页面
```
内置默认错误页(编译进二进制,零配置也可用):
```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 / 恶意请求。
```toml
# 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 响应。
```toml
# 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` cratetokio + brotli + gzip)。响应体流式压缩,不缓冲到内存。
---
#### 16.2.4 响应缓存
静态资源 + GET 请求短期缓存,减少后端压力。
```toml
# 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 基础上,提供实时聚合指标。
```bash
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 慢请求日志
超阈值的请求单独记录,用于性能排查。
```toml
# proxy.toml
[slow_log]
enabled = true
# 慢请求阈值(ms
threshold_ms = 3000
# 独立文件
file = "/var/lib/raisfast/proxy/logs/slow.log"
```
---
#### 16.3.2 灰度/蓝绿部署
按比例将流量切换到新版后端,零风险上线。
```toml
# 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 解析。
```toml
# 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 内置负载均衡。
```toml
# 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 天 |