Apache Spark Fundamentals — Phần 2: Spark Core và RDD

karcuta
7 min readMay 29, 2020

--

Trong bài này, mình sẽ tập trung giải thích về các thành phần trong 1 ứng dụng và các khái niệm quan trọng cần biết khi làm việc với Spark.

Như mọi người cũng đã biết rằng Apache Spark ra đời sau Hadoop, mục đích ban đầu để khắc phục các điểm yếu của Hadoop Map-Reduce job, sau đó đã phát triển thành 1 framework mạnh mẽ và tích hợp chặt chẽ với Hadoop Ecosystem như hiện nay.

1. Distributed computing

Trong Hadoop, Hdfs là thành phần giữ vai trò lưu trữ dữ liệu, và dữ liệu này là phân tán. Vậy khi muốn xử lý dữ liệu thì phải làm sao? Rõ ràng ta không thể kéo dữ liệu phân tán về xử lý và sau đó lại “phân tán” output ra để lưu trữ được đúng không. Hiểu đơn giản thì Apache Spark là 1 framework hỗ trợ xử lý dữ liệu phân tán, Spark có khả năng sinh và quản lý các task chạy song song, ổn định và chịu lỗi rất tốt.

Nếu như theo truyền thống, ta sẽ “kéo” dữ liệu từ nhiều nơi về xử lý tập trung, thì với Spark, ta sẽ đưa code xử lý đến dữ liệu và xử lý ở đó luôn. Nếu hơi khó hiểu thì các bạn có thể xem hình dưới đây:

Mô hình Distributed Computing đơn giản

Giả sử các máy tính màu đỏ chứa dữ liệu và phân tán trong 1 cụm, thông thường, khi muốn xử lý dữ liệu này, ta sẽ “kéo” dữ liệu liên quan từ các máy màu đỏ về máy mày đen để xử lý. Tuy nhiên, với Spark sẽ ngược lại, ta sẽ gửi code xử lý từ máy màu đen đến các máy màu đỏ, sau đó dữ liệu sẽ được xử lý trên các máy màu xanh, rồi lại lưu luôn tại máy màu đỏ. Vừa không mất công chuyển đổi dữ liệu qua lại, lại giảm nhẹ gánh nặng tính toán lên 1 máy tính.

Tất nhiên, tổng quan là như vậy, có những tính toán vẫn cần phải xử lý tập trung (VD count số bản ghi mà phân tán thì tất nhiên cần phải 1 máy tính tổng từng phần từ các máy khác), Spark cung cấp các API linh hoạt, tối ưu việc gửi nhận dữ liệu giữa các máy tính.

2. Spark Application

Các thành phần trong 1 chương trình Spark

Một chương trình Spark sẽ gồm 2 thành phần chính:

  • Driver Program: Là 1 JVM Process, chứa hàm main() như bất kì 1 chương trình JVM nào khác, nó đóng vai trò điều phối code/ logic xử lý trên Driver.
    Driver program chứa SparkSession
  • Executors: Là các worker, chịu trách nhiệm thực hiện các tính toán các logic nhận từ Driver. Dữ liệu cần xử lý có thể được load trực tiếp vào memory của Executor.

Spark session: Đại diện cho khả năng tương tác với executors trong 1 chương trình. Spark session chính là entry point của mọi chương trình Spark. Từ SparkSession, có thể tạo RDD/ DataFrame/ DataSet, thực thi SQL… từ đó thực thi tính toán phân tán.

Khi chạy, từ logic của chương trình (chính là code xử lý thông qua việc gọi các API), Driver sẽ sinh ra các task tương ứng và lên lịch chạy các task, sau đó gửi xuống Executor để thực thi. Dữ liệu được lưu trên memory của Executor nên việc thực thi tính toán sẽ nhanh hơn rất nhiều.

3. RDD

Rõ ràng là dữ liệu phân tán rời rạc trong mạng, vậy làm sao để có thể tác động lên tập dữ liệu này? Spark giới thiệu 1 khái niệm là RDD (Resilient Distributed Dataset), dịch thô ra tiếng việt là: Tập dữ liệu phân tán linh hoạt. Hãy cùng xem hình dưới:

Trong 1 chương trình Spark, RDD là đại diện cho tập dữ liệu phân tán.

Một ví dụ cho các bạn mới dễ hiểu nhé.

Ta có 1 object colors = Array {red, blue, black, white, yellow}. Trong chương trình thông thường, phần dữ liệu (red, blue, black, white, yellow) nằm trên 1 máy tính duy nhất và thông qua biến colors, bạn có thể truy cập đến dữ liệu. Tuy nhiên trong hệ phân tán, nếu phần dữ liệu red, blue nằm trên máy tính A còn black, white, yellow lại nằm trên máy tính B thì sao? RDD sẽ giúp bạn truy cập 2 phần dữ liệu rời rạc này như 1 đối tượng thông thường. Đó là lý do tại sao mình nói RDD là đại diện cho tập dữ liệu phân tán.

Đặc điểm quan trọng của 1 RDD là số partitions. Một RDD bao gồm nhiều partition nhỏ, mỗi partition này đại diện cho 1 phần dữ liệu phân tán. Khái niệm partition là logical, tức là 1 node xử lý có thể chứa nhiều hơn 1 RDD partition. Theo mặc định, dữ liệu các partitions sẽ lưu trên memory. Thử tưởng tượng bạn cần xử lý 1TB dữ liệu, nếu lưu hết trên mem tính ra thì cung khá tốn kém nhỉ. Tất nhiên nếu bạn có 1TB ram để xử lý thì tốt quá nhưng điều đó không cần thiết. Với việc chia nhỏ dữ liệu thành các partition và cơ chế lazy evaluation của Spark bạn có thể chỉ cần vài chục GB ram và 1 chương trình được thiết kế tốt để xử lý 1TB dữ liệu, chỉ là sẽ chậm hơn có nhiều RAM thôi.

Mỗi Executor có thể chứa dữ liệu của 1 hoặc 1 vài partition của 1 RDD.

4. Lazy evaluation

Việc xử lý dữ liệu, nhìn rộng ra, chính là việc biến đổi từ tập dữ liệu này sang tập dữ liêu khác, hay với Spark, là biến đổi từ RDD này sang RDD khác.

VD bạn có 1 tập web log rất lớn gồm cả log call từ app và web, cần tìm xem số lượng log sử dụng trên app là bao nhiêu, từ tập dữ liệu ban đầu sẽ lọc ra tập dữ liệu nhỏ hơn chỉ chứa log call trên di động, rồi count trên tập dữ liệu đó. Đó chính là minh họa cho việc biến đổi các tập dữ liệu.

Làm việc với RDD, Spark có 2 loại operations là Transformation Actions.

Transformations

Spark Transformation

Phép biến đổi từ RDD này sang RDD khác là 1 transformation, như việc biến đổi tập web log ban đầu sang tập web log chỉ chứa log gọi qua app là 1 transformation.

Một số transformations:

  • map(func): RDD mới được tạo thành bằng cách áp dụng func lên tất cả các bản ghi trên RDD ban đầu.
  • filter(func: Boolean): RDD mới được tạo thành bằng cách áp dụng func lên tất cả các bản ghi trên RDD ban đầu và chỉ lấy những bản ghi mà func trả về true.

Có rất rất nhiều các transformations, các bạn có thể xem thêm tại đây: https://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations

Actions

Spark Actions

Sau tất cả các phép biến đổi, khi muốn tương tác với kết quả cuối cùng (VD xem kết quả, collect kết quả, ghi kết quả…) ta gọi 1 action.

Có thể kể đến 1 số Action như:

  • take(n): lấy n bản ghi từ RDD về driver
  • collect(): lấy tất cả RDD về driver
  • saveAsTextFile(“path”): ghi dữ liệu RDD ra file
  • count(): đến số bản ghi của RDD

Lazy evaluation

Khi thực thi, việc gọi các transformations, Spark sẽ không ngay lập tức thực thi các tính toán mà sẽ lưu lại thành 1 lineage, tức là tập hợp các biến đổi từ RDD này thành RDD khác qua mỗi transformation. Khi có 1 action được gọi, Spark lúc này mới thực sự thực hiện các biến đổi để trả ra kết quả.

Để mình nói cho các bạn nghe tại sao cứ phải chờ rồi mới thực thi mà không gọi đến đâu làm đến đấy như truyền thống.

Quay lại ví dụ lọc web log ban đầu. Nếu gọi đến đâu chạy đến đấy thì sau bước đầu tiên, ứng dụng cần load sẵn 1TB ram vào Mem, sẵn sàng thực hiện bước tiếp theo. Tuy nhiên, nếu lazy, Driver sẽ có “cái nhìn” toản cảnh từ đầu đến cuối về output đầu ra, lúc này Driver sẽ sinh các task đọc từng phần nhỏ của 1TB, thực hiện lọc trên tập dữ liệu nhỏ và giữ lại kết quả count bản ghi là log app, load dần dần cho tới khi hết 1TB thì sum tổng các phần nhỏ lại sẽ ra được phần lớn.

Như vậy ta chỉ cần lượng ram nhỏ nhưng vẫn có thể xử lý được lượng dữ liệu lớn gấp nhiều lần.

Ngoài ra, lazy evolution còn giúp Spark chủ động tối ưu việc thực thi (mình sẽ trình bày ở phần sau).

Qua bài này, hy vọng các bạn thực sự hiểu được về thành phần và kiến trúc 1 ứng dụng Spark cũng như các khái niệm quan trọng như RDD/ SparkSession, Actions, Transformations…

--

--