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

38 KiB
Raw Blame History

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,
}

加载优先级:

  1. tenants_dir/*.toml — 文件配置
  2. 管理 API 动态注册 — 运行时增删
  3. 两者合并到路由表

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),
}

路由匹配优先级:

  1. 精确 Host 匹配
  2. 路径前缀匹配(最长前缀优先)
  3. 默认后端(可选)

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::UnixStreamhyper::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::ResolvesServerCert trait 实现动态证书选择
  • 支持 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-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

// 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 自动加载
   方式 BPOST /admin/tenants → API 动态注册
   
2. 运行中
   - 健康检查定时运行
   - 证书自动续签
   - metrics 收集
   
3. 删除租户
   方式 A:删除 .toml 文件 → watcher 自动移除
   方式 BDELETE /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 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. 运维命令速查

# 启动 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 格式(默认,推荐)

{
  "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=truegzip 压缩
        // 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 cratetokio + 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 缓存淘汰LRUmoka 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 天