# 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, // 子域名匹配 prefix: Option, // 路径前缀匹配 backend: String, // "unix:/path" 或 "tcp:host:port" tls_cert: Option, // 自定义证书 tls_key: Option, 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, // host → backend by_prefix: DashMap, // prefix → backend } struct Backend { name: String, addr: BackendAddr, healthy: Arc, 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, backend: &Backend, ) -> Result, 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, acme: Arc, } /// 基于 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, // ACME 账号 } impl AcmeManager { /// 获取指定域名的证书(缓存优先,过期自动续签) async fn get_certificate(&self, domain: &str) -> Result>; /// 通配符证书申请(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) -> 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 自动加载 方式 B:POST /admin/tenants → API 动态注册 2. 运行中 - 健康检查定时运行 - 证书自动续签 - metrics 收集 3. 删除租户 方式 A:删除 .toml 文件 → watcher 自动移除 方式 B:DELETE /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, /// Path prefix (e.g., /user1) #[arg(short, long)] prefix: Option, /// 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, router: Arc, ) -> Result, 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` — 管理 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 # 管理 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 格式(默认,推荐) ```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, 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, pub referer: Option, pub request_id: Option, pub tls_version: Option, pub tls_cipher: Option, } #[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, pub max_size_mb: u64, pub max_files: usize, pub compress: bool, } /// 异步 access log 写入器 /// /// 通过 mpsc channel 接收日志条目,后台线程批量写入文件。 /// 不阻塞请求处理线程。 pub struct AccessLogger { tx: mpsc::Sender, config: Arc, } impl AccessLogger { pub fn new(config: AccessLogConfig) -> Self { let (tx, rx) = mpsc::channel::(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, config: Arc, ) { // 打开文件句柄(全局 + 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, router: Arc, logger: Arc, ) -> Result, 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=true,gzip 压缩 // 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>, max_idle_per_host: usize, // 默认 8 idle_timeout: Duration, // 默认 90s } struct PooledConnection { sender: hyper::client::conn::http1::SendRequest, 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 Service Temporarily Unavailable

Site Under Maintenance

We'll be back shortly.


Powered by raisfast ``` --- #### 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`。 --- #### 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` crate(tokio + 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` 简单缓存,过期重新解析。 --- #### 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 天 |