Data Platform: Những lần vỡ mộng cần thiết
Một vài lần vỡ mộng khi đi từ tool, integration, automation tới semantics, ownership và trust trong data platform.
Mở đầu
Vài hôm trước, tớ thấy một bạn newbie hỏi về việc tự dựng một Apache Spark cluster on-premise. Bạn ấy tin rằng Spark rất mạnh, on-premise thì rẻ và chủ động hơn cloud, rồi bắt đầu hỏi tiếp về bộ ba rất quen thuộc trong data platform: metadata, data lineage và security.
Đọc câu hỏi đó, tớ có một cảm giác khá lẫn lộn.
Một mặt, tớ thấy khá khâm phục. Bạn còn rất trẻ mà đã tự mày mò được nhiều thứ, và cũng thấy rõ thời buổi này AI có thể đẩy nhanh tốc độ học tập của người ta ra sao. Nhưng mặt khác, tớ cũng thấy hơi buồn cười — không phải kiểu chê, mà là vì nó giống mình vài năm trước quá.
Trước khi đi vào những lần “mở mắt”, tớ nghĩ nên nói qua một chút về background của mình.
Tớ tốt nghiệp đúng ngành Computer Science ở Bách khoa Hà Nội. Điểm số cũng chẳng cao lắm, đâu đó 3.0x. Trong này chắc cũng có kha khá bạn học của tớ, và thành tích của các bạn xuất sắc hơn tớ rất rất nhiều.
Sau khi ra trường, tớ đi làm tại một công ty xây dựng nền tảng Cloud/PaaS, trong đó có Data Platform. Mấy thứ này profile tớ cũng ghi rõ. Cũng chả có gì quá tự hào, vì sản phẩm cũng chả bán được mấy — giờ thì tớ không rõ lắm. Nhưng nhìn lại, tớ tin đây là nền tảng cho lần mở mắt đầu tiên của mình.
Lần mở mắt đầu tiên: khoảng cách giữa biết và hiểu
Ngày mới vào nghề cũng là lúc team bắt đầu những bước đầu tiên trong quá trình phát triển sản phẩm. Tớ học được rất nhiều.
Tớ được định hình rằng Data Platform là gì, Data Warehouse và LakeHouse trông như thế nào, thường gồm những thành phần nào. Tớ bắt đầu nghiên cứu các open-source components cho từng lớp cụ thể như:
- processing;
- orchestration;
- metadata;
- storage;
- query engine;
- ingestion;
- monitoring.
Lúc đó, tớ nghĩ Data Platform đơn giản là tập hợp của các công cụ. Gặp một bài toán cụ thể ở lớp nào thì mình chỉ cần kiếm một open-source phù hợp về dựng lên là xong.
Tớ tìm hiểu architecture của nhiều công cụ như Spark, Airflow, Kafka, OpenMetadata, Iceberg, Trino,... Tớ học cách triển khai chúng trên VMs, Kubernetes, rồi dùng GitOps với ArgoCD để rollout hàng loạt. Nói chung là lúc đó nghĩ mình cũng tay to lắm: stack nào cũng biết, architecture nào cũng đọc qua, config nào cũng từng đụng.
Cho tới một ngày, tớ đi phỏng vấn ở một công ty khác.
Trong CV thời điểm đó, tớ ghi khá oách: đầy đủ stacks, vì mình có tham gia tìm hiểu và dựng chúng lên thật mà. Tớ cũng từng xem từng config có ý nghĩa như thế nào, từng deploy qua, từng debug qua. Nhưng rồi tớ gặp một vài câu hỏi rất cơ bản.
“Anh thấy em có dùng Spark. Thế em có biết Lazy Evaluation là gì không? Shuffle là gì?”
Mình cứng họng luôn.
Rõ ràng lúc đó mình biết kiến trúc Spark có Master, Worker, Driver, Executor. Mình biết các submission mode như client mode, cluster mode. Mình cũng hiểu sơ sơ flow request resource diễn ra như thế nào, và từng dùng Spark API để thực hiện một số tác vụ cơ bản.
Nhưng hỏi sâu xuống execution model thì mình không trả lời được.
Một câu khác:
“Anh thấy em ghi có dùng Iceberg. Vậy em phân biệt được column-based và row-based không? Iceberg là row hay column?”
Câu phân biệt thì may mắn mình có tìm hiểu qua. Nhưng trước đó, với Iceberg, thật ra mình cũng chủ yếu dùng SQL để tạo bảng, dùng Spark hoặc Trino để transform và query data. Còn bản chất table format là gì, metadata layout ra sao, nó giải quyết vấn đề gì so với cách lưu file thông thường — mình chưa thật sự hiểu.
Đó là lần “mở mắt” đầu tiên.
Tớ nhận ra có một khoảng cách rất lớn giữa biết tên công cụ, dựng được công cụ, dùng được công cụ, và hiểu công cụ.
Bốn mức này nhìn bên ngoài có vẻ gần nhau, nhưng thực tế cách nhau rất xa.
Và tớ càng nghĩ rằng ở thời điểm hiện tại, đường cong giữa “biết” và “hiểu” đang trở nên dốc hơn. Bây giờ, bạn có thể prompt ChatGPT hoặc Claude kiểu:
Viết cho tôi một Spark application ingest data từ Kafka vào LakeHouse.Hoặc:
Viết một file Docker Compose để dựng Airflow.
Viết manifest để triển khai Trino cluster trên Kubernetes.Tớ tin rằng trong vòng không quá 30 phút, bạn có thể có một thứ gì đó chạy được. Nhưng chuyện đó không đồng nghĩa là bạn đã hiểu Spark, Kafka, Airflow, Trino, Kubernetes hay LakeHouse.
AI có thể giúp mình đi nhanh hơn rất nhiều. Nhưng cái hiểu vẫn cần nền tảng. Và nền tảng đó thường không đến sau vài prompt, mà đến từ việc dành hàng tuần, hàng tháng để học, đọc, thử, sai, debug, và tự đặt câu hỏi vì sao hệ thống hoạt động như vậy.
Lần mở mắt tiếp theo: khoảng cách giữa dùng được và vận hành được
Một điều khác mà tớ dần nhận ra là: dựng được một pipeline chạy demo và vận hành được một pipeline trong môi trường thật là hai chuyện rất khác nhau.
Ở giai đoạn đầu, mình thường tập trung vào câu hỏi “làm sao để nó chạy được”. Điều này hoàn toàn hợp lý, vì trước khi nói tới scale, reliability hay correctness, hệ thống ít nhất phải chạy đã. Nhưng khi pipeline bắt đầu xử lý dữ liệu thật, phục vụ người dùng thật, và ảnh hưởng tới báo cáo hoặc hệ thống downstream thật, câu hỏi sẽ đổi khác:
Nó chạy được chưa?không còn đủ nữa. Mình phải hỏi thêm:
Nó có chạy đúng không?
Nó fail thì fail như thế nào?
Nó có tự recover được không?
Nó có làm sai dữ liệu âm thầm không?
Nó có gây áp lực ngược lên source system không?
Ai sẽ biết khi nó bắt đầu lệch khỏi source?Hôm trước, tớ thấy có bạn đăng bài trong nhóm về CDC, cụ thể là realtime pipeline đưa dữ liệu từ PostgreSQL lên ClickHouse. Đọc xong tớ mới nhớ ra mình cũng đã từng đau vài lần với những bài toán tương tự.
Không phải vì demo kiểu đó sai. Ngược lại, demo là cách rất tốt để hiểu flow tổng quan. Nhưng demo thường chỉ trả lời được phần “dùng như thế nào”, còn production mới ép mình trả lời phần “vận hành như thế nào”.
Vì flow demo thường chỉ cho thấy đường đi chính
Một pipeline CDC nhìn ở mức high-level thường khá gọn:
Nhìn vào flow đó, mọi thứ có vẻ khá hợp lý. Với sự hỗ trợ của AI hiện tại, tớ tin rằng bạn có thể dành một ngày, hoặc cùng lắm là một tuần, để dựng ra một pipeline demo trông tương tự như vậy.
Bạn có thể có Docker Compose, PostgreSQL, Kafka, Debezium hoặc một CDC connector tương tự, ClickHouse, một dashboard đơn giản, và cảm giác rằng mình đã hiểu realtime data pipeline.
Nhưng demo chạy được khác rất xa với hệ thống vận hành được lâu dài.
Vì phần đau thường nằm sau khi hệ thống bắt đầu chạy thật
Những phần khó hơn thường chỉ xuất hiện khi pipeline phải sống trong môi trường thật.
Ví dụ, khi nói về CDC, người ta thường nhắc nhiều tới các công cụ như Debezium hoặc Oracle GoldenGate, và giải thích rằng chúng đọc WAL logs của PostgreSQL hoặc binlogs của MySQL để detect change. Cách giải thích này đúng, nhưng thường dừng lại ở mức “CDC tool đọc log của database”.
Điều ít được giải thích kỹ hơn là WAL hoặc binlog thực chất là gì, vì sao database cần chúng ngay từ đầu, chúng được ghi theo lifecycle nào, retention/cleanup hoạt động ra sao, replication slot trong PostgreSQL giữ WAL như thế nào, và điều gì xảy ra nếu CDC consumer lag quá lâu.
WAL/binlog không sinh ra chỉ để làm CDC
Ở mức rất đơn giản, các loại log này sinh ra trước hết không phải để làm CDC cho data engineer đọc, mà là một phần trong cơ chế đảm bảo durability/recovery của database.
Một transaction không thể tự nhiên atomic chỉ bằng cách sửa data file trực tiếp. Ví dụ một transaction cần cập nhật dữ liệu ở hai row hoặc hai table khác nhau. Database sẽ phải tìm các page/block chứa dữ liệu đó, load chúng lên memory, rồi sửa từng phần một.
Vậy chuyện gì xảy ra nếu database crash sau khi sửa page đầu tiên, nhưng chưa kịp sửa page thứ hai?
Nếu chỉ sửa trực tiếp vào data page, database rất khó biết trạng thái nào là trạng thái đúng sau khi restart. Một nửa thay đổi đã xảy ra, một nửa chưa xảy ra. Transaction đáng lẽ phải all-or-nothing, nhưng storage-level write lại diễn ra từng bước.
Vì vậy database cần một cơ chế kiểu write-ahead logging: trước khi coi transaction là durable, nó ghi lại ý định/thay đổi vào một log append-only trước. Khi log đã được ghi an toàn, database mới có cơ sở để tiếp tục apply thay đổi vào data pages, hoặc recover lại trạng thái đúng nếu crash giữa chừng.
Với PostgreSQL, WAL là cơ chế trung tâm cho crash recovery và replication. Với MySQL thì câu chuyện chi tiết hơn một chút: InnoDB có redo/undo logs cho recovery ở storage engine, còn binlog thường được dùng nhiều cho replication, point-in-time recovery và CDC. Nhưng từ góc nhìn CDC, điểm quan trọng là các hệ thống này đều tận dụng những dòng log tuần tự mô tả thay đổi đã xảy ra trong database.
Tuy nhiên database cũng không thể giữ các log này mãi mãi. Chúng cần được prune/cleanup để tránh phình storage vô hạn.
Với MySQL, binlog thường được cleanup theo cấu hình retention định kỳ như binlog_expire_logs_seconds hoặc các cơ chế tương tự tuỳ version/config. Nếu CDC pipeline down quá lâu, binlog cũ có thể bị xoá trước khi consumer kịp đọc. Khi đó connector không còn đủ log để resume từ offset cũ nữa.
Vấn đề nguy hiểm là lỗi này không phải lúc nào cũng biểu hiện như một incident rất rõ ràng ở downstream. Pipeline có thể fail một đoạn, restart lại, hoặc được rebuild từ một offset/snapshot không đúng, rồi tiếp tục chạy như thể mọi thứ đã ổn. Nhưng thực tế một phần change events đã mất. Dữ liệu downstream bắt đầu lệch khỏi source, dashboard vẫn có số, job vẫn xanh, nhưng số đó sai từ lúc nào thì không ai biết chắc.
Với PostgreSQL, câu chuyện có thể nguy hiểm hơn nếu CDC dùng replication slot. Replication slot sẽ giữ lại WAL cho tới khi consumer xác nhận đã đọc tới vị trí tương ứng. Điều này giúp CDC consumer có thể resume an toàn hơn sau downtime ngắn, nhưng nếu consumer lag hoặc chết quá lâu, WAL sẽ tiếp tục bị giữ lại.
CDC lag có thể làm sập database nguồn
Đây là đoạn mà demo thường không nói kỹ: với PostgreSQL, một CDC consumer bị down hoặc lag lâu có thể khiến WAL bị giữ lại liên tục vì replication slot chưa advance. WAL cứ phình ra, disk của database nguồn có thể đầy, và cuối cùng chính database nguồn có thể sập.
Nhấn mạnh là database nguồn — không phải chỉ pipeline CDC bị fail.
Có một chi tiết rất dễ bị bỏ qua: Debezium chỉ advance/commit LSN theo phần WAL mà connector đã xử lý cho những table nằm trong cấu hình CDC. Nhưng WAL là log ở cấp database/cluster, không phải chỉ log riêng cho vài table mình quan tâm. Nghĩa là các table khác dù không nằm trong CDC config vẫn có thể phát sinh transaction rất nhiều và vẫn ghi WAL ầm ầm.
Nếu những table được CDC có traffic thấp, trong khi các table khác trong cùng database lại ghi rất nhiều, replication slot vẫn có thể giữ lại một lượng WAL lớn. Connector nhìn từ phía table mình quan tâm có vẻ không bận lắm, nhưng disk của source database thì vẫn bị WAL đẩy lên liên tục.
Tớ từng gặp đúng kiểu này: bảng cần CDC thì traffic thấp, nhưng những bảng khác trong cùng database ghi log rất nhiều, hậu quả là WAL tràn ổ đĩa và làm sập database khách hàng. May là cuối cùng cũng không phải đền gì 😄. Nhưng mấy chi tiết kiểu này thì chắc tới mùa quít các demo trên mạng mới nói.
Với tớ, trường hợp này đáng sợ vì một pipeline downstream tưởng như chỉ để analytics/realtime sync, nếu vận hành sai, lại có thể tạo áp lực ngược lên hệ thống source production của khách hàng.
Lúc này câu chuyện không còn là “CDC tool đọc log rồi đẩy sang ClickHouse” nữa. Nó trở thành bài toán vận hành source database, log retention, replication slot, connector health, alerting và blast radius.
Nếu không hiểu lớp này, bạn rất dễ dựng được một pipeline CDC chạy demo, nhưng lại không hiểu vì sao production có thể gặp các vấn đề như:
- binlog bị prune mất khiến CDC với MySQL không resume được từ offset cũ, hoặc tệ hơn là downstream tiếp tục chạy nhưng đã mất một đoạn change events nên dữ liệu âm thầm sai;
- WAL bị giữ lại quá lâu khiến disk PostgreSQL phình ra và đe doạ database nguồn, kể cả khi table được CDC có traffic thấp nhưng các table khác trong cùng database ghi transaction rất nhiều;
- schema source thay đổi bất ngờ;
- connector lag hoặc restart không sạch;
- snapshot phase và streaming phase lệch nhau;
- duplicate events, out-of-order events hoặc missing events;
- transaction boundary không còn rõ khi đi qua nhiều layer;
- downstream table bị ghi sai rồi phải backfill;
- data type mapping giữa PostgreSQL, Kafka/CDC event format và ClickHouse không thống nhất, đặc biệt với các kiểu số chính xác cao;
- ClickHouse Materialized View hoạt động trên insert path, stack nhiều view lên Kafka input có thể kéo theo I/O, merge pressure và consumer lag;
- một job nhỏ trong demo trở thành dependency của nhiều báo cáo hoặc model quan trọng.
Đây là những chi tiết thuộc về vận hành thật hơn là cách dùng cơ bản. Khi mới học hoặc dựng demo, mình rất dễ bỏ qua chúng vì pipeline vẫn chạy được trong điều kiện sạch.
Vì data type mapping không đơn giản như nhìn trên sơ đồ
Một nhóm vấn đề khác là mapping kiểu dữ liệu giữa PostgreSQL và ClickHouse. Trên sơ đồ, mình thường chỉ vẽ một mũi tên:
Nhưng khi đi vào production, mỗi kiểu dữ liệu lại có semantics riêng: precision, scale, timezone, nullability, overflow, encoding, default value,... Chỉ cần map sai một chút là downstream vẫn chạy, nhưng số liệu đã sai.
Case tớ nhớ nhất là các kiểu số có độ chính xác cao, đặc biệt trong những bài toán liên quan tới tiền. Có những column phía PostgreSQL dùng kiểu numeric/decimal với precision rất lớn, ví dụ kiểu decimal(40, 19) hoặc tương tự. Khi đưa sang ClickHouse, không phải lúc nào cũng có một kiểu tương đương đủ an toàn để biểu diễn y hệt.
Tớ không nhớ chính xác lúc đó vướng ở kiểu double, decimal, hay giới hạn precision cụ thể nào của ClickHouse, nhưng vấn đề bản chất là: với dữ liệu tài chính, “gần đúng” không đủ. Nếu map sang floating-point thì có nguy cơ mất chính xác. Nếu map sang decimal mà precision/scale không đủ thì có nguy cơ overflow hoặc làm tròn sai.
Cuối cùng có những case phải cắn răng cắn lưỡi lưu dạng String =))))
Nó rất xấu về mặt typing và query experience, nhưng ít nhất giữ được representation gốc, tránh việc hệ thống tự âm thầm làm tròn hoặc làm sai dữ liệu tiền.
Những thứ này cũng là một phần của khoảng cách giữa “dùng được” và “vận hành được”. Demo thường dùng các kiểu dữ liệu tương đối hiền như integer, varchar, timestamp. Nhưng production schema thì không hiền như vậy.
Demo schema:
id, name, created_at
Production schema:
amount numeric(40,19), exchange_rate, tax_amount,
timezone-aware timestamp, nullable flags, enum-ish strings,
legacy columns, weird defaults, historical garbageVì ClickHouse Materialized View không phải miễn phí
Một điểm nữa trong các demo PostgreSQL → ClickHouse realtime pipeline là hay dùng ClickHouse Materialized View để transform/route dữ liệu ngay khi ingest. Nhìn trên demo thì rất gọn:
Từ đó rất dễ có cảm giác rằng cứ thêm Materialized View là pipeline sẽ realtime và dễ mở rộng.
Nhưng Materialized View trong ClickHouse, trong nhu cầu này, hoạt động khá giống một trigger trên luồng insert. Khi dữ liệu đi vào source table, materialized view sẽ được kích hoạt để xử lý và ghi tiếp sang target table. Điều này rất tiện, nhưng không miễn phí.
Nếu stack nhiều materialized views lên cùng một input — ví dụ một Kafka source table fan-out sang nhiều target tables hoặc nhiều transformation khác nhau — mỗi batch/message đi vào không còn chỉ là một lần ingest đơn giản nữa. Nó kéo theo thêm compute, thêm write, thêm merge pressure, thêm I/O ở các target phía sau.
Nhìn thì có vẻ scale ngang bằng cách thêm view, nhưng thực tế nó có thể làm chậm luôn input path. Kafka consumer phía ClickHouse bắt đầu log warning oé oé, lag tăng lên, throughput ingest giảm, rồi cuối cùng realtime pipeline không còn realtime nữa :)))
Điểm đau ở đây là: Materialized View giúp mình biểu diễn transformation rất gọn, nhưng nếu dùng nó như một fan-out/processing framework mà không hiểu execution model và cost của nó, thì phần ingest path có thể trở thành bottleneck rất nhanh.
Với những pipeline nghiêm túc hơn, mình cần nghĩ thêm về:
- số lượng materialized views attach vào cùng source;
- cost mỗi view tạo thêm trên insert path;
- target table engine, partition, primary key và merge behavior;
- consumer lag từ Kafka Engine;
- backpressure khi target write/merge chậm;
- việc tách pipeline thành nhiều stage thay vì nhồi hết vào một tầng MV.
Vì kinh nghiệm thật thường nằm ở phần trade-off
Một lý do khác là không phải ai viết bài cũng đã trải qua đầy đủ các tình huống production. Có người viết rất tốt ở mức concept hoặc demo, nhưng chưa chắc đã phải chịu trách nhiệm vận hành hệ thống đó qua nhiều tháng, nhiều năm, nhiều lần incident.
Ngược lại, có những người đã trải qua rất nhiều vấn đề thật, nhưng phần kinh nghiệm đó lại không dễ viết ra thành một bài blog ngắn. Nó nằm ở những câu hỏi kiểu:
Khi nào nên accept duplicate và xử lý idempotency ở downstream?
Khi nào nên backfill toàn bộ thay vì cố patch incremental?
Schema evolution nên strict tới mức nào?
Ai được phép thay đổi source schema?
Nếu lineage nói pipeline vẫn chạy nhưng business metric sai thì debug từ đâu?Đó mới là phần làm nên giá trị của một kỹ sư thực sự: không chỉ biết dựng pipeline theo happy path, mà biết hệ thống sẽ hỏng ở đâu, hỏng như thế nào, và cần thiết kế gì để giảm blast radius khi nó hỏng.
Nói cách khác, một bản thiết kế high-level có thể giúp mình bắt đầu rất nhanh. Nhưng khi vận hành thật, mình mới thấy đường thật có ổ gà, đường cụt, biển báo sai, và đôi khi Google Maps cũng dẫn mình xuống ruộng.
Lần mở mắt tiếp theo: dùng được, hiểu được, tối ưu cho user, và vẫn phải quản lý được
Sau một thời gian, tớ lại nhận ra thêm một lớp nữa: một hệ thống có thể dùng được, người làm platform cũng có thể hiểu được nó khá sâu, nhưng như vậy vẫn chưa đủ.
Nếu người dùng cuối phải tự gánh quá nhiều complexity, thì về mặt platform product, hệ thống vẫn chưa thật sự tốt.
Ở góc nhìn engineering, mình rất dễ hài lòng khi đã làm được những thứ như:
pipeline chạy được
metadata có đủ
lineage capture được
security enforce được
runtime ổn định
monitoring có dashboardNhưng với user, họ thường không quan tâm hệ thống phía dưới phức tạp như thế nào. Họ chỉ muốn trả lời vài câu rất cụ thể:
Tôi nên dùng bảng nào?
Bảng này có đáng tin không?
Cột này nghĩa là gì?
Dữ liệu này có realtime không?
Nếu sai thì báo ai?
Tôi có được phép dùng dữ liệu này không?
Nếu không có quyền thì xin quyền ở đâu?Đây là chỗ tớ bắt đầu thấy khoảng cách giữa platform capability và user experience.
Một data platform có thể có rất nhiều capability mạnh: CDC, streaming, catalog, lineage, access control, data quality, workflow orchestration. Nhưng nếu user phải hiểu hết Debezium, Kafka, ClickHouse, Spark, Airflow, Unity Catalog, permission model, lineage graph,... chỉ để tìm và dùng đúng một dataset, thì platform đang đẩy complexity ngược lại cho user.
Nói cách khác, platform team không chỉ có nhiệm vụ làm cho hệ thống chạy đúng. Platform team còn phải quyết định phần complexity nào nên được expose, phần nào nên được abstract đi, và phần nào cần biến thành một experience đủ đơn giản để người dùng ra quyết định.
Jupyter/Spark là một ví dụ rất rõ
Trong môi trường data platform có Spark, gần như sớm muộn gì cũng phải có một bề mặt làm việc kiểu Jupyter Notebook/JupyterHub cho analyst, data engineer hoặc ML engineer.
Về mặt kỹ thuật, để user đọc/ghi được Lakehouse một cách đúng chuẩn, notebook thường phải khởi tạo Spark session với khá nhiều cấu hình:
- catalog/metastore endpoint;
- warehouse path hoặc object storage path;
- Iceberg/Delta extensions;
- credential hoặc identity context;
- Ranger/Spark authorization extension nếu có;
- OpenLineage/metadata listener nếu có;
- default namespace/schema;
- package, jar, connector version;
- các Spark configs liên quan tới performance và security.
Nếu để user tự viết toàn bộ phần này trong notebook, code sẽ rất nhanh biến thành một đống boilerplate dài dằng dặc.
Ví dụ minh hoạ kiểu rất rút gọn:
spark = (
SparkSession.builder
.appName("user-notebook")
.config("spark.sql.catalog.lakehouse", "org.apache.iceberg.spark.SparkCatalog")
.config("spark.sql.catalog.lakehouse.uri", "thrift://metastore:9083")
.config("spark.sql.catalog.lakehouse.warehouse", "s3a://warehouse")
.config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
.config("spark.extraListeners", "io.openlineage.spark.agent.OpenLineageSparkListener")
.config("spark.openlineage.transport.url", "http://openlineage/api")
.config("spark.sql.authorization.enabled", "true")
.config("spark.hadoop.fs.s3a.access.key", "...")
.config("spark.hadoop.fs.s3a.secret.key", "...")
.getOrCreate()
)Đoạn trên chỉ là minh hoạ, nhưng nó cho thấy vấn đề: nếu user phải tự copy/paste những cấu hình kiểu này, sẽ có rất nhiều rủi ro:
- user không nhớ hết config cần thiết;
- mỗi notebook bootstrap Spark theo một kiểu khác nhau;
- lineage hoặc authorization dễ bị thiếu vì quên config;
- secret/credential dễ bị lộ trong notebook;
- upgrade package/config rất khó vì logic nằm rải rác trong hàng trăm notebook;
- platform team không kiểm soát được runtime assumptions.
Lúc này, việc “Spark dùng được trong notebook” vẫn chưa đủ. Platform phải tối ưu trải nghiệm để user không cần tự assemble toàn bộ runtime mỗi lần muốn làm việc.
Có nhiều hướng để abstract phần này đi:
- viết và maintain internal libraries kiểu
get_spark()hoặccreate_lakehouse_session()để gom toàn bộ bootstrap logic vào một chỗ; - pre-provision sẵn biến
sparkkhi kernel start; - dùng init script, IPython startup hook hoặc kernel hook để inject config;
- custom JupyterHub Spawner/Profile Manager để chọn đúng runtime profile;
- quản lý credential/identity ở platform layer thay vì để user tự paste secret;
- validate và giới hạn những config user được phép override.
Khi làm như vậy, user chỉ cần:
spark.sql("select * from lakehouse.sales.orders limit 10")hoặc cùng lắm:
from platform_sdk import get_spark
spark = get_spark(profile="lakehouse")Cách làm này không chỉ là bọc vài dòng config cho ngắn hơn. Khi internal library trở thành entrypoint chuẩn, platform team có thể maintain default config, upgrade connector/jar versions, bật/tắt lineage hoặc authorization, đổi catalog endpoint, đổi credential mechanism, thêm validation,... ở một nơi duy nhất thay vì đi sửa từng notebook của user.
Còn phần catalog, lineage, authorization, credential, package version và runtime assumptions được platform bootstrap một cách nhất quán phía dưới.
Đây là ví dụ rất rõ cho việc: hiểu kỹ thuật là để biết cần giấu bớt kỹ thuật nào khỏi user. Không phải giấu vì user kém, mà vì platform tốt nên giảm những việc lặp lại, dễ sai, dễ lộ secret, và không tạo thêm giá trị cho họ.
Airflow/Spark Operator cũng là một ví dụ tương tự
Một ví dụ khác là orchestration. Khi cung cấp một data platform, việc tích hợp Airflow để submit các Spark jobs chạy internal trên Kubernetes gần như là nhu cầu rất tự nhiên.
Ở mức kỹ thuật, flow có thể là:
Với platform engineer, flow này khá dễ hiểu. Mình có thể vào Kubernetes xem pod, describe resource, đọc logs, port-forward Spark UI, xem event, xem driver/executor đang chết ở đâu.
Nhưng không thể bắt non-tech user, analyst hoặc thậm chí một số data engineer phải chạy vào Kubernetes để debug một Spark job.
Họ không nên cần biết:
kubectl get sparkapplications
kubectl describe pod ...
kubectl logs ...
kubectl port-forward ...chỉ để biết job của mình đang chạy, fail ở bước nào, hoặc xem Spark UI.
Nếu platform bắt user tự đi qua những bước đó, thì về mặt kỹ thuật hệ thống có thể chạy được, nhưng trải nghiệm người dùng vẫn rất tệ.
Lúc này, platform team có thể phải build thêm một lớp abstraction phía trên. Ví dụ, viết Airflow plugin/operator/custom UI để:
- submit Spark job theo một template chuẩn;
- validate config trước khi submit;
- tự tạo SparkApplication CRD phía dưới;
- track trạng thái driver/executor;
- gom logs và hiển thị trực tiếp trong Airflow UI;
- expose link tới Spark UI mà user không cần tự port-forward;
- surface các lỗi phổ biến theo cách dễ hiểu hơn;
- chuẩn hoá resource/profile/runtime để user không tự cấu hình bừa;
- cleanup hoặc retry theo policy của platform.
Khi đó user vẫn làm việc trong interface quen thuộc như Airflow, còn phần Kubernetes/Spark Operator/Spark pod lifecycle được platform che bớt phía dưới.
Đây lại là cùng một bài học: tối ưu cho user không có nghĩa là làm cho hệ thống đơn giản hơn ở bên dưới. Nhiều khi bên dưới còn phức tạp hơn, vì platform team phải hiểu sâu hơn, nhúng tay sâu hơn vào lifecycle của open-source tools, và đôi khi phải mở rộng chúng bằng plugin, hook, wrapper hoặc internal service.
Tức là muốn user thấy ít complexity hơn, platform team thường phải own nhiều complexity hơn.
Tối ưu cho user cũng phải quản lý được
Đây là lúc câu chuyện quay lại đúng bộ ba mà bạn newbie kia hỏi: metadata, lineage và security.
Ban đầu mình rất dễ nghĩ rằng mỗi bài toán chỉ cần thêm một tool là xong:
Nhưng khi ghép các công cụ đó thành một data platform thật, vấn đề không còn nằm ở từng tool riêng lẻ nữa. Phần lớn open-source components đều giải quyết khá tốt một mảnh bài toán của nó. Cái khó là các mảnh đó có cùng vận hành như một platform thống nhất hay không.
Nếu không có một lớp control chung phía trên, mỗi tool sẽ trở thành một local optimization:
Từng mảnh đều có vẻ hợp lý. Nhưng khi nối lại, khoảng trống bắt đầu lộ ra: identity không nhất quán, policy không xuyên suốt, runtime không đồng bộ, metadata/tag không fresh, lineage chỉ là signal, audit không chắc ghi đúng người thật, và governance rất dễ drift theo thời gian.
Một cú vỡ rất đau với security là: nhìn trên bề mặt, hệ thống có vẻ đã có phân quyền theo user/group ở storage. User bình thường không có quyền thì bị deny. Nhưng nếu runtime cho phép người dùng tự khai identity từ phía client, ví dụ một số môi trường Hadoop/HDFS cấu hình lỏng có thể bị lách bằng cách đổi runtime identity, thì toàn bộ authorization phía sau gần như chỉ còn là ảo giác.
Điểm đáng sợ là với người dùng không biết trick, hệ thống trông có vẻ an toàn. Nhưng với người biết một chút về runtime phía dưới, permission boundary có thể bị bypass. Khi đó vấn đề không còn là “thiếu permission rule”, mà là authentication/trust boundary không đáng tin, nên authorization phía sau cũng mất ý nghĩa.
Từ góc nhìn platform, security không phải chỉ là set ACL hay tạo policy. Security phải đi cùng identity propagation, runtime bootstrap, credential management, audit, và quan trọng nhất là phải đảm bảo user không có đường bypass control path.
Metadata và lineage cũng tương tự. Có catalog không có nghĩa metadata đáng tin. Có agent lineage không có nghĩa graph dùng được. Nếu mỗi notebook, mỗi DAG, mỗi runtime tự inject config theo một kiểu khác nhau, platform team rất khó biết:
- runtime nào đang được dùng ở đâu;
- notebook/job nào đang dùng version library nào;
- config nào là chuẩn, config nào là override tạm;
- lineage/authorization có được bật nhất quán không;
- credential/secret có đang bị leak qua notebook hoặc DAG không;
- user identity đi xuống Spark, storage, catalog và policy engine có còn đúng không;
- khi upgrade connector, catalog endpoint hoặc policy engine thì sẽ ảnh hưởng những workload nào.
Vậy nên sau khi abstract complexity khỏi user, platform còn phải biến abstraction đó thành một thứ quản lý được. Đây là chỗ câu chuyện bắt đầu giống một control plane hơn là vài đoạn helper code.
Platform cần có source of truth cho runtime, profile, config, identity, ownership và policy; cần có cách validate input; cần có cách audit ai dùng gì; cần có lifecycle rõ ràng cho version, deprecation, rollback.
Ví dụ với Jupyter/Spark, nếu đã có get_spark(profile="lakehouse"), thì profile="lakehouse" không nên chỉ là một string tuỳ tiện. Nó nên map về một runtime profile được platform quản lý:
Tương tự với Airflow/Spark jobs, nếu user submit job qua plugin, thì plugin không chỉ giúp submit dễ hơn. Nó cũng nên giúp platform chuẩn hoá và quản lý:
Nói cách khác, tối ưu cho user và quản lý cho platform phải đi cùng nhau. Nếu chỉ tối ưu UX mà không có control phía sau, về lâu dài platform sẽ rất khó vận hành. Ngược lại, nếu chỉ tối ưu control mà bắt user tự hiểu hết complexity, platform sẽ rất khó được adoption.
Vì vậy, hướng đi hợp lý không phải là cứ thêm tool mãi, mà là xây dần một lớp control plane đứng phía trên stack hiện tại, từng bước wrapping các thành phần phía dưới. Tớ đã viết kỹ hơn phần này trong một note riêng về stacks integration: Stacks Integration. TODO: link lại đúng bài phân tích chi tiết sau khi migrate note cũ vào DataBrain.
Nhưng càng nghĩ sâu hơn, tớ càng thấy để làm chuyện này cho thật xuyên suốt thì platform gần như phải own/control được toàn bộ các layer quan trọng: identity, user management, compute/runtime, catalog/metadata, policy engine, credential, storage access, audit và lineage.
Đó cũng là lý do auth/authz trong một database đóng gói thường dễ reason hơn nhiều. Database engine thường là authority duy nhất kiểm soát session, query execution, permission check và storage access. Người dùng muốn đọc dữ liệu gần như bắt buộc phải đi qua engine.
Còn trong lakehouse/open data platform, các phần này bị tách ra:
Nếu platform không own được cách các layer này nối với nhau, thì governance rất dễ trở thành một lớp policy “trên giấy”: tool này enforce, tool kia bypass; catalog biết policy nhưng storage credential quá rộng; lineage có signal nhưng không có semantic; audit có log nhưng không chắc đó là user thật.
Với tớ, đây là lý do Databricks hiện vẫn là một trong những platform đứng top của industry. Không phải vì nó chỉ có Spark, mà vì nó kiểm soát và productize được rất nhiều phần của execution path và governance boundary: workspace, notebook, jobs, clusters/runtime, SQL warehouse, Unity Catalog, permission model, credential, lineage, audit,... thành một control plane tương đối thống nhất.
Nói hơi thật là lúc nhận ra điều này, tớ cũng thấy khá rõ giới hạn của bản thân và môi trường lúc đó. Để tự build được một thứ tương tự, không phải chỉ cần thêm vài open-source components hay viết vài plugin là xong. Nó cần quyền sở hữu rất sâu trên nhiều layer của platform, cần thời gian, team, mandate và product direction đủ mạnh. Và tớ không nghĩ mình có thể làm được chuyện đó trong bối cảnh lúc ấy.
Nên đó cũng là một trong những lý do tớ quyết định nhảy việc 😂
Khi đi theo hướng control plane đủ sâu, platform mới dần chuyển từ mô hình “nhiều công cụ ghép lại” sang mô hình “một sản phẩm data platform thống nhất”.
Đây cũng là điểm khác biệt cốt lõi giữa việc triển khai một tập hợp công cụ và xây dựng một data platform: không chỉ chọn đúng thành phần, mà phải chủ động sở hữu cách chúng được tích hợp và vận hành như một hệ thống thống nhất.
Cuối cùng, technical không phải là tất cả
Tới một điểm nào đó, tớ bắt đầu thấy technical depth là rất cần, nhưng không phải toàn bộ câu chuyện.
Những ví dụ như Jupyter/Spark session hay Airflow/Spark Operator cho thấy một pattern rất rõ: để tối ưu cho user, mình phải hiểu rất sâu trước đã. Không chỉ hiểu API ở mức dùng được, mà phải hiểu lifecycle, failure mode, config surface, security boundary và những điểm user dễ làm sai.
Đôi khi, chỉ dùng open-source theo mặc định là không đủ. Mình phải viết thêm internal library, plugin, hook, wrapper, custom UI, hoặc nhúng tay vào những flow sâu hơn của công cụ để biến một capability kỹ thuật thành một experience tử tế cho user.
Mình có thể hiểu Spark bootstrap, hiểu CDC, hiểu lineage, hiểu access control, hiểu catalog metadata. Nhưng nếu những hiểu biết đó không được chuyển hoá thành một trải nghiệm dễ dùng hơn cho người dùng cuối, thì platform vẫn mới chỉ tốt ở phía người xây, chưa chắc đã tốt ở phía người dùng.
Một ví dụ đơn giản là data catalog.
Về mặt technical, data catalog có thể được nhìn như tập hợp của:
- tables;
- schemas;
- columns;
- owners;
- grants;
- tags;
- lineage edges;
- usage statistics.
Nhưng user không mở catalog chỉ để xem một đống metadata object. Họ mở catalog vì họ có một công việc cần làm:
Tôi cần tìm đúng dataset.
Tôi cần hiểu dataset đó có dùng được không.
Tôi cần biết có nên tin nó không.
Tôi cần biết xin quyền ở đâu.
Tôi cần biết nếu số liệu sai thì hỏi ai.Vì vậy, nếu catalog chỉ expose lại toàn bộ metadata kỹ thuật, nó rất dễ trở thành một cái information_schema có UI đẹp hơn.
User không cần nhìn thấy mọi thứ. Họ cần một catalog được tối ưu cho ngữ cảnh của họ:
- dataset nào thuộc project/team của tôi;
- dataset nào là source of truth;
- bảng nào production, bảng nào sandbox/tmp/deprecated;
- owner thật sự là ai;
- grain của bảng là gì;
- key fields là gì;
- cột nào là PII/sensitive;
- lineage nào là dependency production, lineage nào chỉ là noise từ ad-hoc query;
- access request phải đi qua workflow nào.
Đây là lý do chỉ “có metadata” chưa đủ. Metadata phải được tổ chức lại thành trải nghiệm giúp người dùng tìm đúng, hiểu đúng, và dùng đúng dữ liệu.
Lineage cũng vậy. Lineage graph raw có thể rất đầy đủ về mặt signal, nhưng nếu render thẳng cho người dùng thì rất dễ thành một đống hairball không đọc nổi.
User không cần thấy mọi edge từng xảy ra. Họ cần biết:
- upstream production chính là gì;
- downstream nào sẽ bị ảnh hưởng nếu bảng này đổi;
- dependency nào đang active;
- dependency nào cross-project;
- PII đi qua những đâu;
- bảng nào là hub/god table cần cẩn thận.
Vậy nên bài toán không chỉ là capture lineage. Bài toán là biến lineage thành một explanation mà user có thể hiểu và hành động được.
Với tớ, đây là phần rất quan trọng: technical signal chỉ là nguyên liệu. Platform vẫn cần semantic, curation, ownership và UX để biến nguyên liệu đó thành thứ user thật sự dùng được.
Platform cuối cùng vẫn là một sản phẩm
Đây là chỗ tớ thấy mindset thay đổi khá nhiều.
Ban đầu mình nghĩ platform là infra. Sau đó mình nghĩ platform là một tập hợp capability được tích hợp tốt. Nhưng càng làm, tớ càng thấy platform cũng là một sản phẩm nội bộ.
Sản phẩm đó có user, có journey, có UX, có trade-off, có support burden, có adoption problem. Nếu user không hiểu, không tin, hoặc không muốn dùng, thì dù phía dưới kỹ thuật rất xịn, platform vẫn chưa tạo ra đủ giá trị.
Câu hỏi vì thế không chỉ là:
Mình đã build được chưa?mà còn là:
User có dùng được mà không cần hiểu hết complexity phía dưới không?
User có ra quyết định tốt hơn nhờ platform không?
Platform có giảm burden cho user hay chỉ chuyển burden sang dạng khác?Với tớ, đây là một lần mở mắt quan trọng: technical depth giúp mình build đúng, product thinking giúp platform được dùng đúng, còn control-plane thinking giúp platform quản lý được ở scale. Để tối ưu được cho user, nhiều khi mình phải đi sâu hơn vào kỹ thuật chứ không phải nông hơn: hiểu trước, rồi mới biết nên abstract cái gì, giấu cái gì, expose cái gì, chuẩn hoá cái gì, và mở rộng open-source ở đâu.
Đây cũng là lý do, với góc nhìn cá nhân của tớ, Databricks hiện vẫn là một trong những platform đứng top của industry. Không phải vì nó chỉ “có Spark”, mà vì nó productize rất nhiều lớp complexity xung quanh Spark: workspace, job, notebook, cluster/runtime, permission, Unity Catalog, lineage, audit, SQL warehouse, credential, governance workflow,... thành một control plane tương đối thống nhất.
Nói hơi thô một chút: nếu chỉ nhìn ở tầng compute engine rồi bảo “tự dựng Spark on-premise là được”, thì mới nhìn thấy một mảnh rất nhỏ của bài toán. Spark on-premise có thể giúp mình chạy distributed compute, nhưng để biến nó thành một platform mà nhiều team có thể dùng, quản lý, phân quyền, audit, debug, govern và scale được thì còn cả một núi việc phía sau.
Cái Databricks bán không chỉ là Spark. Nó bán phần rất đau mà nhiều team tự build mới thấm: làm sao để Spark và toàn bộ data workflow xung quanh nó trở thành một sản phẩm platform dùng được và quản lý được.
Lần mở mắt cuối: Databricks giải quyết rất nhiều thứ, nhưng data governance vẫn không tự chạy
Khi chuyển sang một môi trường dùng Databricks, lúc đầu tớ cũng nghĩ khá đơn giản: những vấn đề mình từng đau với open-source data platform trước đây có lẽ đã có thể dùng tiền để solve.
Nghĩ như vậy cũng không phải vô lý. Sau khi từng tự ghép Spark, notebook, orchestration, metadata, lineage, security, storage và monitoring lại với nhau, việc bước vào một managed platform như Databricks cho cảm giác rất khác.
Rất nhiều thứ từng là pain rõ ràng trong open-source stack đã được productize:
- notebook/workspace có sẵn;
- job orchestration có sẵn;
- cluster/runtime được quản lý tốt hơn;
- SQL warehouse có sẵn;
- Unity Catalog làm metadata/governance layer;
- permission model thống nhất hơn;
- lineage/audit/system tables có sẵn;
- credential/storage access được kiểm soát qua platform boundary tốt hơn.
Nên suy nghĩ ban đầu của tớ khá là:
Mình từng đau vì phải tự build control plane.
Databricks đã có control plane rồi.
Vậy chắc metadata, lineage, catalog, governance sẽ nhẹ hơn rất nhiều.Và đúng là Databricks giải quyết rất nhiều thứ thật. Nhưng sau đó tớ gặp lại những bài toán rất đời.
Có data, nhưng owner là ai thì méo biết
Bài toán đầu tiên không phải là “có table hay không”. Table thì có rất nhiều. Vấn đề là: ai thật sự chịu trách nhiệm cho table đó?
Trong catalog, một object có thể có owner. Nhưng owner đó chưa chắc là người hiểu dữ liệu:
- có thể là service principal tạo bảng;
- có thể là người chạy migration;
- có thể là platform/admin account;
- có thể là team kỹ thuật vận hành pipeline;
- có thể khác hoàn toàn với business owner hoặc data steward thật sự.
Khi số lượng tables lên tới hàng chục nghìn, câu hỏi “owner là ai?” không còn là metadata field cho đẹp nữa. Nó quyết định rất nhiều thứ:
Ai xác nhận description đúng?
Ai confirm column này là PII?
Ai approve access request?
Ai chịu trách nhiệm nếu số liệu sai?
Ai biết bảng này còn dùng hay deprecated?
Ai quyết định bảng nào là source of truth?Nếu không trả lời được mấy câu đó, catalog chỉ cho mình biết “dữ liệu tồn tại”, chứ chưa giúp tổ chức chịu trách nhiệm với dữ liệu.
Có data catalog, nhưng không có data dictionary thì user vẫn méo hiểu
Bài toán thứ hai là data catalog. Lúc đầu nghe có vẻ đơn giản: đã có Unity Catalog rồi, kéo metadata ra, build catalog riêng cho từng project là xong.
Nhưng user không chỉ cần thấy danh sách bảng. User cần hiểu bảng đó nghĩa là gì.
Một catalog không có data dictionary rất dễ thành một cái object browser:
catalog.schema.table
column_name
data_type
created_at
owner
permissionNhìn thì có vẻ đầy đủ metadata, nhưng user vẫn không biết:
- bảng này đại diện business concept gì;
- grain của bảng là gì;
- mỗi row là một order, một customer, một transaction hay một snapshot;
- column này tính như thế nào;
- key field là gì;
- bảng này nên dùng cho use case nào;
- bảng nào tương tự nhưng không nên dùng;
- dữ liệu có đáng tin không.
Lúc này rất dễ nghĩ: “AI generate data dictionary được mà”. Đúng, AI có thể giúp rất nhiều. Nó có thể generate table description, column description, business grain, key field suggestion, PII classification, governance tags,... nhanh hơn con người rất nhiều.
Nhưng câu hỏi tiếp theo mới quan trọng:
Ai confirm là AI gen đúng?Nếu description sai, PII tag sai, grain sai, hoặc key field suggestion sai, hậu quả không chỉ là documentation xấu. User có thể dùng nhầm dữ liệu, policy có thể apply sai, access control/masking có thể thiếu, data quality rule có thể đặt sai chỗ.
Vậy nên cuối cùng vẫn phải có human-in-the-loop review. AI giúp tạo draft và giảm rất nhiều manual effort, nhưng governance cần accountability. Phải có người hoặc team đủ context để confirm, sửa, approve và chịu trách nhiệm cho metadata đó.
Đến đây, bài toán cũng không còn là việc một cá nhân ngồi generate description cho thật nhiều bảng. Nó trở thành một workflow phối hợp giữa platform team, data owner, data steward, domain team và consumer. Platform có thể cung cấp tooling, AI draft, validation UI, access workflow và audit trail; nhưng phần xác nhận ý nghĩa, ownership và policy vẫn cần người có context của domain tham gia.
Lineage table cũng chỉ là signal, không dùng thẳng được
Bài toán thứ ba là lineage.
Về mặt kỹ thuật, agent lineage ngày xưa hay Databricks lineage table bây giờ khá giống nhau ở một điểm: chúng đều capture observed execution signal.
Nó nói rằng tại một thời điểm nào đó, một workload/query/notebook/job từng đọc hoặc ghi một object nào đó. Nhưng nó không tự biết điều đó có ý nghĩa gì với project, với business, hay với governance.
Và execution ngoài đời thì chứa đủ thứ trên đời:
- production transform;
- ad-hoc read;
- debug query;
- clone bảng tạm;
- backup table;
- test run;
- stale dependency lịch sử;
- downstream ghi ngược upstream;
- cross-project dependency;
- god table 200-300 columns làm down/upstream của hàng nghìn bảng.
Nếu collect đủ và render thẳng, lineage graph không phải “full lineage” đẹp đẽ gì cả. Nó rất dễ thành một đống noise không dùng được.
Không phải edge nào cũng nên được show mặc định. Một project lineage dùng được phải có thêm rất nhiều lớp xử lý:
Tức là bài toán không phải là “Databricks có lineage không?”. Có. Bài toán là: lineage đó có đủ sạch, đủ ngữ cảnh, đủ project boundary và đủ explainable để user dùng không.
Technical automation không đủ
Đây là lần vỡ mộng cuối cùng trong chuỗi này của tớ.
Ban đầu tớ nghĩ vấn đề là thiếu tool. Sau đó tớ nghĩ vấn đề là thiếu integration. Rồi tớ nghĩ nếu automation đủ tốt — agent lineage, metadata crawler, system tables, Unity Catalog, LLM generate description — thì governance sẽ nhẹ đi rất nhiều.
Nhưng cuối cùng tớ nhận ra: automation kỹ thuật chỉ tạo ra signal nhanh hơn và nhiều hơn. Nếu thiếu ownership, semantic model, validation và curation, nó cũng có thể tạo ra rác nhanh hơn và nhiều hơn.
Databricks giúp mình có một nền tảng rất mạnh để capture và enforce nhiều thứ. Nhưng nó không tự biết owner thật là ai, không tự biết business meaning nào là đúng, không tự biết lineage edge nào là noise, và không tự chịu trách nhiệm thay tổ chức khi metadata sai.
Những bài toán kiểu này lớn hơn rất nhiều so với một script enrich metadata hay một dashboard lineage. Nó cần process, ownership model, review workflow, incentive, tooling và sự phối hợp của nhiều nhóm. Một ví dụ rất hay là cách Grab xây Signals Marketplace như một sản phẩm nội bộ để discovery, understand và reuse signals/features ở quy mô tổ chức. Điểm đáng chú ý không chỉ là công nghệ, mà là cách họ biến metadata, ownership, validation và consumption workflow thành một marketplace có người dùng, producer, consumer và governance process.
Đây là thứ mà tớ không nghĩ một cá nhân đơn lẻ có thể tự làm đến nơi đến chốn. Một cá nhân có thể khởi tạo, prototype, hoặc kéo những viên gạch đầu tiên; nhưng để nó trở thành platform capability thật sự thì cần mandate, team, process, domain buy-in và thời gian.
Câu này với tớ là điểm chốt:
Technical signals tell what happened. Platform governance has to decide what matters.
Và cũng là lý do tớ ngày càng thấy data platform không chỉ là hệ thống xử lý dữ liệu. Nó là hệ thống giúp tổ chức hiểu, tin, kiểm soát và chịu trách nhiệm với dữ liệu của mình.