Databricks 应用扩展指南:把状态拆成软和硬,再决定加机器还是加实例
Databricks 的 Bruno Tafur 和 Tushar Madan 在 2026 年 11 月发布了一篇面向应用构建者的扩展指南,讨论当一个 Databricks App 从几个用户增长到成百上千时,如何让它保持快速响应和高可用。文章的核心判断是:直觉答案「加更多计算资源」只有在你知道请求的时间花在哪里、以及应用本身是否具备扩展能力之后才有效。
作者将扩展过程分为四个阶段:设计、构建、测试部署和运维。在架构设计阶段,最关键的决策是把应用状态分成两类。软状态是临时工作数据,比如内存中的上下文、缓存和进行中的工作,丢了可以从硬状态重建;硬状态是业务记录、检查点和用户偏好,必须持久化在 Lakebase 或 Unity Catalog 表中,让任何实例都能恢复会话。这个分类决定了水平扩展是否真正可行——如果会话状态只存在内存里,实例重启或迁移时用户就会丢失工作。
文章给出了一个具体的零售商场景来演示如何估算负载:2000 名店长在黑色星期五最忙的一小时里,200 人同时使用库存查询应用,每人每分钟两次查询,每次执行三条 SQL 语句,结果是每分钟 1200 条语句、平均每秒 20 条。作者强调这估算的是查询速率而非并发数,测试时需要验证突发流量和增长,并且单独预算数据库连接。
在连接预算方面,文章给出了一个可操作的公式:当每个 worker 维护一个连接池时,连接数的上限是实例数乘以每实例 worker 数乘以池最大连接数。例如五个实例、每个八个 worker、池上限十,最多可打开 400 个连接。加上其他客户端和代用户打开的连接后,需要与数据库文档限制对比并留出部署期间的余量。对于需要以用户身份查询 Lakebase 的场景,每个用户需要独立的 Postgres 角色,而 Lakebase 每个分支限制 500 个角色,添加实例不会改变这个上限。
构建阶段的核心建议是使用异步代码。大多数应用的时间花在等待外部服务上——SQL 查询、模型调用、外部 API。阻塞调用会让一个等待占住一个执行槽位,少量慢调用就能耗尽容量。当 handler 用 await 等待模型和数据库调用时,一个 worker 可以在等待期间继续服务其他请求。文章还建议用 asyncio.gather 或 Promise.all 并行启动独立查询,让请求耗时接近最慢的那条查询而非所有查询之和,同时限制并行数量避免压垮后端。
关于优雅关闭,Databricks 在重新部署时发送 SIGTERM,如果应用未在 15 秒内退出则发送 SIGKILL。这个窗口用于最终检查点保存,但作者强调重要工作应该在正常运行期间就已保存。对于 FastAPI 应用,建议将 Uvicorn 的 --timeout-graceful-shutdown 设为 15 秒以下(例如 10 秒),防止长时间请求吃掉检查点时间。
在测试部署阶段,作者推荐用 Locust 等工具在预期峰值下用真实用户旅程做负载测试,逐步增加并发用户直到吞吐量不再增长或延迟和错误率突破目标。文章列出了一个决策表帮助选择实例规格:如果请求慢但应用 CPU 低,说明在等后端,应该先修查询或连接池而不是加计算;如果 CPU 接近上限且服务器运行多个 worker,可以升级到 Large;如果是单进程应用(如 Streamlit 或默认配置的 AppKit),CPU 接近上限时应该加实例而非升规格,因为更大的实例只在应用能用上额外核心时才有用。
运维阶段建议保持遥测开启,用自定义 span 包裹 SQL、模型和 API 调用来定位时间消耗,通过 trace 和 request ID 跨实例追踪单个用户的请求。当出现慢请求时,文章提供了从症状到原因的对照表:空闲后首次请求慢指向冷启动,CPU 或内存接近上限指向应用本身是瓶颈,数据库连接超时指向池竞争或长事务。
整套方法的关键前提是:应用计算只负责服务 UI 和协调调用,重活交给专门的服务。长时间运行的工作应该放到后台任务中,让 UI 轮询状态或接收流式更新,而不是让一个请求一直挂着。如果应用需要运行用户编写或代理生成的代码,应该放到 Databricks Sandbox 隔离环境中,使其 CPU 和内存消耗不影响服务用户的实例。
信息源:Databricks