Lakehouse migration: 2PB without downtime
Moving two petabytes of data while keeping production pipelines running is an exercise in controlled paranoia. A hard cutover is a gamble; a dual-write migration is a proof. The strategy: write to both the old warehouse and the new Delta tables simultaneously, run a reconciliation job to confirm row counts and checksums match, then flip the read path one consumer at a time.
The migration took six weeks end-to-end. The hardest part wasn’t the data movement — it was convincing every downstream team to test against the new tables before the cutover date. Stakeholder alignment, it turns out, is the real bottleneck in any large-scale migration. The technical pattern is repeatable; this post walks through both.
Why big-bang migrations fail
A big-bang migration looks simple on paper: snapshot the old warehouse, load it into Delta, update the connection string. In practice, three things kill it:
- Schema drift at the edges. Legacy warehouses accumulate undocumented columns, silent type coercions, and NULL semantics that differ by table. You find them when a downstream dashboard breaks at 3 AM on day one.
- No rollback path. Once you’ve pointed 40 ETL jobs at the new system and something is wrong, reverting means re-pointing all 40. Under pressure, humans make mistakes.
- The “it looked fine in staging” problem. Production data has edge cases that staging never sees: 14-character country codes that were supposed to be 2, floats stored as VARCHAR, event timestamps from 1970 because a timezone conversion went wrong in 2019.
The 2PB environment had all three. The legacy Hive-on-HDFS warehouse had been running for seven years. There were 4,200 tables. Roughly 340 of them had at least one column with a type that didn’t match the documented schema. A big-bang approach would have required a complete audit before a single byte moved. That audit would have taken longer than the migration itself.
Phase 1: Dual-write
The dual-write layer sat inside the existing Spark ingestion jobs. Each job was modified to accept a target parameter: legacy, delta, or both. Rollout was incremental — we enabled both for 50 tables in week one, confirmed the reconciliation job agreed on all 50, then expanded to 500, then to the full 4,200.
The key implementation detail: Delta writes happened in a separate transaction from the legacy write. If the Delta write failed, the job logged the error, incremented a counter, and continued — the legacy system remained the source of truth throughout Phase 1. Delta failures were noise, not incidents.
For the initial backfill, we used DEEP CLONE in Databricks SQL to copy each table’s history as well as its current state. This preserved _change_data_feed compatibility for downstream consumers that needed CDC.
-- Backfill with history preserved
CREATE TABLE delta_catalog.prod.events_v2
DEEP CLONE hive_metastore.prod.events
LOCATION 's3://datalake-prod/delta/events_v2';
Phase 2: Reconcile (trust but verify)
The reconciliation job ran nightly on every dual-write table. It compared three things: row count, NULL count per column, and a checksum over the 10 most-recently-written partitions.
def reconcile_table(spark, legacy_table: str, delta_table: str, partition_col: str) -> dict:
legacy = spark.table(legacy_table)
delta = spark.table(delta_table)
# Compare row counts on last 10 partition values
partitions = (
legacy.select(partition_col)
.distinct()
.orderBy(f.col(partition_col).desc())
.limit(10)
.rdd.flatMap(lambda x: x)
.collect()
)
results = []
for p in partitions:
l_count = legacy.filter(f.col(partition_col) == p).count()
d_count = delta.filter(f.col(partition_col) == p).count()
l_crc = legacy.filter(f.col(partition_col) == p).selectExpr("crc32(concat_ws('|', *)) as crc").agg(f.sum("crc")).collect()[0][0]
d_crc = delta.filter(f.col(partition_col) == p).selectExpr("crc32(concat_ws('|', *)) as crc").agg(f.sum("crc")).collect()[0][0]
results.append({
"partition": p,
"row_match": l_count == d_count,
"crc_match": l_crc == d_crc,
"legacy_rows": l_count,
"delta_rows": d_count,
})
return results
Any partition that disagreed triggered a Slack alert and added the table to a reconcile_failures tracking table. We maintained a dashboard showing per-table convergence over time. By the end of week three, 98.7% of tables had zero reconciliation failures over a 7-day window — the threshold we had defined for Phase 3 eligibility.
The remaining 1.3% (about 55 tables) had structural issues: duplicates from legacy compaction jobs that never ran cleanly, timezone-coerced timestamps, and one table where a upstream producer was writing non-deterministically. Each was fixed individually before being promoted to Phase 3.
Phase 3: Cutover and rollback
Cutover was not a single event. Each team that owned a downstream consumer was given a two-week window to switch their read path from hive_metastore.prod.* to delta_catalog.prod.*. During that window, both catalogs served identical data. The legacy writes continued until the last consumer confirmed they had switched.
The rollback plan was explicit and tested before we started the cutover window:
- A feature flag in the ingestion jobs switched any table from
bothback tolegacyin under 30 seconds — no code deploy required. - The reconciliation job would run on demand (not just nightly) during the cutover window.
- Any team could request a rollback for their tables with a single Slack message to the data platform channel; the on-call engineer would toggle the flag within 15 minutes.
No team triggered a rollback. Fourteen teams completed their switches in the first week of the cutover window; the remaining eleven finished by day 10. On day 14, we disabled dual-write for all tables and decommissioned the legacy HDFS cluster.
Takeaways
Six weeks, 4,200 tables, 2PB — zero production incidents during the migration. The concrete results after 30 days on Delta:
- Compute cost down 38%. Delta’s Z-ordering and liquid clustering eliminated the full-table scans that the legacy Hive jobs ran on every query.
- P95 query latency cut in half. The slowest analytical queries dropped from an average of 14 minutes to 6.8 minutes. Dashboards that used to time out now return in under 90 seconds.
- Data compaction is automatic. The Hive cluster needed a weekly manual
OPTIMIZEscript that took 11 hours. Delta Auto Optimize runs incrementally and adds zero operational overhead.
The pattern generalizes: dual-write is not specific to Delta or Databricks. The same approach works for any migration where you can’t afford downtime and can’t trust a big-bang cutover. The reconciliation job is the insurance policy. The feature flag is the escape hatch. Together, they mean you can move at the speed of trust — table by table, team by team — instead of the speed of a scheduled maintenance window.
Di chuyển hai petabyte dữ liệu trong khi vẫn giữ pipeline production hoạt động là một bài tập trong sự kiểm soát cẩn thận. Cutover cứng là canh bạc; migration dual-write là bằng chứng. Chiến lược: ghi đồng thời vào kho dữ liệu cũ và bảng Delta mới, chạy job đối chiếu để xác nhận số hàng và checksum khớp nhau, rồi chuyển đổi đường đọc từng consumer một.
Toàn bộ quá trình migration mất sáu tuần. Phần khó nhất không phải là di chuyển dữ liệu — mà là thuyết phục từng nhóm downstream kiểm thử với bảng mới trước ngày cutover. Alignment với stakeholder, hoá ra, mới là nút thắt thực sự trong bất kỳ migration quy mô lớn nào. Mẫu kỹ thuật có thể lặp lại; bài viết này đi qua cả hai.
Tại sao migration “big-bang” thất bại
Migration big-bang trông đơn giản trên giấy: snapshot kho cũ, load vào Delta, cập nhật connection string. Trong thực tế, ba vấn đề giết chết nó:
- Schema drift ở các ngoại vi. Kho dữ liệu legacy tích luỹ các cột không có tài liệu, type coercion ngầm, và ngữ nghĩa NULL khác nhau theo từng bảng. Bạn phát hiện chúng khi một dashboard downstream sập lúc 3 giờ sáng ngày đầu tiên.
- Không có đường rollback. Một khi đã trỏ 40 ETL job vào hệ thống mới và có sự cố, việc revert nghĩa là trỏ lại tất cả 40 job. Dưới áp lực, con người mắc sai lầm.
- Vấn đề “trông ổn trong staging”. Dữ liệu production có các edge case mà staging không bao giờ thấy: mã quốc gia 14 ký tự vốn phải là 2, float lưu dưới dạng VARCHAR, timestamp sự kiện từ năm 1970 vì một lỗi timezone conversion năm 2019.
Môi trường 2PB có cả ba. Kho Hive-on-HDFS legacy đã chạy được bảy năm. Có 4.200 bảng. Khoảng 340 bảng có ít nhất một cột với kiểu dữ liệu không khớp với schema tài liệu. Phương pháp big-bang sẽ cần một cuộc audit hoàn chỉnh trước khi di chuyển một byte nào. Cuộc audit đó sẽ mất thời gian lâu hơn bản thân migration.
Giai đoạn 1: Dual-write
Lớp dual-write nằm bên trong các Spark ingestion job hiện có. Mỗi job được chỉnh sửa để nhận một tham số target: legacy, delta, hoặc both. Triển khai theo từng bước — tuần một bật both cho 50 bảng, xác nhận job đối chiếu đồng ý trên cả 50, rồi mở rộng lên 500, rồi toàn bộ 4.200.
Chi tiết quan trọng trong triển khai: Delta write xảy ra trong một transaction riêng biệt với legacy write. Nếu Delta write thất bại, job ghi lại lỗi, tăng counter, và tiếp tục — hệ thống legacy vẫn là nguồn chân lý xuyên suốt Giai đoạn 1. Lỗi Delta là nhiễu, không phải sự cố.
Để backfill ban đầu, chúng tôi dùng DEEP CLONE trong Databricks SQL để sao chép lịch sử của mỗi bảng cùng với trạng thái hiện tại. Điều này giữ nguyên khả năng tương thích _change_data_feed cho các consumer downstream cần CDC.
-- Backfill với lịch sử được bảo toàn
CREATE TABLE delta_catalog.prod.events_v2
DEEP CLONE hive_metastore.prod.events
LOCATION 's3://datalake-prod/delta/events_v2';
Giai đoạn 2: Đối chiếu (tin tưởng nhưng phải kiểm chứng)
Job đối chiếu chạy hàng đêm trên mỗi bảng dual-write. Nó so sánh ba thứ: số hàng, số NULL theo cột, và checksum trên 10 partition được ghi gần nhất.
def reconcile_table(spark, legacy_table: str, delta_table: str, partition_col: str) -> dict:
legacy = spark.table(legacy_table)
delta = spark.table(delta_table)
# Compare row counts on last 10 partition values
partitions = (
legacy.select(partition_col)
.distinct()
.orderBy(f.col(partition_col).desc())
.limit(10)
.rdd.flatMap(lambda x: x)
.collect()
)
results = []
for p in partitions:
l_count = legacy.filter(f.col(partition_col) == p).count()
d_count = delta.filter(f.col(partition_col) == p).count()
l_crc = legacy.filter(f.col(partition_col) == p).selectExpr("crc32(concat_ws('|', *)) as crc").agg(f.sum("crc")).collect()[0][0]
d_crc = delta.filter(f.col(partition_col) == p).selectExpr("crc32(concat_ws('|', *)) as crc").agg(f.sum("crc")).collect()[0][0]
results.append({
"partition": p,
"row_match": l_count == d_count,
"crc_match": l_crc == d_crc,
"legacy_rows": l_count,
"delta_rows": d_count,
})
return results
Bất kỳ partition nào không khớp sẽ kích hoạt cảnh báo Slack và thêm bảng đó vào bảng tracking reconcile_failures. Chúng tôi duy trì một dashboard hiển thị mức độ hội tụ theo từng bảng theo thời gian. Đến cuối tuần thứ ba, 98,7% bảng có zero lỗi đối chiếu trong cửa sổ 7 ngày — ngưỡng chúng tôi đã định nghĩa để đủ điều kiện vào Giai đoạn 3.
1,3% còn lại (khoảng 55 bảng) có vấn đề cấu trúc: duplicate từ các job compaction legacy chưa bao giờ chạy sạch, timestamp bị coerce timezone, và một bảng nơi upstream producer ghi không xác định. Từng cái được sửa riêng trước khi được thăng cấp lên Giai đoạn 3.
Giai đoạn 3: Cutover và rollback
Cutover không phải là một sự kiện duy nhất. Mỗi nhóm sở hữu consumer downstream được cấp một cửa sổ hai tuần để chuyển đường đọc từ hive_metastore.prod.* sang delta_catalog.prod.*. Trong cửa sổ đó, cả hai catalog phục vụ dữ liệu giống hệt nhau. Legacy write tiếp tục cho đến khi consumer cuối cùng xác nhận đã chuyển xong.
Kế hoạch rollback được định rõ ràng và kiểm thử trước khi chúng tôi bắt đầu cửa sổ cutover:
- Một feature flag trong các ingestion job chuyển bất kỳ bảng nào từ
bothtrở lạilegacytrong dưới 30 giây — không cần deploy code. - Job đối chiếu sẽ chạy theo yêu cầu (không chỉ hàng đêm) trong cửa sổ cutover.
- Bất kỳ nhóm nào có thể yêu cầu rollback cho các bảng của họ bằng một tin nhắn Slack duy nhất vào kênh data platform; kỹ sư on-call sẽ bật flag trong vòng 15 phút.
Không nhóm nào kích hoạt rollback. Mười bốn nhóm hoàn thành chuyển đổi trong tuần đầu tiên của cửa sổ cutover; mười một nhóm còn lại hoàn thành vào ngày 10. Ngày 14, chúng tôi tắt dual-write cho tất cả các bảng và ngừng vận hành cụm HDFS legacy.
Kết luận
Sáu tuần, 4.200 bảng, 2PB — không có sự cố production nào trong quá trình migration. Kết quả cụ thể sau 30 ngày trên Delta:
- Chi phí compute giảm 38%. Z-ordering và liquid clustering của Delta loại bỏ các full-table scan mà các Hive job legacy chạy trên mỗi truy vấn.
- P95 query latency giảm một nửa. Các truy vấn phân tích chậm nhất giảm từ trung bình 14 phút xuống còn 6,8 phút. Các dashboard trước đây timeout nay trả về trong dưới 90 giây.
- Compaction dữ liệu tự động. Cụm Hive cần một script
OPTIMIZEthủ công hàng tuần mất 11 giờ. Delta Auto Optimize chạy theo từng bước và không tốn thêm chi phí vận hành.
Mẫu này có thể tổng quát hoá: dual-write không đặc thù cho Delta hay Databricks. Cách tiếp cận tương tự hoạt động cho bất kỳ migration nào bạn không thể cho phép downtime và không thể tin tưởng vào big-bang cutover. Job đối chiếu là chính sách bảo hiểm. Feature flag là lối thoát hiểm. Cùng nhau, chúng có nghĩa là bạn có thể di chuyển theo tốc độ của sự tin tưởng — từng bảng, từng nhóm — thay vì tốc độ của một cửa sổ bảo trì theo lịch.