Bài đầu tiên giúp bạn nắm vững nền móng: Spark chạy phân tán như thế nào, cách mở một phiên làm việc (SparkSession), đọc dữ liệu vào DataFrame, biến đổi cơ bản, và hiểu sự khác nhau then chốt giữa transformation (lười) và action (chạy thật). Nắm chắc bài này, mọi bài sau sẽ nhẹ nhàng.
Spark là một engine xử lý dữ liệu phân tán: thay vì một máy làm hết, công việc được chia nhỏ và chạy song song trên nhiều tiến trình. Có 4 nhân vật chính bạn cần phân biệt:
SparkSession, phân tích code thành kế hoạch thực thi, chia thành các task và gửi đi. Driver là "bộ não" điều phối, nhưng không tự xử lý dữ liệu.Luồng đi của một job nhìn đơn giản như sau:
Điểm mấu chốt: dữ liệu được chia thành nhiều partition (phân vùng). Mỗi partition là một mẩu dữ liệu được một task xử lý độc lập. Nhờ vậy, nếu có 6 partition và 3 core rảnh, Spark xử lý 3 partition cùng lúc — đó chính là tính song song giúp Spark nhanh trên dữ liệu lớn.
master("local[*]"), Spark chạy mọi thứ (driver + executor) trong một tiến trình JVM duy nhất trên máy của bạn — không cần cụm thật. Phần [*] nghĩa là dùng tất cả các core CPU đang có làm worker thread. Bạn có thể giới hạn, ví dụ local[2] để chỉ dùng 2 core. Đây là chế độ lý tưởng để học vì nhẹ, bật là chạy, nhưng vẫn mô phỏng đầy đủ mọi khái niệm phân tán.SparkSession là điểm vào duy nhất của mọi chương trình Spark hiện đại. Qua nó bạn đọc dữ liệu, tạo DataFrame, chạy SQL... Tạo session như sau:
from pyspark.sql import SparkSession spark = ( SparkSession.builder .appName("bai1") .master("local[*]") .getOrCreate() )
Giải nghĩa từng phần:
getOrCreate().local[*] = chạy local dùng hết core. Nếu submit lên cụm thật, chỗ này sẽ là URL của cluster manager (vd spark://host:7077).spark, nhưng viết lại đoạn trên vẫn an toàn nhờ getOrCreate().DataFrame là một bảng dữ liệu phân tán có schema (tên cột + kiểu dữ liệu) — giống bảng trong SQL hay DataFrame của pandas, nhưng khác ở hai điểm quan trọng: nó được chia thành nhiều partition nằm rải trên các executor, và nó lazy (lười) — chỉ ghi nhận kế hoạch chứ chưa thực thi cho đến khi gặp một action. Ta đọc CSV:
sales = spark.read.csv( "/home/jovyan/work/data/sales.csv", header=True, inferSchema=True, ) sales.printSchema() # in cấu trúc cột + kiểu sales.show(5) # in 5 dòng đầu
Đọc JSON — lưu ý có hai dạng khác nhau:
# JSON dạng "đẹp" (một array trải nhiều dòng) → cần multiLine customers = ( spark.read .option("multiLine", True) .json("/home/jovyan/work/data/customers.json") ) # JSON-lines: mỗi dòng là một object JSON độc lập → KHÔNG cần multiLine events = spark.read.json("/home/jovyan/work/data/events.jsonl")
Sự khác biệt: file JSON array là một cấu trúc lớn trải dài nhiều dòng, nên cần multiLine=True để Spark hiểu cả file là một khối. Còn JSON-lines (.jsonl) là mỗi dòng một object JSON hoàn chỉnh — định dạng này lý tưởng cho dữ liệu lớn vì Spark đọc và chia partition theo dòng rất hiệu quả, không cần multiLine.
inferSchema, định nghĩa kiểu rõ ràng bằng StructType. Spark chỉ quét file một lần và không bao giờ đoán sai kiểu:
from pyspark.sql.types import ( StructType, StructField, IntegerType, StringType, DoubleType, ) schema = StructType([ StructField("order_id", IntegerType(), True), StructField("category", StringType(), True), StructField("qty", IntegerType(), True), StructField("unit_price", DoubleType(), True), ]) sales = spark.read.csv( "/home/jovyan/work/data/sales.csv", header=True, schema=schema, )Tham số thứ ba
True trong mỗi StructField nghĩa là cột đó được phép null.Để xử lý cột, ta dùng module hàm dựng sẵn của Spark, theo quy ước đặt bí danh là F:
from pyspark.sql import functions as F df = ( sales .withColumn("amount", F.col("qty") * F.col("unit_price")) .filter(F.col("qty") > 0) .select("order_id", "category", "amount") ) df.show(5)
qty dưới dạng biểu thức. Bạn có thể tính toán trên nó: nhân, cộng, so sánh... Ở đây F.col("qty") * F.col("unit_price") tạo biểu thức "số lượng × đơn giá".amount (hoặc ghi đè nếu đã tồn tại) bằng giá trị của biểu thức.Quan trọng: cả ba withColumn, filter, select đều là transformation — chúng chỉ mô tả bạn muốn biến đổi dữ liệu ra sao và trả về một DataFrame mới. Chưa có dòng dữ liệu nào được xử lý cho đến khi gọi show() ở cuối (đó mới là action).
Đây là khái niệm quan trọng nhất của bài. Spark chia mọi thao tác làm hai loại:
| Transformation (lười — chưa chạy) | Action (chạy thật — kích hoạt job) |
|---|---|
select | show |
filter | count |
withColumn | collect |
groupBy | take |
join | write |
orderBy | first |
distinct | toPandas |
drop |
Muốn xem Spark định chạy thế nào mà không thật sự chạy job, dùng explain() để in ra physical plan:
df.explain() # in physical plan, KHÔNG chạy job df.explain(True) # in cả logical + physical plan chi tiết hơn
Bạn sẽ thấy một cây thực thi mô tả Spark sẽ quét file, lọc, tính cột... theo thứ tự nào — nhưng không có dòng dữ liệu nào bị xử lý.
filter xuống sát lúc đọc file (predicate pushdown) để đọc ít dữ liệu hơn, hay gộp các bước lại để bớt việc thừa. Nếu Spark chạy ngay từng lệnh thì không thể tối ưu toàn cục được — sự "lười" chính là điều khiến Spark vừa linh hoạt vừa nhanh.Mở notebook và thử ba bài sau (gợi ý đã có trong nội dung phía trên):
df (đã có cột amount), đếm số đơn vừa thỏa payment == "card" vừa có amount > 1000. Gợi ý: dùng filter với điều kiện kết hợp (F.col("payment") == "card") & (F.col("amount") > 1000) rồi .count().df theo amount giảm dần rồi lấy 10 dòng đầu. Gợi ý: df.orderBy(F.desc("amount")).show(10).sales gốc (chưa lọc), đếm số dòng có qty < 0 để biết dữ liệu bẩn nhiều cỡ nào. Gợi ý: sales.filter(F.col("qty") < 0).count()..count() hay .show(), mở Spark UI và để ý một job mới xuất hiện — đó chính là lúc action kích hoạt. Còn khi chỉ viết filter/select mà chưa gọi action thì UI im lìm. Quan sát điều này sẽ giúp bạn "thấy" được tính lazy.