# Goose SessionStorage SQLite → PostgreSQL 迁移指南 > 目标:让 goosed 多实例真正 stateless,session 状态存 Postgres 而非本地 SQLite。 > > 工作量:改 1 个结构体 + 3 处 SQL,新增 connection pool 初始化。预计 2-4 小时。 > > 前置:**fork `/Users/john/Project/tkmind_go`** 或在那个仓库新建分支。 ## 改造点 ### 1. `crates/goose/src/session/session_manager.rs:525` **当前代码结构:** ```rust pub struct SessionStorage { pool: SqlitePool, // <-- 改这里 // ... } impl SessionStorage { pub fn new(data_dir: PathBuf) -> Self { let options = SqliteConnectOptions::default() .filename(data_dir.join(SESSIONS_FOLDER)) .create_if_missing(true) .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); let pool = SqlitePoolOptions::new() .connect_lazy_with(options); // <-- 这里要改 // ... } } ``` **改成:** ```rust pub struct SessionStorage { pool: sqlx::postgres::PgPool, // 改这行 // ... } impl SessionStorage { pub fn new(connection_string: String) -> Self { // 从环境变量或参数读取 PostgreSQL 连接串 // e.g. GOOSE_SESSION_DB_URL=postgresql://... let pool = sqlx::postgres::PgPoolOptions::new() .max_connections(20) .connect_lazy(&connection_string); // ... } } ``` ### 2. `schema.sql` / 迁移 SQL **SQLite 专用语法转 PostgreSQL:** | 问题 | SQLite | PostgreSQL | 改法 | |------|--------|-----------|------| | **主键自增** | `INTEGER PRIMARY KEY AUTOINCREMENT` | `SERIAL PRIMARY KEY` | 改为 `BIGSERIAL` 或 `GENERATED ALWAYS AS IDENTITY` | | **类型** | `TEXT`(什么都行) | 严格类型(JSONB/TEXT/TIMESTAMP) | 用 `JSONB` 存 JSON,用 `TIMESTAMP` 存时间戳 | | **模式检查** | `sqlite_master` | `information_schema` | 改用 `information_schema.tables` | | **WAL/Journal** | `PRAGMA journal_mode=WAL` | N/A | 删除,PG 有自己的 WAL | | **Collate** | `COLLATE NOCASE` | 用 collation 或 LOWER() | 改成 `LOWER(column) = LOWER($1)` | **例:** ```sql -- SQLite CREATE TABLE sessions ( id TEXT PRIMARY KEY, data JSON, created_at INTEGER ); -- PostgreSQL CREATE TABLE sessions ( id TEXT PRIMARY KEY, data JSONB, created_at BIGINT ); ``` ### 3. `FromRow` → `FromRow` **当前:** ```rust impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session { fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result { // column 取值方式对 SQLite 优化 } } ``` **改成:** ```rust impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for Session { fn from_row(row: &sqlx::postgres::PgRow) -> Result { // 取值方式对 PG 优化(JSONB 用 get_raw 后 JSON 解析) } } ``` ### 4. 调用处:初始化时读环境变量 **地点:** `main.rs` / 启动 `SessionManager` **改法:** ```rust // 改前 let session_manager = SessionManager::new(data_dir); // 改后 let session_db_url = std::env::var("GOOSE_SESSION_DB_URL") .expect("set GOOSE_SESSION_DB_URL env"); let session_manager = SessionManager::new(session_db_url); ``` ## 环境变量 启动 goosed 时设置: ```bash export GOOSE_SESSION_DB_URL=postgresql://boot:PASSWORD@120.26.184.105:5432/goose_sessions goosed agent ``` 数据库需提前创建: ```bash psql -U boot -h 120.26.184.105 -c "CREATE DATABASE goose_sessions;" ``` ## 测试步骤 1. **编译:** ```bash cd /Users/john/Project/tkmind_go GOOSE_SESSION_DB_URL=... cargo build --release --package goose-server --bin goosed ``` 2. **启两个实例,挂同一 PG:** ```bash goosed agent & goosed agent & # 另一个进程/端口 ``` 3. **同一 session 轮换到两个实例:** - 客户端起 session on instance 1 - 后续命令路由到 instance 2 —— 状态应无缝恢复 ## 文件清单 修改的文件(全在 crates/goose/src/): - `session/session_manager.rs` —— 3-5 处改动 - `session/mod.rs` —— 可能有 FromRow(如果有多个 impl) - `session/legacy.rs` —— 如果涉及导入(通常不用改) - `Cargo.toml` —— 检查 sqlx features(`postgres` 要打开) ## 风险与回滚 - **数据迁移:** SQLite→PG 用 `pgloader` 命令行工具(一条命令搞定 schema+数据+类型映射) - **灰度:** 先让新 goosed 实例用 PG,老实例仍用 SQLite,流量按 session 分配。验证无误后全量切换。 - **回滚:** PG 有完整 session 数据,可随时起新 SQLite 实例从 PG 导回。 ## 相关文档 - [Goose Scale Architecture](goose-scale-architecture-2026-06-26.md) —— 背景与决策 - [g2 Load Balancing](g2-load-balancing.md) —— 路由与灰度机制