Hai Cấu Hình Sai Khiến Spark Bị OOM Trên Kubernetes
Sau khi chuyển đổi các pipeline Spark sang Azure Kubernetes Service, hai cài đặt hạ tầng đã tương tác tiêu cực khiến dữ liệu shuffle tràn vào RAM thay vì ổ cứng, buộc tất cả các executor phải nằm trên một node. Sự kết hợp này đã gây ra lỗi thiếu bộ nhớ (OOM) lặp đi lặp lại mà các công cụ chẩn đoán tiêu chuẩn khó phát hiện.

Hai Cấu Hình Sai Khiến Spark Bị OOM Trên Kubernetes
Ngay sau khi chuyển đổi một số pipeline Spark từ hạ tầng tại chỗ (on-premises) sang Azure Kubernetes Service (AKS), chúng tôi bắt đầu gặp phải lỗi thiếu bộ nhớ (OOM) ở các executor trong một trong các công việc xử lý dữ liệu lớn. Các lỗi này xuất hiện trong các giai đoạn trộn dữ liệu (shuffle stages) và ban đầu trông giống như một vấn đề điều chỉnh bộ nhớ điển hình của Spark. Tuy nhiên, nguyên nhân gốc rễ thực sự không nằm ở cấu hình Spark, mà là do hai cài đặt ở cấp độ hạ tầng đã tương tác theo cách không mong muốn.
Bài viết này sẽ ghi lại quá trình điều tra, nguyên nhân gốc rễ và các thay đổi cấu hình đã giải quyết vấn đề này.
Bối cảnh hệ thống và quá trình di chuyển
Môi trường Pipeline
Nền tảng dữ liệu của chúng tôi quản lý các pipeline batch sản xuất hỗ trợ tổng hợp và chuyển đổi dữ liệu giao dịch quy mô lớn tại một tổ chức tài chính lớn của Mỹ. Khối lượng công việc trong câu hỏi này xử lý một tệp dữ liệu phẳng (flat file) có độ rộng cố định khoảng 3 gigabyte mỗi ngày. Tệp này chứa nhiều bố cục bản ghi xen kẽ yêu cầu nhiều lần phân tích cú pháp và hợp nhất (union), khiến công việc này tốn nhiều thao tác shuffle hơn nhiều so với kích thước đầu vào 3GB gợi ý. Trên hệ thống on-premises, các pipeline này đã chạy ổn định trong hơn ba năm.
Cấu hình cụm AKS
Dưới đây là so sánh môi trường nơi sự cố xảy ra trước và sau khi được khắc phục:
| Tham số | Trước (Lỗi) | Sau (Đã sửa) |
|---|---|---|
| RAM Node | 64GB | 64GB |
| Executor trên mỗi job | 4 | 4 |
| Bộ nhớ Executor | 8g → 10g (khi chẩn đoán) | 10g |
| spark.kubernetes.local.dirs.tmpfs | true ❌ | false ✅ |
| Kích thước tmp-volume | 1Gi (dựa trên RAM) ❌ | 10Gi (dựa trên ổ cứng) ✅ |
| Quy tắc đặt Pod | podAffinity bắt buộc ❌ | podAntiAffinity ưu tiên ✅ |
Dòng thời gian sự cố
Tuần đầu sau khi di chuyển
Các lần chạy ban đầu có vẻ ổn định. Các công việc nhỏ hoàn thành mà không gặp vấn đề gì. Những lỗi OOM đầu tiên xuất hiện trong công việc batch nhiều bố cục trong các giai đoạn shuffle nặng.
Ngày 2-3: Giả thuyết đầu tiên và sai lệch cấu hình Heap
Chẩn đoán ban đầu tập trung vào việc điều chỉnh kích thước bộ nhớ executor. Chúng tôi đã tăng spark.executor.memory từ 8GB lên 10GB. Tuy nhiên, các lỗi vẫn tiếp diễn dưới tải, loại trừ việc thiếu bộ nhớ heap đơn giản là nguyên nhân gốc rễ.
Ngày 4: Giả thuyết thứ hai và số lượng Executor
Chúng tôi tăng số lượng executor để phân tán khối lượng công việc thêm nữa, nhưng không có tác động ý nghĩa nào; các lỗi vẫn tiếp diễn trong các giai đoạn shuffle nặng.
Ngày 4-5: Điều tra việc đặt Pod trên Kubernetes
Chúng tôi kiểm tra việc đặt pod trên Kubernetes bằng cách sử dụng nhật ký pod AKS và Datadog. Bảng điều khiển tổng quan Node Kubernetes của Datadog cho thấy mức sử dụng bộ nhớ node tăng vọt trên 90% trong các giai đoạn shuffle. Các sự kiện của Kubernetes báo cáo liên tục:
Reason: OOMKilled Exit Code: 137
Chúng tôi cũng quan sát thấy một mô hình thay thế executor rõ ràng trong nhật ký pod AKS. Thay vì công việc hoàn thành với 4 executor ban đầu, nó liên tục thay thế các executor bị OOM giết chết và cuối cùng đạt đến executor thứ 50 trước khi thất bại. Đây là tín hiệu rõ ràng cho thấy vấn đề không phải là kích thước heap mà là sự cạn kiệt bộ nhớ ở cấp độ node.
Ngày 5: Xác nhận nguyên nhân gốc rễ
Một bài xem xét cấu hình Spark và Kubernetes đã tiết lộ rằng spark.kubernetes.local.dirs.tmpfs đã được đặt thành true trong quá trình di chuyển, khiến tất cả các thư mục tạm cục bộ, bao gồm cả đường dẫn tràn shuffle (shuffle spill), được hỗ trợ bởi RAM thay vì ổ cứng. Các khối lượng (volumes) tạm cũng chỉ được định kích thước 1GB mỗi cái, quá nhỏ cho dữ liệu shuffle mà công việc này tạo ra.
Ngày 5: Triển khai giải pháp
Quy tắc podAffinity cùng vị trí đã được thay thế bằng podAntiAffinity sử dụng preferredDuringSchedulingIgnoredDuringExecution. Ngoài ra, spark.kubernetes.local.dirs.tmpfs được đặt thành false và giới hạn kích thước của tmp-volume và workdir được tăng từ 1GB lên 10GB. Các lỗi OOM đã dừng lại ngay lập tức.
Phân tích nguyên nhân gốc rễ
Ba yếu tố đóng góp đã được xác định cho các lỗi này, chúng chỉ xuất hiện khi yếu tố thứ hai và thứ ba tương tác dưới tải shuffle nặng.
Yếu tố 1: Áp lực bộ nhớ do Shuffle trong công việc dữ liệu lớn
Các giai đoạn shuffle lớn trong các công việc dữ liệu chuyên sâu kích hoạt các đỉnh bộ nhớ executor sắc nhọn. Đây là hành vi mong đợi trong Spark; các thao tác shuffle yêu cầu giữ dữ liệu trung gian trong bộ nhớ trước khi tràn ra đĩa. Một mình yếu tố này có thể quản lý được với cấu hình tài nguyên phù hợp.
Yếu tố 2: Sai lệch Affinity buộc Executor cùng vị trí
Quy tắc đặt executor thực sự đã trở thành một ràng buộc cùng vị trí cứng. Một quy tắc podAffinity sử dụng requiredDuringSchedulingIgnoredDuringExecution đã có mặt trong cấu hình. Thay vì phân tán các pod executor trên các node, nó buộc chúng phải nằm trên cùng một node. Điều này tập trung áp lực bộ nhớ và I/O thời gian shuffle lên một máy duy nhất.
Yếu tố 3: Thư mục tạm cục bộ dựa trên RAM (spark.kubernetes.local.dirs.tmpfs=true)
Đây là cấu hình sai gây tác động lớn nhất. Khi thuộc tính này được đặt thành true, Spark hướng dẫn Kubernetes hỗ trợ tất cả các thư mục tạm cục bộ bao gồm cả đường dẫn tràn shuffle bằng các khối lượng thư mục rỗng dựa trên bộ nhớ (tmpfs). Thay vì ghi dữ liệu shuffle ra đĩa, Spark đã ghi mọi thứ vào RAM của node.
Điều khiến vấn đề trở nên đặc biệt nghiêm trọng là sự kết hợp giữa lưu trữ dựa trên tmpfs với giới hạn kích thước khối lượng rất nhỏ (chỉ 1GB). Khi tràn shuffle vượt quá dung lượng hiệu quả của các khối lượng tạm, áp lực bộ nhớ tăng nhanh và các quy trình executor bắt đầu bị kernel OOM killer chấm dứt.
Hiệu ứng cộng hưởng
Một mình, mỗi yếu tố đều có thể quản lý được:
- Áp lực shuffle một mình: được xử lý bằng cách tràn ra đĩa.
- Cùng vị trí một mình: tăng áp lực nhưng không thảm khốc nếu có tràn đĩa.
- Hệ thống tệp tạm (tmpfs) một mình: gây vấn đề nhưng các executor phân tán giới hạn tác động trên mỗi node.
Nhưng khi kết hợp lại, tất cả các executor bị buộc lên một node trong khi tràn shuffle của chúng tiêu thụ RAM node thay vì đĩa, làm cạn kiệt bộ nhớ nhanh chóng dưới tải và kích hoạt các cái chết OOM lặp đi lặp lại.
Các giải pháp
Giải pháp 1: Pod Anti-Affinity để phân tán Executor
Chúng tôi đã giới thiệu podAntiAffinity với preferredDuringSchedulingIgnoredDuringExecution để phân tán các executor trên các node. Thuật ngữ "ưu tiên" (preferred) thay vì "bắt buộc" (required) được chọn có chủ đích. Nó cho phép cùng vị trí khi cụm chịu áp lực dung lượng, tránh thất bại công việc do các pod không thể lập lịch, trong khi ưu tiên mạnh mẽ việc phân tán trong điều kiện bình thường.
Giải pháp 2: Tắt tmpfs và Tăng giới hạn kích thước Volume
Hai thay đổi liên quan đã được thực hiện cùng nhau: tmpfs bị tắt và giới hạn kích thước khối lượng được tăng lên.
Đầu tiên, tắt tmpfs:
# TRƯỚC: Thư mục tạm cục bộ dựa trên RAM
spark.kubernetes.local.dirs.tmpfs: true
# SAU: Thư mục tạm cục bộ dựa trên ổ cứng
spark.kubernetes.local.dirs.tmpfs: false
Tiếp theo, tăng giới hạn kích thước volume:
# TRƯỚC: Volumes 1Gi dựa trên RAM
volumes:
- emptyDir:
sizeLimit: 1Gi
name: tmp-volume
# SAU: Volumes 10Gi dựa trên ổ cứng
volumes:
- emptyDir:
sizeLimit: 10Gi
name: tmp-volume
Việc đặt spark.kubernetes.local.dirs.tmpfs thành false hướng dẫn Spark sử dụng các volume emptyDir dựa trên đĩa cho tất cả các thư mục tạm cục bộ. Tăng giới hạn kích thước khối lượng từ 1GB lên 10GB cũng quan trọng không kém; giới hạn 1GB ban đầu quá nhỏ để hấp thụ dữ liệu tràn shuffle.
Kết quả
Sau khi áp dụng các bản sửa đổi:
- Sự cố OOM: Không có trong vòng sáu tháng.
- Hành vi của Executor: Công việc hoàn thành sạch sẽ với 4 executor ban đầu; không có thay thế, không bị giết.
- Thời gian hoàn thành công việc: Khoảng 1 giờ, ổn định và có thể dự đoán.
- Kích thước cụm: Không thay đổi; cùng một nhóm node và diện tích.
Bài học rộng hơn cho Spark trên Cloud-Native
Sự cố này phản ánh ba mô hình xuất hiện lặp đi lặp lại trong việc di chuyển khối lượng công việc Spark lên đám mây.
Ngữ nghĩa lưu trữ đám mây không trung lập về hạ tầng
Lưu trữ đám mây dựa trên RAM có thể cải thiện hiệu suất cho các tập dữ liệu nhỏ vừa vặn với bộ nhớ. Tuy nhiên, vấn đề nảy sinh khi áp dụng nó cho các đường dẫn tràn shuffle trong công việc có lượng dữ liệu trung gian lớn kết hợp với giới hạn kích thước volume nhỏ. Các nhóm di chuyển khối lượng công việc Spark nên kiểm tra rõ ràng spark.kubernetes.local.dirs.tmpfs và xác nhận hành vi dưới tải trước khi đưa vào sản xuất.
Lập lịch Kubernetes không nhận thức Spark
Kubernetes tối ưu hóa cho việc đặt khối lượng công việc mục đích chung, không phải cho hành vi bộ nhớ executor và shuffle của Spark. Việc đặt executor nên được coi là một lựa chọn thiết kế rõ ràng trong mọi triển khai Spark trên Kubernetes.
Danh sách kiểm tra tính tương đồng cấu hình bị sử dụng ít
Hai cấu hình sai trong sự cố này được đưa ra không phải thông qua các quyết định có chủ đích mà thông qua sự bỏ sót. Các cấu hình tồn tại trên on-premises đơn giản là không được chuyển sang.
Danh sách kiểm tra ngăn chặn OOM cho Spark trên Kubernetes
Dưới đây là danh sách kiểm tra dựa trên các khoảng trống cấu hình đã gây ra các lỗi OOM trong bài viết này:
Lập lịch:
- Pod anti-affinity được cấu hình cho các pod executor.
- Chế độ affinity được chọn có chủ đích (ưu tiên so với bắt buộc).
Bộ nhớ:
- Áp lực của OS và kubelet được tính đến trong tính toán bộ nhớ node.
- Yêu cầu bộ nhớ executor cộng với giới hạn để lại khoảng cách an toàn trên mỗi node.
Lưu trữ:
spark.kubernetes.local.dirs.tmpfsđược đặt rõ ràng thànhfalse(thư mục tạm dựa trên ổ cứng).- Kích thước volume cục bộ được cung cấp cho tràn shuffle đỉnh điểm.
- Hành vi tràn shuffle được xác nhận với các lần chạy tập dữ liệu lớn.
Giám sát:
- Cảnh báo cho các sự kiện giết executor OOM được cấu hình.
- Ngưỡng sử dụng bộ nhớ node được đặt.
- Tỷ lệ thử lại công việc được giám sát.
Kinh nghiệm này là một lời nhắc nhở rằng việc di chuyển đám mây không chỉ di chuyển khối lượng công việc; chúng cũng thay đổi hợp đồng hạ tầng mà các khối lượng công việc đó phụ thuộc vào. Các thất bại được ghi lại ở đây không phải do lỗi ứng dụng hay thay đổi phiên bản Spark, mà do các cài đặt hạ tầng âm thầm thay đổi hành vi thời gian chạy trong quá trình di chuyển.
Bài viết liên quan

Công nghệ
Google thay đổi hoàn toàn thanh tìm kiếm: Bước nhảy vọt với Gemini 3.5 Flash và Tác nhân AI
19 tháng 5, 2026

Công nghệ
CEO Palantir: 10% thế giới "ghét chúng tôi một cách chuyên nghiệp"
05 tháng 5, 2026

Phần mềm
Chính phủ Mỹ yêu cầu Instructure giải trình về sự cố tấn công mạng và lộ dữ liệu Canvas
13 tháng 5, 2026
