Courses
Bạn đã từng cố gắng debug một job Spark bỗng dưng thất bại rồi nhận ra mình hoàn toàn lạc lối vì “hang thỏ” Spark sâu đến mức nào chưa?
Khi tôi mới làm việc với Apache Spark, tôi nghĩ chỉ cần viết vài phép biến đổi PySpark là Spark sẽ “kỳ diệu” tự mở rộng trên cụm. Tôi đã nhầm. Hiệu năng của Spark phụ thuộc hoàn toàn vào việc hiểu rõ những gì diễn ra phía sau hậu trường.
Bài viết này dành cho bất kỳ ai không muốn coi Spark như một “hộp đen”. Chúng ta sẽ cùng tìm hiểu cách kiến trúc của Spark được thiết kế, từ mô hình master–worker và quy trình thực thi, đến quản lý bộ nhớ và cơ chế chịu lỗi.
Nếu bạn muốn xây dựng các ứng dụng dữ liệu lớn nhanh, chịu lỗi tốt và hiệu quả, bạn đang ở đúng nơi!
Kiến trúc nền tảng của Apache Spark
Trước khi bạn viết dòng PySpark đầu tiên, Spark đã đưa ra một số quyết định kiến trúc cho bạn. Spark không chỉ nhanh nhờ tính toán trong bộ nhớ, mà còn vì nó được xây dựng trên kiến trúc master–worker có khả năng mở rộng và “sống sót” trước hỗn loạn thực tế như node crashes, Java Virtual Machine (JVM) trục trặc và khối lượng dữ liệu thất thường.
Hãy phân tách kiến trúc cốt lõi của Spark và lý do tại sao nó vẫn mạnh mẽ, hiện diện trong các quy trình dữ liệu lớn hiện đại.
Mô hình master–worker
Cốt lõi của Spark là mô hình master–worker . Hãy hình dung như sau:
- Driver (master): Bộ não của Spark. Nó chạy hàm
main()của bạn, tạo Spark context, xử lý lập lịch DAG và chỉ đạo cụm phải làm gì. - Executors (workers): Đây là cơ bắp. Chúng thực thi các tác vụ, giữ dữ liệu trong bộ nhớ và báo cáo lại cho driver.
Thiết lập này cho phép bạn tập trung vào việc định nghĩa các phép biến đổi, còn Spark quyết định chạy chúng ở đâu và như thế nào theo kiểu song song trên các executor.
Điều tôi thích ở thiết kế này là nó trung lập với môi trường triển khai. Cùng một đoạn mã chạy được, bất kể bạn triển khai trên máy cục bộ, trong Kubernetes hay Mesos. Nhờ vậy, bạn dễ phát triển và thử nghiệm cục bộ, rồi mở rộng lên cụm mà không cần viết lại mã.
Và đây là một lợi ích mạnh mẽ khác của sự tách biệt driver–worker: Nó cải thiện khả năng cô lập lỗi. Nếu một worker chết khi đang chạy tác vụ, Spark có thể gán lại tác vụ đó cho worker khác mà không làm sập ứng dụng.
Các thành phần cốt lõi
Hãy bóc tách những gì diễn ra bên trong driver và các node.

Kiến trúc Spark. Hình minh họa của tác giả.
Spark context
Khi bạn gọi SparkContext() hoặc dùng SparkSession.builder.getOrCreate(), bạn đang mở cánh cổng vào toàn bộ “phép màu” nội bộ của Spark.
Spark context:
- Kết nối tới bộ quản lý cụm
- Cấp phát các executor
- Theo dõi trạng thái job và kế hoạch thực thi
Spark xây dựng một Đồ thị có hướng không chu trình (DAG) các phép biến đổi phía sau hậu trường. DAG đó được chia thành các stage và task, rồi thực thi song song.
Bộ lập lịch DAG xác định những task nào có thể chạy cùng nhau, và Bộ lập lịch Task gán chúng cho các executor. Trong khi đó, Bộ quản lý Block đảm bảo dữ liệu được cache, shuffle hoặc nạp lại khi cần.
Thiết kế phân lớp này giúp Spark vô cùng linh hoạt, vì bạn có thể tinh chỉnh độc lập bộ nhớ, lưu trữ và tính toán.
Nếu bạn đang làm với các phép biến đổi Spark hoặc kỹ thuật đặc trưng, hãy xem khóa Kỹ thuật đặc trưng với PySpark để thấy kiến trúc này vận hành thực tế.
Môi trường chạy của executor
Executors là nơi công việc được thực hiện.
Mỗi executor chạy:
- Một hoặc nhiều task (dạng luồng)
- Một phần bộ nhớ để cache dữ liệu và ghi kết quả shuffle
- Thực thể JVM riêng, tách biệt với các thực thể khác
Bạn có thể cấu hình lượng bộ nhớ mỗi executor nhận, số lõi nó dùng, và việc có nên ghi xuống đĩa khi hết bộ nhớ hay không.
Nhưng hãy cẩn thận: Nếu cấp phát không đủ bộ nhớ, bạn sẽ thường xuyên gặp lỗi tràn bộ nhớ. Tuy nhiên, cũng đừng cấp phát quá nhiều vì sẽ lãng phí tài nguyên. Giám sát và tinh chỉnh là tối quan trọng ở đây.
Quy trình thực thi: Từ mã đến cụm
Viết mã PySpark khá đơn giản. Bạn lọc một DataFrame, thực hiện join, tổng hợp gì đó rồi bấm chạy. Nhưng đằng sau API gọn gàng đó, Spark âm thầm khởi động một bộ máy thực thi có thể phân tán công việc lên nhiều node.
Hãy xem điều gì diễn ra phía sau.
Chuyển đổi từ kế hoạch logic sang kế hoạch vật lý
Điều đa số người dùng Spark không nhận ra ban đầu: Khi bạn viết mã PySpark, không có gì chạy ngay lập tức. Bạn đang xây dựng một kế hoạch, và Catalyst Optimizer của Spark sẽ lấy kế hoạch đó để biến đổi thành chiến lược thực thi hiệu quả.
Nó hoạt động qua bốn giai đoạn:
- Phân tích: Spark xử lý tên cột, kiểu dữ liệu, tham chiếu bảng, đảm bảo mọi thứ hợp lệ.
- Tối ưu logic: Spark áp dụng các quy tắc như đẩy điều kiện xuống nguồn và rút gọn hằng. Nó tối ưu bộ lọc và gộp các phép chiếu.
- Lập kế hoạch vật lý: Spark cân nhắc nhiều chiến lược thực thi và chọn phương án hiệu quả nhất (dựa trên kích thước dữ liệu, phân vùng, v.v.).
- Sinh mã: Cuối cùng, nó dùng kỹ thuật sinh mã toàn giai đoạn để tạo bytecode JVM.

Catalyst Optimizer của Spark. Hình từ Databricks.
Vì vậy chuỗi .select(), .join() và .groupBy() không chỉ chạy tuần tự từng dòng. Nó được phân tích, tối ưu và biên dịch thành thứ có thể chạy rất nhanh trên cụm.
Xem PySpark Cheat Sheet nếu bạn muốn một tờ phao cho các lệnh PySpark hữu ích nhất.
Bộ lập lịch DAG & tạo stage
Khi kế hoạch hoàn tất, bộ lập lịch DAG tiếp quản.
Nó chia nhỏ job thành các stage dựa trên ranh giới shuffle, nơi Spark quyết định điều gì phải chạy tuần tự và điều gì có thể thực thi song song.
Có hai loại stage chính:
- ShuffleMapStage: Liên quan đến shuffle, thường do các phép biến đổi “rộng” như
groupBy()hoặcjoin()gây ra. Dữ liệu sau đó được phân vùng và gửi qua mạng. Loại stage này là tiền đề để tính toán ResultStage. - ResultStage: Các stage tạo ra đầu ra, như ghi xuống đĩa hoặc trả kết quả về driver.
Một điều quan trọng tôi rút ra được là tối thiểu hóa shuffle. Shuffle phải diễn ra trước khi một stage kết thúc và rất tốn kém. Bạn cần hiểu chúng xuất hiện ở đâu trong DAG và liệu bạn có thể tối ưu mã hơn nữa để giảm số lần shuffle không.
Vòng đời thực thi task
Khi bộ lập lịch DAG đã tạo xong các stage, chúng có thể được thực thi trên các executor khác nhau.
Vòng đời thực thi task trông như sau:
- Tuần tự hóa task: Driver tuần tự hóa chỉ thị task và gửi tới các executor.
- Giai đoạn ghi shuffle: Spark ghi đầu ra đã phân vùng xuống đĩa cục bộ.
- Giai đoạn lấy dữ liệu: Executor ở stage kế tiếp lấy các tệp shuffle liên quan từ những executor khác trong cụm.
- Giải tuần tự và thực thi: Executor giải tuần tự dữ liệu, chạy logic của bạn, và có thể cache hoặc ghi kết quả.
- Thu gom rác: JVM tự động thu hồi bộ nhớ không còn được ứng dụng Spark sử dụng. Bước này thiết yếu để ngăn rò rỉ bộ nhớ và đảm bảo ứng dụng Spark chạy trơn tru.
Một mẹo nhỏ từ kinh nghiệm của tôi: nếu job Spark của bạn bị treo sau khi trước đó chạy ổn, thường là do thu gom rác hoặc độ trễ khi lấy shuffle. Luôn kiểm tra mã và đảm bảo bạn hiểu kiến trúc Spark để tối ưu hiệu quả các vấn đề này.
Kiến trúc quản lý bộ nhớ
Quản lý bộ nhớ của Spark là một chủ đề rất phức tạp và có thể khiến bạn mất hàng giờ debug nếu không hiểu rõ.
Vì vậy, hãy xem Spark quản lý bộ nhớ như thế nào bên dưới “nắp capo” để bạn ý thức được và tránh tốn hàng giờ debug do mã chậm hoặc lỗi tràn bộ nhớ.
Mô hình bộ nhớ hợp nhất
Trước Spark 1.6, bộ nhớ được chia cứng giữa thực thi (cho shuffle và join) và lưu trữ (cho cache). Điều đó thay đổi với Spark 1.6, giới thiệu mô hình bộ nhớ hợp nhất.
Trong mô hình hợp nhất, dữ liệu được chia thành ba vùng chính:
- Bộ nhớ dự phòng: Một lượng nhỏ dành cho nội bộ Spark và hệ thống.
- Bộ nhớ Spark: Dùng cho dữ liệu thực thi và cache. Nó được chia sẻ động. Nếu job của bạn cần nhiều bộ nhớ cho shuffle và ít cho cache (hoặc ngược lại), Spark sẽ thích ứng.
- Bộ nhớ người dùng: Không gian cho các cấu trúc dữ liệu do người dùng định nghĩa, cần thiết để thực thi mã người dùng trong ứng dụng Spark.
Vùng bộ nhớ Spark tiếp tục được chia thành hai vùng:
- Bộ nhớ thực thi: Lưu trữ dữ liệu tạm thời cần trong các giai đoạn xử lý task (ví dụ: shuffle, join, tổng hợp, …).
- Vùng bộ nhớ lưu trữ: Dùng để cache dữ liệu và lưu trữ các cấu trúc dữ liệu nội bộ.
Tính đàn hồi này cho phép Spark linh hoạt hơn với khối lượng dữ liệu khó đoán.
Tuy nhiên, điều đó cũng đồng nghĩa bạn sẽ mất chút kiểm soát nếu không nắm chuyện gì đang xảy ra. Ví dụ, nếu bạn cache() một DataFrame lớn nhưng cũng có các phép tổng hợp đắt đỏ trong cùng stage, Spark có thể loại bỏ dữ liệu đã cache để nhường chỗ cho shuffle.
Bộ nhớ off-heap & lưu trữ dạng cột
Với bộ nhớ off-heap và lưu trữ dạng cột của Spark, động cơ Tungsten bắt đầu phát huy tác dụng.
Tungsten giới thiệu một số tối ưu giúp cải thiện hiệu năng của Spark:
- Quản lý bộ nhớ off-heap: Spark hiện lưu trữ một phần dữ liệu ngoài heap của JVM, giảm chi phí thu gom rác và giúp quản lý bộ nhớ dự đoán hơn.
- Lưu trữ định dạng nhị phân: Dữ liệu được lưu ở dạng nhị phân gọn nhẹ, thân thiện với cache, cải thiện sử dụng CPU và cho phép thực thi vector hóa.
- Thuật toán tối ưu cache: Spark có thể dùng cache CPU hiệu quả hơn, tránh đọc không cần thiết từ RAM hoặc đĩa.
Nếu bạn đang làm việc với DataFrame, bạn đã mặc định hưởng những tối ưu này. Đó là lý do tôi khuyến khích dùng DataFrame và SQL API thay vì RDD thô. Bạn nhận trọn sức mạnh của Catalyst và Tungsten mà không cần tinh chỉnh thêm.
Nếu bạn đang xây dựng pipeline làm sạch dữ liệu, bạn sẽ thấy điều này trong Cleaning Data with PySpark.
Cơ chế chịu lỗi
Nếu bạn làm với hệ thống phân tán, bạn biết một điều chắc chắn: Chúng sẽ hỏng. Node sập. Lỗi mạng xảy ra. Executor hết bộ nhớ và tắt.
Nhưng Spark được xây dựng để xử lý những vấn đề này và vẫn đảm bảo job của bạn thành công.
Hãy đi sâu hơn vào cách Spark đảm bảo job của bạn vẫn hoàn tất, ngay cả khi có bất ổn xảy ra.
Theo dõi phả hệ RDD
Resilient Distributed Datasets (RDD) là cấu trúc dữ liệu nền tảng trong Spark. Và chúng được gọi là “resilient” (bền bỉ) là có lý do.
Spark sử dụng phả hệ (lineage) để đảm bảo mỗi RDD có thể được tính toán lại khi node lỗi và mất dữ liệu.
Vì vậy khi một node hỏng, Spark đơn giản tính lại dữ liệu bị mất bằng đồ thị phả hệ.
Cách hoạt động trong thực tế:
- Phụ thuộc hẹp (như
map()hoặcfilter()): Spark chỉ cần partition bị mất để tính lại. - Phụ thuộc rộng (như
groupBy()hoặcjoin()): Spark có thể cần lấy dữ liệu từ nhiều partition, vì có thể yêu cầu đầu ra của nhiều stage.
Lineage giúp bạn không phải xử lý lỗi thủ công. Tuy nhiên, nếu đồ thị phả hệ quá dài, chứa hàng trăm phép biến đổi, việc tính lại dữ liệu bị mất sẽ tốn kém. Khi đó, checkpointing là giải pháp.
Checkpointing & nhật ký ghi trước (WAL)
Khi gặp workflow phức tạp hoặc job streaming, Spark không thể chỉ dựa vào phả hệ. Đó là lúc checkpointing phát huy tác dụng.
Bạn có thể gọi rdd.checkpoint() để lưu trạng thái RDD hiện tại vào nơi lưu trữ tin cậy (như HDFS).
Spark sau đó cắt ngắn phả hệ. Nếu lỗi xảy ra, nó tải dữ liệu trực tiếp thay vì tính lại.
Trong structured streaming, Spark cũng dùng nhật ký ghi trước (WAL) để đảm bảo dữ liệu không bị mất khi truyền.
Đây là những yếu tố tạo nên độ ổn định:
- Receiver tin cậy: Ghi dữ liệu đến vào log trước khi xử lý.
- Heartbeat của executor: Các tín hiệu định kỳ xác nhận executor vẫn sống và khỏe mạnh.
- Thư mục checkpoint: Với job streaming, lưu offset, metadata và trạng thái đầu ra để bạn có thể tiếp tục từ nơi đã dừng.
Checkpointing là tùy chọn với job batch, nhưng bắt buộc đối với pipeline streaming.
Hãy tưởng tượng bạn có một job Spark thất bại sau 10 giờ chạy, nhưng bạn có thể tiếp tục từ chỗ dở dang nhờ checkpointing và WAL.
Các tính năng kiến trúc nâng cao
Đến giờ, bạn đã thấy Spark xử lý job ra sao và cách nó quản lý bộ nhớ, xử lý lỗi.
Trong phần này, chúng ta tìm hiểu một số nâng cấp kiến trúc nâng cao giúp Spark linh hoạt hơn, thời gian thực hơn và thích ứng tốt hơn.
Thực thi truy vấn thích ứng (AQE)
AQE được giới thiệu trong Spark 3.0 và tăng cường hiệu năng truy vấn bằng cách điều chỉnh kế hoạch thực thi động khi chạy, dựa trên thống kê thu thập trong quá trình thực thi.
Các tính năng của AQE gồm:
- Chuyển đổi chiến lược join động: Nếu broadcast join không vừa bộ nhớ, AQE chuyển sang sort-merge join.
- Gộp partition shuffle: Gộp các partition shuffle nhỏ thành partition lớn hơn, giảm chi phí quản lý.
- Xử lý dữ liệu lệch (skew): AQE có thể tách các partition bị lệch để cân bằng thời gian thực thi.
Tính năng này là “cú nổ” vì cho phép các job vốn cần tinh chỉnh thủ công và thử–sai có thể tự thích ứng theo thời gian thực.
Hãy đảm bảo bật nó rõ ràng qua cấu hình (spark.sql.adaptive.enabled = true). Và nếu bạn chạy Spark 3.0+, không có lý do gì để không bật.
Kiến trúc structured streaming
Structured Streaming đưa động cơ của Spark vào miền thời gian thực, mà không buộc bạn học một API hoàn toàn mới.
Phía sau hậu trường, nó vẫn áp dụng micro-batching. Nhưng nó xử lý:
- Quản lý offset: Spark theo dõi chính xác dữ liệu nào đã đọc từ nguồn của bạn (Kafka, socket, tệp, v.v.). Điều này cung cấp đảm bảo exactly-once mạnh khi cấu hình đúng.
- Watermarking: Với tổng hợp theo thời gian, Spark dùng watermark để quyết định khi nào dữ liệu đến muộn là quá muộn để tính. Điều này rất quan trọng cho xử lý theo thời gian sự kiện.
- Kho trạng thái (state store): Khi bạn làm tổng hợp theo cửa sổ hoặc join streaming, Spark duy trì trạng thái qua các micro-batch. Trạng thái này được lưu trên đĩa và checkpoint để tránh mất dữ liệu.
Điểm mạnh ở đây là cảm giác “streaming như batch”. Bạn viết groupBy() hay filter() và Spark lo phần còn lại, giúp phân tích streaming dễ tiếp cận mà không cần chuỗi công cụ chuyên biệt.
Kiến trúc bảo mật
Nếu bạn chạy Spark trong môi trường production, đặc biệt là tài chính, y tế hoặc các lĩnh vực tương tự, bạn cần biết Spark xử lý xác thực, mã hóa và khả năng kiểm toán như thế nào.
Hãy đi sâu vào các chủ đề này và cách Spark giải quyết chúng.
Xác thực & mã hóa
Spark có nhiều tính năng bảo mật mà bạn cần bật trước. Nhưng khi đã bật, Spark cung cấp một bộ công cụ vững chắc cho giao tiếp và xác thực an toàn:
- Xác thực (SASL): Spark dùng Simple Authentication and Security Layer (SASL) để xác minh chỉ người dùng và dịch vụ được phép mới có thể gửi job hoặc kết nối tới cụm.
- Mã hóa khi truyền (AES-GCM, SSL/TLS): Spark mã hóa giao tiếp giữa các node bằng AES-GCM (mã hóa xác thực) hoặc TLS. Điều này bảo vệ dữ liệu job khỏi bị nghe lén, đặc biệt quan trọng trong môi trường đa người dùng hoặc trên cloud.
- Tích hợp Kerberos: Nếu bạn chạy trên Hadoop/YARN, Spark tích hợp với Kerberos để xác thực người dùng an toàn. Điều này gắn job Spark trực tiếp vào hệ thống quản lý danh tính và truy cập doanh nghiệp.
- Kiểm soát truy cập UI: Spark Web UI có thể để lộ thông tin nhạy cảm (như log, đường dẫn đầu vào, truy vấn SQL), vì vậy hãy đặt
spark.acls.enable=truevà cấu hìnhspark.ui.view.aclscùngspark.ui.view.acls.groupsđể hạn chế truy cập.
Bạn có thể xem toàn bộ tính năng bảo mật trong tài liệu chính thức của Spark. Hãy xem và đảm bảo bật các tính năng cần thiết để bảo vệ ứng dụng Spark của bạn.
Kiểm toán & tuân thủ
Ghi lại ai làm gì và khi nào cũng rất quan trọng.
Spark hỗ trợ:
- Ghi sự kiện: Khi bật (
spark.eventLog.enabled=true), Spark ghi mọi sự kiện job, stage, task xuống đĩa. Bạn có thể dùng log này để phát lại lịch sử job hoặc đáp ứng yêu cầu kiểm toán. - Kiểm soát truy cập theo vai trò (RBAC): Spark không cung cấp RBAC, nhưng nếu bạn dùng Spark qua nền tảng như Databricks, EMR hoặc OpenShift, thường sẽ có RBAC ở tầng hạ tầng. Spark gửi job bằng một danh tính xác định, qua đó kiểm soát truy cập cả dữ liệu lẫn tài nguyên tính toán.
- Che giấu dữ liệu và kiểm soát truy cập tại nguồn: Spark đọc từ nhiều nguồn (Parquet, Delta Lake, Hive, v.v.), và kiểm soát truy cập nên được áp dụng tại đó.
Mẫu tối ưu hiệu năng
Spark rất mạnh và nhanh, và có thể tối ưu để còn nhanh hơn nữa nếu bạn biết tinh chỉnh ở đâu.
Có nhiều khu vực bạn có thể tối ưu để khai thác tối đa Spark. Hãy đi sâu vào từng khu vực.
Tối ưu shuffle
Điểm yếu của Spark, nếu có, là shuffle. Shuffle xảy ra khi dữ liệu cần di chuyển giữa các partition, thường sau các phép biến đổi “rộng” như groupByKey(), distinct() hoặc join().
Và khi shuffle trục trặc, bạn có thể gặp I/O đĩa khổng lồ, các lần dọn rác kéo dài hoặc task bị lệch không bao giờ kết thúc.
Đây là cách bạn có thể cải thiện shuffle:
- Ưu tiên
reduceByKey()thay vìgroupByKey():reduceByKey()tổng hợp cục bộ trước khi shuffle.groupByKey()gửi mọi thứ qua mạng. - Phân vùng lại một cách thông minh: Dùng
.repartition(n)để tăng song song, hoặc.coalesce(n)để giảm. Đừng phó mặc cho giá trị mặc định của Spark. - Dùng broadcast join (hợp lý): Nếu một tập dữ liệu đủ nhỏ, hãy broadcast tới tất cả worker. Đặt
spark.sql.autoBroadcastJoinThresholdđể kiểm soát giới hạn kích thước. - Tránh
collect(): Tránh khi có thể, vì kéo dữ liệu về driver sẽ giết hiệu năng.
Hướng dẫn cấu hình bộ nhớ
Tinh chỉnh bộ nhớ của Spark có thể là cả một khoa học, nhưng bạn có thể dùng danh sách kiểm sau để đơn giản hóa:
- Cấp phát đủ bộ nhớ: Bắt đầu với ít nhất 6 GB bộ nhớ cho cụm Spark và điều chỉnh theo nhu cầu cụ thể.
- Cân nhắc tỷ lệ bộ nhớ Spark: Mặc định, Spark dùng 60%. Tăng nếu ứng dụng phụ thuộc mạnh vào thao tác DataFrame/Dataset hoặc nếu bạn cần nhiều bộ nhớ người dùng hơn.
- Dùng số lõi phù hợp cho mỗi executor: Thường 3–5 là tối ưu. Quá ít gây không tận dụng hết tài nguyên, quá nhiều gây tranh chấp task.
- Bật cấp phát động (nếu được hỗ trợ): Spark có thể tăng/giảm executor dựa trên tải.
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20
- Điều chỉnh tỷ lệ lưu trữ: Nếu bạn cần cache nhiều hơn, tăng giá trị
spark.memory.storageFraction. - Giám sát và phân tích việc dùng bộ nhớ: Sử dụng các công cụ như Spark UI hoặc VisualVM để theo dõi tiêu thụ bộ nhớ và xác định nút thắt cổ chai.
Điều chỉnh cấu hình bộ nhớ có thể giúp rất nhiều. Tôi từng giảm một job 30 phút xuống còn 8 phút chỉ bằng cách chỉnh cấu hình bộ nhớ, không đổi một dòng mã.
Công thức xác định kích thước cụm
Đây là phần hầu hết các đội làm sai, vì họ đoán kích thước cụm thay vì ước tính đúng.
Bạn có thể làm tốt hơn bằng cách dùng các công thức sau:
- Xác định số lượng partition:
- Tính số partition cần thiết dựa trên kích thước dữ liệu và kích thước partition mong muốn.
- Hướng dẫn tiêu chuẩn: một partition cho mỗi 128 MB đến 256 MB dữ liệu chưa nén.
- Công thức: Số partition = Làm tròn lên (Tổng kích thước dữ liệu ÷ Kích thước partition).
- Tính tổng số lõi:
- Số lõi cần đủ để xử lý tất cả partition song song.
- Công thức: Tổng số lõi = Làm tròn lên (Số partition ÷ Partition trên mỗi lõi).
- Xác định bộ nhớ mỗi executor:
- Tính lượng bộ nhớ mỗi executor cần dựa trên số lõi, kích thước partition và phần overhead.
- Công thức: Bộ nhớ mỗi executor = Bộ nhớ cơ sở × (1 + Tỷ lệ overhead).
- Tính số lượng executor:
- Xác định số executor dựa trên tổng số lõi và số lõi mỗi executor.
- Công thức: Số executor = Làm tròn lên (Tổng số lõi ÷ Lõi mỗi executor).
- Tính tổng bộ nhớ:
- Tính tổng bộ nhớ cần cho cụm dựa trên số executor và bộ nhớ mỗi executor.
- Công thức: Tổng bộ nhớ = Số executor × Bộ nhớ mỗi executor.
Ví dụ:
- Đầu vào: 500GB dữ liệu và kích thước partition khoảng 128MB
- Partition: ~4.000 partition
- Lõi: 4.000 partition / 4 partition mỗi lõi = 1.000
- Bộ nhớ mỗi executor: Giả sử 8 GB mỗi executor và overhead 20%. 8 GB * 1,20 = 9,6 GB
- Executor: 1.000 lõi / 4 lõi mỗi executor = 250 executor
- Tổng bộ nhớ: 250 executor * 9,6GB = 2.400 GB
Nhưng hãy nhớ: Đây chỉ là ước tính. Hãy dùng làm điểm khởi đầu rồi tối ưu tiếp thông qua profiling.
Xu hướng kiến trúc mới nổi
Spark đã tồn tại hơn một thập kỷ, nhưng vẫn rất cập nhật. Nó đang phát triển nhanh hơn bao giờ hết nhờ nền tảng cloud-native, tăng tốc bằng GPU và tích hợp ML chặt chẽ hơn.
Nếu hôm nay bạn dùng Spark giống hệt ba năm trước, có lẽ bạn đang bỏ lỡ hiệu năng và những tính năng mới tuyệt vời.
Hãy xem một vài điểm mới nhất.
Động cơ Photon (Databricks)
Nếu bạn làm với Databricks, có lẽ bạn đã làm việc với và nghe về Photon.
Nếu muốn tìm hiểu thêm về Databricks, tôi khuyên bạn nên học khóa Introduction to Databricks.
Photon là động cơ thế hệ tiếp theo trên nền tảng Databricks Lakehouse, cung cấp hiệu năng truy vấn nhanh với chi phí thấp. Nó tương thích với API Spark, vì vậy bạn không cần chỉnh mã Spark để sử dụng.
Nó giúp tăng tốc đáng kể mã SQL và PySpark của bạn.
Photon bao gồm các tính năng sau:
- Thực thi vector hóa: Photon xử lý dữ liệu theo lô dạng cột, tận dụng chỉ thị CPU SIMD (Single Instruction, Multiple Data) để thực hiện thao tác trên nhiều giá trị đồng thời. Spark truyền thống dùng thực thi theo từng hàng và phụ thuộc nhiều vào JVM cho cấp phát bộ nhớ và thu gom rác.
- Runtime C++ (không có overhead JVM): Không có thu gom rác Java, vốn có thể là nút thắt trong các job Spark lớn. Bộ nhớ được quản lý chính xác trong C++.
- Tối ưu truy vấn cải tiến: Photon tích hợp sâu với Catalyst Optimizer của Spark, đồng thời có các tối ưu trong lúc chạy (như lọc động khi chạy, nhánh mã thích ứng, tối ưu join và tổng hợp).
- Tăng tốc phần cứng: Hỗ trợ phần cứng hiện đại (như GPU NVIDIA, tập lệnh AVX-512 cho CPU Intel, vi xử lý Graviton (ARM) trên AWS).
Serverless Spark
Serverless thật tuyệt, vì bạn không phải quản lý cụm, không cần cấp phát trước tài nguyên, và chỉ trả tiền cho thời gian Spark chạy.
Serverless cho Spark đã có trên các dịch vụ như Databricks Serverless, AWS Glue và GCP Dataproc Serverless.
Và đây là lý do nó tuyệt vời:
- Tự động mở rộng: Nền tảng mở rộng tính toán dựa trên nhu cầu thực tế của job, nghĩa là bạn không cần đoán số node.
- Hiệu quả chi phí: Bạn chỉ trả cho những gì sử dụng. Không còn cảnh trả tiền cho máy nhàn rỗi.
- Đơn giản: Không cần thiết lập, cấu hình hay bảo trì cụm, vì đã có nền tảng lo.
- Hiệu năng: Thời gian thực thi có thể nhanh hơn, vì cấu hình và thiết lập đã được tối ưu sẵn cho bạn.
Serverless Spark lý tưởng cho phân tích tương tác, job ad-hoc hoặc tải công việc khó dự đoán.
Nhưng hãy cẩn trọng: pipeline chạy lâu, ổn định có thể vẫn rẻ hơn trên cụm cố định. Luôn đo cả chi phí lẫn độ trễ.
Tích hợp MLflow
Khi ngành chuyển dịch, ranh giới giữa kỹ thuật dữ liệu và AI đang mờ dần. Như Deepak Goyal, CEO & Founder tại Azurelib Academy, đã bàn trong podcast DataFramed
Kỹ thuật dữ liệu sẽ đóng vai trò cốt lõi và nền tảng trong làn sóng chuyển dịch sang AI sắp tới.
Deepak Goyal, CEO & Founder at Azurelib Academy
Nếu bạn làm machine learning ở quy mô lớn và muốn đưa mô hình vào production, chỉ mỗi Spark là chưa đủ. Bạn cần các nguyên tắc MLOps, như theo dõi thí nghiệm, quản lý phiên bản mô hình và khả năng tái lập. Đó là lúc MLflow phát huy tác dụng.
MLflow hiện tích hợp với Spark và mang trọn bộ MLOps vào pipeline của bạn.
Bạn có thể:
- Theo dõi thí nghiệm: Ghi tham số, chỉ số và hiện vật từ các job Spark ML bằng
mlflow.log_param()vàmlflow.log_metric(). - Quản lý phiên bản mô hình: Lưu mô hình từ
pyspark.mlhoặcsklearntrực tiếp vào kho đăng ký mô hình của MLflow. - Triển khai mô hình: Phục vụ mô hình đã huấn luyện qua endpoint REST bằng tính năng model serving của MLflow.
Bạn không cần đổi công cụ. Tiếp tục dùng Spark để huấn luyện, kỹ thuật đặc trưng và chấm điểm, đồng thời dùng MLflow cho các tác vụ MLOps.
Kết luận
Nếu bạn chưa biết nhiều về Spark, nó giống như một chiếc hộp đen khổng lồ. Bạn viết chút mã PySpark, bấm chạy và hy vọng mọi thứ ổn.
Đôi khi điều đó hiệu quả với tôi, đôi khi dẫn đến những buổi debug dài để tìm ra lỗi.
Chỉ khi tôi bắt đầu nhìn vào hậu trường, mọi thứ mới trở nên có lý. Và tôi cũng mất kha khá thời gian để hiểu chuyện gì đang diễn ra.
Nếu được bắt đầu lại từ con số 0, đây là những điều tôi sẽ tập trung:
- Học cách Spark chia mã của bạn thành job, stage và task.
- Hiểu về bộ nhớ.
- Cảnh giác với shuffle.
- Bắt đầu nhỏ và chạy ở chế độ local. Hãy lăn tay vào làm.
Đó chính xác là những gì chúng ta đã học trong bài viết này.
Nếu muốn học tiếp, đây là vài tài nguyên thân thiện cho người mới mà tôi khuyên dùng:
- Introduction to PySpark: Điểm khởi đầu thực hành tuyệt vời nếu bạn vẫn đang làm quen.
- Cleaning Data with PySpark: Học cách làm sạch dữ liệu, vì dữ liệu thực tế luôn lộn xộn.
- The Top 20 Spark Interview Questions: Không chỉ cho phỏng vấn, mà còn để đào sâu hiểu biết của bạn.
- Top 4 Apache Spark Certifications in 2025: Nếu bạn muốn được công nhận kỹ năng qua chứng chỉ.
Tìm hiểu thêm về Spark với các khóa học này!
How do I choose the right cluster manager for my Spark deployment?
Spark supports several cluster managers (YARN, Mesos, Kubernetes, and standalone). Your choice depends on existing infrastructure, resource sharing needs, and operational expertise: YARN integrates well on Hadoop clusters, Kubernetes offers containerized portability, and Mesos excels at multi-tenant isolation.
What is the external shuffle service and how does it improve performance?
The external shuffle service decouples shuffle file serving from executor lifecycles, enabling dynamic allocation and reducing data loss during executor eviction. It keeps shuffle files available even after executors shut down, which speeds up stage retries and conserves disk I/O under heavy load.
How do broadcast joins work internally and when should I use them?
For broadcast joins, Spark sends a small lookup table to every executor to avoid full data shuffles. Use them when one side of the join is below the spark.sql.autoBroadcastJoinThreshold (default 10 MB), as they drastically reduce network I/O and speed up joins on skewed key distributions.
What are best practices for tuning JVM garbage collection in Spark?
Monitor GC pauses via the Spark UI or tools like VisualVM and prefer the G1GC collector for its low pause times. Allocate executor memory with headroom for overhead (spark.executor.memoryOverhead) and tune -XX:InitiatingHeapOccupancyPercent to trigger GC earlier, preventing long stop-the-world pauses.
How can I leverage GPU acceleration to speed up Spark jobs?
Use the NVIDIA RAPIDS Accelerator for Apache Spark to transparently offload SQL and DataFrame operations to GPUs. It plugs into Spark’s execution engine, replacing CPU-based operators with GPU-accelerated equivalents and offering up to 10× faster processing for suitable workloads.
What’s the difference between static and dynamic resource allocation in Spark?
Static allocation fixes the number of executors for the job’s lifetime, offering predictability at the cost of potential idle resources. Dynamic allocation lets Spark request or release executors based on pending tasks and workload, improving cluster utilization for fluctuating jobs—ideal for shared environments.
How should I configure Spark for optimal performance on cloud storage systems like S3?
Enable S3 transfer acceleration, tune spark.hadoop.fs.s3a.connection.maximum, and use consistent view (S3A v2) to handle eventual consistency. Coalesce small files before writing and consider the S3A committers to reduce list-operation overhead and improve write throughput.
How can I secure Spark communications with Kerberos and TLS?
Enable TLS for RPC (spark.ssl.enabled) and configure SASL/Kerberos (spark.authenticate and spark.kerberos.keytab) to enforce mutual authentication. Store credentials in a secure, HDFS-accessible keytab and restrict Spark UI access via ACL settings to prevent unauthorized data exposure.
What are Pandas UDFs and when are they more efficient than regular UDFs?
Pandas UDFs (vectorized UDFs) use Apache Arrow to batch-exchange data between the JVM and Python, drastically reducing serialization overhead. They outperform traditional row-by-row UDFs for complex numerical operations, especially when processing large columnar batches in PySpark.
What benefits does the DataSource V2 API provide over V1 for custom data sources?
DataSource V2 offers a cleaner, more modular interface that supports push-down filters, partition pruning, and streaming sources natively. It enables fine-grained read/write control and better integration with Spark’s Catalyst optimizer, resulting in higher performance and easier maintainability for bespoke connectors.
Tôi là Kỹ sư Cloud với nền tảng vững chắc về Kỹ thuật Điện, học máy và lập trình. Sự nghiệp của tôi bắt đầu từ lĩnh vực thị giác máy tính, tập trung vào phân loại ảnh, trước khi chuyển sang MLOps và DataOps. Tôi chuyên xây dựng nền tảng MLOps, hỗ trợ các nhà khoa học dữ liệu và triển khai các giải pháp dựa trên Kubernetes để tinh gọn quy trình làm việc của học máy.
