3. 医疗湖仓存储底座选型与设计

医疗湖仓的存储底座并不是“买一堆磁盘”这么简单。它要同时承载三类差异极大的负载:一是 HIS、LIS、PACS 等业务系统持续写入的增量数据;二是影像、病理切片、心电波形等大对象;三是科研、质控、医保结算等场景下的批量扫描与回溯分析。因此,存储底座通常采用“对象存储 + 开放表格式 + 元数据服务”的三层结构:对象存储负责廉价、可扩展的字节持久化,开放表格式负责表级事务、快照与模式演进,元数据服务负责目录、权限与血缘。理解这三层的分工,是后续所有分区、分桶、索引与治理决策的前提。

对象存储:医疗数据的持久化基座

对象存储以桶(Bucket)和对象键(Key)组织数据,天然适合医疗场景中海量小文件与超大文件并存的形态。典型选型包括 S3、OSS、COS、MinIO 等,它们都提供兼容 S3 的 API。医疗数据上云时,建议按“数据域—业务系统—时间”设计对象键前缀,例如 raw/his/encounter/dt=2025-03-01/、raw/pacs/study/2025/03/01/。这样既便于生命周期策略按前缀生效,也便于跨系统对账。对象存储本身不提供表级 ACID,也不擅长高频 rename 与 list,因此必须由开放表格式在其上构建“表”的语义。

Iceberg、Hudi、Delta Lake 对比

三者都是开放表格式,但设计重心不同。Iceberg 以“快照 + 清单文件 + 元数据树”为核心,擅长隐藏分区、模式演进和时间旅行,适合读多写少、批量回溯的科研与质控场景。Hudi 以“时间线 + 文件组 + 增量日志”为核心,提供 Copy-on-Write 与 Merge-on-Read 两种表类型,擅长近实时 upsert 与增量拉取,适合患者主索引、检验结果等需要频繁更新的数据。Delta Lake 以“事务日志 _delta_log + Parquet”为核心,与 Spark 生态结合紧密,ACID 与流批一体体验好,适合以 Spark 为主的团队快速落地。选型时不要只看功能列表,而要看写入模式:如果每天有大量按主键更新的记录,Hudi 的 MOR 或 Iceberg 的 merge-on-read 更合适;如果以追加和批量重写为主,Iceberg 与 Delta 都足够。

医疗湖仓存储底座:对象存储 + 开放表格式 + 元数据服务 对象存储层(S3 / OSS / COS / MinIO) raw/his/encounter dt=2025-03-01/ raw/pacs/study 2025/03/01/ raw/lis/result dt=2025-03-01/ curated/quality/indicator dt=2025-03-01/ 开放表格式层(事务、快照、模式演进) Iceberg 快照 + 清单文件 + 元数据树 隐藏分区 / 时间旅行 / 模式演进 适合:批量回溯、科研质控 Hudi 时间线 + 文件组 + 增量日志 COW / MOR / 增量拉取 适合:患者主索引、结果更新 Delta Lake _delta_log + Parquet ACID / 流批一体 / Spark 生态 适合:Spark 为主的团队 元数据服务层(目录 / 权限 / 血缘 / 审计) Hive Metastore / REST Catalog Ranger / IAM 权限 Atlas 血缘 审计日志 治理动作:分区裁剪 · 分桶并行 · 索引加速 · 小文件合并 · 生命周期分层 热数据 SSD/标准存储 → 温数据 低频 → 冷数据 归档/ Glacier,按 dt 前缀自动沉降

医疗数据分区与分桶

分区决定“扫描时能裁掉多少数据”,分桶决定“并行度与 join 效率”。医疗数据最常用的分区键是时间(dt)与业务域(domain),例如 dt=2025-03-01/domain=lis。但要注意分区粒度:按天分区在门诊量大的三甲医院可能产生数万个小文件,按小时又会让元数据膨胀。经验做法是:原始层按天分区,中间层按天 + 业务系统分区,汇总层按天 + 指标域分区。分桶则用于高频 join 的键,例如 patient_id 或 encounter_id,桶数取 2 的幂(如 64、128、256),使同一患者的数据落在同一桶,减少 shuffle。Iceberg 支持隐藏分区,可以在不改变查询 SQL 的情况下按 days(ts) 自动分区;Hudi 则通过 hoodie.datasource.write.partitionpath.field 指定分区字段。

索引、小文件治理与生命周期

对象存储上的表没有传统 B+ 树索引,因此索引主要依赖三类机制:一是分区裁剪与列统计(min/max、null count),Iceberg 与 Delta 都会在清单/日志中记录;二是布隆过滤器,适合 patient_id 这类高基数等值查询;三是 Z-Order 或 Hilbert 曲线排序,把多维相关数据聚簇,提升范围查询命中率。小文件治理是医疗湖仓最容易被忽视的运维项:PACS 每天可能产生数万个几 KB 到几 MB 的 DICOM 文件,若直接入湖,NameNode 或对象存储 list 都会成为瓶颈。建议在写入端做“微批合并”,把同一 study 的多个序列合并为较大 Parquet 文件;在表格式侧定期执行 rewrite_data_files(Iceberg)或 compaction(Hudi),目标文件大小控制在 128~256 MB。生命周期管理则按“热—温—冷”分层:近 30 天数据保留在标准存储并开启高频缓存,30~365 天转低频,超过 365 天转归档;同时利用 Iceberg 的 expire_snapshots 与 Hudi 的 clean 清理过期快照,避免元数据无限增长。

选型建议:以 Spark 为主、更新频繁选 Hudi MOR;以 Flink + 批量回溯为主选 Iceberg;已有 Databricks 或强 Spark 依赖选 Delta Lake。三者都支持对象存储,差异主要在写入模式与生态绑定。

一个可参考的建表示例

-- Iceberg 建表:按天分区,按 patient_id 分桶
CREATE TABLE lake.lis_result (
  patient_id   BIGINT,
  encounter_id STRING,
  test_code    STRING,
  result_value DECIMAL(10,2),
  result_time  TIMESTAMP,
  dt           DATE
)
USING iceberg
PARTITIONED BY (days(result_time), bucket(64, patient_id))
TBLPROPERTIES (
  'write.target-file-size-bytes' = '268435456',
  'write.distribution-mode' = 'hash'
);

-- 小文件合并(示例)
CALL lake.system.rewrite_data_files(
  table => 'lake.lis_result',
  options => map('target-file-size-bytes','268435456')
);

执行后可通过 SELECT count(*) FROM lake.lis_result.files 观察文件数量变化,并结合 snapshots 元数据表确认快照是否按预期过期。判断治理是否达标,可以看三个指标:单分区文件数是否稳定在合理区间、平均文件大小是否接近目标值、查询扫描字节数是否随分区裁剪明显下降。若文件数持续增长而查询变慢,说明合并策略或分区粒度需要调整。

医疗数据涉及患者隐私,分区与分桶设计必须与脱敏、加密、访问审计同步考虑。例如 patient_id 分桶后仍可能通过 join 反推身份,需在元数据层限制敏感列的可见范围,并对归档数据启用服务端加密与密钥轮换。

综上,医疗湖仓存储底座的选型与设计,核心是把对象存储的弹性、开放表格式的事务能力与医疗数据的访问模式对齐。分区解决“扫得少”,分桶解决“算得快”,索引解决“找得准”,小文件治理解决“跑得稳”,生命周期解决“存得起”。这五件事共同决定了湖仓在真实医疗负载下能否长期稳定运行。