一、踩坑现场:某零售公司的突发性能灾难

上周帮朋友排查他们公司的一个棘手问题:他们用Snowflake做数据仓库,平时跑的一个日活统计任务,原本15分钟能跑完,突然某一天卡了整整4个小时,中途还报了超时错误。这个任务是给运营看的核心报表,晚出结果直接影响当天的促销策略调整,急得运营团队团团转。

先给没接触过Snowflake的朋友补个基础:Snowflake是个云原生的数据仓库,不用自己搭服务器、调参数,按用的资源收费,特别适合存大量业务数据跑报表。朋友他们的任务是查“过去30天各区域的日活用户数”,数据存在Snowflake的外部表——简单说就是数据没存在Snowflake自己的存储空间里,而是存在AWS S3桶里(相当于云盘),Snowflake只是把S3的文件映射成能查的表,省了存两份数据的麻烦。

一开始大家猜是S3卡了、网络慢,或者查询语句写得烂,但查了一圈都没问题:S3的访问速度正常,查询语句是标准的SQL,之前跑了半年都好好的,为啥突然慢了?

二、问题拆解:先搞懂外部表的两个核心逻辑

要找到原因,得先搞清楚Snowflake外部表的两个关键规则,不然根本摸不着头脑。

2.1 外部表的“偷懒小技巧”:元数据缓存

Snowflake查外部表的时候,不会每次都去S3里翻所有文件的详情——比如这个文件多大、最后改了啥时候、里面有啥内容。它会把这些信息(叫“元数据”)存在自己的缓存里,下次再查直接用缓存的元数据,不用再跑S3,这样能省时间。这个缓存的有效期是多久呢?默认是1小时,也就是说1小时内再查同一个外部表,只要元数据没变化,就用缓存的旧信息。

举个例子,比如你S3里有个叫2024-05-01的文件夹,里面存的是5月1号的日活数据,Snowflake第一次查的时候,会把“这个文件夹里有10个Parquet文件、总大小10G、最后修改时间是2024-05-01 23:59:59”这些信息存在缓存里。接下来1小时内再查,直接用这个缓存,不用再去S3核对。

2.2 外部表的“强制刷新”:同步操作

如果S3里的文件变了(比如新增了2024-05-02的文件夹、或者改了某个旧文件),Snowflake得知道这个变化,不然查出来的结果会错。所以Snowflake有个操作叫“同步外部表”,相当于主动去S3里扫一遍,更新缓存里的元数据。朋友他们之前的操作是:每天凌晨3点跑完ETL(把业务数据转成Parquet文件存在S3),然后马上跑一次同步操作,之后的查询都用这个同步后的元数据,一直没问题。

三、根因定位:缓存和文件格式的冲突

问题出在朋友他们的一个操作变化上:他们前一天改了ETL的配置,把原来的Parquet文件的“分区规则”改了。原来的分区是按“日期”分,比如s3://my-bucket/date=2024-05-01/,改完之后加了“区域”,变成s3://my-bucket/date=2024-05-01/region=华北/。而且他们改完之后,没有马上同步外部表,而是等了40分钟,等缓存自动过期才同步。

这就出大问题了:

  1. 改完ETL后,S3里的分区结构变了,但缓存还是旧的——旧缓存里只有“date”这一个分区,没有“region”分区。
  2. 朋友他们的查询语句里加了“按区域统计”的条件,也就是会用到“region”这个新分区。
  3. 当查询执行时,Snowflake先去缓存里找元数据,发现缓存里没有“region”分区的信息,怎么办?它不会直接去S3扫,而是会触发一次“强制同步”——也就是主动去S3里扫一遍所有文件的元数据。

重点来了:这次同步为什么慢?因为S3里有多少文件?朋友他们存了2年的日活数据,每天10个Parquet文件,总共7000多个文件。原来的同步是怎么同步的?原来的缓存里有“date”分区,同步的时候只需要扫“date”分区下的新文件,比如当天新增的2024-05-02文件夹,扫10个文件就完了。但这次缓存里没有“region”分区,同步的时候得扫S3里所有7000多个文件,一个一个核对元数据,这就花了快4个小时!

四、验证过程:用实验复现问题

为了确认根因,我们做了个小实验,实验用的技术栈是:Snowflake(外部表)、AWS S3、Parquet文件。

4.1 实验准备

首先创建一个S3桶,然后准备两个版本的Parquet文件:

  • 旧版本:按“date”分区,比如s3://test-bucket/date=2024-05-01/part-00000.parquet
  • 新版本:按“date”和“region”分区,比如s3://test-bucket/date=2024-05-01/region=华北/part-00000.parquet

然后在Snowflake里创建外部表,映射到S3桶:

-- 创建外部表,映射S3的路径,分区列是date
CREATE EXTERNAL TABLE ext_user_daily (
    user_id INT,
    event_time TIMESTAMP,
    region STRING
)
PARTITION BY (date DATE)
LOCATION 's3://test-bucket/'
FILE_FORMAT = (TYPE = PARQUET);

4.2 实验步骤

  1. 先上传旧版本的Parquet文件,然后同步外部表,把元数据缓存更新为旧版本:
-- 同步外部表,更新缓存
ALTER EXTERNAL TABLE ext_user_daily REFRESH;
  1. 等5分钟(缓存还没过期),上传新版本的Parquet文件,覆盖旧版本的分区。
  2. 跑一个带“region”条件的查询:
-- 按日期和区域统计日活
SELECT date, region, COUNT(DISTINCT user_id) AS dau
FROM ext_user_daily
WHERE date >= '2024-05-01'
GROUP BY date, region;

4.3 实验结果

查询执行了快10分钟,查看Snowflake的查询历史,发现这次查询的“REFRESH EXTERNAL TABLE”步骤占了99%的时间,原因是缓存里没有“region”分区,需要扫所有S3文件。

五、解决方案:避开冲突的3个方法

找到了问题,就有对应的解决方法,这里给大家总结3个实用的方案,覆盖不同的场景。

5.1 方案一:改完文件格式马上同步

这是最直接的方案,适合所有场景。只要你改了S3里的文件格式(比如分区规则、文件结构),马上跑一次同步操作,不要等缓存自动过期。这样缓存会马上更新,下次查询的时候就会用新的元数据,不会触发全量同步。

比如朋友他们改完ETL之后,应该马上跑:

ALTER EXTERNAL TABLE ext_user_daily REFRESH;

这样同步的时候,因为缓存里的旧元数据会被新的覆盖,下次查询就不会触发全量同步了。

5.2 方案二:调整缓存的有效期

如果你没办法马上同步(比如改ETL的操作和同步操作是分开的,中间有时间差),可以调整元数据缓存的有效期,把它改短一点,比如改成10分钟,这样缓存过期的时间会提前,同步的时间差就会缩短,减少触发全量同步的风险。

调整缓存有效期的方法是在创建外部表的时候加参数:

CREATE EXTERNAL TABLE ext_user_daily (
    user_id INT,
    event_time TIMESTAMP,
    region STRING
)
PARTITION BY (date DATE)
LOCATION 's3://test-bucket/'
FILE_FORMAT = (TYPE = PARQUET)
AUTO_REFRESH = TRUE  -- 开启自动刷新
AUTO_REFRESH_INTERVAL = 600;  -- 缓存有效期600秒(10分钟)

5.3 方案三:查询前先强制同步

如果你不确定缓存是不是最新的,可以在跑查询之前,先主动跑一次同步操作,这样查询的时候就会用最新的元数据,不会触发全量同步。这个方案适合查询频率不高的场景,比如每天只跑一次的报表。

比如每天凌晨3点跑报表之前,先跑:

-- 先同步外部表,确保元数据是最新的
ALTER EXTERNAL TABLE ext_user_daily REFRESH;
-- 再跑查询
SELECT date, region, COUNT(DISTINCT user_id) AS dau
FROM ext_user_daily
WHERE date >= '2024-05-01'
GROUP BY date, region;

六、应用场景、优缺点、注意事项

6.1 应用场景

这个问题主要出现在以下场景:

  • 用Snowflake外部表存大量历史数据的场景,比如零售、互联网的日活统计、订单分析等。
  • 经常修改S3里的文件格式(比如分区规则、文件结构)的场景,比如ETL配置调整、数据结构优化等。
  • 对查询性能要求高的场景,比如实时报表、促销策略调整等。

6.2 技术优缺点

元数据缓存的优缺点

  • 优点:减少S3的访问次数,提升查询性能,降低S3的费用(S3按访问次数收费)。
  • 缺点:如果缓存和实际文件不一致,会导致查询结果错误,或者触发全量同步,导致性能骤降。

外部表的优缺点

  • 优点:不用把数据存在Snowflake自己的存储空间里,省了存储费用,适合存大量历史数据。
  • 缺点:查询性能依赖S3的访问速度和元数据缓存,容易出现缓存和文件格式冲突的问题。

6.3 注意事项

  • 改文件格式后必须马上同步:只要你改了S3里的文件格式(比如分区规则、文件结构),一定要马上跑一次同步操作,不要等缓存自动过期。
  • 定期检查缓存的一致性:可以定期跑一次同步操作,确保缓存和实际文件是一致的。
  • 避免频繁修改文件格式:尽量减少修改S3里的文件格式的次数,比如把分区规则定好之后就不要随便改。
  • 查看查询历史:如果查询性能骤降,可以查看Snowflake的查询历史,看看是不是触发了全量同步。

七、总结

这次的性能问题,本质上是Snowflake的元数据缓存和S3里的文件格式变化没有同步导致的。很多人用Snowflake外部表的时候,只知道用缓存提升性能,却忽略了缓存和实际文件不一致的风险。只要记住:改文件格式马上同步,定期检查缓存一致性,就能避开这个坑。

最后再给大家提个醒:云原生工具虽然好用,但也有自己的逻辑,不能想当然地用,一定要搞清楚背后的原理,不然遇到问题就会手足无措。