Spark self learning syllabus
Nguyên tắc xuyên suốt
Mọi thứ chậm trong Spark đều quy về 4 thứ: shuffle, skew, spill, và serialization/GC. Mỗi khi tối ưu, hỏi: cái nào trong 4 cái này?
Spark UI là bạn thân nhất. Từ Ngày 1, mở Spark UI sau mọi job. Đọc DAG, Stages, Tasks, SQL tab, Storage tab. Đừng coi UI là công cụ debug khẩn cấp.
.explain(mode="formatted")cho mọi DataFrame query. Đọc physical plan trở thành phản xạ.Đọc source khi mơ hồ. Spark là open-source Scala. Khi không chắc một transformation làm gì, mở source.
PHASE 0 - Nền tảng bắt buộc
Ngày 0.1 - Khái niệm hệ phân tán & mô hình tính toán
Chủ đề: Tại sao cần distributed compute (giới hạn của single-node), data parallelism vs task parallelism, partitioning là gì và tại sao là khái niệm trung tâm, locality (đưa code tới data chứ không ngược lại), fault tolerance qua lineage vs replication, mô hình MapReduce và vì sao Spark ra đời để khắc phục (in-memory, DAG thay vì chuỗi MR job ghi xuống đĩa).
Trọng tâm kiến thức: Hiểu sự đánh đổi CAP ở mức trực giác; hiểu vì sao "shuffle" (di chuyển data qua mạng giữa các node) là thao tác đắt nhất và là ranh giới quyết định mọi thứ.
Bài tập:
Vẽ sơ đồ một MapReduce word count và chỉ ra mỗi lần data phải ghi xuống đĩa/đọc lại. Sau đó vẽ phiên bản Spark và đánh dấu data nào ở memory.
Viết 1 trang giải thích "partition" cho người chưa biết, dùng phép ẩn dụ của riêng bạn.
Ngày 0.2 - JVM & bộ nhớ (vì Spark chạy trên JVM)
Chủ đề: JVM heap, garbage collection (vì sao GC pause giết hiệu năng Spark), on-heap vs off-heap, serialization là gì và vì sao tốn kém, Java vs Kryo serializer. Tổng quan kiến trúc bộ nhớ JVM (Eden/Survivor/Old gen, vì sao long-lived cached data gây vấn đề).
Trọng tâm kiến thức: Đây là nền cho Phase 2 (memory management) và Phase 8 (GC tuning). Hiểu vì sao object Java "béo" (header, padding, pointer) → động lực ra đời Tungsten.
Bài tập:
Viết một object Scala/Java đơn giản và ước lượng overhead bộ nhớ của nó so với data thực (dùng JOL - Java Object Layout - nếu muốn nghiêm túc).
Đọc một bài giải thích G1GC và tóm tắt khi nào nó tốt cho Spark.
Ngày 0.3 - Dựng môi trường nghiêm túc (không chỉ pip install)
Chủ đề: Cài Spark local; chạy ở local mode vs standalone cluster mode (1 master + 2 worker, kể cả trên cùng một máy qua Docker Compose) để thấy được nhiều executor. Hiểu cấu trúc thư mục Spark,
spark-shell,pyspark,spark-sql,spark-submit. Cài cả Scala (sbt) lẫn PySpark - bạn sẽ cần đọc cả hai.Trọng tâm kiến thức: Chạy cluster nhiều node (dù giả lập) ngay từ đầu để mọi bài tập sau đều có nhiều executor thật → bạn thấy shuffle, data movement, locality thật thay vì local mode che giấu hết.
Bài tập:
Dựng Docker Compose: 1 Spark master + 2 worker. Submit một job và mở Spark Master UI (cổng 8080) + Application UI (4040) quan sát executor.
Chạy cùng một word count ở
local[*]và trên cluster, so sánh số task và sự xuất hiện của shuffle stage.
PHASE 1 - Mô hình thực thi & kiến trúc cluster
Ngày 1 - Bức tranh lớn: chuyện gì xảy ra khi submit một job
Chủ đề: Giới thiệu Spark (đặt trong bối cảnh: unified engine, các module Spark Core/SQL/Streaming/MLlib/GraphX). Kiến trúc runtime: Driver (chạy
SparkContext/SparkSession, giữ DAGScheduler, TaskScheduler, BlockManagerMaster) - Cluster Manager (Standalone/YARN/Kubernetes) - Executor (JVM process chạy task, cache data qua BlockManager). Vòng đời ứng dụng: Application → Job → Stage → Task.Trọng tâm kiến thức: Vẽ được sơ đồ luồng đầy đủ: code → logical plan/lineage → action kích hoạt job → DAGScheduler chia stage tại ranh giới shuffle → TaskScheduler giao task cho executor qua SchedulerBackend → task chạy, ghi shuffle/trả kết quả. Stage = chuỗi narrow transformation không bị cắt bởi shuffle. Có 2 loại:
ShuffleMapStagevàResultStage.Bài tập:
Chạy một job có đúng 1 shuffle (vd
groupByKeyrồicount). Mở Spark UI tab Jobs → Stages. Chỉ ra: bao nhiêu stage? Ranh giới shuffle nằm ở đâu? Bao nhiêu task mỗi stage và tại sao con số đó?So sánh Spark vs Hadoop MapReduce: viết bảng đối chiếu về mô hình thực thi (DAG vs chuỗi MR), nơi lưu trung gian, fault recovery.
So sánh Spark với Flink (true streaming vs micro-batch) và nêu khi nào chọn cái nào.
Câu hỏi Tại sao một job có thể có nhiều stage? Điều gì quyết định ranh giới giữa hai stage? Nếu tôi thêm một
filtervào pipeline, số stage có đổi không - vì sao?
Ngày 2 - Deploy modes & Cluster Managers
Chủ đề:
clientmode vsclustermode (driver chạy ở đâu, hệ quả gì). Cluster managers: Standalone, YARN, và Kubernetes (hiện là hướng chính cho triển khai mới). Resource allocation: executor, core/executor, memory/executor - và toán sizing cơ bản.--num-executors,--executor-cores,--executor-memory.Trọng tâm kiến thức: Hiểu driver và executor giao tiếp ra sao; vì sao
collect()về driver có thể OOM driver; cluster manager chỉ cấp tài nguyên, còn lập lịch task là việc của Spark scheduler.Bài tập:
Submit cùng một app ở client mode và cluster mode, quan sát driver chạy ở đâu.
Tính: cluster có 6 node, mỗi node 16 core / 64GB. Đề xuất cấu hình executor hợp lý (gợi ý: chừa core/memory cho OS và overhead, tránh executor quá to gây GC). Giải thích lựa chọn.
Câu hỏi Vì sao thường KHÔNG nên cấu hình 1 executor chiếm trọn 1 node với toàn bộ core? Vì sao executor quá nhỏ (1 core) cũng tệ?
Ngày 2.5 - Scheduling: FIFO/FAIR pool & multi-tenancy trong một SparkContext
Chủ đề: Bên trong một
SparkContext, nhiều job (từ nhiều thread, nhiều query concurrent) cạnh tranh tài nguyên. Scheduler mode: FIFO (mặc định - job vào trước chạy hết trước) vs FAIR (spark.scheduler.mode=FAIR, chia sẻ tài nguyên theo round-robin giữa các pool). Scheduler pool: định nghĩa qua XML (spark.scheduler.allocation.file), mỗi pool cóweight,minShare,schedulingModeriêng; gán job vào pool quaspark.scheduler.pool(local property, set theo thread). Phân biệt với cluster-level scheduling (YARN queue/K8s namespace) - đây là scheduling bên trong một application, không phải giữa các application.Trọng tâm kiến thức: Hiểu vì sao một job
count()nhỏ có thể bị "kẹt" phía sau một job ETL lớn nếu dùng FIFO trên cùng context - và vì sao dịch vụ kiểu Spark Thrift Server (nhiều user chạy concurrent query) luôn cần FAIR + pool.Bài tập:
Viết app có 2 thread, mỗi thread submit một job vào cùng SparkContext (một job nhẹ, một job nặng). Chạy với FIFO, quan sát job nhẹ bị chờ. Chuyển sang FAIR + 2 pool có weight khác nhau, quan sát cả hai job chạy đan xen trong tab Jobs.
Viết file cấu hình pool XML với 2 pool:
high_priority(weight cao, minShare đảm bảo) vàdefault. Gán job vào từng pool quasc.setLocalProperty("spark.scheduler.pool", ...).
Câu hỏi FAIR scheduling trong một SparkContext khác gì với việc chạy nhiều SparkContext/application riêng trên cùng cluster?
minSharegiải quyết vấn đề gì mà chỉweightkhông giải quyết được?
Ngày 3 - SparkSession/SparkContext & cấu hình
Chủ đề:
SparkSession(entry point hợp nhất từ 2.0) vsSparkContextcũ; phân cấp cấu hình (spark-defaults.conf<spark-submit --conf< code); các config trọng yếu sẽ gặp lại (spark.sql.shuffle.partitions,spark.default.parallelism,spark.serializer, memory configs).Trọng tâm kiến thức: Hiểu cấu hình nào áp dụng lúc khởi tạo (không đổi được sau đó) vs runtime;
spark.sql.shuffle.partitions=200mặc định là cái bẫy hiệu năng kinh điển.Bài tập:
- In ra toàn bộ effective config của một session. Đổi
spark.sql.shuffle.partitionsvà quan sát số partition sau shuffle thay đổi trong UI.
- In ra toàn bộ effective config của một session. Đổi
Câu hỏi Khác nhau giữa
spark.default.parallelismvàspark.sql.shuffle.partitions? Cái nào ảnh hưởng RDD, cái nào ảnh hưởng DataFrame/SQL?
Ngày 4 - Đọc Spark UI
Chủ đề: Đào sâu từng tab: Jobs, Stages (timeline, task distribution, shuffle read/write), Tasks (xem phân bố thời gian → phát hiện skew), Storage (cached blocks), Environment, Executors (GC time, memory, shuffle), SQL/DataFrame tab (đọc query plan có chú thích metric), Streaming tab. History Server để xem lại job đã chạy.
Trọng tâm kiến thức: Đây là kỹ năng bạn dùng suốt phần đời còn lại làm Spark. Học cách phát hiện skew (1 task lâu hơn hẳn 99 task khác), spill (Spill memory/disk trong stage), GC overhead (cột GC Time trong Executors).
Bài tập:
Cố ý tạo data skew (một key chiếm 90% data) rồi
groupByKey. Vào tab Stages, tìm bằng chứng skew trong phân bố thời gian task.Cố ý tạo spill (shuffle lớn với ít memory) và tìm số liệu spill trong UI.
Câu hỏi Chỉ nhìn tab Stages, làm sao phân biệt một job chậm vì skew với một job chậm vì thiếu parallelism?
PHASE 2 - RDD core & internals của engine
Ngày 5 - RDD: trừu tượng cốt lõi
Chủ đề: RDD là gì (immutable, distributed, resilient). 5 thuộc tính định nghĩa một RDD: danh sách partition, hàm
computecho mỗi partition, danh sách dependency lên RDD cha, (tùy chọn) partitioner, (tùy chọn) preferred locations. Cách tạo RDD (parallelize, từ nguồn ngoài). Các loại RDD nguồn (textFile,wholeTextFiles,sequenceFile,hadoopRDD).Trọng tâm kiến thức: Hiểu RDD chỉ là mô tả cách tính, không phải data. Đọc source của
RDD.scalaxem 5 thuộc tính được khai báo abstract như thế nào.Bài tập:
Tạo RDD từ file văn bản lớn, kiểm tra số partition (
getNumPartitions), giải thích vì sao con số đó (liên quan block size HDFS /defaultParallelism).Đọc source
RDD.scala(các abstract method) và đối chiếu với 5 thuộc tính.
Câu hỏi Khi gọi
sc.textFile("file_2GB"), số partition được quyết định bởi cái gì?
Ngày 6 - Transformations, Actions & Lazy Evaluation
Chủ đề: Transformation (lazy, trả RDD mới) vs Action (kích hoạt thực thi). Lazy evaluation hoạt động ra sao và vì sao nó cho phép tối ưu (pipelining narrow transformation). Transformation phổ biến (
map,filter,flatMap,distinct,sample,union) và action (collect,count,take,reduce,saveAsTextFile).Trọng tâm kiến thức: Hiểu chuỗi narrow transformation được "fuse" trong cùng một stage và chạy pipelined trên mỗi partition không cần materialize trung gian.
Bài tập:
Tạo chuỗi
map().filter().map()rồicount(). Xác nhận trong UI tất cả nằm trong 1 stage.Áp dụng một hàm tùy chỉnh lên mỗi phần tử; lọc dòng theo điều kiện.
Câu hỏi Vì sao lazy evaluation cho phép Spark tránh đọc toàn bộ data khi bạn chỉ cần
take(5)?
Ngày 7 - Dependencies: Narrow vs Wide
Chủ đề: Narrow dependency (mỗi partition cha góp vào tối đa 1 partition con -
map,filter,union) vs Wide/shuffle dependency (groupByKey,reduceByKey,join,distinct,repartition). Các classDependencytrong source (NarrowDependency,ShuffleDependency). Lineage graph và fault recovery (mất partition → tính lại theo lineage).Trọng tâm kiến thức: Wide dependency = shuffle = ranh giới stage. Đây là khái niệm trung tâm nhất của hiệu năng Spark. Recompute theo lineage là cơ chế fault tolerance (khác replication).
Bài tập:
Với mỗi transformation trong danh sách, phân loại narrow/wide. Kiểm chứng bằng UI (đếm stage).
Gây lỗi một executor giữa chừng (kill process) và quan sát Spark tính lại partition mất theo lineage.
Câu hỏi Vì sao
reduceByKeyrẻ hơngroupByKeydù cả hai đều shuffle? (Gợi ý: map-side combine.)
Ngày 8 - Pair RDD & Partitioning
Chủ đề: Pair RDD (key-value). Phép toán:
reduceByKey,groupByKey,aggregateByKey,combineByKey,sortByKey,join,cogroup. Partitioner:HashPartitioner,RangePartitioner, custom partitioner. Vì sao partitioning đúng giúp tránh shuffle (co-partitioned join).Trọng tâm kiến thức: Hiểu
combineByKeylà nền củareduceByKey/aggregateByKey(đọc source). Hiểu khi hai RDD cùng partitioner, join là narrow (không shuffle). Đây là kỹ thuật tối ưu nâng cao.Bài tập:
Tính trung bình điểm theo lớp dùng
aggregateByKey(không dùnggroupByKey) - giải thích vì sao tốt hơn.Tìm sản phẩm bán chạy nhất mỗi tháng; làm phép
cogroup.Pre-partition hai RDD cùng một
HashPartitionerrồi join; xác nhận trong UI rằng join không tạo shuffle stage mới.
Câu hỏi Hai RDD đã hash-partition cùng số partition trên cùng key thì khi join có shuffle không? Vì sao?
Ngày 9 - SHUFFLE INTERNALS
Chủ đề: Shuffle là gì ở mức cơ chế. Map side: mỗi task ghi shuffle output phân theo target partition dùng partitioner; Sort-based shuffle ghi 1 data file + 1 index file mỗi map task. Reduce side: mỗi reduce task fetch các block liên quan từ mọi map output (shuffle read). External Shuffle Service (tách việc phục vụ shuffle file khỏi executor → cho phép dynamic allocation gỡ executor mà không mất shuffle data). Push-based shuffle / Magnet (Spark 3.2+, gộp block phía service giảm random small reads). Lịch sử: HashShuffle (deprecated) → SortShuffle → Tungsten/Unsafe sort shuffle (sort trên dữ liệu serialized).
Trọng tâm kiến thức: Đây là nơi tiền của bạn cháy. Hiểu shuffle write tạo bao nhiêu file, vì sao "small files / too many partitions" gây nghẽn, vì sao shuffle là điểm fail và là điểm chậm. Đọc khái niệm
ShuffleManager,SortShuffleWriter.Bài tập:
Chạy một
reduceByKeylớn; trong UI đọc Shuffle Write (stage map) và Shuffle Read (stage reduce). Đối chiếu số byte.Bật/tắt external shuffle service (nếu môi trường cho phép) + dynamic allocation, quan sát hành vi.
Đo ảnh hưởng của
spark.sql.shuffle.partitionslên thời gian một aggregation: thử 8, 200, 2000 partition trên cùng data nhỏ và giải thích đường cong hiệu năng.Salting thủ công (làm trước khi học AQE ở Ngày 15): Tạo một
joinbị skew nặng (một key chiếm >80% data ở bảng lớn). Tự tay fix bằng salting: thêm cộtsalt = key + "_" + rand(0, N)vào bảng lớn (N ví dụ = 10), explode bảng nhỏ thành N bản mỗi bản gắn một giá trị salt tương ứng (0..N-1), join theo(key, salt)thay vìkey, rồigroupBylại theokeygốc để gộp kết quả. Đo lại phân bố thời gian task trong UI trước/sau khi salting - task lâu nhất có còn lệch xa 99 task khác không?
Câu hỏi Vì sao đặt số shuffle partition quá cao trên data nhỏ lại làm CHẬM? Vì sao quá thấp trên data lớn gây spill/OOM? Nguyên tắc chọn? Salting giải quyết skew bằng cách nào ở mức phân bố key vào partition - vì sao phải explode bên nhỏ ra N lần chứ không salt cả hai bên?
Ngày 10 - Memory Management, Persistence & Serialization
Chủ đề: Unified Memory Manager (từ 1.6): vùng dùng được = (heap −
300MB reserved) ×40% còn lại cho cấu trúc dữ liệu của bạn/UDF). Spill khi execution memory không đủ. Persistence:spark.memory.fraction(mặc định 0.6), chia thành execution memory (shuffle/join/sort/agg) và storage memory (cache/broadcast) với ranh giới mềmstorageFraction(0.5) - execution có thể mượn và evict storage, storage không evict được execution. Off-heap (spark.memory.offHeap.*, Tungsten dùngUnsafe). User memory (cache()vspersist(StorageLevel), các mức (MEMORY_ONLY, MEMORY_AND_DISK, ser/deser, replicated),unpersist. Checkpointing (cắt lineage, ghi xuống reliable storage) khác cache (chỉ tăng tốc). Serialization: Java vs Kryo (đăng ký class), ảnh hưởng tới cả storage lẫn shuffle. Broadcast variables & Accumulators.Trọng tâm kiến thức: Hiểu OOM và spill từ gốc rễ thay vì đoán. Hiểu vì sao cache RDD lớn lâu dài ép execution phải spill. Hiểu broadcast variable phân phối read-only data hiệu quả (tránh gửi copy theo từng task).
Bài tập:
Cache một DataFrame và xem tab Storage: bao nhiêu % cached, fraction nào memory/disk. Đổi StorageLevel sang MEMORY_AND_DISK_SER và so sánh footprint.
Bật Kryo, đăng ký class, đo chênh lệch shuffle/storage size.
Dùng broadcast variable chia sẻ một từ điển lớn cho mọi worker; dùng accumulator đếm bản ghi lỗi. Quan sát giá trị accumulator (lưu ý: accumulator chỉ đáng tin trong action, vì task có thể chạy lại).
Cố tình gây spill và đọc số liệu Spill (Memory)/Spill (Disk) trong UI.
Câu hỏi Khi execution cần memory mà storage đang giữ cache, điều gì xảy ra? Ngược lại thì sao? Vì sao thiết kế bất đối xứng này hợp lý?
PHASE 3 - Spark SQL, Catalyst & Tungsten
Ngày 11 - DataFrame, Dataset, SQL & tại sao chúng nhanh hơn RDD
Chủ đề: Giới thiệu Spark SQL. DataFrame (=
Dataset[Row]) vs Dataset (typed) vs RDD. Các cách tạo DataFrame (từ RDD, CSV/JSON/Parquet, JDBC). Schema & các kiểu dữ liệu. Vì sao DataFrame nhanh hơn RDD (Spark hiểu cấu trúc data → tối ưu được; RDD là hộp đen với engine).Trọng tâm kiến thức: Hiểu vì sao một
df.filter(...)có thể được tối ưu cònrdd.filter(closure)thì không - engine không nhìn được vào closure. Đây là động lực cho cả Catalyst.Bài tập:
Cùng một phép tính viết bằng RDD và DataFrame; so sánh thời gian + đọc plan của bản DataFrame.
Kết nối Spark SQL với PostgreSQL/MySQL qua JDBC, truy vấn (chú ý partitioning khi đọc JDBC để song song hóa).
Câu hỏi Vì sao DataFrame API thường nhanh hơn RDD ngay cả khi cùng logic?
Ngày 12 - CATALYST OPTIMIZER (ngày cốt lõi)
Chủ đề: Catalyst là rule-based + cost-based optimizer dựa trên cây (tree) và luật (rule). Pipeline đầy đủ: SQL/DataFrame API → Unresolved Logical Plan → (Analysis: resolve tên cột/bảng theo Catalog) → Resolved Logical Plan → (Logical Optimization: các rule như predicate pushdown, projection/column pruning, constant folding, boolean simplification, null propagation) → Optimized Logical Plan → (Physical Planning: sinh nhiều physical plan, chọn strategy) → Physical Plan → (Code Generation: Tungsten). Tree & expression.
Rule[LogicalPlan].Trọng tâm kiến thức: Đây là nơi "magic" nằm. Học đọc
.explain(mode="formatted")và.explain(mode="extended")(in cả 4 plan). Học nhận diện predicate pushdown và column pruning trong plan thật.Bài tập:
Viết query có
filter+selectvài cột trên Parquet.explain(mode="extended")và chỉ ra: filter có bị đẩy xuống scan (PushedFilters) không? Cột có bị prune không?So sánh plan của một query trước/sau khi thêm filter, quan sát cách Catalyst di chuyển filter.
Đọc tài liệu/source về một vài optimization rule cụ thể (vd
PushDownPredicates,ColumnPruning).
Câu hỏi "Predicate pushdown" và "column pruning" là gì, và chúng diễn ra ở phase nào của Catalyst? Vì sao chúng giảm I/O đáng kể trên Parquet?
Ngày 13 - Physical Planning, Join Strategies, CBO & Dynamic Partition Pruning
Chủ đề: Từ logical → physical: chọn chiến lược thực thi. Join strategies: Broadcast Hash Join (khi một bên đủ nhỏ -
spark.sql.autoBroadcastJoinThreshold), Sort-Merge Join (mặc định cho bảng lớn), Shuffle Hash Join, Broadcast Nested Loop. Cost-Based Optimization (CBO): cần statistics (ANALYZE TABLE ... COMPUTE STATISTICS), join reordering, ước lượng cardinality. Dynamic Partition Pruning (DPP, Spark 3.0+): cho star-schema join - bảng fact lớn partition theo key (vddate) join với bảng dimension nhỏ có filter (vdWHERE region = 'X'). Spark tạo subquery filter trên dimension, tái sử dụng kết quả đó (ReusedSubquery) để cắt partition đọc ở bảng fact - cắt I/O trước khi scan chứ không phải sau. Phân biệt static DPP (filter là literal, quyết định lúc compile) vs dynamic DPP (filter phụ thuộc kết quả runtime của bên dimension). Điều kiện để DPP kích hoạt: bảng fact phải partition theo đúng cột join, và optimizer phải chọn broadcast bên dimension.Trọng tâm kiến thức: Hiểu vì sao broadcast join cực nhanh (tránh shuffle bên lớn) nhưng nguy hiểm nếu bên "nhỏ" thực ra không nhỏ (OOM). Học đọc loại join trong physical plan. DPP khác predicate pushdown thông thường - predicate pushdown đẩy filter đã biết xuống scan; DPP đẩy filter phụ thuộc kết quả một nhánh khác của plan xuống scan nhánh này. Đọc plan sẽ thấy
PartitionFilters: [isnotnull(...), dynamicpruning#...].Bài tập:
Join một bảng lớn với bảng nhỏ; xác nhận Spark chọn BroadcastHashJoin. Tăng kích thước bảng nhỏ vượt ngưỡng và xem nó chuyển sang SortMergeJoin.
Ép broadcast bằng
broadcast()hint và quan sát plan.Bật CBO, chạy
ANALYZE TABLE, so sánh plan của một multi-join query.Tạo bảng fact partition theo
date(hàng chục partition) và bảng dimension nhỏ chứa danh sáchdateđược chọn. Join + filter trên dimension,explain(mode="formatted"), tìmdynamicpruningtrongPartitionFilters. Tắt DPP (spark.sql.optimizer.dynamicPartitionPruning.enabled=false), so sánh số partition/file được đọc và thời gian.
Câu hỏi Khi nào Sort-Merge Join thắng Broadcast Join? Rủi ro của việc ép broadcast một bảng "tưởng là nhỏ"? DPP khác predicate pushdown ở điểm nào? Vì sao DPP chỉ hoạt động hiệu quả khi bên dimension được broadcast?
Ngày 14 - TUNGSTEN: execution engine
Chủ đề: Tungsten là gì và giải quyết vấn đề gì (object Java béo, GC, virtual call overhead). UnsafeRow: biểu diễn nhị phân compact, cache-friendly, lưu được off-heap. Whole-Stage Code Generation: fuse nhiều operator trong một stage thành một hàm Java sinh ra (từ Volcano iterator model → vòng lặp hợp nhất), giảm virtual call và object trung gian. Cache-aware computation, explicit memory management.
Trọng tâm kiến thức: Đọc plan có ký hiệu
*(dấu sao = node nằm trong whole-stage codegen). Dùng.explain(mode="codegen")để xem code Java sinh ra. Hiểu vì sao một số operation "phá" codegen (boundary).Bài tập:
explain(mode="formatted")một aggregation; tìm các node có*(n)(codegen stage). Sau đóexplain(mode="codegen")đọc một đoạn code sinh ra.Tìm một query mà codegen bị ngắt (vd UDF Python) và quan sát plan mất dấu
*.
Câu hỏi Dấu
*trong physical plan nghĩa là gì? Vì sao whole-stage codegen nhanh hơn mô hình iterator truyền thống?
Ngày 15 - Adaptive Query Execution (AQE) & Data Source V2
Chủ đề: AQE (Spark 3.0+, mặc định bật từ 3.2): tối ưu lại physical plan lúc runtime dựa trên statistics thật từ các shuffle stage đã hoàn thành. Ba tính năng chính: (1) coalesce shuffle partitions động (gộp partition nhỏ), (2) chuyển join strategy động (sang broadcast khi một bên hóa ra nhỏ), (3) xử lý skew join động (chia partition bị skew). Data Source V2 API: cách connector hoạt động, pushdown (predicate, projection, partition pruning, aggregate pushdown mới hơn).
Trọng tâm kiến thức: AQE là lý do nhiều vấn đề skew/partition tự khắc phục ở Spark mới - nhưng phải hiểu nó để biết khi nào nó không cứu được bạn. Trong UI, plan AQE hiển thị "AdaptiveSparkPlan" và đổi sau khi chạy.
Bài tập:
Bật/tắt AQE trên một aggregation tạo nhiều partition nhỏ; quan sát số output partition và thời gian.
Tạo skew join, bật AQE skew handling (
spark.sql.adaptive.skewJoin.enabled), quan sát partition skew bị chia nhỏ trong UI. So sánh kết quả với salting thủ công đã làm ở Ngày 9 - cái nào cắt partition mịn hơn, cái nào tốn công viết code hơn?
Câu hỏi AQE lấy statistics ở đâu để tái tối ưu, và tại sao nó chỉ làm được điều đó GIỮA các stage chứ không phải lúc compile?
Ngày 16 - SQL Functions, UDF & Window Functions
Chủ đề: Built-in functions (aggregate, string, date/time, collection, higher-order functions trên array/map). UDF (và cái giá của nó: phá codegen, hộp đen với Catalyst - luôn ưu tiên built-in). Window functions (
over(partitionBy ... orderBy ... rowsBetween ...)) và cách chúng shuffle. Transformation trên DataFrame:select,filter,withColumn,groupBy,agg,orderBy,join,union.Trọng tâm kiến thức: Hiểu vì sao UDF Scala/Java đã đắt, UDF Python còn đắt hơn (sẽ đào ở Phase 5). Hiểu window function tạo shuffle theo
partitionBy.Bài tập:
Tính DAU/WAU/MAU; tìm bài đăng top like/comment; dùng window function xếp hạng trong nhóm.
Viết một logic bằng UDF, rồi viết lại hoàn toàn bằng built-in/higher-order function; so sánh plan (codegen có quay lại không?) và thời gian.
Câu hỏi Vì sao "tránh UDF khi có built-in" là lời khuyên hiệu năng quan trọng?
Ngày 17 - Datasets, Encoders & Performance Tuning của Spark SQL
Chủ đề: Dataset typed, type-safe operations, Encoder (cầu nối object JVM ↔ Tungsten binary), đánh đổi giữa type-safety và tối ưu (lambda trên Dataset có thể là hộp đen với Catalyst, giống RDD). Tuning Spark SQL: caching/persistence, partitioning & bucketing (tránh shuffle khi join lặp lại), broadcast joins, kết hợp AQE.
Trọng tâm kiến thức: Hiểu Encoder sinh code để serialize/deserialize hiệu quả; hiểu lambda trên Dataset (
ds.map(x => ...)) làm Catalyst "mù" giống RDD nên đôi khi DataFrame API thắng Dataset typed về tốc độ.Bài tập:
Chuyển DataFrame sang Dataset với case class; làm vài phép type-safe; tạo Encoder cho case class phức tạp.
Bucket hai bảng theo cùng key & cùng số bucket; join nhiều lần; xác nhận không còn shuffle (so với non-bucketed).
Câu hỏi Vì sao bucketing cải thiện join lặp lại, và khác gì với partitioning theo thư mục?
PHASE 4 - Storage formats, partitioning chiến lược & Lakehouse
Ngày 18 - File formats internals: Parquet, ORC, Avro
Chủ đề: Row-based vs columnar. Parquet internals: row groups → column chunks → pages; encoding (dictionary, RLE, bit-packing); statistics (min/max per column chunk) cho phép page/row-group skipping; predicate pushdown qua statistics. ORC (stripes, index). Avro (row-based, tốt cho streaming/ingest). CSV/JSON: vì sao chậm và nguy hiểm (không splittable khi nén gzip, schema inference tốn kém, không pushdown). Compression & splittability (snappy vs gzip vs zstd).
Trọng tâm kiến thức: Hiểu vì sao "đọc 2 cột từ Parquet 100 cột" chỉ tốn ~2% I/O (column pruning + columnar) còn CSV phải đọc hết. Hiểu vì sao file gzip không splittable → 1 file = 1 task = nghẽn.
Bài tập:
Cùng một dataset lưu CSV, JSON, Parquet; so sánh kích thước và thời gian một query lọc + chọn vài cột.
Đọc Parquet và xác nhận PushedFilters trong plan; xác nhận chỉ các cột cần thiết được đọc.
Câu hỏi Vì sao Parquet cho phép "predicate pushdown" mà CSV thì không?
Ngày 19 - Partitioning trên đĩa, bucketing & vấn đề small files
Chủ đề: Partitioning khi ghi (
partitionBy) → cấu trúc thư mục → partition pruning lúc đọc. Chọn partition column đúng (cardinality vừa phải, theo pattern query). Bucketing. Small files problem (quá nhiều file nhỏ giết metadata/scheduling).repartitionvscoalesce(coalesce tránh full shuffle nhưng có thể gây skew),maxRecordsPerFile.Trọng tâm kiến thức: Hiểu partition pruning cắt I/O ở tầng thư mục trước cả khi đọc file. Hiểu vì sao over-partitioning (vd theo
user_id) tạo hàng triệu thư mục → thảm họa.Bài tập:
Ghi data có/không
partitionBy(date); query lọc theo date; so sánh số file/partition được đọc (partition pruning) trong plan.Tạo tình huống small files rồi compact bằng
coalesce/repartition; đo chênh lệch thời gian đọc.
Câu hỏi Khác nhau giữa
repartition(n)vàcoalesce(n)? Khi nào coalesce gây skew?
Ngày 20 - Lakehouse: Delta Lake / Iceberg / Hudi & concurrency conflicts
Chủ đề: Vì sao cần table format trên data lake (file thô không có ACID, không atomic, không schema enforcement). Delta Lake: transaction log (
_delta_log), ACID, time travel, schema evolution/enforcement,MERGE/UPDATE/DELETE,OPTIMIZE+ Z-ordering / liquid clustering, compaction, vacuum. Iceberg: hidden partitioning, snapshot isolation, metadata layers. Hudi: copy-on-write vs merge-on-read. Kiến trúc medallion (bronze/silver/gold). Concurrent write conflicts: cả Delta/Iceberg dùng optimistic concurrency control - hai writer commit cùng lúc vào cùng bảng/partition có thể khiến một bên fail với conflict exception (vdConcurrentAppendException,ConcurrentDeleteReadException). Cách giảm conflict: partition ghi riêng theo writer/pipeline khi có thể, thu hẹp điều kiệnMERGEđể giảm vùng chồng lấn, retry với backoff ở tầng orchestration.Trọng tâm kiến thức: Hiểu transaction log của Delta hoạt động ra sao (optimistic concurrency, mỗi commit là một file JSON + checkpoint Parquet định kỳ). Hiểu optimistic concurrency đánh đổi gì so với locking truyền thống - throughput cao khi ít conflict, nhưng cần logic retry khi conflict xảy ra. Đây là nền của data engineering hiện đại trên Spark.
Bài tập:
Tạo Delta table; làm vài
INSERT/UPDATE/MERGE; xem_delta_logthay đổi; dùng time travel đọc version cũ.Chạy
OPTIMIZEvới Z-order trên một cột query thường xuyên; đo chênh lệch.Chạy 2 job đồng thời cùng ghi/update vào cùng bảng Delta (cùng partition, điều kiện
MERGEchồng lấn); quan sát một job fail với conflict exception. Sửa lại để 2 job ghi vào 2 partition khác nhau, xác nhận cả hai chạy thành công không conflict. Viết logic retry đơn giản (catch exception, backoff, thử lại) bao ngoài job.
Câu hỏi Delta đảm bảo atomic write như thế nào trên object store (S3) vốn không có rename nguyên tử thật sự? Vai trò của transaction log? Optimistic concurrency control khiến job của bạn có thể fail dù logic đúng - trong trường hợp nào và tại sao đó là đánh đổi hợp lý?
Ngày 21 - Tích hợp hệ lưu trữ: HDFS, S3/object store, Cassandra, MongoDB, JDBC
Chủ đề: Đọc/ghi HDFS, S3 (và các đặc thù: eventual consistency lịch sử, committer, vì sao
_SUCCESS/commit protocol quan trọng). Connector Cassandra/MongoDB. JDBC đọc song song (partitionColumn,lowerBound,upperBound,numPartitions) - bẫy kinh điển: đọc JDBC 1 partition = 1 luồng nghẽn.Trọng tâm kiến thức: Hiểu commit protocol khi ghi ra object store và vì sao "ghi xong rồi job fail" có thể để lại file rác.
Bài tập:
Đọc JDBC một bảng lớn không partition (1 task) rồi có partition (
numPartitions=8); so sánh.Đọc/ghi Cassandra hoặc MongoDB; quan sát pushdown nếu connector hỗ trợ.
Câu hỏi Khi đọc JDBC, điều gì quyết định mức song song? Vì sao thiếu
partitionColumnthường khiến cả terabyte chui qua một executor?
PHASE 5 - PySpark internals & hiệu năng Python
Ngày 22 - PySpark internals: Py4J & ranh giới JVM-Python
Chủ đề: Kiến trúc PySpark: Py4J nối Python driver ↔ JVM driver. Với DataFrame/SQL: mọi xử lý xảy ra trong JVM (Catalyst/Tungsten) → PySpark DataFrame nhanh ngang Scala. Với RDD/UDF Python row-at-a-time: data phải serialize JVM→Python worker (pickle), xử lý, serialize ngược → cực đắt. SparkSession trong PySpark, chuyển đổi RDD↔DataFrame↔Pandas.
Trọng tâm kiến thức: Đây là kiến thức quan trọng nhất của PySpark. Vẽ sơ đồ: data đi qua bao nhiêu lần serialize trong một Python UDF vs một built-in.
Bài tập:
Cùng một phép biến đổi: (a) bằng built-in DataFrame function, (b) bằng Python UDF row-at-a-time. Đo chênh lệch (thường 3–10×) và giải thích bằng sơ đồ serialize.
Viết lại vài bài tập Spark SQL trước đó bằng PySpark thuần DataFrame (không UDF).
Câu hỏi Vì sao một PySpark DataFrame query (không UDF) chạy nhanh ngang Scala, nhưng một Python UDF lại chậm hơn nhiều so với UDF Scala?
Ngày 23 - Arrow & Vectorized (pandas) UDFs
Chủ đề: Apache Arrow làm gì (định dạng cột chung để truyền data JVM↔Python hiệu quả, không pickle từng dòng). Pandas UDF / vectorized UDF: Series→Series, iterator-of-Series, grouped map, grouped agg - xử lý theo batch bằng pandas, nhanh hơn nhiều row-at-a-time. Arrow-optimized Python UDF (Spark 3.5/4.0). Pandas API on Spark (Koalas cũ). Python Data Source API & Python UDTF (Spark 4.0).
Trọng tâm kiến thức: Hiểu vì sao "vectorized" thắng "row-at-a-time": amortize chi phí qua batch + tránh interpreter overhead + truyền cột qua Arrow.
Bài tập:
Viết cùng logic bằng (a) Python UDF thường, (b) pandas UDF (Series→Series); đo chênh lệch.
Dùng grouped-map pandas UDF để áp một hàm pandas lên mỗi nhóm; quan sát.
Câu hỏi Pandas UDF nhanh hơn UDF thường nhờ HAI cơ chế nào (truyền data + xử lý)?
Ngày 24 - Tiền xử lý văn bản & NLP với PySpark
Chủ đề: Pipeline tiền xử lý văn bản (lowercase, bỏ ký tự đặc biệt/URL/emoji, tokenization, stop words, stemming/lemmatization). Tích hợp NLTK/spaCy (chạy trong Python worker - cẩn thận chi phí). Trích xuất feature: bag-of-words, TF-IDF (
HashingTF/CountVectorizer+IDFtrong MLlib).Trọng tâm kiến thức: Khi nào nên làm NLP trong Spark (data lớn, song song) vs gom về một máy. Chi phí phân phối model spaCy tới worker.
Bài tập:
Tiền xử lý data văn bản mạng xã hội bằng PySpark; trích xuất hashtag/mention bằng built-in string functions (không UDF nếu được).
Tạo TF-IDF features chuẩn bị cho mô hình sentiment.
Câu hỏi Khi nào việc dùng spaCy trong một UDF là hợp lý, và khi nào nó biến cluster của bạn thành cái máy đắt tiền chạy chậm?
Ngày 25 - Trực quan hóa & cầu nối ra hệ sinh thái Python
Chủ đề:
toPandas()(và bẫy: kéo toàn bộ về driver → OOM; dùng Arrow để tăng tốc), sampling trước khi visualize, tích hợp Matplotlib/Seaborn/Plotly cho kết quả đã aggregate. Native plotting trong PySpark (Spark 4.0).Bài tập:
Aggregate data lớn trong Spark, chỉ
toPandas()phần kết quả nhỏ, visualize.Cố ý
toPandas()một DataFrame lớn để thấy/giải thích rủi ro driver OOM.
Câu hỏi Vì sao
toPandas()trên một DataFrame lớn là nguy hiểm, và pattern đúng là gì?
PHASE 6 - Structured Streaming: internals & ứng dụng
Ngày 26 - Mô hình Structured Streaming & incremental execution
Chủ đề: Stream-as-unbounded-table. Cùng DataFrame API cho batch và stream (unified). Micro-batch engine (mặc định) vs Continuous Processing (experimental, latency thấp). Trigger types (default, fixed interval, once/availableNow). Sources & sinks. So sánh ngắn với DStreams (và vì sao DStreams legacy).
Trọng tâm kiến thức: Hiểu engine biến stream thành chuỗi micro-batch và chạy incremental query - kết quả mỗi batch là phần "mới" của một query batch chạy trên toàn bộ data tới giờ.
Bài tập:
- Đọc stream từ socket/file source; viết một aggregation; quan sát từng micro-batch trong Streaming tab.
Câu hỏi "Stream như một bảng vô hạn" nghĩa là gì về mặt thực thi?
Ngày 27 - Checkpoint, offset & exactly-once
Chủ đề: Source tracking offsets; engine commit offset vào checkpoint (write-ahead log) để fault tolerance & exactly-once (end-to-end khi sink idempotent/transactional). Cấu trúc thư mục checkpoint (offsets, commits, state, metadata). Recovery sau khi restart.
Trọng tâm kiến thức: Hiểu vì sao đổi code/schema có thể làm checkpoint không tương thích; vì sao checkpoint là bắt buộc cho stateful stream.
Bài tập:
Chạy stream có checkpoint; kill và restart; xác nhận nó resume đúng offset (không mất/không trùng).
Mở thư mục checkpoint, xem các file offsets/commits.
Câu hỏi Spark đạt exactly-once đầu-cuối nhờ sự kết hợp của những thành phần nào (offset tracking + sink)?
Ngày 28 - Watermarking, late data & STATE STORE internals
Chủ đề: Event-time vs processing-time. Watermark bound state và xử lý late data (drop dữ liệu trễ quá ngưỡng). Windowed aggregation (tumbling, sliding, session). State Store internals: keyed state, mặc định HDFS-backed state store vs RocksDB state store (Spark 3.2+, cho state lớn - giảm áp lực JVM heap/GC), state versioning & checkpoint. Streaming state data source (Spark 4.0 - query/debug state).
Trọng tâm kiến thức: Đây là phần "internal" của streaming. Hiểu state lưu ở đâu, lớn lên thế nào, vì sao watermark cần thiết để state không phình vô hạn, vì sao RocksDB state store cứu các ứng dụng state lớn.
Bài tập:
Windowed count theo event-time với watermark; gửi vài bản ghi trễ và quan sát cái nào được tính / bị drop.
Bật RocksDB state store cho một aggregation state lớn; so sánh GC/memory với HDFS state store.
Câu hỏi Không có watermark, điều gì xảy ra với state của một windowed aggregation chạy mãi? RocksDB state store giải quyết áp lực nào?
Ngày 29 - Stateful operations & Arbitrary Stateful Processing
Chủ đề: Stream-stream joins (cần watermark cả hai phía), deduplication,
flatMapGroupsWithState/mapGroupsWithState, và Arbitrary Stateful Processing V2 /transformWithState(Spark 4.0) cho logic state tùy ý (timers, multiple state types). (Đối chiếu ngắn DStreamsupdateStateByKey/mapWithStateđể biết lịch sử.)Bài tập:
Theo dõi xu hướng hashtag theo thời gian thực bằng stateful aggregation.
Xây dựng phát hiện bất thường đơn giản bằng stateful op (giữ trạng thái theo key, raise khi vượt ngưỡng).
Câu hỏi Vì sao stream-stream join cần watermark trên cả hai stream?
Ngày 30 - Kafka & kiến trúc streaming production
Chủ đề: Kafka source/sink sâu (offset management,
startingOffsets,maxOffsetsPerTriggercho rate limit, partition→task mapping). Kiến trúc ingest: Kafka → Structured Streaming → Delta (bronze) → silver/gold. Kinesis/Flume (ngắn). Backpressure, monitoring streaming.Trọng tâm kiến thức: Hiểu Kafka partition ánh xạ 1-1 tới Spark task → song song của bạn bị giới hạn bởi số partition Kafka.
Bài tập:
Xây pipeline Kafka → Structured Streaming → Delta table; lọc theo hashtag, đếm tweet/phút; ghi exactly-once vào Delta.
Đặt
maxOffsetsPerTriggerđể rate-limit; quan sát batch size ổn định.
Câu hỏi Nếu topic Kafka có 4 partition, mức song song tối đa của stage đọc Kafka là bao nhiêu? Làm sao tăng?
PHASE 7 - MLlib & Graph (GraphX/GraphFrames)
Ngày 31 - Spark ML Pipelines: kiến trúc
Chủ đề:
Transformer,Estimator,Pipeline,PipelineModel,Param. Feature transformers (Tokenizer,StopWordsRemover,HashingTF/CountVectorizer,IDF,StringIndexer,OneHotEncoder,VectorAssembler,StandardScaler). Phân loại sentiment với Logistic Regression / Naive Bayes.Trọng tâm kiến thức: Hiểu vì sao Pipeline (fit→transform) tránh data leakage và đóng gói tái lập được. Estimator phân tán fit thế nào (mỗi iteration là một job với shuffle để aggregate gradient/statistics).
Bài tập:
Xây pipeline đầy đủ Tokenizer→StopWords→TF-IDF→LogisticRegression dự đoán sentiment.
Đánh giá: accuracy, precision, recall, F1.
Câu hỏi Vì sao dùng Pipeline thay vì tự nối các bước thủ công? Data leakage xảy ra thế nào nếu fit scaler trước khi split?
Ngày 32 - Thuật toán phân tán: chúng thực sự chạy thế nào
Chủ đề: Phân tán của: Logistic Regression / Linear (gradient aggregation qua treeAggregate mỗi iteration), K-means (mỗi iteration một pass tính centroid), ALS collaborative filtering (block partitioning), tree ensembles. Clustering: K-means, Gaussian Mixture, LDA (topic modeling). Giảm chiều: PCA, SVD.
Trọng tâm kiến thức: Hiểu mỗi iteration của gradient descent phân tán = một job Spark có shuffle để gộp gradient → vì sao iterative ML trên Spark cần cache data và vì sao quá nhiều iteration tốn kém.
Bài tập:
Cluster người dùng theo hành vi (K-means); chọn k bằng elbow.
LDA tìm chủ đề tiềm ẩn trong bài đăng.
Giảm chiều TF-IDF bằng PCA/SVD; so sánh hiệu năng mô hình sentiment.
Câu hỏi Vì sao nên cache training data khi chạy thuật toán iterative như Logistic Regression hay K-means?
Ngày 33 - Feature engineering nâng cao, tuning & evaluation
Chủ đề: Tạo feature mới (polynomial, interactions, độ dài bài/số hashtag), chọn feature (ChiSqSelector),
CrossValidator/TrainValidationSplit,ParamGridBuilder(grid/random search), metric (accuracy/precision/recall/F1/AUC-ROC), confusion matrix.Trọng tâm kiến thức: Cross-validation × grid = nhiều job; hiểu chi phí tính toán bùng nổ và cách giới hạn.
Bài tập:
Tune mô hình sentiment bằng CrossValidator + grid; tìm hyperparameter tốt nhất.
Vẽ confusion matrix, phân tích lỗi.
Ngày 34 - GraphX & Pregel
Chủ đề: GraphX:
Graph,VertexRDD,EdgeRDD, triplet view, partitioning strategy của đồ thị (vì sao quan trọng cho hiệu năng). Thuật toán: PageRank, Connected Components, Triangle Counting, Shortest Paths. Pregel API (think-like-a-vertex, superstep, message passing).Trọng tâm kiến thức: Hiểu mô hình Pregel (mỗi superstep: vertex nhận message → cập nhật → gửi message) và vì sao nó hợp với đồ thị phân tán.
Bài tập:
Xây đồ thị mạng xã hội; tính PageRank tìm người ảnh hưởng nhất; Connected Components tìm cộng đồng.
Dùng Pregel mô phỏng lan truyền thông tin (mô hình dịch).
Câu hỏi Mỗi superstep của Pregel tương ứng gì trong mô hình thực thi Spark?
Ngày 35 - GraphFrames & tích hợp
Chủ đề: GraphFrames (API DataFrame-based, tạo từ vertices/edges DataFrame),
degrees/inDegrees/outDegrees, motif finding (tìm mẫu con qua pattern), thuật toán tích hợp (PageRank, CC, SCC, Label Propagation, BFS, Triangle Count). Tích hợp NetworkX để visualize (trên đồ thị nhỏ/sample).Bài tập:
Tạo GraphFrame từ data mạng xã hội; tìm người nhiều bạn nhất; tìm tam giác bằng motif.
Tìm cộng đồng bằng Label Propagation; shortest path giữa hai user.
Câu hỏi Khi nào chọn GraphFrames vs GraphX?
PHASE 8 - Production, deployment, tuning mastery & Spark Connect
Ngày 36 - spark-submit, đóng gói & deploy
Chủ đề:
spark-submitđầy đủ (--master,--deploy-mode,--conf,--files,--jars,--packages,--py-files), đóng gói (uber JAR với sbt-assembly cho Scala; zip/wheel/--archives+ conda-pack cho Python deps), quản lý dependency conflict, phân cấp config runtime.Bài tập:
- Đóng gói app phân tích mạng xã hội thành JAR (Scala) hoặc bundle (Python) và submit lên cluster.
Câu hỏi Làm sao phân phối thư viện Python (vd spaCy) tới mọi executor một cách tái lập?
Ngày 36.5 - Orchestration & idempotency: Spark job không sống một mình
Chủ đề: Vì sao production luôn cần một orchestrator (Airflow, Dagster, hoặc Argo Workflows trên K8s) điều phối Spark job: quản lý dependency giữa các bước (bronze→silver→gold), schedule, retry policy (số lần thử lại, backoff), backfill (chạy lại cho khoảng thời gian trong quá khứ), SLA/alerting khi job trễ hoặc fail. Khái niệm idempotency: một job phải chạy lại nhiều lần (do retry/backfill) mà không sinh ra kết quả trùng hoặc sai - kỹ thuật đạt idempotent write trên Spark:
overwritetheo partition (INSERT OVERWRITE/replaceWheretrên Delta) thay vìappendvô điều kiện, dùngMERGEvới key duy nhất, hoặc transactional sink. Phân biệt orchestration-level retry (submit lại cả job) vs Spark-level task retry (Spark tự retry task fail, đã có sẵn) - đây là hai lớp khác nhau.Trọng tâm kiến thức: Hiểu vì sao một pipeline
append-only sẽ tạo dữ liệu trùng lặp nếu orchestrator retry job đã ghi được nửa đường - và tại sao đây là lỗi production rất thường gặp với người mới học Spark (vốn chỉ quen chạy job một lần trong notebook).Bài tập:
Viết một Airflow DAG (hoặc Dagster job) đơn giản với 3 task nối tiếp mô phỏng bronze→silver→gold, có retry policy và dependency rõ ràng.
Viết một job ghi Delta theo kiểu không idempotent (
appendvô điều kiện) cho một ngày dữ liệu, giả lập job fail giữa chừng rồi chạy lại - quan sát dữ liệu bị trùng. Sửa lại dùngreplaceWheretheo partition ngày để idempotent, chạy lại nhiều lần và xác nhận kết quả không đổi.
Câu hỏi Vì sao "Spark tự động retry task fail" không đủ để đảm bảo pipeline production an toàn - cần thêm lớp nào ở orchestrator? Idempotency khác exactly-once ở điểm nào?
Ngày 37 - Spark on Kubernetes, dynamic allocation & lưu ý bảo mật cơ bản
Chủ đề: Spark trên Kubernetes (driver/executor là pod; Spark K8s Operator), dynamic allocation (
spark.dynamicAllocation.enabled) + external/persistent shuffle để gỡ executor an toàn, resource sizing (toán executor/core/memory + memoryOverhead). So sánh YARN/Standalone/K8s. Cloud managed (EMR, Databricks, Dataproc) - ưu/nhược. Bảo mật (tổng quan, không đi sâu): Kerberos cho xác thực trên YARN/HDFS truyền thống, ACL kiểm soát ai đọc/ghi bảng nào, mã hóa dữ liệu at-rest (S3/HDFS encryption) và in-transit (TLS giữa driver-executor,spark.ssl.*) - biết các khái niệm này tồn tại để không bị bất ngờ khi vào môi trường có compliance/audit.Trọng tâm kiến thức: Hiểu dynamic allocation phụ thuộc external shuffle service (hoặc shuffle tracking) để không mất shuffle data khi scale down.
Bài tập:
(Nếu có thể) chạy một Spark app trên K8s local (minikube/kind) hoặc triển khai trên EMR/Dataproc.
Bật dynamic allocation, tạo workload biến thiên, quan sát executor tăng/giảm.
Câu hỏi Vì sao dynamic allocation cần external shuffle service mới an toàn?
Ngày 37.5 - Data quality & validation frameworks
Chủ đề: Vì sao "job chạy xong không lỗi" không đồng nghĩa "data đúng". Great Expectations hoặc Deequ (built trên Spark, chạy check phân tán) để enforce constraint: null-rate tối đa trên một cột, range hợp lệ, uniqueness của key, schema khớp kỳ vọng, referential check giữa bảng. Đặt check ở đâu trong pipeline: ngay sau ingest (bronze), sau khi làm sạch (silver), trước khi phục vụ (gold). Xử lý khi check fail: quarantine record lỗi (ghi vào bảng riêng để điều tra) vs fail cả job (khi vi phạm nghiêm trọng) - đánh đổi giữa availability và correctness.
Trọng tâm kiến thức: Hiểu data quality check chạy như một job Spark bình thường (có shuffle/aggregate để tính null-rate, cardinality...) nên cũng cần tối ưu như mọi job khác - không phải một "lớp phụ" đứng ngoài.
Bài tập:
Dùng Deequ (Scala) hoặc Great Expectations (Python) viết một bộ check cho một bảng: null-rate của cột
user_id< 1%,timestampnằm trong khoảng hợp lệ,post_idlà unique.Cố ý đưa data bẩn (null tràn lan, id trùng) qua pipeline; xác nhận check phát hiện và quarantine đúng record, không làm hỏng toàn bộ batch.
Câu hỏi Vì sao đặt data quality check ở gold thì đã quá muộn để cứu silver/bronze? Trade-off giữa quarantine record lỗi và fail cả job là gì - khi nào chọn cái nào?
Ngày 38 - TUNING MASTERY: phương pháp chẩn đoán (ngày tổng hợp)
Chủ đề: Quy trình chẩn đoán từ UI/event log: (1) skew → fix bằng salting, AQE skew join, repartition; (2) spill → tăng partition/memory, giảm data per task; (3) shuffle quá lớn → giảm data trước shuffle, broadcast join, bucketing; (4) GC/serialization → Kryo, off-heap, ít object béo; (5) small files → compaction, coalesce; (6) thiếu/thừa parallelism → chỉnh shuffle partitions/AQE. Speculative execution. Đọc event log bằng History Server.
Trọng tâm kiến thức: Đây là nơi mọi thứ học từ Phase 2–4 hội tụ. Mỗi triệu chứng → nguyên nhân gốc → biện pháp.
Bài tập:
Cho 3 job "bệnh" khác nhau (skew / spill / small files - tự tạo), chẩn đoán chỉ bằng UI rồi fix, đo cải thiện. Viết một "post-mortem" ngắn mỗi job.
Fix một skew join bằng salting thủ công rồi bằng AQE; so sánh.
Câu hỏi Cho một stage có 200 task trong đó 199 task xong sau 5 giây và 1 task chạy 4 phút - chẩn đoán và 2 cách fix?
Ngày 38.5 - Production incident patterns: khi job fail giữa đêm
Chủ đề: Các pattern sự cố production thường gặp, khác với tuning "chậm nhưng chạy được" ở Ngày 38 - đây là job chết hẳn:
Driver OOM vs Executor OOM: driver OOM thường do
collect()/toPandas()kéo quá nhiều data về, hoặc broadcast một bảng quá lớn; executor OOM thường do task xử lý partition quá to (skew) hoặc quá ít memory/core. Cách phân biệt qua log và UI: driver OOM crash cả application ngay, executor OOM chỉ mất một số task (Spark retry trên executor khác)."Lost task" / executor bị kill bởi cluster manager (YARN/K8s): thường do vượt
memoryOverhead(off-heap, native memory của JVM/Python worker không tính trongexecutor-memory) - triệu chứng là log kiểu "Container killed by YARN for exceeding memory limits" hoặc pod bị OOMKilled trên K8s. Cách fix: tăngspark.executor.memoryOverhead(YARN) hoặcspark.kubernetes.memoryOverheadFactor, không chỉ tăngexecutor-memory.Network timeout & shuffle fetch retry storm:
spark.network.timeouthết hạn khi executor bận GC lâu hoặc mạng chậm → reduce task fetch shuffle block fail → retry → nếu nhiều task cùng retry cùng lúc có thể tạo "retry storm" làm tình huống tệ hơn. Liên hệ ngược với Ngày 9 (external shuffle service) và Ngày 0.2 (GC).Straggler task do phần cứng, KHÁC skew do data: một task chạy chậm không phải vì key lệch mà vì node đó yếu/bận (disk I/O chậm, CPU noisy neighbor). Đây là lý do speculative execution (
spark.speculation) tồn tại - Spark chạy lại bản sao task trên node khác và lấy kết quả về trước; khác hoàn toàn với salting/AQE skew handling (sửa phân bố data, không sửa phần cứng).
Trọng tâm kiến thức: Ngày 38 dạy chẩn đoán "chậm", ngày này dạy chẩn đoán "chết" - kỹ năng đọc log executor/driver và log cluster manager (YARN RM UI,
kubectl describe pod,kubectl logs) để phân loại đúng 4 pattern trên trước khi tìm cách fix, vì mỗi pattern fix khác nhau.Bài tập:
Cố ý gây driver OOM (
collect()một dataset lớn) và executor OOM (task xử lý một partition được ép cực to) riêng biệt; so sánh log và hành vi application ở hai trường hợp.Cấu hình
executor-memorysát mức off-heap cần dùng để cố ý gây "container killed for exceeding memory limits"; sửa bằng tăngmemoryOverheadthay vì tăngexecutor-memory, giải thích vì sao cách đó đúng hơn.Bật
spark.speculation; tạo một straggler task giả (vd sleep có điều kiện trên một task cụ thể không liên quan tới key/data) và quan sát Spark chạy bản sao trên executor khác trong UI (tab Tasks sẽ có task đánh dấu speculative).
Câu hỏi Vì sao "container killed by YARN for exceeding memory limits" thường KHÔNG được fix bằng tăng
executor-memory? Straggler task do phần cứng khác skew do data ở điểm nào, và vì sao salting không giúp được straggler?
Ngày 39 - Spark Connect & kiến trúc tách client-server
Chủ đề: Spark Connect (Spark 3.4+, GA & feature-parity cao ở 4.0): kiến trúc client-server qua gRPC, client gửi unresolved logical plan, server chạy Spark thật. Thin client (~vài MB, không JVM), nhúng Spark vào IDE/app, cách ly tốt hơn, đa ngôn ngữ (Python/Scala/Go/Rust/Swift).
spark.api.mode=connect.Trọng tâm kiến thức: Hiểu vì sao gửi logical plan (không phải bytecode) cho phép tách rời client-server và đa ngôn ngữ. Đây là hướng tương lai của Spark.
Bài tập:
- Kết nối tới Spark server qua Spark Connect từ một client mỏng (vd
pip installconnect client) và chạy query.
- Kết nối tới Spark server qua Spark Connect từ một client mỏng (vd
Câu hỏi Spark Connect khác "monolith driver" cũ ở điểm gì, và lợi ích kiến trúc là gì?
Ngày 40 - Observability, testing & CI cho ứng dụng Spark
Chủ đề: Metrics (Spark metrics system → Prometheus/Ganglia/JMX), structured logging (Spark 4.0), History Server, event log. Testing: unit test transformation (local SparkSession),
chispa/spark-testing-base, kiểm thử schema, integration test với data nhỏ. CI cho Spark job.Bài tập:
Viết unit test cho vài transformation chính của dự án (so khớp DataFrame kỳ vọng).
Export metrics ra Prometheus (hoặc ít nhất bật và đọc metrics endpoint).
Câu hỏi Làm sao test một transformation Spark mà không cần cluster?
PHASE 9 - Capstone: pipeline phân tích mạng xã hội
Kiến trúc mục tiêu: Ingest (Kafka) → Bronze (Delta, raw) → Silver (Delta, đã làm sạch/chuẩn hóa) → Gold (Delta, aggregate/feature) → ML (sentiment + topic) → Graph (influence) → Serving/Dashboard. Vừa batch vừa streaming. Deploy trên cluster. Có test + tuning + orchestration + data quality + tài liệu.
Ngày 41 - Ingestion & Bronze: Thu thập từ Twitter/Facebook API (hoặc dataset offline lớn); stream qua Kafka; ghi raw vào Delta bronze (exactly-once). Chọn schema, lưu cả raw VARIANT/JSON (tận dụng VARIANT của Spark 4 cho semi-structured). Thiết kế write idempotent ngay từ bronze (theo kỹ thuật Ngày 36.5) để chịu được retry từ orchestrator.
Ngày 42 - Silver (làm sạch): Bỏ ký tự đặc biệt/URL/emoji, chuẩn hóa, tokenization, stop words, lemmatization. Schema enforcement của Delta. Xử lý dedup (stateful nếu streaming). Data quality checkpoint (áp dụng framework Ngày 37.5): định nghĩa bộ constraint cho bảng silver (null-rate, uniqueness key, range hợp lệ), quarantine record vi phạm vào bảng riêng, log số record bị quarantine mỗi batch để theo dõi chất lượng nguồn theo thời gian.
Ngày 43 - EDA & Gold aggregate: Phân tích thăm dò có trực quan hóa (aggregate trong Spark, visualize phần nhỏ). Bảng gold: metrics theo thời gian, hashtag trends, DAU/WAU/MAU bằng window functions.
Ngày 44 - Sentiment model: Pipeline ML (TF-IDF + LR/NB; tùy chọn so sánh với một mô hình mạnh hơn). Tune bằng CrossValidator. Đánh giá đầy đủ + confusion matrix.
Ngày 45 - Topic & Graph: LDA/K-means tìm chủ đề; GraphFrames xây mạng tương tác, PageRank tìm influencer, community detection.
Ngày 46 - Orchestration, tuning & deploy: Đưa toàn bộ pipeline bronze→silver→gold→ML→graph vào một Airflow/Dagster DAG với dependency và retry policy rõ ràng (Ngày 36.5). Profiling toàn pipeline qua UI/History Server; fix skew/spill/small-files (Ngày 38) và soát lại các incident pattern có thể xảy ra khi chạy dài hạn (Ngày 38.5); OPTIMIZE Delta + Z-order; đóng gói; deploy lên cluster (K8s/EMR); bật metrics. Viết "performance post-mortem".
Ngày 47 - Trình bày & ADR: Tổng hợp kết quả, viết báo cáo + Architecture Decision Records (vì sao Delta, vì sao broadcast join ở đâu, vì sao chọn số partition đó, vì sao dùng orchestrator X, vì sao đặt data quality check ở silver mà không phải gold...), slide, thực hành Q&A.
Phần phụ lục
Đọc source code Spark
Repo:
apache/sparktrên GitHub (Scala). Bắt đầu từ:core/src/main/scala/org/apache/spark/rdd/RDD.scala(5 thuộc tính),Dependency.scala,scheduler/DAGScheduler.scala,shuffle/(ShuffleManager, SortShuffleWriter),memory/UnifiedMemoryManager.scala, vàsql/catalyst/(optimizer rules, plans).Cách đọc: chọn một transformation bạn dùng (vd
reduceByKey) và lần theo nó từ API → physical execution.
Papers
RDD paper - "Resilient Distributed Datasets" (Zaharia et al., NSDI 2012).
Spark SQL / Catalyst paper (SIGMOD 2015).
Structured Streaming paper (SIGMOD 2018).
Các blog kỹ thuật về Project Tungsten (Databricks engineering blog) và Whole-Stage Codegen.
Sách
Learning Spark, 2nd ed. (O'Reilly)
Spark: The Definitive Guide
High Performance Spark (Karau & Warren).
The Internals of Apache Spark.
Theo dõi tiến hóa của Spark
Đọc release notes mỗi minor version (Spark đang chuyển sang cadence quý từ 4.3.0).
Theo SPIP (Spark Project Improvement Proposals) trên JIRA để thấy hướng phát triển.
Lưu ý ANSI mode mặc định (4.0): kiểm lại các giả định cũ về NULL/overflow/cast.
Bộ tự kiểm tra
Vẽ toàn bộ luồng từ
.count()đến task chạy trên executor.Wide vs narrow dependency, và vì sao nó định nghĩa stage.
Sort-based shuffle ghi gì ra đĩa, reduce side fetch thế nào.
Unified memory model: execution vs storage borrow/evict.
4 plan của Catalyst và optimization xảy ra ở đâu.
Whole-stage codegen làm gì, dấu
*nghĩa là gì.AQE tái tối ưu khi nào và bằng statistics gì.
Vì sao Python UDF chậm; pandas UDF cứu bằng cơ chế nào.
State store của Structured Streaming lưu ở đâu; vai trò watermark.
Chẩn đoán skew/spill/small-files chỉ bằng Spark UI và cách fix mỗi loại.
Dynamic Partition Pruning khác predicate pushdown ở điểm nào, và cần điều kiện gì để kích hoạt.
FIFO vs FAIR scheduler pool giải quyết vấn đề gì trong một SparkContext.
Optimistic concurrency control của Delta/Iceberg là gì; conflict xảy ra khi nào và xử lý ra sao.
Vì sao một pipeline production cần orchestrator retry riêng biệt với Spark task retry; idempotent write đạt được bằng kỹ thuật gì.
Phân biệt driver OOM / executor OOM / container bị kill do memoryOverhead / straggler do phần cứng - mỗi loại chẩn đoán và fix khác nhau ra sao.
Data quality check nên đặt ở layer nào trong bronze/silver/gold, và trade-off giữa quarantine vs fail cả job.