返回 文章 build CMS 文章

Cloudflare 如何构建统一数据平台及 AI 数据代理

Cloudflare 通过 Town Lake 统一数据平台和 Skipper AI 代理,让非技术人员也能用自然语言查询海量数据。

Cloudflare数据平台AI代理Trino
成长分 / 100 75 综合收获、行动、留存与影响

Cloudflare 如何构建统一数据平台及 AI 数据代理
为什么值得读了解超大规模公司如何解决数据蔓延、多系统查询和权限治理的痛点。

学习如何用 AI 代理(Skipper)结合多层上下文(模式、血缘、代码)生成准确 SQL 查询。

关键洞察
  1. Town Lake 基于 Trino + Iceberg + R2 构建,统一查询 Postgres、ClickHouse、R2 等异构数据源。
  2. Skipper 通过 5 层上下文(模式、人工注释、代码衍生知识、精选模型、运行时自省)减少 LLM 幻觉。
  3. 采用“代码模式”MCP 服务器,让模型用 JavaScript 编程式调用工具,减少往返次数。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:20579

Cloudflare 每秒处理超过十亿个事件。我们的网络覆盖 120 多个国家的 330 多个城市。每一个 HTTP 请求、每一次 Worker 调用、每一次 R2 读取操作背后都有数据,而且数量庞大。

多年来,这些数据并不容易获取。它们分散在几十个生产数据库、ClickHouse 集群、Kafka 流、Google Cloud 存储桶、BigQuery 数据集以及一长串零散的流水线里。要回答“今天注册的域名中,有多少属于流量 Top 100?”这样一个简单的问题,Cloudflare 的分析师必须知道该问哪个系统、用什么凭证、写什么查询语言,还要知道自己看到的数据是经过采样的、足够新鲜的,还是已经过期七天。结果就是,很难从数据中获得有根据的洞察。

为了解决这个问题,我们自研了两个工具:Town Lake,Cloudflare 的统一数据分析平台;以及 Skipper,运行在其上的 AI 数据智能体。Town Lake 为 Cloudflare 掌握的一切数据提供了单一的 SQL 接口,而 Skipper 则让 Cloudflare 的任何人都能用日常英语提问,并在几秒内得到正确且可审计的答案。

这就是我们如何构建这两者的故事。

问题的形态

如果你在经历过高速增长期的公司工作过,就会知道数据泛滥是什么样子。我们的情况有几个具体症状:

  1. 零散的系统太多。想排查客户问题的产品工程师,可能需要查询 Postgres 获取账户元数据,查询 ClickHouse 获取分析事件,查询 BigQuery 获取用量汇总,查询 R2 获取原始日志,还要查看 Kafka 主题获取实时信号。每个系统都有各自的凭证、各自的语言和各自的保留策略。
  2. 数据经过采样。这对仪表盘来说没问题,但对计费这类领域行不通。我们的分析流水线为了应对每秒 7 亿以上的事件而进行降采样。当你希望分析仪表盘能顺利加载时,这是正确的做法;但当你要计算某人的用量以开具发票时,这恰恰是错误的做法。
  3. 内部数据依赖外部服务。我们之前的内部报表体系有一部分由外部供应商驱动。除了成本之外,我们的一些关键数据还对另一家云厂商存在硬性的外部依赖。
  4. 没人找得到数据。即使你拥有所有正确的凭证,你还得知道“按账户统计的可计费 Workers 请求数”所对应的正确表位于某个特定的 ClickHouse 集群、某个特定的 schema 中,要与某张特定的 Postgres 维度表关联,而且这个关联还需要一次晦涩的客户 ID 转换。太多知识只存在于口口相传之中。

我们还面临一个文化上的挑战:数据基础设施历来被视为服务于业务的后台职能,而不是一项本身就至关重要的基础设施。

我们想要什么

我们想建立一个统一的地方,让公司里拥有相应权限且有需要的人都能获得关于 Cloudflare 的问题的答案:“给我上个季度收入最高的前 100 位客户”“列出过去 48 小时内来自某个特定 ASN、评分 > 0.9 的所有 Bot Management 机器学习评分事件”“找出消费超过 >$100 的客户提交的前 100 个计费支持工单”,等等。

我们希望这个地方能为需要的查询(如计费或安全调查)提供新鲜、准确、未经采样的数据,同时为不需要的查询(如仪表盘或探索性分析)提供快速的降采样数据。

我们希望把安全与治理内置其中:自动检测个人身份信息(PII),敏感表默认锁定。所有访问都应可审计,并采用有时间限制的权限授予,让用户只有在积极处理需要这些数据的任务时才能访问。

我们希望它构建在 Cloudflare 自己的平台上:用 R2 做存储,用 Workers 做计算,用 Cloudflare Access 做身份认证,用 Workflows 做编排。既然要对数据基础设施做一次大的投入,就应当基于我们卖给客户的同一批产品来构建。

最终,我们还希望有一个无需懂 SQL 的界面。目标是让公司里拥有相应权限且有需要的每个人都能审视流经我们网络的数据流,而不只是分析师。

最后这个需求,就成了 Skipper。

Town Lake,平台本身

从核心上看,我们数据平台的架构是一个数据湖仓(data lakehouse):由一个从对象存储读取数据的查询引擎,加上一个让存储表现得像数据库的元数据层构成。我们以德克萨斯州奥斯汀的同名湖泊,将其命名为 Town Lake。

BLOG-3311 Image 1

其中最重要的组件有:

查询引擎。我们选择了 Apache Trino:一条 SQL 查询可以同时关联一张 Postgres 表、一张 ClickHouse 表和一张 R2 上的 Iceberg 表,而无需把中间结果物化到另一个系统里。比如查询“本周按 Workers 请求数排名的前 100 名付费客户”,会被编译成这样一个执行计划:把过滤条件下推到 ClickHouse,与 Postgres 中的账户维度表做关联,再对 R2 中的计费汇总数据排序,一气呵成。

R2 Data Catalog 是我们托管的 Apache Iceberg 服务,冷数据和温数据都存放在这里。Iceberg 为我们提供了 schema 演进、时间旅行、分区演进,以及随着数据变旧而进行压缩合并的能力。上周按分钟统计的用量会变成按小时,上季度按小时的会变成按天,依此类推。数据越旧,存储成本越低,同时数据依然可以查询。R2 中的 Parquet 文件比把同样的数据放在 OLAP 数据库中便宜得多。

DataHub 是我们的元数据目录。每一张表、每一列、每个所有者、每条血缘关系和每个术语表条目都存放在这里。当用户问“townlake.dim.accounts 里有什么”时,DataHub 会给出答案,包括表的描述、各列的描述、所属团队、为其提供数据的上游表,以及消费它的下游表。

Lifeguard 是我们的访问控制服务:它把访问规则存储在 D1 中,从内部访问管理系统动态拉取用户和用户组成员关系,并渲染出一份合并的 JSON 策略,供 Trino 通过 HTTP 读取。Lifeguard 还会把基本的访问信息提供给 Skipper 和 Gateway,因此用户在大门口就被拦下,而不是等到查询时才被拦下。

Skimmer 是一个 PII 检测扫描器。它持续运行,从每张表的每一列中抽样行,并使用 Workers AI 判断每一列是否包含 PII。它分两轮完成:第一轮是快速的逐列分类器;如果有任何内容被标记,则进入智能体式的第二轮,该轮会获得完整的表上下文,并可以直接查询 Trino 进行验证。检测结果会流入 DataHub 以及 Lifeguard 的白名单,以便进行人工参与的审核。

Transformer 是我们基于 Workflows 构建的 ELT(提取、加载、转换)引擎。用户通过带有 YAML 前置元数据(目标表、物化模式、依赖项、调度)的 SQL 转换来定义有向无环图(DAG)。Transformer 编译该图并在 Trino 上运行,状态由 Durable Objects 管理,定义存储在 R2 中,运行历史记录在 D1 中。

Ingestion(数据摄取)是从业务系统通向数据湖的桥梁。一个编排器作为长期运行的 Kubernetes 部署运行,读取流水线配置,并派生出短生命周期的工作任务,从 Postgres 或 ClickHouse 中提取数据,转换为 Parquet,再作为 Iceberg 表加载到 R2。每条流水线以全量替换或增量追加两种方式之一运行。

BLOG-3311 Image 2

默认关闭:从构造上保证治理

构建统一数据平台时,一个现实的担忧是:你刚刚搭建起了一个庞大的敏感数据暴露面。传统的回答是:默认开放,例外限制。先允许访问一切,等有人发现问题后再审计并锁定敏感表。

Town Lake 采取了相反的做法。表在经过审核之前不可查询。当有新数据库接入 Trino 或创建了新表时,Skimmer 会扫描它、对其各列分类,并在中央白名单中将其登记为待审核状态。在审核人批准该表及其中特定列之前,用户无法查询它。这听起来很痛苦,本来也确实会如此,除了两点原因。

第一,它是自动化的。Skimmer 的分类器相当不错:既能抓出明显的 PII(邮箱、IP、姓名、电话号码),也能抓出长尾的不明显的敏感数据(匹配特定前缀的 API 令牌、可追溯到用户的不透明 ID)。审核人能看到检测到了什么,然后选择批准、覆盖或拒绝。大多数审核只需几秒钟。

第二,流程是自助式的。如果你查询了一张自己没有权限的表,错误信息不是“权限被拒绝”,而是“这张表需要审核,点击此处提交申请”。AI 智能体 Skipper 甚至会建议你应申请哪个 RBAC 用户组,并直接给你链接。

我们把 schema 发现与数据访问分离开来。用户可以看到有哪些表存在,但未经审核的列会从 DESCRIBE 、SHOW COLUMNS 以及 SELECT * 中隐藏。这个微妙的区别很重要:它意味着一个新增的、未经审核的列不会破坏建立在已批准表其余部分之上的现有仪表盘。

PII 按会话主动开启。默认情况下,Trino 会在敏感列到达你的屏幕之前就对其脱敏。如果你确有正当理由需要原始 PII(例如欺诈调查),就在会话上打开开关,系统会检查你的权限,然后解除脱敏。开关的切换以及每一次查询都会被记录。

Skipper:AI 数据智能体

如今仅有查询引擎已经不够了。SQL 仍然是一道门槛,要知道在成千上万张表里该查哪一张也是——你得了解规范的 schema。

Skipper 是我们对对话式 AI 智能体的实践,它能从自然语言问题出发,给出经过验证的答案,并以公司真实的数据、代码和机构知识为依据。我们将它构建在 Town Lake 之上,也构建在我们的开发者平台之上:Workers、Workers AI、Durable Objects、D1、R2、Workflows、KV。

界面就是一个聊天框。提出一个问题:

给我过去 30 天按 R2 存储成本排名的前 10 位客户,以及相较前 30 天的变化。

Skipper 会找到正确的表(DataHub 搜索),拉取它们的 schema 和血缘,编写 SQL,提交给 Trino,轮询结果,然后给你展示一张表格或图表。再追问:

现在按区域拆分,并忽略 Cloudflare 内部账户。

它会保留上下文,改进查询并重新运行。如果有地方看起来不对,比如某个关联产生了零行结果,或某个过滤条件排除了你预期的内容,Skipper 会在闭环推理中自行排查、调整并重试。难点在于拥有正确的上下文。

Skipper 还可以把图表打包成仪表盘,在内部共享,并嵌入其他内部应用中。它还具备通过 Transformer 构建转换图、通过 Lifeguard 检查访问与权限的工具。

Skipper 在用户所在的地方与他们相遇。所有这些工具都可以通过一个 Worker 使用,它由 Workers AI 驱动的内置智能体框架支撑。另一方面,我们的许多内部用户使用本地智能体工作流,因此 Skipper 的工具也可以通过 MCP 服务器获得。

上下文的层次

给 LLM 一个 SQL 提示和一份表名列表,它可能会凭空编造关联、误用列,并自信地给出一个完全错误的数字。我们在早期实验中吃过这个苦头。解决办法是提供多层有依据的上下文,让模型在检索时按需获取。

BLOG-3311 Image 3

第 1 层:schema 与使用元数据。DataHub 知道每张表的每一列、每种类型、每个主键、每个外键。它还根据历史查询模式知道哪些表经常被关联在一起。Skipper 的 search_datasets 和 get_entity_details 工具会直接呈现这些信息。

第 2 层:人工标注。当拥有 dim.accounts 的团队写下类似“账户级实体。每个 account_id 对应一行。每个账户恰好属于一位客户(通过 customer_id 外键)”这样的描述时,该描述会存放在 DataHub 中,并最终进入 Skipper 的上下文。像 curated 这样的标签用来标记经过验证的表,Skipper 应优先选用这些表而不是临时存放区。

第 3 层:来自代码的知识。一些最有价值的上下文并不在任何目录里:它在生成该表的 SQL 中。Transformer 流水线在每次成功运行时,都会向 DataHub 输出每个节点的 .meta.json 文档。因此当 Skipper 查看 fct.billings_allocated 时,它看到的不只是 schema,还会看到这是一张由 dim.accounts、dim.customers 和 seed.product_classification 预先关联而成的事实表,其 alloc_amount 列的计算方式为 billed_amount / 12 for annual; billed_amount for monthly。正是这类细微差别,把正确的答案与自信的错误答案区分开来。

第 4 层:精选的数据模型。我们维护了一小批“数据模型”页面:简短的、由人撰写的文档,描述如何理解计费、客户、账户和域名区(zone)。“优先选用带有‘curated’标签的表。避免使用 scratch_r2 以及带有‘internal’标签的表。搜索时使用数据模型术语(如‘billing product revenue’),而不是自然语言。”当问题与之匹配时,这些页面会作为 MCP 资源提供给智能体获取。

第 5 层:运行时内省。当其他手段都失败时,Skipper 可以向 Trino 发出实时查询:DESCRIBE table, SELECT DISTINCT col LIMIT 20, SELECT COUNT(*)。由于运行时上下文代价高昂,它很少使用这一手段,但正是这张安全网让系统的其余部分变得稳健。

Skipper 作为 MCP:Code Mode

有一个具体的实现细节值得单独拿出来讲,因为它是一种独具 Cloudflare 特色的解决方案。

当你用工具构建 AI 智能体时,标准模式是在提示词中定义工具,让模型一次调用一个,解析响应、执行,再返回结果。这没问题,但很啰嗦:一个五步工具的工作流就是五次模型往返,每一次都得重新建立上下文。

对于我们的 MCP 服务器,我们使用 Code Mode。我们没有定义 30 个独立的工具,而是只暴露两个:search 和 execute。模型编写一段 JavaScript 代码片段,以编程方式调用我们的整套工具:

const datasets = await skipper.search_datasets({ query: "billing product revenue" })
const queryId = await skipper.start_query({ sql: "SELECT ..." })
const results = await skipper.fetch_results({ queryId, mode: "inject" })
return skipper.create_chart({ chartType: "bar", data: results.rows, ... })

这段 JavaScript 通过 WorkerLoader 在沙箱化的 Dynamic Worker 隔离环境中运行。模型可以用它极为熟悉的语言,在一次往返中表达复杂的多步骤工作流。这样更快、更便宜,而且它生成的工作流作为代码是可审计的。

安全模型就是数据模型

Skipper 所做的一切都以调用者的身份运行。如果你没有某张表的访问权限,Skipper 就无法替你查询它。如果你请求 PII,会检查你的权限。如果你保存的查询被分享给同事,会在查看时而非保存时检查他们的访问权限,因为用户组成员关系是会变化的。

共享仪表盘还有自己独特的处理。只需一个占位 div 和一个 script 标签,就能把它们嵌入 Cloudflare 的任何内部工具:

<div data-skipper-dashboard="dash-123"></div>
<script src="https://skipper.cloudflare.com/embed.js" async></script>

iframe 会自动调整大小以适应内容。内容安全策略(CSP)frame-ancestors 会阻止从企业域名之外的任何地方进行嵌入。Cloudflare Access 仍然会对 iframe 的内容把关,因此未经认证的查看者在 iframe 中看到的是 Access 登录页,而不是数据。非所有者的查看者会根据底层表进行检查:如果他们没有访问权限,就会被指引到应申请的正确用户组。

BLOG-3311 Image 4

它支撑了什么:非常快的答案

计费。这是最初的用例。我们的 Billable Usage Dashboard 是面向客户的仪表盘,向按量付费的用户准确展示他们应付多少,它由一条计量流水线驱动,其事实来源是 R2 中的一组 Iceberg 表,通过 Trino 查询。仪表盘的 API 拉取的是与开票系统所用相同的紧凑 (date, account_id, metric_name, usage) 行,因此仪表盘上的数字与账单上的数字一致。

与计费相关的查询占 Town Lake 所服务查询总量的 53%:在最近的一个统计周期内,有 324 位不同的 Cloudflare 员工发起了 91,760 次此类查询。过去用来计算按客户划分的收入汇总、长达 200 到 300 行的旧版 SQL 查询,现在只有五行。

商业智能。“收入最高的前 100 位客户”这个问题,现在在 Skipper 中大约需要三秒钟。“今天注册的域名中有多少属于前 100”也是如此。我们过去需要提 Jira 工单的大多数数据类问题同样如此。

安全分析。我们的 Bot Management 团队使用 Town Lake 查询过去 48 小时内评分 > 0.9、并按 ASN 和地域过滤的机器学习评分事件。威胁研究人员在它之上构建了自己的查询工具集。Trust & Safety 团队从中获取信号,协助打击滥用行为。

客户支持。“找出消费超过 >$100 的客户提交的前 100 个计费支持工单”过去是一个耗时数天的项目,现在只是一次 Skipper 查询。

我们学到了什么

有几件事让我们感到意外。

少提示反而更好。早期版本的 Skipper 有精心设计的、指令式的系统提示词:“首先使用 search_datasets。然后使用 get_entity_details。再根据需要使用 list_schema_fields……”结果质量下降了。模型本身擅长对分析型工作流进行推理,不需要事无巨细地管着它。我们把指令式提示词换成了高层次的指引,让模型自己选择路径,结果变好了。

工具重叠是毒药。我们最初暴露了每个工具的每一种变体:三个不同的“获取结果”工具、两个“搜索”工具、好几个“列出”工具。模型被搞糊涂了,调用了错误的工具。我们做了整合。现在 fetch_results 有一个 mode 参数 (inject / display / both),而不是三个独立的工具。每个工具都只有一个存在的理由。

是代码而非元数据承载了含义。当我们开始摄取生成一张表的实际 SQL,而不仅仅是它的 schema 时,准确率的提升最为显著。一个值为 contract、paygo、free 的 customer_type 列,在两种情形下看起来完全一样,但 SQL 会告诉你,当缺少 Salesforce 数据时,customer_type 默认取 paygo。这类上下文从来不会出现在列描述里。

记忆比我们预想的更重要。有一长串零散的纠正,形如“你得像这样过滤 X”或“忽略带有 Y 标签的表”。没有记忆层,智能体在每次对话中都要重新发现、重新学习这些内容;有了记忆层,它在团队实际反复提出的问题上会持续单调地变好。

枯燥的基础设施才是难点。Trino + Iceberg 并不是新技术。真正的硬功夫在于那些枯燥的东西:逐行访问控制、默认关闭的表白名单、查询审计、有时间限制的凭证、PII 检测、幂等的数据摄取、schema 演进。正是这些东西,让一个数据平台能够被安全地真正使用。

下一步

我们正在扩展智能体的覆盖面。Skipper 已经可以作为 MCP 服务器接入任何支持 MCP 的 IDE。下一步是与我们自己的内部聊天和工单系统进行更深度的集成,使“问数据”成为任何人在排查故障、规划项目或验证假设时顺理成章的第一步。

我们正在大力投入 Transformer 流水线。目标是让 Cloudflare 的任何团队都能用几个 SQL 文件和一段 .meta.json 描述构建出精选数据集,将其部署为 Workflow,自动获得调度与监控,并且无需任何额外工作就能出现在 DataHub 和 Skipper 中。其理念是自助式的数据工程,与自助式的软件工程如出一辙。

R2 SQL 是 Cloudflare 的无服务器分布式分析查询引擎,正日益稳健。随着其功能集不断扩展,我们计划把 Town Lake 工作流的许多部分迁移到它上面。

我们下的赌注是:下一个突破性的产品,来自某个人盯着数据,看到了别人都没看到的东西。我们仍在押注这一点,而 Town Lake 就是确保他们能找到它的方式。