烘焙数据架构模式

2021-07-29 22:08:44

在过去的几年里,我一直在探索一种用于发布网站的架构模式,我称之为“烘焙数据”模式。它提供了静态站点生成器的许多优点,同时避免了它们的大部分限制。我认为它值得被更广泛地使用。烘焙数据:将数据的只读副本与应用程序的代码捆绑在一起,作为同一部署的一部分 大多数动态网站将其代码和数据分开:代码在应用程序服务器上运行,数据以某种形式独立存在外部数据存储——比如 PostgreSQL、MySQL 或 MongoDB。使用烘焙数据,数据被部署为应用程序包的一部分。每当内容发生更改时,都会部署包含这些更新的站点的全新副本。为此,我主要使用 SQLite 数据库文件,但许多其他格式也可以在这里使用。这对于所谓的“无服务器”部署平台特别有效——支持无状态部署并且只对用于服务传入请求的资源收费(“缩放到零”)的平台。由于对数据的每次更改都会导致全新部署,因此这种模式不适用于经常更改的站点——但根据我的经验,许多面向内容的站点每天最多更新几次内容。考虑博客、文档网站、项目网站——任何内容由一小群作者编辑的地方。

主机便宜。任何可以运行应用程序代码的地方都可以托管烘焙数据应用程序——无需为托管数据库系统支付额外费用。将无服务器主机(例如 Cloud Run、Vercel 或 AWS Lambda)扩展到零,每月只需为低流量部署收取几美分的费用。易于扩展。需要处理更多流量?运行应用程序及其捆绑数据的更多副本。水平扩展烘焙数据应用程序是微不足道的。它们也非常适合在 Cloudflare 或 Fastly 等缓存代理 CDN 后面运行——当您部署新版本时,您可以清除整个缓存。很难打破。在 VPS 上托管服务器端应用程序总是令人不安,因为可能出错的地方太多了——服务器可能受到威胁,或者流氓日志文件可能导致磁盘空间不足。对于烘焙数据,最糟糕的情况是您需要重新部署应用程序——完全没有数据丢失的风险,并且可以自动重启代码的提供者可以自动从错误中恢复。支持服务器端功能。静态站点生成器提供了许多上述优点,但存在任何动态功能都需要在客户端 JavaScript 中实现的限制。使用烘焙数据应用程序,您也可以执行服务器端代码。模板化页面。对静态站点生成器的另一个改进:如果您有 10,000 个页面,则静态站点生成器将需要生成 10,000 个 HTML 文件。使用烘焙数据,这 10,000 个页面可以作为单个 SQLite 数据库文件中的行存在,并且可以在运行时使用服务器端模板生成这些页面。易于支持多种格式。由于您的内容位于动态数据存储中,因此以替代格式输出相同的内容很容易。我为此使用了 Datasette 插件:datasette-atom 可以从 SQL 查询生成 Atom 提要,而 datasette-ics 对 iCalendar 提要执行相同的操作。与版本控制很好地集成。我喜欢将我的网站内容置于版本控制之下。烘焙数据模式适用于从 git 存储库读取内容并使用它来构建与部署捆绑在一起的资产的构建脚本。

我对烘焙数据的最初实现都使用了 SQLite。它是此类应用程序的理想格式:一个二进制文件,可以存储任何可以表示为关系表、JSON 文档或二进制对象的东西——基本上是任何东西。任何可以由您的动态服务器端代码从磁盘读取的格式也可以使用:YAML 或 CSV 文件、Berkeley DB 文件或任何其他可以由磁盘文件中的只读字节存储桶表示的任何格式。 [我有一种预感,你甚至可以使用 PostgreSQL、MySQL 或 Elasticsearch 之类的东西,通过打包它们的磁盘表示并将它们作为 Docker 容器的一部分发送,但我自己还没有尝试过。] 一旦你的数据在文件中可用,您的应用程序代码可以从该文件中读取并使用它来生成和返回网页。您可以使用任何服务器端语言编写执行此操作的代码。我使用 Python,通常与我的 Datasette 应用程序服务器一起使用,它可以从 SQLite 数据库文件中读取并使用 Jinja 模板生成页面。最后一块拼图是构建和部署脚本。我为此使用 GitHub Actions,但任何 CI 工具都可以在这里很好地工作。该脚本将站点内容构建为可部署资产,然后将该资产与应用程序代码一起部署到托管平台。我自己发布的最复杂的 Baked Data 站点是我的 Datasette 项目的官方网站,datasette.io——这个 repo 中的源代码。

该站点是使用 Cloud Run 部署的。它实际上是一个高度自定义的 Datasette 实例,使用主页的自定义模板、站点其他部分的自定义页面和 datasette-template-sql 插件来执行 SQL 查询并显示来自这些模板的结果。 content.db 包含大部分站点内容。它是通过 build.sh 脚本在 GitHub Actions 中构建的,它执行以下操作:使用 markdown-to-sqlite 将 for/ 文件夹(Datasette 的用例)中的 markdown 文件导入到 uses 表中。使用来自更多 YAML 文件的数据填充 plugin_repos 和 tool_repos 单列表。这些将在下一步中使用。运行 build_directory.py Python 脚本。这使用 GitHub GraphQL API 来获取有关所有这些插件和工具存储库的信息,包括它们的 README 文件和它们最近的标记版本。使用所有 Datasette 生态系统 PyPI 包的最新下载统计信息填充统计表。该数据是从我的 simonw/package-stats 存储库中的 stats.json 文件导入的,该存储库本身由在 GitHub Actions 中运行的这个 git 抓取脚本填充。我也将它用于我的 Datasette Downloads Observable 笔记本。 blog.db 包含来自我的博客的内容,这些内容带有任何数据集、dogsheep 或 sqliteutils 标签。这是由 fetch_blog_content.py 脚本获取的,该脚本为我的博客内容点击分页的每个标签 Atom 提要,在 Django 中实现。 docs-index.db 是一个数据库表,包含最新稳定 Datasette 版本的文档,按部分分解。这个数据库文件是从一个单独的站点 stable-docs.datasette.io 下载的,它是作为 Datasette 发布过程的一部分构建和部署的。

dogsheep-index.db 是支持站点搜索的搜索索引(例如,此搜索 dogsheep)。搜索索引由 dogsheep-beta 使用从此 YAML 文件配置的其他数据库文件中的表中提取的数据构建。该站点每天通过计划操作自动部署一次,如果我想确保新的软件版本反映在主页上,我也可以手动触发该操作。 Niche Museums 是关于我参观过的小型博物馆的博客。同样,它是带有自定义模板的 Datasette。大部分内容来自这个museums.yaml 文件,但我也运行了一个脚本来确定每个项目是何时从 git 历史记录中创建或更新的。我的 TILs 站点在 Vercel 上运行,并通过此构建脚本(填充此 tils 表)从我的 simonw/til GitHub 存储库构建。它使用 GitHub API 将 GitHub Flavored Markdown 转换为 HTML。我还运行了一个脚本,该脚本生成每个页面的小屏幕截图,并将它们存放在 SQLite 的 BLOB 列中,以提供社交媒体预览卡,请参阅我的 TIL 的社交媒体卡。在一个我自己没有工作过的站点中,我最喜欢的这种模式的例子是 Mozilla.org。他们于 2018 年开始在他们称为 Bedrock 的系统中使用 SQLite——Paul McLanahan 详细描述了其工作原理。他们的网站内容存在于一个约 22MB 的 SQLite 数据库文件中,该文件被构建并上传到 S3,然后定期下载到他们的每个应用程序服务器。

您可以查看他们的健康检查页面以查看上次下载数据库的时间,并自己获取 SQLite 文件的副本。使用 Datasette 探索这一点很有趣:静态站点生成器在过去十年中大受欢迎。它们将托管站点的成本降低到几乎为零,提供出色的性能,与 CDN 配合良好,并生成极不可能损坏的站点。谨慎使用,烘焙数据保留了大部分这些特征,同时仍然支持服务器端代码执行。 datasette.io 提供 1,588 种不同内容的搜索,以及在插件和工具页面上更简单的搜索。所有三个站点都提供使用服务器端 SQL 查询配置的 Atom 提要:Datasette、Niche Museums、TIL。 Niche Museums 提供了一个“使用我的位置”按钮,然后可以使用利用 datasette-haversine 插件的 SQL 查询为您附近的博物馆提供服务。当用于大型站点时,对静态站点生成器的一个常见抱怨是,如果构建器必须生成数万个页面,则构建时间会变得很长。

使用烘焙数据,单个模板文件和 SQLite 数据库表中的 10,000 行可以生成 10,000 个页面。这也使得开发过程中的迭代周期更快:您可以编辑模板并点击“刷新”以立即查看新模板呈现的任何页面,而无需重建任何页面。如果您想尝试烘焙数据模式,我建议您开始使用 Datasette、GitHub Actions 和 Vercel 的组合。希望我上面提供的示例是一个很好的起点——如果有任何问题,也可以随时在 Twitter 或 Datasette 讨论论坛上与我联系。