博客

和 PostgreSQL 的二十年·7 篇,共 7

用 PostgreSQL 发 HTTP 请求这件丧心病狂的事

Cover Image for 用 PostgreSQL 发 HTTP 请求这件丧心病狂的事

番外篇。不在主线叙事里,但太有意思了,不写出来可惜。


在那家金融公司,我们干过一件让我自己都觉得有点离谱的事。

需求是这样的:有一张数据量极大的业务表,客户需要查询任意 60 个月的聚合数据。60 个月,跨度五年,数据分散,单次查询在 PG 里跑起来很慢。

常规的解法是在应用层拆——循环 60 次,每次查一个月,结果拼起来返回。或者上缓存,或者预聚合,或者换一个更适合 OLAP 的引擎。

我们做了一个不太一样的选择。


AWS 有一个扩展叫 aws_lambda,给 RDS for PostgreSQL 和 Aurora PostgreSQL 用的,官方出品。装上之后,你可以在 SQL 里直接调用 Lambda 函数:

SELECT * FROM aws_lambda.invoke(
  aws_commons.create_lambda_function_arn('monthly-aggregator', 'ap-east-1'),
  '{"month": "2023-01"}'::json
);

这一行 SQL,会同步调用一个 Lambda 函数,把 JSON payload 传进去,拿回 JSON 结果。Lambda 那边跑什么都行——查另一个数据源、做复杂计算、调外部 API。

数据已经通过 ETL 预先抽取到了 S3——60 个月的数据,按月份分区存放。Lambda 的职责是读取 S3 上指定月份的数据,做单月聚合,把结果以 JSON 返回。

PG 在这里扮演的是协调者和桥梁:它知道业务要查哪 60 个月,把请求逐月分发给 Lambda,收回 60 份结果,做最终汇总,包装成一个视图暴露给应用层。

S3 是存储层,Lambda 是计算层,PG 是业务逻辑和具体工程方案之间的桥。

应用层看到的,是一张普通的视图。SELECT * FROM monthly_summary WHERE ...,结果回来了。

它不知道,也不需要知道,这个查询背后 PG 悄悄调了 60 次 Lambda,Lambda 悄悄读了 60 份 S3 数据。


这个方案乍看起来很荒唐——数据库发 HTTP 请求?SQL 调 Lambda?这不是在滥用工具吗?

说实话,它的诞生原因很朴素:预算有限,RDS 规格升不上去,ClickHouse 没有条件上,Redshift 也不在考虑范围内。手边有 S3、有 Lambda、有一个支持 aws_lambda 扩展的 PG。在这个约束框架里,这是能想到的最合理的因地制宜。

几个现实因素让它跑得通:

60 个月是并行计算,不是串行。 这是这个方案最酷的地方。PG 发出去的 60 个 Lambda 调用,不是一个一个等着——Lambda 会同时起 60 个实例,同时计算,同时返回结果,PG 等着收齐再做最终聚合。能并行多少,取决于你的 Lambda 并发限制,默认是 1000,60 个月根本不是问题。不需要写任何并发控制代码,不需要线程池,不需要消息队列,AWS 天生就是这样工作的。

用 Lambda 的弹性算力换掉了本地 CPU 瓶颈。 RDS 规格升不了,但 Lambda 可以横向扩展。花的是 Lambda 的钱,省的是 RDS 的升配费用。在预算约束下,这是一笔划算的账。

Lambda 的调用成本极低。 60 次调用,每次几十毫秒,费用可以忽略不计。

对应用层完全透明。 不需要改应用代码,不需要引入新的中间件,不需要改数据模型。接口不变,行为不变,只是背后的实现换了。

系统边界清晰。 Lambda 可以独立部署、独立测试、独立监控。聚合逻辑有问题,改 Lambda 就好,不需要动 PG。


aws_lambda 扩展的安装和配置不复杂,但有几个前提:

-- 安装扩展(aws_commons 会自动一起装)
CREATE EXTENSION IF NOT EXISTS aws_lambda CASCADE;

-- 调用示例
SELECT payload FROM aws_lambda.invoke(
  aws_commons.create_lambda_function_arn(
    'arn:aws:lambda:ap-east-1:123456789:function:monthly-aggregator'
  ),
  jsonb_build_object('month', to_char(d, 'YYYY-MM'))
)
FROM generate_series(
  '2019-01-01'::date,
  '2023-12-01'::date,
  '1 month'::interval
) AS d;

generate_series 生成 60 个月份,每个月份对应一次 Lambda 调用,结果集回来之后在外层做聚合。PG 把这 60 次调用当成普通的行处理,逻辑上和扫描一张有 60 行的表没有区别。

IAM 权限要配好——RDS 实例需要有调用目标 Lambda 的权限,通过 IAM Role 绑定到 DB instance 上。VPC 配置也要注意,Lambda 和 RDS 需要能互相访问。


这件事让我重新理解了 PostgreSQL 的边界在哪里。

它不只是一个存数据的地方。FDW 机制让它可以连接外部数据源,aws_lambda 扩展让它可以调用任意计算服务,pg_notify 让它可以发消息,jsonb 让它可以存文档,分区表让它可以管理时序数据……

它是一个平台。

不是所有的需求都适合用这种方式解决——60 次同步 Lambda 调用在高并发场景下会是瓶颈,延迟也不可控。但在我们当时的场景里,这是最干净、最省事、对应用层影响最小的解法。

工具没有对错,只有合不合适。

PostgreSQL 给了你这个能力,怎么用,是你的判断。


说到底,aws_lambda 是 AWS 环境下的特供方案。本地的 PostgreSQL 没有这个扩展,但同样的思路可以用 pg_http 实现——底层是 libcurl,装上之后 PG 可以直接发任意 HTTP 请求。

为了让这些扩展能顺利跑起来,当时自己打了一个 Docker 镜像:postgresql-fat,推到了 Docker Hub。名字叫胖子,因为把能装的都装进去了——pg_http、各种 FDW、pg_vector,甚至试图集成 V8(plv8,在 PG 里跑 JavaScript)。精简不是目标,方便才是。

SELECT content::jsonb
FROM http_get('https://your-api.example.com/aggregate?month=2023-01');

原理完全一样,只是目标从 Lambda ARN 变成了一个普通的 HTTP 接口。在本地开发环境、自建服务器、或者任何不在 AWS 上的场景里,pg_http 是更通用的选择。

两套方案,同一个思路:让 PostgreSQL 出门去拿A数据,而不是等数据送上门来。

pg_httpaws_lambda 走得更远。aws_lambda 的边界是 AWS——你在它的生态里,调的是 Lambda,走的是 IAM。pg_http 的边界是 HTTP,而 HTTP 意味着 anywhere, anything。

任意 REST API、内部微服务、第三方数据接口、另一套系统的聚合层、甚至 AI 推理服务——只要有 HTTP 接口,PG 就能调。SQL 变成了调度语言,PostgreSQL 变成了一个可以连接任何东西的协调平台。

这个想象空间,比"数据库发 HTTP 请求"听起来要大得多。


写到这里,我突然意识到一件事。

第五篇里说过,当初为了解决磁盘膨胀的问题,本来想研究存算分离方案,最后被 ScaleFlux 一块硬盘绕开了,没有走那条路。

但你看,S3 是存储,Lambda 是计算,PG 是协调和接口层——这套组合,已经是存算分离了。

不是主动设计出来的,是被业务需求和 AWS 工具链自然推着走到了这里。而且比 TiDB 那种轻得多,没有新的分布式系统需要运维,每一层都是托管服务,边界清晰,出了问题各自排查。

想绕开的东西,从另一个门走进来了。


对了,这种"SQL 调 Lambda"的模式,AWS 官方有一篇博客专门讲过,标题叫 From SQL to Microservices: Integrating AWS Lambda with Relational Databases。他们用的词是"微服务架构",听起来比"丧心病狂"体面一点。

但本质是一样的。