Có nhiều công cụ khác nhau trên thế giới, mỗi công cụ giải quyết một loạt vấn đề. Nhiều người trong số họ được đánh giá bằng cách họ giải quyết vấn đề này hay vấn đề kia tốt và chính xác như thế nào, nhưng có những công cụ mà bạn chỉ thích và bạn muốn sử dụng chúng. Chúng được thiết kế phù hợp và vừa vặn trong tay bạn, bạn không cần phải đào sâu vào tài liệu và hiểu cách thực hiện hành động này hoặc hành động đơn giản kia. Về một trong những công cụ này cho tôi, tôi sẽ viết loạt bài này.

Tôi sẽ mô tả các phương pháp và mẹo tối ưu hóa giúp tôi giải quyết một số vấn đề kỹ thuật nhất định và đạt được hiệu quả cao bằng Apache Spark. Đây là bộ sưu tập cập nhật của tôi.

Nhiều tối ưu hóa mà tôi sẽ mô tả sẽ không ảnh hưởng quá nhiều đến các ngôn ngữ JVM, nhưng nếu không có các phương pháp này, nhiều ứng dụng Python có thể không hoạt động.

Toàn bộ loạt: 


Các cụm sẽ không được sử dụng đầy đủ trừ khi bạn đặt mức độ song song cho mỗi hoạt động đủ cao . Khuyến nghị chung cho Spark là có 4 lần phân vùng với số lõi trong cụm có sẵn cho ứng dụng và đối với giới hạn trên - tác vụ sẽ mất 100ms + thời gian để thực thi. Nếu nó mất ít thời gian hơn dữ liệu được phân vùng của bạn quá nhỏ và ứng dụng của bạn có thể dành nhiều thời gian hơn để phân phối các tác vụ.

Quá nhỏ và quá nhiều vách ngăn có một số nhược điểm nhất định. Do đó, bạn nên phân vùng một cách khôn ngoan tùy thuộc vào cấu hình và yêu cầu của cụm.

Quá ít phân vùng - không sử dụng tất cả các lõi có sẵn trong cụm.

Quá nhiều phân vùng - quá nhiều chi phí trong việc quản lý nhiều tác vụ nhỏ cũng như di chuyển dữ liệu.

Khi nghi ngờ, hầu như luôn luôn tốt hơn nếu sai ở một số tác vụ lớn hơn (và do đó phân vùng).

Mặc dù những khuyến nghị đó vẫn hợp lý, nó vẫn rất phân biệt chữ hoa chữ thường, vì vậy cấu hình Spark phải được tinh chỉnh theo một kịch bản nhất định.

Các yếu tố ảnh hưởng đến phân vùng

Khi bạn đi đến chi tiết làm việc với Spark, bạn nên hiểu các phần sau trong đường dẫn Spark của mình, điều này cuối cùng sẽ ảnh hưởng đến lựa chọn phân vùng dữ liệu:

  • Logic kinh doanh
  • Dữ liệu
  • Môi trường

Tiếp theo, tôi sẽ đi qua các cấp độ này và đưa ra một số mẹo về từng cấp độ đó.

Logic kinh doanh

Hãy bắt đầu với điểm đa dạng nhất - logic kinh doanh. Ở đây rất khó để đưa ra bất kỳ chi tiết cụ thể nào vì các chi tiết cụ thể cho từng đường ống cụ thể có thể khác nhau.

Giảm kích thước tập dữ liệu hoạt động

Lời khuyên hiệu quả nhất thường là ngớ ngẩn nhất - để tăng tốc đường ống Spark của bạn, hãy giảm lượng dữ liệu mà cụm Spark cần xử lý . Có một số cách để làm điều này, và tất nhiên, tất cả phụ thuộc vào những gì thực sự cần được thực hiện với dữ liệu.

Có thể lọc dữ liệu của bạn bằng cách bỏ qua các phân vùng nếu chúng không đáp ứng điều kiện của bạn (tất nhiên giả sử rằng dữ liệu đã được phân vùng). Một điều kiện được chọn đúng có thể tăng tốc đáng kể việc đọc và truy xuất dữ liệu cần thiết. Trong một số trường hợp (ví dụ: s3) tránh phát hiện phân vùng không cần thiết, trong một số trường hợp, có thể giúp sử dụng các cơ chế định dạng dữ liệu dựng sẵn (ví dụ: cắt bớt phân vùng).

Phân vùng lại trước nhiều lần tham gia

Tham gia là một trong những hoạt động tốn kém nhất thường được sử dụng rộng rãi trong Spark, tất cả đều đáng trách là sự xáo trộn luôn khét tiếng. Chúng ta có thể nói về xáo trộn nhiều hơn một bài đăng, ở đây chúng ta sẽ thảo luận về mặt liên quan đến phân vùng.

Xáo trộn

Để nối dữ liệu, Spark cần dữ liệu có cùng điều kiện trên cùng một phân vùng. Việc triển khai mặc định của phép nối trong Spark kể từ phiên bản 2.3 là phép nối hợp nhất sắp xếp.

Phép nối hợp nhất sắp xếp được thực hiện theo ba bước cơ bản:

  1. Điều cần thiết là dữ liệu trên mỗi phân vùng có các giá trị khóa giống nhau, vì vậy các phân vùng phải được định vị đồng thời (trong ngữ cảnh này, nó giống như đồng phân vùng). Điều này được thực hiện bằng cách xáo trộn dữ liệu.
  2. Sắp xếp song song dữ liệu trong mỗi phân vùng.
  3. Nối dữ liệu được sắp xếp và phân vùng. Về cơ bản, đây là việc hợp nhất một tập dữ liệu bằng cách lặp lại các phần tử và nối các hàng có cùng giá trị cho khóa nối.

Mặc dù phương pháp này luôn hoạt động, nhưng nó có thể đắt hơn mức cần thiết vì nó đòi hỏi một sự xáo trộn. Có thể tránh xáo trộn nếu:

  1. Cả hai khung dữ liệu đều có một Partitioner chung.
  2. Một trong các khung dữ liệu đủ nhỏ để vừa với bộ nhớ, trong trường hợp đó chúng ta có thể sử dụng phép nối băm phát sóng.

Bạn có thể phân vùng lại khung dữ liệu sau khi tải nếu bạn biết rằng bạn sẽ tham gia nhiều lần vào nó. Luôn kiên trì sau khi phân vùng lại.

users = spark.read.load('/path/to/users').repartition('userId')
joined1 = users.join(addresses, 'userId')
joined1.show() # <-- 1st shuffle for repartition
joined2 = users.join(salary, 'userId')
joined2.show() # <-- skips shuffle for users since it's already been repartitioned

Bằng cách này, bạn sẽ xáo trộn dữ liệu một lần và sau đó sử dụng lại dữ liệu đã xáo trộn cho lần tham gia tiếp theo. Bạn cũng có thể sử dụng bucketing cho mục đích đó.

Phân vùng lại sau flatMap

Kết quả của flatMaphoạt động thường là RDD với nhiều hàng hơn, nhưng số lượng phân vùng vẫn giữ nguyên. Điều này có nghĩa là tải bộ nhớ trên mỗi phân vùng có thể trở nên quá lớn và bạn có thể thấy tất cả các lỗi tràn ổ đĩa và lỗi GC. Trong trường hợp này, tốt hơn là phân vùng lại đầu ra của flatMapdựa trên sự mở rộng bộ nhớ được dự đoán.

Loại bỏ sự cố tràn đĩa

Từ tài liệu Tuning Spark :

Đôi khi, bạn sẽ nhận được OutOfMemoryError, không phải vì RDD của bạn không phù hợp với bộ nhớ, mà bởi vì tập hợp làm việc của một trong các tác vụ của bạn, chẳng hạn như một trong các tác vụ giảm trong groupByKey, quá lớn. Các hoạt động xáo trộn của Spark (sortByKey, groupByKey, ReduceByKey, join, v.v.) xây dựng một bảng băm trong mỗi tác vụ để thực hiện nhóm, thường có thể lớn ...

Nếu spark.shuffle.spilllà true (là mặc định) Spark sẽ sử dụng ExternalAppendOnlyMap trong quá trình trộn để lưu trữ dữ liệu trung gian. Cấu trúc này có thể làm tràn dữ liệu trên đĩa khi không có đủ bộ nhớ, điều này làm tăng áp lực bộ nhớ lên trình thực thi, dẫn đến chi phí bổ sung của I / O đĩa và tăng khả năng thu gom rác. Nếu bạn sử dụng Pyspark, áp lực bộ nhớ cũng sẽ làm tăng khả năng Python hết bộ nhớ.

Tràn đĩa

Để kiểm tra xem có xảy ra tràn đĩa hay không, bạn có thể tìm kiếm các mục tương tự trong nhật ký:

INFO ExternalSorter: Task 1 force spilling in-memory map to disk it will release 232.1 MB memory

Các bản sửa lỗi có thể như sau:

  1. giảm kích thước của dữ liệu, chẳng hạn như chỉ chọn các cột bắt buộc hoặc di chuyển các hoạt động lọc trước khi chuyển đổi rộng.
  2. tăng mức độ song song để dữ liệu đầu vào của mỗi tác vụ nhỏ hơn. Hoặc bạn có thể thủ công repartition()giai đoạn trước của bạn.
  3. Tăng bộ đệm xáo trộn bằng cách tăng bộ nhớ của các tiến trình thực thi của bạn ( spark.executor.memory).
  4. Nếu tài nguyên bộ nhớ khả dụng là đủ, bạn có thể tăng kích thước spark.shuffle.file.bufferđể giảm số lần bộ đệm bị tràn trong quá trình ghi ngẫu nhiên, điều này có thể giảm số lần I / O của đĩa.
Dữ liệu Dữ liệu

nguồn

Trong quản lý dữ liệu, dữ liệu là một thành phần thiết yếu của bất kỳ công việc nào. Và nó luôn điều chỉnh. Nếu không biết dữ liệu của mình, bạn đang cố gắng điều chỉnh đường ống Spark của mình thành một số mẫu số chung, điều này thường không hoạt động tốt, cả về tốc độ và về việc sử dụng các tài nguyên có sẵn.

Dữ liệu Skew

Lý tưởng nhất là khi Spark thực hiện một phép nối chẳng hạn, các khóa nối sẽ được phân phối đồng đều giữa các phân vùng. Tuy nhiên, dữ liệu thực hiếm khi lý tưởng, ahem ... ít nhất là phần nào tốt. Thông thường, chúng tôi gặp dữ liệu sai lệch dẫn đến giảm hiệu suất của quá trình xử lý song song hoặc thậm chí là sự cố OOM.

Dữ liệu Skew

Đây không phải là vấn đề cụ thể đối với Spark, mà là vấn đề về dữ liệu - hiệu suất của hệ thống phân tán phụ thuộc nhiều vào cách dữ liệu được phân phối . Một cách để đảm bảo phân phối chính xác nhiều hơn hoặc ít hơn là phân vùng lại dữ liệu một cách rõ ràng. Vì nó là một hoạt động rất tốn kém, chúng tôi không muốn thực hiện nó ở những nơi không cần thiết. Chúng ta có thể thiết lập xác nhận số lượng phân vùng RDD / DataFrame ngay trước khi thực hiện bất kỳ thao tác nặng nào. Bạn có thể sử dụng mã sau làm ví dụ:

def repartition(
    df: DataFrame, 
    min_partitions: int, 
    max_partitions: int
) -> DataFrame:
    num_partitions = df.rdd.getNumPartitions()
    if(num_partitions < min_partitions):
        df = df.repartition(min_partitions)
    elif(num_partitions > max_partitions):
        df = df.coalesce(max_partitions)
    return df

Thông thường, dữ liệu được chia thành các phân vùng dựa trên khóa, ví dụ như ngày trong tuần, quốc gia, v.v. Nếu các giá trị không được phân bổ đồng đều trên khóa này, thì nhiều dữ liệu sẽ được đặt trong một phân vùng hơn là ở một phân vùng khác.

Hãy cùng xem đoạn mã sau

import pandas as pd
import numpy as np
from pyspark.sql import functions as F

# set smaller number of partitions so they can fit the screen
spark.conf.set('spark.sql.shuffle.partitions', 8)
# disable broadcast join to see the shuffle
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", -1)

length = 100
names = np.random.choice(['Bob', 'James', 'Marek', 'Johannes', None], length)
amounts = np.random.randint(0, 1000000, length)
# generate skewed data
country = np.random.choice(
    ['United Kingdom', 'Poland', 'USA', 'Germany', 'Russia'], 
    length,
    p = [0.05, 0.05, 0.8, 0.05, 0.05]
)
data = pd.DataFrame({'name': names, 'amount': amounts, 'country': country})

transactions = spark.createDataFrame(data).repartition('country')
countries = spark.createDataFrame(pd.DataFrame({
    'id': [11, 12, 13, 14, 15], 
    'country': ['United Kingdom', 'Poland', 'USA', 'Germany', 'Russia']
}))

df = transactions.join(countries, 'country')

# check the partitions data
for i, part in enumerate(df.rdd.glom().collect()):
    print({i: part})

Ở đây chúng ta thấy rằng một trong số 8 phân vùng nhận được rất nhiều dữ liệu và phần còn lại nhận được rất ít hoặc không có gì. Nó có mùi lệch.

Chúng tôi có thể chẩn đoán độ lệch bằng cách xem giao diện người dùng Spark và kiểm tra thời gian dành cho mỗi tác vụ. Một số tác vụ tốn thời gian, trong khi những tác vụ khác thì không. Ngoài ra, trong giao diện người dùng Spark, chỉ báo về độ lệch dữ liệu có thể là sự khác biệt lớn giữa thời gian tác vụ tối thiểu và thời gian tác vụ tối đa.

Để giải quyết vấn đề lệch dữ liệu, chúng ta có thể:

  1. Phân vùng lại dữ liệu trên một khóa được phân phối đồng đều hơn nếu bạn có.
  2. Phát khung dữ liệu nhỏ hơn nếu có thể
  3. Chia dữ liệu thành dữ liệu lệch và không lệch và làm việc với chúng song song bằng cách phân phối lại dữ liệu lệch (sao chép vi phân)
  4. Sử dụng một khóa ngẫu nhiên bổ sung để phân phối dữ liệu tốt hơn (muối).
  5. Tham gia truyền phát lặp lại
  6. Những phương pháp phức tạp hơn mà tôi chưa bao giờ sử dụng trong đời.
Sao chép sai biệt

Các phím hiếm không cần phải sao chép nhiều như các phím lệch. Ý tưởng ở đây là xác định các khóa thường xuyên trước khi sao chép, sau đó sử dụng một chính sách sao chép khác cho chúng.

replication_high = 7
high = F.broadcast(spark.range(replication_high).withColumnRenamed('id', 'replica_id'))
replication_low = 2
low = F.broadcast(spark.range(replication_low).withColumnRenamed('id', 'replica_id'))

# determine which keys are highly over-represented, broadcast them
skewed_keys = F.broadcast(
	transactions.freqItems(['country'], 0.6)
	.select(F.explode('country_freqItems').alias('country_freqItems'))
)
# replicate uniform data, one copy of each row per bucket
countries_skewed_keys = (
    countries
    .join(
        skewed_keys, 
        countries.country == skewed_keys.country_freqItems, 
        how='inner'
    )
    .crossJoin(high)
    .withColumn('composite_key', F.concat('country', F.lit('@'), 'replica_id'))
)
countries_rest = (
    countries
    .join(
        skewed_keys, 
        countries.country == skewed_keys.country_freqItems, 
        how='leftanti'
    )
    .crossJoin(low)
    .withColumn('composite_key', F.concat('country', F.lit('@'), 'replica_id'))
    .withColumn('country_freqItems', F.lit(None))
)
# this is now the entire uniform dataset replicated differently
countries_replicated = countries_skewed_keys.union(countries_rest)

transactions_tagged = (
    transactions
    .join(
        skewed_keys, 
        transactions.country == skewed_keys.country_freqItems, 
        how='left'
    )
    .withColumn('replica_id',
        F.when(
            F.isnull(F.col('country_freqItems')), 
            (F.rand() * replication_low).cast('int'),
        )
        .otherwise((F.rand() * replication_high).cast('int'))
    )
    .withColumn('composite_key', F.concat('country', F.lit('@'), 'replica_id'))
)

# now we can join on the composite key
df = transactions_tagged.join(countries_replicated, 'composite_key')

for i, part in enumerate(df.rdd.glom().collect()):
    print({i: part})

Điều này cho phép bạn sao chép các khóa rất thường xuyên hơn và dữ liệu không bị lệch chỉ được sao chép nếu cần.

Muối

Ở đây, ý tưởng là các cột muối được sử dụng trong hoạt động nối (thêm ngẫu nhiên vào chúng) với một số ngẫu nhiên. Bạn có thể xem một ví dụ trong đoạn mã sau:

salt = np.random.randint(1, int(spark.conf.get('spark.sql.shuffle.partitions')) - 1)
salted_countries = countries.withColumn(
    'salt', 
    F.explode(F.array([F.lit(i) for i in range(salt)])))
salted_transactions = transactions.withColumn('salt', F.round(F.rand() * salt))
df = salted_transactions.join(salted_countries, ['country', 'salt'] )
    .drop('salt')

# check the partitions data
for i, part in enumerate(df.rdd.glom().collect()):
    print({i: part})

Theo bất kỳ cách nào, cuối cùng, chúng ta sẽ thấy sự phân phối dữ liệu giữa các phân vùng tương đối mượt mà hơn.

Tham gia truyền phát lặp lại

Đây là một phương pháp khác, ở đây chúng tôi cố gắng thực hiện kết nối phát sóng trên các phần của khung dữ liệu thứ hai với giả định rằng nó không thể dễ dàng được phát sóng. Một cái gì đó như sau:

num_of_passes = 2
cur_pass = 0
df = None
countries = countries.withColumn('pass', F.round(F.rand() * num_of_passes))
while cur_pass <= num_of_passes:
    out = transactions.join(
        F.broadcast(countries.filter(F.col('pass') == F.lit(cur_pass))),
        'country'
    )
    df = out if not df else df.union(out)
    cur_pass += 1

countries = countries.drop('pass')
df = df.drop('pass')

Tôi chưa bao giờ sử dụng phương pháp này ngoài đời nên đừng phán xét tôi ở đây.

Kết dính sau khi lọc

Chúng tôi đã đề cập rằng việc lọc tốt có thể giúp tăng hiệu suất rất nhiều. Nhưng ngay cả khi bạn quản lý, chẳng hạn, để biến 2 tỷ dòng dữ liệu (2 Tb) được chia nhỏ thành 15.000 phân vùng thành 2 triệu dòng dữ liệu, số lượng phân vùng vẫn không thay đổi. Chỉ là hầu hết các phân vùng này sẽ trống và về cơ bản sẽ chiếm tài nguyên.

Chúng ta đã thấy hoạt động coalescecho phép giảm số lượng phân vùng và đây là thời điểm thích hợp để áp dụng nó.

df = huge_data.sample(withReplacement = False, fraction = 0.001)
df = df.coalesce(4)

Cần lưu ý rằng điều này không chỉ áp dụng cho việc lọc mà còn cho phép tổng hợp.

Môi trường

Ở giai đoạn này, bạn không chỉ nên hiểu logic kinh doanh của mình và mọi thứ đang diễn ra bên trong đường dẫn của bạn mà còn phải hiểu môi trường mà nó sẽ hoạt động. Bởi điều này, tôi muốn nói đến số lõi cụm, bộ nhớ trên mỗi trình thực thi, cấu trúc liên kết mạng (ví dụ: các vùng của các nút), các giới hạn đối với thiết lập cụm của bạn, tài nguyên của bên thứ ba và cuối cùng là chi phí.

Phân vùng lại trước khi ghi vào bộ nhớ

Spark  DataFrameWriter cung cấp  partitionBy phương thức có thể được sử dụng để phân vùng dữ liệu khi ghi. Nó phân vùng lại dữ liệu thành các tệp riêng biệt khi ghi bằng cách sử dụng một tập hợp các cột được cung cấp.

df.write.partitionBy('key').json('/path/to/foo.json')

Điều này cho phép đẩy xuống vị từ để đọc các truy vấn dựa trên khóa. Nó giới hạn số lượng tệp và phân vùng mà Spark đọc khi truy vấn.

df = spark.read.schema(schema).json('/path/to/foo.json')
df.where(df.key == 'bar')

Ngoài ra, khi bạn lưu khung dữ liệu vào đĩa, hãy đặc biệt chú ý đến kích thước phân vùng. Khi ghi, Spark tạo ra một tệp cho mỗi tác vụ (tức là một tệp trên mỗi phân vùng) và sẽ đọc ít nhất một tệp cho mỗi tác vụ trong khi đọc. Vấn đề ở đây là nếu thiết lập cụm, trong đó khung dữ liệu được lưu, có tổng bộ nhớ nhiều hơn và do đó có thể xử lý kích thước phân vùng lớn mà không gặp bất kỳ vấn đề nào, thì một cụm nhỏ hơn sau có thể gặp vấn đề với việc đọc khung dữ liệu đã lưu đó.

Nhưng partitionBykhông tương đương với repartitiontrong một số tổng hợp như:

counts = df.groupBy('key').sum()

Nó vẫn sẽ yêu cầu  Exchangeshuffle aka. partitionByphương thức không kích hoạt bất kỳ xáo trộn nào.

Thật tuyệt, phải không?

Có, nhưng có một chi tiết nhỏ ở đây. partitionByphương thức không kích hoạt bất kỳ xáo trộn nào nhưng nó có thể tạo ra rất nhiều tệp.

Hãy tưởng tượng chúng ta có 10 phân vùng và chúng ta muốn phân vùng dữ liệu bằng ngày. Mỗi tác vụ tia lửa sẽ tạo ra 365 tệp ở HDFS (1 tệp mỗi ngày), dẫn đến tổng cộng 365 × 10 = 3650 tệp được tạo bởi công việc. Nhưng giả sử chúng ta có 200 phân vùng ngay sau giai đoạn xáo trộn, bây giờ chúng ta sẽ nhận được 365 × 200 = 73k tệp. Không hiệu quả lắm và có thể gây ra các vấn đề hay còn gọi là tệp nhỏ trong HDFS.

Sử dụng tất cả các lõi cụm có sẵn

Nếu bạn có 200 lõi trong cụm của mình và chỉ có 10 phân vùng để đọc, bạn chỉ có thể sử dụng 10 lõi để đọc dữ liệu. Tất cả 190 lõi khác sẽ không hoạt động.

Một vấn đề khác có thể xảy ra khi phân vùng là có quá ít phân vùng để bao phủ đúng số lượng trình thực thi có sẵn. Hãy tưởng tượng rằng bạn có 2 trình thực thi và 3 phân vùng. Trình thực thi 1 có thêm một phân vùng, do đó, mất gấp đôi thời gian để hoàn thành trình thực thi 2. Do đó, trình thực thi 2 không hoạt động.

Giải pháp đơn giản nhất cho hai vấn đề trên là tăng số lượng phân vùng được sử dụng để xử lý. Điều này sẽ làm giảm ảnh hưởng của sự lệch phân vùng và cũng cho phép sử dụng tài nguyên cụm tốt hơn.

Sử dụng rào cản giai đoạn giữa giai đoạn trộn và thao tác ghi

Cách duy nhất để thay đổi số lượng tệp sau khi xáo trộn là tạo rào cản giai đoạn. Nó có thể được thực hiện bằng cách ghi khung dữ liệu vào bộ nhớ tạm thời hoặc cách hiệu quả hơn để thực hiện việc này bằng cách sử dụng localCheckpoint.

df.localCheckpoint(...) \
    .repartition(n) \
    .write(...)

Điều này rất hữu ích vì nó phá vỡ rào cản giai đoạn để liên kết hoặc phân vùng lại sẽ không đi lên đường dẫn thực thi của bạn hoặc quy trình làm việc song song sử dụng cùng một khung dữ liệu không cần phải xử lý lại khung dữ liệu hiện tại. Hãy nhớ Spark là thực thi lười biếng, localCheckpoint()sẽ kích hoạt thực thi để hiện thực hóa khung dữ liệu.

Phân vùng với các nguồn JDBC

Cơ sở dữ liệu SQL truyền thống không thể xử lý một lượng lớn dữ liệu trên các nút khác nhau như một tia lửa. Để cho phép Spark đọc song song dữ liệu từ cơ sở dữ liệu qua JDBC, bạn phải chỉ định mức độ đọc / ghi song song được kiểm soát bởi tùy chọn sau .option('numPartitions', parallelismLevel). Số được chỉ định kiểm soát số lượng tối đa các kết nối JDBC đồng thời. Theo mặc định, bạn đọc dữ liệu đến một phân vùng duy nhất thường không sử dụng đầy đủ cơ sở dữ liệu SQL của bạn và rõ ràng là Spark.

Phần kết luận
  1. Tôi muốn chỉ ra rằng sự cân bằng là rất quan trọng trong các hệ thống phân tán ở những nơi khác nhau.
  2. Đôi khi OOM là một vấn đề tốt hơn công việc Spark không bao giờ kết thúc, ít nhất bạn hiểu vấn đề.
  3. Trong trường hợp nghi ngờ, hãy thực hiện một sai lầm ở bên của nhiều nhiệm vụ hơn (và do đó phân vùng).