Spark 1 — Tổng quan & kiến trúc thực thi

13 thg 7, 2026 3 lượt xem
#data-engineering
#spark
#distributed
#big-data
#pyspark

Apache Spark là gì

Apache Spark là một engine xử lý dữ liệu phân tán (distributed data processing engine) dành cho khối lượng công việc lớn — những tập dữ liệu vượt quá sức chứa hoặc sức tính của một máy đơn lẻ. Thay vì bắt một CPU/một ổ đĩa gánh toàn bộ, Spark chia dữ liệu thành nhiều mảnh, phân phát tính toán ra hàng chục đến hàng nghìn nhân xử lý trên nhiều máy, rồi gom kết quả lại. Điểm khác biệt cốt lõi so với thế hệ trước là Spark ưu tiên giữ dữ liệu trung gian trong bộ nhớ (in-memory) thay vì ghi xuống đĩa sau mỗi bước.

Spark còn là một unified engine — một nền tảng duy nhất phục vụ nhiều kiểu tải khác nhau bằng chung một lõi thực thi và chung các API:

  • Batch: xử lý theo lô — đọc một khối dữ liệu lớn, biến đổi, ghi ra.
  • Streaming: xử lý luồng gần thời gian thực qua Structured Streaming.
  • SQL: truy vấn bằng SQL chuẩn qua Spark SQL.
  • Machine Learning: huấn luyện mô hình quy mô lớn qua MLlib.
  • Graph: xử lý đồ thị qua GraphX.

Việc có chung một lõi nghĩa là bạn dùng lại cùng một mô hình lập trình, cùng bộ tối ưu, cùng cách vận hành cho cả bốn kiểu tải — thay vì học và ghép nối bốn hệ thống rời rạc.

Ở thời điểm hiện tại, dòng ổn định phổ biến trong sản xuất là Spark 3.5.x, và Spark 4.0 cũng đã ra mắt. Bài viết này bám theo mô hình của Spark 3.5+.

Vì sao Spark nhanh hơn MapReduce

Để hiểu tại sao Spark "thắng" MapReduce (mô hình xử lý phân tán của thế hệ Hadoop đời đầu), cần nhìn vào đĩa.

MapReduce chạy theo chuỗi map → reduce, và sau mỗi bước, kết quả trung gian được ghi xuống đĩa (HDFS) rồi bước sau đọc lại lên. Với một pipeline nhiều bước — điển hình là các thuật toán lặp (iterative) như machine learning, hay các truy vấn nhiều tầng join/aggregate — chi phí đọc-ghi đĩa lặp đi lặp lại trở thành nút thắt cổ chai khổng lồ.

Spark giải quyết đúng chỗ đau này: nó giữ dữ liệu trung gian trong RAM giữa các bước, chỉ ghi đĩa khi thật cần (ví dụ khi shuffle tràn bộ nhớ, hoặc khi bạn chủ động ghi ra). Với các workload lặp, khác biệt có thể lên tới một bậc độ lớn hoặc hơn. Ngoài ra Spark còn nhanh nhờ:

  • Lazy evaluation + DAG: Spark nhìn toàn bộ chuỗi phép biến đổi trước khi chạy, nên có thể tối ưu tổng thể (gộp bước, đẩy filter xuống sớm) thay vì chạy mù từng bước một.
  • Catalyst optimizer & Tungsten: với DataFrame/Spark SQL, Spark có bộ tối ưu truy vấn (Catalyst) và cơ chế quản lý bộ nhớ/sinh mã (Tungsten) tối ưu hoá kế hoạch thực thi và cách dùng CPU/RAM.

Lưu ý cân bằng: "in-memory" không có nghĩa là mọi thứ luôn nằm gọn trong RAM. Khi dữ liệu lớn hơn bộ nhớ, Spark vẫn tràn (spill) xuống đĩa. Điểm mấu chốt là nó không bắt buộc ghi đĩa sau mỗi bước như MapReduce.


Kiến trúc thực thi

Một ứng dụng Spark (Spark application) không chạy trên một tiến trình duy nhất. Nó là một tập hợp các tiến trình phối hợp trên cụm máy, gồm ba vai trò chính: Driver, Executor, và Cluster Manager.

Driver

Driver là tiến trình chạy hàm main của ứng dụng bạn. Nó là "bộ não" điều phối và chịu trách nhiệm:

  • Tạo SparkSession (bao bọc SparkContext) — điểm vào (entry point) của mọi ứng dụng Spark hiện đại.
  • Dựng logical plan từ code của bạn: mỗi phép biến đổi bạn viết được ghi lại thành một kế hoạch, chưa chạy ngay.
  • Lập DAG và chia stage: khi một action được gọi, Driver biến kế hoạch logic thành DAG (Directed Acyclic Graph — đồ thị có hướng, không chu trình) các bước, rồi chia thành các stage.
  • Lập lịch (schedule) và phân phát task cho các Executor, theo dõi tiến độ, xử lý thất bại (chạy lại task hỏng), gom kết quả cuối cùng.

Driver là điểm đơn (single point) điều phối: nếu Driver chết, cả ứng dụng dừng. Vì thế Driver cần đủ bộ nhớ và không nên gánh việc kéo quá nhiều dữ liệu về (ví dụ collect() một tập khổng lồ sẽ làm Driver hết RAM).

Executor

Executor là các tiến trình (JVM) chạy trên các worker node của cụm. Đây là nơi công việc thực sự diễn ra:

  • Chạy task: mỗi Executor có một số core (slot) và chạy song song nhiều task tương ứng.
  • Giữ cache: khi bạn .cache() / .persist() một tập dữ liệu, dữ liệu được giữ trong bộ nhớ của Executor để các bước sau dùng lại mà không tính lại từ đầu.
  • Báo cáo về Driver: trạng thái, kết quả, và các số liệu (metrics).

Một ứng dụng có nhiều Executor, mỗi Executor lại có nhiều core, nên tổng mức song song = số Executor × số core mỗi Executor.

Cluster Manager

Cluster Manager là hệ thống cấp phát tài nguyên (CPU, RAM) cho ứng dụng Spark. Driver xin tài nguyên từ Cluster Manager, và Cluster Manager cấp các Executor trên các node. Spark hỗ trợ nhiều loại:

  • YARN — trình quản lý tài nguyên của hệ sinh thái Hadoop; phổ biến trong các cụm on-prem truyền thống.
  • Kubernetes — chạy Executor/Driver dưới dạng pod container; ngày càng là lựa chọn mặc định cho cloud-native.
  • Standalone — trình quản lý đơn giản đi kèm sẵn với Spark, dễ dựng cụm.

Spark tách rời engine tính toán khỏi trình quản lý tài nguyên, nên cùng một job có thể chạy trên YARN, Kubernetes hay Standalone mà gần như không đổi code.

Job → Stage → Task

Đây là "đơn vị công việc" bên trong Spark — nắm chắc ba tầng này thì đọc Spark UI và gỡ lỗi hiệu năng sẽ dễ hơn nhiều:

  • Job: một job được tạo ra mỗi khi bạn gọi một action (ví dụ count(), write(), collect()). Một action = một job.
  • Stage: mỗi job được chia thành các stage tại ranh giới shuffle (shuffle boundary) — tức tại những điểm dữ liệu phải xáo trộn qua mạng giữa các Executor (như groupBy, join, repartition). Trong một stage, các phép biến đổi có thể chạy nối tiếp mà không cần dịch chuyển dữ liệu.
  • Task: mỗi stage được chia thành nhiều task, mỗi task xử lý một partition dữ liệu. Task là đơn vị thực thi nhỏ nhất và là đơn vị song song thực tế.

Partition (phân vùng) là đơn vị song song cốt lõi: dữ liệu được cắt thành N partition, sinh ra N task chạy song song. Quá ít partition → không tận dụng hết cụm; quá nhiều → phí tổn quản lý task lấn át. Chuyện chọn số partition và tránh shuffle thừa là chủ đề riêng — xem Shuffle & partitionTối ưu Spark.


Lazy evaluation & DAG

Spark chia mọi phép toán làm hai loại:

  • Transformation (phép biến đổi): tạo ra một tập dữ liệu mới từ tập cũ — select, filter, map, groupBy, join... Điểm mấu chốt: transformation lười (lazy) — gọi nó không khởi động tính toán nào cả, Spark chỉ ghi thêm một bước vào kế hoạch (DAG).
  • Action (hành động): yêu cầu một kết quả cụ thể — count, collect, show, write, take... Chỉ khi một action được gọi, Spark mới thực sự chạy toàn bộ chuỗi transformation dẫn tới kết quả đó.

Cơ chế lazy evaluation này chính là nguồn sức mạnh: vì Spark nhìn thấy toàn bộ kế hoạch trước khi chạy, nó có thể tối ưu tổng thể — đẩy filter xuống sát nguồn đọc để đọc ít dữ liệu hơn (predicate pushdown), gộp nhiều phép biến đổi liên tiếp vào chung một stage, loại bỏ cột không dùng (column pruning), và lập lịch thông minh.

DAG scheduler trong Driver nhận kế hoạch này, dựng nó thành đồ thị các bước, rồi chia stage theo ranh giới shuffle như đã nói ở trên. Các phép "hẹp" (narrow — mỗi partition đầu ra chỉ phụ thuộc một partition đầu vào, như map, filter) nằm gọn trong một stage; các phép "rộng" (wide — cần trộn dữ liệu qua mạng, như groupBy, join) tạo ranh giới stage mới.

Một hệ quả thực tế: nếu code của bạn dựng một chuỗi transformation rất dài mà không có action nào, sẽ không có gì chạy. Đây là lỗi phổ biến của người mới — chờ mãi không thấy kết quả vì thiếu action.


Ví dụ: một Spark job tối giản (minh hoạ)

Ví dụ PySpark dưới đây (minh hoạ) đọc dữ liệu, biến đổi, rồi gọi action. Lưu ý sandbox SQL của Knowledge Base là PostgreSQL nên code Spark không chạy tại đây.

# (minh hoạ) — cần môi trường Spark thực để chạy
from pyspark.sql import SparkSession
from pyspark.sql import functions as F

# 1) Điểm vào: tạo SparkSession
spark = (
    SparkSession.builder
    .appName("doanh_thu_theo_ngay")
    .getOrCreate()
)

# 2) Đọc dữ liệu (transformation — lazy, chưa chạy)
don_hang = spark.read.parquet("s3a://lake/orders/")

# 3) Biến đổi: lọc + gộp (vẫn lazy — chỉ dựng DAG)
doanh_thu = (
    don_hang
    .filter(F.col("trang_thai") == "COMPLETED")   # narrow
    .groupBy("ngay")                                # wide -> ranh giới shuffle
    .agg(F.sum("thanh_tien").alias("doanh_thu"))
    .orderBy("ngay")
)

# 4) Action: đến đây Spark MỚI chạy toàn bộ DAG
doanh_thu.show(20)                 # action 1 -> job 1
doanh_thu.write.mode("overwrite").parquet("s3a://lake/marts/daily_revenue/")  # action 2 -> job 2

spark.stop()

Cùng logic viết bằng Scala (minh hoạ) trông rất tương đồng — cùng API DataFrame:

// (minh hoạ)
val doanhThu = donHang
  .filter($"trang_thai" === "COMPLETED")
  .groupBy("ngay")
  .agg(sum("thanh_tien").as("doanh_thu"))
  .orderBy("ngay")

doanhThu.show(20)   // action

Trong ví dụ này: bước 2–3 chỉ dựng kế hoạch; showwrite là hai action, mỗi cái sinh một job. groupBy tạo một shuffle boundary, nên mỗi job được chia thành các stage trước và sau shuffle.


Các API & thành phần

Spark cung cấp nhiều tầng API, từ thấp lên cao:

  • RDD (Resilient Distributed Dataset) — API cấp thấp, coi dữ liệu như tập hợp phân tán các object và bạn tự viết các phép biến đổi kiểu hàm. Mạnh và linh hoạt nhưng không được Catalyst tối ưu, nên ngày nay chỉ dùng cho các trường hợp đặc biệt. Đây vẫn là lớp nền mà mọi thứ khác được xây trên.
  • DataFrame / Dataset — API cấp cao, coi dữ liệu như bảng có schema (cột, kiểu). Đây là API được khuyến nghị cho hầu hết công việc: dễ đọc, và được Catalyst tối ưu tự động. Chi tiết RDD vs DataFrame xem bài RDD & DataFrame.
  • Spark SQL — chạy SQL chuẩn ngay trên DataFrame/bảng; bạn có thể trộn SQL và code lập trình tự nhiên.
  • Structured Streaming — xử lý luồng gần thời gian thực bằng cùng API DataFrame: một streaming DataFrame được tính toán tăng dần (incremental) như một bảng không ngừng lớn lên. Xem Structured Streaming.
  • MLlib — thư viện machine learning phân tán: pipeline, feature engineering, các thuật toán phân loại/hồi quy/gom cụm ở quy mô lớn.
  • GraphX — xử lý đồ thị (ít dùng hơn, và hầu như chỉ có ở Scala).

PySpark vs Scala: Spark viết bằng Scala (chạy trên JVM). Bạn có thể lập trình bằng Scala/Java (native JVM), hoặc PySpark (Python) — vốn phổ biến nhất trong giới data engineering/khoa học dữ liệu nhờ hệ sinh thái Python. Với DataFrame/Spark SQL, PySpark gần như không thua hiệu năng vì công việc thực sự chạy trong JVM và được Catalyst tối ưu; khoảng cách hiệu năng chủ yếu xuất hiện khi dùng Python UDF cấp thấp trên RDD.


Deployment modes

Có hai trục cần phân biệt khi triển khai:

Nơi Driver chạy (deploy mode):

  • Client mode: Driver chạy trên máy bạn gõ lệnh spark-submit (ví dụ máy edge node, hay pod điều phối). Phù hợp khi cần tương tác trực tiếp (notebook, shell) và Driver ở gần cụm. Nhược điểm: máy client phải sống suốt vòng đời job.
  • Cluster mode: Driver chạy bên trong cụm (do Cluster Manager cấp một node cho Driver). Phù hợp cho job sản xuất chạy nền — gửi job xong client có thể ngắt kết nối; Driver độc lập trong cụm.

Local mode: Spark chạy toàn bộ (Driver + Executor) trong một JVM trên một máy, dùng nhiều thread giả lập nhiều core (local[*]). Không cần cụm — lý tưởng để học, phát triển và chạy test đơn vị.


Khi nào dùng Spark — và khi nào KHÔNG

Spark rất mạnh nhưng cũng nặng nề; dùng đúng chỗ mới đáng công.

Nên dùng Spark khi:

  • Dữ liệu lớn vượt sức một máy — hàng chục GB đến hàng TB/PB, không nạp nổi vào RAM một node.
  • ETL/ELT nặng — biến đổi, join, aggregate trên khối lượng lớn, chạy theo lô định kỳ.
  • Machine learning quy mô lớn — huấn luyện/feature engineering trên tập dữ liệu khổng lồ (MLlib).
  • Cần một engine hợp nhất cho cả batch, SQL và streaming, dùng chung kỹ năng và hạ tầng.

KHÔNG nên dùng Spark khi:

  • Dữ liệu nhỏ (vừa RAM một máy, cỡ MB đến vài GB) → pandas, Polars hoặc DuckDB nhanh hơn nhiều mà không có chi phí khởi động cụm và shuffle. Spark có "thuế cố định" (overhead) đáng kể; với dữ liệu nhỏ, thuế này lấn át lợi ích.
  • Real-time độ trễ cực thấp (mili-giây, per-event) → Structured Streaming xử lý theo micro-batch nên độ trễ thường ở mức trăm mili-giây trở lên; nếu cần độ trễ sub-giây thật sự theo từng sự kiện, Apache Flink phù hợp hơn.
  • Truy vấn giao dịch OLTP (đọc/ghi bản ghi lẻ với độ trễ thấp) → đó là việc của cơ sở dữ liệu giao dịch, không phải Spark.

Vị trí trong hệ dữ liệu

Spark hiếm khi đứng một mình — nó là "cỗ máy tính toán" trong một kiến trúc dữ liệu lớn hơn:

  • Airflow điều phối Spark job: bản thân Spark không phải trình lập lịch. Trong sản xuất, Apache Airflow (hoặc công cụ orchestration khác) chịu trách nhiệm kích hoạt, sắp thứ tự và giám sát các Spark job theo lịch/phụ thuộc.
  • Kafka làm nguồn streaming: Structured Streaming thường đọc trực tiếp từ Apache Kafka để xử lý luồng sự kiện gần thời gian thực, rồi ghi kết quả ra lakehouse hoặc trả lại Kafka.
  • Lakehouse là nơi Spark đọc/ghi: Spark là engine hàng đầu để làm việc với các định dạng bảng của lakehouse (Delta Lake, Apache Iceberg, Apache Hudi) trên object storage — đọc dữ liệu thô, biến đổi, và ghi các bảng đã tinh chế theo mô hình phân tầng (bronze/silver/gold).

Nói cách khác: Kafka/lakehouse là kho và ống dẫn, Airflow là nhạc trưởng, còn Spark là dàn nhạc thực sự chơi nhạc — nơi phép biến đổi dữ liệu quy mô lớn thực sự diễn ra.


Use case thực tế

1) ETL kho dữ liệu ban đêm. Mỗi đêm, Airflow kích hoạt một Spark job đọc hàng trăm GB log giao dịch thô (Parquet) từ object storage, làm sạch, khử trùng lặp, join với dữ liệu tham chiếu, tổng hợp thành các bảng mart theo ngày rồi ghi lại lakehouse. Spark xử lý được nhờ chia dữ liệu ra hàng nghìn partition chạy song song trên nhiều Executor.

2) Feature engineering cho ML. Đội khoa học dữ liệu cần tính đặc trưng (feature) hành vi người dùng trên hai năm dữ liệu clickstream — vài TB. Spark tính các đặc trưng cửa sổ thời gian (window) trên toàn bộ lịch sử, rồi MLlib huấn luyện mô hình ngay trên tập kết quả mà không cần rời khỏi cụm.

3) Làm giàu luồng sự kiện gần real-time. Structured Streaming đọc sự kiện đơn hàng từ Kafka, join với bảng thông tin sản phẩm, tính doanh thu cộng dồn theo cửa sổ 5 phút và ghi ra bảng phục vụ dashboard — độ trễ cỡ vài giây, hoàn toàn phù hợp cho giám sát vận hành (dù không phải là real-time mili-giây).


Ghi nhớ

  • Spark = engine xử lý dữ liệu phân tán, in-memory, hợp nhất cho batch + SQL + streaming + ML.
  • Nhanh hơn MapReduce chủ yếu vì không bắt buộc ghi đĩa giữa các bước + có DAG/Catalyst tối ưu tổng thể.
  • Ba vai trò: Driver (lập kế hoạch & lịch), Executor (chạy task, giữ cache), Cluster Manager (cấp tài nguyên: YARN/K8s/Standalone).
  • Job → Stage → Task; một action sinh một job; stage chia tại ranh giới shuffle; task = một partition = đơn vị song song.
  • Lazy evaluation: transformation chỉ dựng DAG, action mới kích hoạt tính toán — thiếu action thì không có gì chạy.
  • Ưu tiên DataFrame/Spark SQL (được Catalyst tối ưu) hơn RDD thô.
  • Deployment: client vs cluster (nơi Driver chạy); local mode để học/dev.
  • Dùng Spark cho dữ liệu lớn; dữ liệu nhỏ → pandas/Polars/DuckDB; real-time độ trễ cực thấp → Flink.
  • Trong hệ dữ liệu: Airflow điều phối, Kafka cấp luồng, lakehouse lưu trữ, Spark tính toán.

Bài tiếp theo trong series: RDD & DataFrame — hai tầng API cốt lõi. Xem thêm: Shuffle & partition, Tối ưu Spark, Structured Streaming.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 7

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5