
本文详解在 aws lambda 等无服务器环境中,如何避免 polars 每次调用重复建立 s3 连接,并对比原生 boto3 下载 + polars 本地解析的性能优势,提供可落地的连接池替代方案。
Polars 自 0.20 版本起通过 object_store 库统一管理云存储 I/O,支持直接读取 s3:// 路径的 Parquet 文件(如 pl.read_parquet("s3://bucket/data.parquet"))。但需明确:当前 Polars(截至 v0.21)不支持传入已初始化的 boto3.client 实例进行连接复用。其 storage_options 参数仅接受配置字典(如 {"aws_region": "us-east-1", "aws_access_key_id": "...", ...}),用于每次 I/O 时新建 object_store::aws::Client——这意味着在 Lambda 中,若将 read_parquet 直接写在 handler 内,每次调用(包括温启动)都会触发新的 HTTP 客户端初始化与连接建立,无法复用底层 TCP 连接或凭证缓存。
虽然 Polars 团队已在 PR #14598 中优化了 object_store 的内部客户端缓存逻辑(例如复用 reqwest::Client 实例),但该缓存作用于库内部、不可由用户控制,且实测仍无法匹敌原生 boto3 的连接复用效率。根据真实 Lambda 场景压测(见 issue #14572 讨论),直接使用全局 boto3.client("s3") 下载对象至内存/临时磁盘,再交由 Polars 解析,平均快 2–5 倍,尤其在小文件高频读取场景下优势显著。
因此,推荐采用以下经过验证的生产级模式:
import boto3
import polars as pl
from io import BytesIO
# ✅ 全局初始化:Lambda 冷启动时创建一次,后续所有 invocations 复用
s3_client = boto3.client("s3", region_name="us-east-1")
def handler(event, context):
bucket = "my-bucket"
key = "data/part-00000.parquet"
# 步骤1:复用 s3_client 下载(自动复用连接池、凭证缓存)
response = s3_client.get_object(Bucket=bucket, Key=key)
parquet_bytes = response["Body"].read()
# 步骤2:Polars 从内存解析(零磁盘 IO)
df = pl.read_parquet(BytesIO(parquet_bytes))
return df.shape[0]⚠️ 注意事项: 若 Parquet 文件较大(>100MB),建议改用 tempfile.NamedTemporaryFile 写入本地 /tmp 后再 pl.read_parquet(),避免内存压力; 确保 Lambda 执行角色拥有对应 S3 s3:GetObject 权限; boto3.client 默认启用连接池(max_pool_connections=10),无需额外配置; 避免在 handler 内重复创建 boto3.client 或 pl.Config,否则失去复用意义。
总结而言,Polars 当前的 S3 集成更适合开发调试或低频批量场景;在 Lambda、Fargate 等对冷启动和吞吐敏感的环境,“boto3 下载 + Polars 本地解析”仍是更可控、更高效的选择。未来若 Polars 开放 client 参数或支持 reqwest::Client 注入,可平滑迁移。在此之前,拥抱成熟稳定的 boto3 生态,是兼顾性能与可靠性的务实之选。











