尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Data Formulator 工作区存储架构深度解析:磁盘布局、Manager/Workspace 双层设计与三种后端模式

Data Formulator 工作区存储架构深度解析:磁盘布局、Manager/Workspace 双层设计与三种后端模式 Data Formulator 工作区存储架构深度解析磁盘布局、Manager/Workspace 双层设计与三种后端模式【免费下载链接】data-formulator Data Formulator is an interactive AI-powered data analysis system makes it easy to connect, explore and visualize data.项目地址: https://gitcode.com/GitHub_Trending/da/data-formulator本篇技术指南以 Data Formulator 的开发者文档 9-workspace-storage-architecture.md 为骨架结合 datalake 目录下的源码实现系统讲解工作区的磁盘目录结构、每个持久化文件的职责、WorkspaceManager与Workspace的分工以及 local / azure_blob / ephemeral 三种后端模式的差异。读完本文你将掌握 Data Formulator 数据层的完整存储布局理解后端表元数据与前端状态快照如何分离与协作并能在修改 workspace 相关代码时遵循其一致性约定与安全检查清单。1. 存储架构总览数据放在哪里Data Formulator 的数据层采用目录即工作区的设计哲学每个用户拥有一个独立目录目录下每个 workspace 对应一个子目录workspace 内的表数据、元数据与前端状态分别落到不同类型的文件中。整个存储根目录由DATA_FORMULATOR_HOME决定默认是~/.data_formulator可通过--data-dir命令行参数覆盖。从源码看get_data_formulator_home() 的解析顺序是Flask 应用配置中的CLI_ARGS[data_dir]由--data-dirCLI 参数设置环境变量DATA_FORMULATOR_HOME兜底默认值Path.home() / .data_formulator。同时get_user_home() 将每个用户的根目录固定为DATA_FORMULATOR_HOME/users/safe_id/身份标识经过secure_filename清洗后作为目录名这保证了多用户场景下目录不会发生路径逃逸或非法字符冲突。2. 整体目录结构与 identity 格式2.1 目录树DATA_FORMULATOR_HOME/ # 默认 ~/.data_formulator可通过 --data-dir 覆盖 ├── credentials.db # SQLite — 加密的 Data Connector 凭据 ├── .vault_key # AES 密钥用于加解密 credentials.db ├── connectors.yaml # 管理员级 Data Connector 配置全局 │ └── users/ └── identity_id/ # 每个用户一个目录 ├── connectors/ # 用户级 Data Connector 配置每连接器一个 JSON │ ├── postgresql_prod-db.json │ └── mysql_analytics.json ├── catalog_cache/ # 数据源 catalog 元数据快照 │ ├── postgresql_prod-db.json │ └── mysql_analytics.json └── workspaces/ └── workspace_id/ # 每个 workspace 一个目录 ├── workspace_meta.json # 轻量索引 — 用于列表页快速展示 ├── workspace.yaml # 表元数据 — 后端数据层的核心索引 ├── session_state.json # 前端 Redux 状态快照 ├── .workspace.lock # 并发写锁运行时产生 └── data/ ├── gapminder.parquet ├── sales_data.parquet └── query_1.xlsx # 上传的原始文件注意Workspace 类 在初始化时还会创建scratch/子目录用于存放 Agent 执行中间产物execute_python的 DataFrame、fetch_url的载荷、上传缓存等并通过 LRU 淘汰策略控制总大小默认上限SCRATCH_MAX_BYTES 1 GiB可用--scratch-max-size-mb覆盖。data/与scratch/都有对应的ConfinedDir路径监狱确保所有文件读写都被限制在工作区根目录之内参见 workspace.py 中的 confined_* 属性。2.2 identity_id 格式来源格式示例匿名浏览器browser:uuidbrowser:101fdf9b-6213-456d-8fa9-6b9adeb32b0aOIDC 登录user:subuser:7本地开发local:os_userlocal:admin目录名经过secure_filename()清洗例如browser:101fdf9b-...→browser101fdf9b-...。清洗逻辑在 sanitize_identity_dirname() 中实现超过 256 字符或清洗结果为空会直接抛出ValueErrorWorkspaceManager._safe_id() 对 workspace_id 也做了同样的清洗清洗为空时回退为unnamed。这保证所有落在磁盘上的目录名都是安全的单一路径分量。3. 每个持久化文件的职责3.1workspace_meta.json— 列表页索引Owner:WorkspaceManagerPython大小: ~150 bytes写入时机:create_workspace()、save_session_state()、update_display_name()update_display_name()采用write-through 策略同时写workspace_meta.json和session_state.jsonpatchactiveWorkspace.displayName确保两个文件的 displayName 始终一致——即使被重命名的 workspace 不是当前前端打开的那个。相关实现在 workspace_manager.update_display_name()先写 meta 文件再尽力 patch 已存在的 session_state.jsonbest-effort失败不阻塞下次 auto-save 会同步。Azure Blob 后端因 session blob 可能好几 MB下载重上传开销大不做 write-through见 AzureBlobWorkspaceManager.update_display_name()只更新 meta blob如果出现不一致用户再改一次名字即可下次 auto-save 会同步。{ id: session_20260426_212411_1503, displayName: 全球发展洞察台, updatedAt: 2026-04-26T14:48:43.03748900:00, tableCount: 6, chartCount: 4 }用途:list_workspaces()只读这个文件不读 session_state.json几 MB实现 O(n × 150B) 的列表扫描。源码中_write_meta()会在写入时保留已有的createdAt缺失时用updatedAt回填避免老 workspace 的创建时间全部跳到刚刚save_session_state()每次保存还会同步刷新tableCount与chartCountworkspace_manager.py 第 455-482 行。3.2workspace.yaml— 后端表元数据Owner:Workspace类Python大小: 几 KB ~ 几十 KB只存 schema不存数据行写入时机:Workspace.__init__首次创建、write_parquet()、add_table_metadata()等表操作version: 1.1 created_at: 2026-04-26T13:24:1600:00 updated_at: 2026-04-26T14:37:5700:00 tables: gapminder: source_type: data_loader # upload | data_loader filename: gapminder.parquet # data/ 下的物理文件名 file_type: parquet content_hash: f4cca39e... # 内容指纹用于刷新去重 file_size: 16211 row_count: 682 description: 源系统提供的表描述 # 可选只读 system description columns: - {name: year, dtype: int64, description: 年份} - {name: country, dtype: object} loader_type: superset # Data Loader 来源信息可选 source_table: gapminder # 远程表名可选该文件的 schema 定义在 workspace_metadata.py 中WorkspaceMetadata记录version、created_at、updated_at与tables字典TableMetadata与ColumnInfo是两张核心 dataclassto_dict()/from_dict()负责与 YAML 互转。当前元数据版本号是METADATA_VERSION 1.1。用途:Agent 生成代码时获取表 schema列名、类型Agent 和 UI 获取源系统只读描述TableMetadata.description、ColumnInfo.descriptionDuckDB SQL 查询通过 filename 定位 parquet 文件run_parquet_sql()会把{parquet}占位符替换为read_parquet(path)实现列裁剪与 row-group 跳过读取见 workspace.py刷新导入数据时通过 content_hash 判断是否变化refresh_parquet_from_arrow()中 hash 相同则只更新last_synced见 workspace.pyData Loader 记录数据来源溯源信息loader_type、loader_params、source_table、source_query、import_options等字段随source_info写入。不存: 数据行、图表配置、UI 状态、用户编辑的attachedMetadata。源系统描述与用户描述是两个独立字段字段Owner来源权限写入时机TableMetadata.description后端Workspace源系统表/数据集/报表描述只读展示Data Loader import / refreshColumnInfo.description后端Workspace源系统字段注释只读展示Data Loader import / refreshDictTable.attachedMetadata前端 Redux用户手写业务说明用户可编辑元数据弹窗保存规则源系统描述永远不覆盖attachedMetadataattachedMetadata不写入workspace.yaml随前端状态进入session_state.jsonData Loader 返回空字符串description表示源端已清空描述缺少descriptionkey 表示保留已有描述旧workspace.yaml没有列描述时必须继续正常加载from_dict()用.get(description)容错处理。3.3session_state.json— 前端状态快照Owner: 前端 Redux store →WorkspaceManager.save_session_state()大小: 几百 KB ~ 几 MB含表的全量行数据写入时机: 前端自动保存定时 切换 workspace{ tables: [ { id: gapminder, names: [year, country, ...], rows: [/* 682 行全量数据 */], source: {type: example, url: ...} } ], charts: [/* 图表配置 */], draftNodes: [/* 编码面板 */], conceptShelfItems: [/* 概念架 */], messages: [/* 聊天记录 */], config: {/* UI 配置 */}, activeWorkspace: {displayName: ...} }用途: 加载 workspace 时恢复完整前端状态表数据 图表 对话 布局。敏感字段自动剥离不持久化:models、selectedModelId、testedModels、dataLoaderConnectParams、identity、agentRules、serverConfig。这些字段以 frozenset 形式定义在 workspace_manager.py 的_SENSITIVE_FIELDS中_strip_sensitive()在写入前统一过滤防止模型配置、身份信息等敏感数据落盘。3.4data/目录 — 物理数据文件存放 parquetAgent 衍生表、Data Loader 导入和用户上传的原始文件csv、xlsx 等。文件名由workspace.yaml中的filename字段索引。所有物理文件路径通过 get_file_path() 解析先经safe_data_filename()做 Unicode 安全清洗保留中文等非 ASCII 字符再经ConfinedDir.resolve()校验路径逃逸会直接抛ValueError。3.5.workspace.lock— 并发写锁WorkspaceLock上下文管理器使用的锁文件。Windows 用LockFileExUnix 用fcntl.flock。保护workspace.yaml的读-改-写原子性。运行时产生无需手动管理。实现细节在 workspace_metadata.pyWindows 通过 ctypes 调用LockFileEx/UnlockFileEx对整文件加排他锁非阻塞 重试Unix 用fcntl.flock(fd, LOCK_EX | LOCK_NB)默认超时MAX_LOCK_WAIT_SECONDS 10超时抛TimeoutError。配套的 update_metadata() 在单次锁持有内完成 read→update→write避免load_metadata与save_metadata各自持锁造成的 lost-update 竞态_write_metadata_file()采用tempfile.mkstempos.replace的原子写入崩溃后遗留的.temp_*.parquet由cleanup_stale_temp_files()在下次初始化时清理超过 24 小时的临时文件。3.6connectors/— Data Connector 配置用户级位置:users/identity/connectors/source_id.json不在 workspace 内用途: 记录用户创建的 Data Connector 实例类型、连接参数不含密码。每个连接器一个 JSON 文件支持原子化增删。凭据存储在全局credentials.db中。{ source_id: postgresql:prod-db, loader_type: postgresql, display_name: Production DB, default_params: {host: db.corp, database: analytics}, icon: postgresql }Breaking change: 旧版connectors.yaml格式已不再支持。升级后需重新创建连接器。3.7catalog_cache/— 数据源 Catalog 元数据快照用户级位置:users/identity/catalog_cache/source_id.json不在 workspace 内用途: 缓存数据源的轻量 catalog 元数据表名、描述、列名、列类型供 Agent 搜索工具在无活跃连接时也能发现数据。文件形状{ source_id: postgresql:prod-db, tables: [ { name: public.orders, metadata: { _source_name: public.orders, description: 订单事实表, columns: [ {name: order_id, type: INTEGER, description: 订单唯一标识}, {name: created_at, type: TIMESTAMP} ] } } ] }写入时机: 连接数据源成功后best-effort 调用list_tables()并持久化。点击刷新 catalog 时也会重新调用list_tables()并覆盖同名缓存文件。删除时机: 断开连接或删除连接器时同步清理。不常驻内存: 搜索时按需加载用完释放。安全边界: cache 位于当前 identity 的用户目录只保存 catalog 轻量信息不能包含loader_params、凭据、连接串或内部文件路径。Agent 的search_data_tables使用两层只读搜索WorkspaceMetadata.search_tables()搜索当前 workspace 已导入表在 workspace_metadata.py 中实现对表名、描述、列名、列描述打分排序catalog_cache/*.json搜索当前用户已连接数据源的轻量 catalog。缓存不存在或损坏时跳过不触发远程连接或实时拉取。4. 两层架构WorkspaceManager vs WorkspaceWorkspaceManager Workspace (管理 workspace 生命周期) (管理单个 workspace 内的数据) ┌──────────────────────┐ ┌──────────────────────┐ │ create_workspace() │ │ write_parquet() │ │ list_workspaces() │ open ──────► │ read_data_as_df() │ │ delete_workspace() │ │ add_table_metadata() │ │ save_session_state() │ │ get_metadata() │ │ load_session_state() │ │ export_session_zip() │ │ workspace_exists() │ │ run_parquet_sql() │ └──────────────────────┘ └──────────────────────┘ 操作 workspace_meta.json 操作 workspace.yaml 操作 session_state.json 操作 data/ 下的文件核心约定:Session 路由list, save, load, create, delete, rename通过WorkspaceManager数据路由upload, table CRUD, Agent通过get_workspace()→Workspaceget_workspace()包含懒创建逻辑frontend 生成 IDbackend 首次使用时创建。对应实现位于 workspace_factory.py前端每次请求通过X-Workspace-Id请求头携带当前 workspace IDget_active_workspace_id()后端get_workspace()检查mgr.workspace_exists(ws_id)不存在则先create_workspace()再open_workspace()workspace_factory.py 第 157-163 行。后端是无状态的workspace ID 完全由前端每请求携带。值得注意的能力边界local 后端支持rename_workspace()目录重命名并更新 meta 中的 id而 Azure Blob 后端由于 blob 前缀没有原子重命名操作rename_workspace()直接抛NotImplementedErrorazure_blob_workspace_manager.py 第 282-287 行。5. Workspace 存在性判定一致性规则Workspace 是否存在由目录是否存在唯一决定。如果目录存在但缺少workspace_meta.json老版本遗留自动补写修复。# workspace_manager.py def workspace_exists(self, workspace_id: str) - bool: 目录存在 workspace 存在。 return (self._root / self._safe_id(workspace_id)).is_dir()list_workspaces()遍历子目录时对缺少workspace_meta.json的目录调用_ensure_meta()自动补写使其在列表中可见def _ensure_meta(self, workspace_id: str) - dict: 缺少 workspace_meta.json 时从已有信息推断并补写。 meta_file self._root / self._safe_id(workspace_id) / WORKSPACE_META_FILENAME if meta_file.exists(): return json.loads(meta_file.read_text(encodingutf-8)) # 从 session_state.json 推断 displayName缺失则用 ID self._write_meta(workspace_id, workspace_id) return json.loads(meta_file.read_text(encodingutf-8))实际上_ensure_meta()会先尝试从session_state.json的activeWorkspace.displayName推断显示名推断失败才回退为 workspace ID见 workspace_manager.py 第 129-162 行。create_workspace()的防重复检查也统一为目录检查与workspace_exists语义一致见 workspace_manager.py 第 324-341 行。以前的问题已修复旧代码中三个方法用不同标准判断 workspace 是否存在方法旧判断依据问题list_workspaces只看workspace_meta.json老 workspace 不可见workspace_existsmeta.json OR yaml OR state.json和 list 不一致create_workspace目录是否存在和 exists 不一致导致幽灵 workspace存在但在列表中看不见也无法用同 ID 创建新的。Azure Blob 后端的存在性判定则等价于该 workspace blob 前缀下是否存在任意 blobazure_blob_workspace_manager.py 第 216-224 行语义与本地目录判定对齐。6. 三种后端模式通过--workspace-backend或WORKSPACE_BACKEND环境变量选择。读取逻辑在 workspace_factory._get_backend()默认值为local。6.1 local默认文件存本地磁盘DATA_FORMULATOR_HOME/users/id/workspaces/ws_id/WorkspaceManager→Workspace适用于单机部署。6.2 azure_blob文件存 Azure Blob Storage容器内按users/id/workspaces/ws_id/组织AzureBlobWorkspaceManager→AzureBlobWorkspace自带下载缓存workspace.yaml 和 data/ 都作为 blob 存储凭据通过 connection string 或 DefaultAzureCredentialEntra ID适用于多用户云部署。凭据解析逻辑在 workspace_factory._build_azure_container_client()优先使用AZURE_BLOB_CONNECTION_STRINGshared key / SAS否则使用AZURE_BLOB_ACCOUNT_URLDefaultAzureCredentialAzure 上走托管身份、本地走az login、Kubernetes 走 workload identity两者都未配置时抛ValueError。容器名默认data-formulator可用AZURE_BLOB_CONTAINER覆盖。blob 前缀布局为users/safe_id/workspaces/与本地目录结构完全同构azure_blob_workspace_manager.py 第 10-17 行。6.3 ephemeral前端 IndexedDB 为唯一数据源每次请求通过_workspace_tables发送全量表数据后端创建临时目录写 parquet 供 Agent/DuckDB 使用Session 路由全部返回 no-op进程退出时atexit清理临时目录适用于--disable-database模式无服务端持久化默认行数限制为 20,000DEFAULT_ROW_LIMIT_EPHEMERAL以兼顾浏览器性能。ephemeral 模式的核心实现在 ephemeral_workspace.pyconstruct_scratch_workspace()从请求体的_workspace_tables[{name: str, rows: [dict, ...]}, ...]读取全量表数据写入临时根目录df_ephemeral_*下的 parquet再返回一个标准的Workspace实例Agent 与 DuckDB 的读取路径与 local/Azure 完全一致。生命周期管理方面_get_ephemeral_root()惰性创建临时根并注册atexit清理启动时_cleanup_stale_roots()会清除上次崩溃遗留的、超过 1 小时的孤儿根目录。get_workspace_manager()在 ephemeral 模式下直接抛RuntimeErrorsession 路由list/save/load/create/rename返回 no-op见 routes/sessions.py。行数限制: 两种模式的数据导入行数由统一的frontendRowLimit前端和MAX_IMPORT_ROWS后端硬上限 200 万控制MAX_IMPORT_ROWS 2_000_000定义于 external_data_loader.py。完整设计详见 13-unified-row-limits.md。7. 数据流概览上传文件用户拖入 Excel → POST /api/upload-data → get_workspace() → Workspace → save_uploaded_file() → data/sales.xlsx → 转 parquet → data/sales_xlsx_sheet1.parquet → workspace.yaml 新增 table entry → 前端收到 rows/schema → Redux → 自动保存 → POST /api/sessions/save → WorkspaceManager.save_session_state() → session_state.json含 rows → workspace_meta.jsontableCountAgent 生成衍生表用户提交 prompt → POST /api/data-agent-streaming → get_workspace() → Workspace → Agent 生成 Python 代码 → sandbox 执行 → 产出 DataFrame → write_parquet() → data/d_result.parquet → workspace.yaml 新增 table entrywrite_parquet()的实现workspace.py 第 657-707 行会先对同名旧文件做清理写入新 parquet 后同步计算content_hash、file_size、row_count、columns再通过add_table_metadata()原子更新workspace.yaml——数据文件与元数据的写操作始终成对出现保证索引与实际文件一致。加载 workspace用户点击 workspace 列表项 → POST /api/sessions/load {id: session_xxx} → WorkspaceManager.load_session_state() → 读 session_state.json → 返回完整前端状态 → 前端 Redux hydrate → 恢复表/图表/对话8. workspace.yaml vs session_state.json 对比维度workspace.yamlsession_state.jsonOwner后端Workspace前端 Redux →WorkspaceManager存什么表的 schema 物理文件索引完整 UI 状态含全量行数据表数据行不存存rows[]图表/对话不存存谁读Agent, DuckDB, 上传逻辑前端加载 workspace 时典型大小几 KB几百 KB ~ 几 MB并发保护.workspace.lock文件锁无单次完整覆写两者在 schema 信息上有有意冗余后端独立于前端状态就能知道表结构。这意味着即使session_state.json丢失或损坏后端仍能通过workspace.yamldata/恢复表数据同理前端状态损坏时workspace.yaml依然是可信的数据层索引。9. New Module Checklist当修改 workspace 相关代码时请逐项核对以下约定均可在源码中找到对应落点确认新文件操作在data/子目录内不在 workspace 根目录写数据文件表元数据变更通过_atomic_update_metadata()而非直接save_metadata()——前者持有单锁完成读-改-写后者只做一次整体覆写前者才是并发安全的更新路径workspace.py 第 413-427 行workspace_exists语义 目录存在不要引入新的文件检查条件新增表字段同时更新TableMetadata.to_dict()和from_dict()workspace_metadata.py新增列字段同时更新ColumnInfo.to_dict()和from_dict()并验证旧 metadata 兼容from_dict()需对缺失字段用.get()兜底修改 Data Loader 源描述链路时确认TableMetadata.description/ColumnInfo.description不覆盖前端attachedMetadata修改 connector 生命周期时确认catalog_cache/source_id.json的写入、刷新、断开和删除语义一致敏感字段加入_SENSITIVE_FIELDS集合禁止持久化到 session_state.jsonworkspace_manager.py 第 32-46 行Azure blob 后端的AzureBlobWorkspaceManager需同步修改同一接口、不同后端任何存储语义变化都要在 azure_blob_workspace_manager.py 中找到对应 override考虑 ephemeral 模式是否需要适配通常 session 路由返回 no-op 即可。这组检查清单既是开发时的纪律约束也是对上述存储架构各个不变量目录存在即存在、双层职责分离、源描述不覆盖用户描述、敏感字段不落盘、三种后端行为对齐的一次系统性回顾——它们是 Data Formulator 数据层稳定运行的根基。【免费下载链接】data-formulator Data Formulator is an interactive AI-powered data analysis system makes it easy to connect, explore and visualize data.项目地址: https://gitcode.com/GitHub_Trending/da/data-formulator创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表