paint-brush
Nói lời tạm biệt với sự cố OOMtừ tác giả@wydfy111
719 lượt đọc
719 lượt đọc

Nói lời tạm biệt với sự cố OOM

từ tác giả Jiafeng Zhang11m2023/06/12
Read on Terminal Reader

dài quá đọc không nổi

Một giải pháp quản lý bộ nhớ mạnh mẽ và linh hoạt hơn với các tối ưu hóa trong cấp phát bộ nhớ, theo dõi bộ nhớ và giới hạn bộ nhớ.
featured image - Nói lời tạm biệt với sự cố OOM
Jiafeng Zhang HackerNoon profile picture

Điều gì đảm bảo sự ổn định của hệ thống trong các tác vụ truy vấn dữ liệu lớn? Nó là một cơ chế giám sát và cấp phát bộ nhớ hiệu quả. Đó là cách bạn tăng tốc độ tính toán, tránh các điểm nóng bộ nhớ, phản hồi kịp thời khi không đủ bộ nhớ và giảm thiểu lỗi OOM.




Từ quan điểm của người dùng cơ sở dữ liệu, làm thế nào để họ bị quản lý bộ nhớ kém? Đây là danh sách những thứ đã từng làm phiền người dùng của chúng tôi:


  • Lỗi OOM khiến các quy trình phụ trợ gặp sự cố. Xin trích lời của một trong những thành viên trong cộng đồng của chúng tôi: Xin chào, Apache Doris, bạn có thể làm mọi thứ chậm lại hoặc không thực hiện được một số nhiệm vụ khi thiếu bộ nhớ, nhưng bỏ qua thời gian chết thì thật không hay chút nào.


  • Các quy trình phụ trợ tiêu tốn quá nhiều dung lượng bộ nhớ, nhưng không có cách nào để tìm ra tác vụ chính xác để đổ lỗi hoặc giới hạn mức sử dụng bộ nhớ cho một truy vấn.


  • Rất khó để đặt kích thước bộ nhớ thích hợp cho mỗi truy vấn, vì vậy rất có thể một truy vấn sẽ bị hủy ngay cả khi có nhiều dung lượng bộ nhớ.


  • Các truy vấn đồng thời cao sẽ chậm một cách không tương xứng và rất khó xác định các điểm truy cập bộ nhớ.


  • Dữ liệu trung gian trong quá trình tạo HashTable không thể được xóa vào đĩa, do đó, tham gia các truy vấn giữa hai bảng lớn thường không thành công do OOM.


May mắn thay, những ngày đen tối đó đã ở phía sau chúng ta vì chúng ta đã cải thiện cơ chế quản lý bộ nhớ từ dưới lên. Bây giờ hãy sẵn sàng; mọi thứ sẽ được chuyên sâu.

Cấp phát bộ nhớ

Trong Apache Doris, chúng tôi có một giao diện duy nhất để cấp phát bộ nhớ: Allocator . Nó sẽ thực hiện các điều chỉnh khi thấy phù hợp để giữ cho việc sử dụng bộ nhớ hiệu quả và trong tầm kiểm soát.


Ngoài ra, MemTrackers sẵn sàng theo dõi kích thước bộ nhớ được cấp phát hoặc giải phóng và ba cấu trúc dữ liệu khác nhau chịu trách nhiệm cấp phát bộ nhớ lớn trong quá trình thực thi của người vận hành (chúng ta sẽ tìm hiểu về chúng ngay lập tức).




Cấu trúc dữ liệu trong bộ nhớ

Vì các truy vấn khác nhau có các mẫu điểm truy cập bộ nhớ khác nhau khi thực thi, Apache Doris cung cấp ba cấu trúc dữ liệu trong bộ nhớ khác nhau: Arena , HashTablePODArray . Tất cả đều nằm dưới sự cai trị của Allocator.



  1. đấu trường

Đấu trường là một nhóm bộ nhớ duy trì một danh sách các khối, sẽ được phân bổ theo yêu cầu từ Bộ cấp phát. Các khối hỗ trợ căn chỉnh bộ nhớ. Chúng tồn tại trong suốt vòng đời của Đấu trường và sẽ được giải phóng khi bị phá hủy (thường là khi hoàn thành truy vấn).


Các khối chủ yếu được sử dụng để lưu trữ dữ liệu được tuần tự hóa hoặc giải tuần tự hóa trong Xáo trộn hoặc Khóa được tuần tự hóa trong HashTables.


Kích thước ban đầu của một đoạn là 4096 byte. Nếu đoạn hiện tại nhỏ hơn bộ nhớ được yêu cầu, một đoạn mới sẽ được thêm vào danh sách.


Nếu đoạn hiện tại nhỏ hơn 128M, thì đoạn mới sẽ tăng gấp đôi kích thước của nó; nếu nó lớn hơn 128M, thì đoạn mới tối đa sẽ lớn hơn 128M so với yêu cầu.


Đoạn nhỏ cũ sẽ không được phân bổ cho các yêu cầu mới. Có một con trỏ để đánh dấu đường phân chia giữa các khối được phân bổ và những khối chưa được phân bổ.


  1. HashTable

HashTables được áp dụng cho Hash Joins, tập hợp, thiết lập hoạt động và chức năng cửa sổ. Cấu trúc PartitionedHashTable hỗ trợ không quá 16 HashTable phụ. Nó cũng hỗ trợ việc hợp nhất song song HashTables và mỗi Hash Join phụ có thể được thu nhỏ một cách độc lập.


Những điều này có thể giảm mức sử dụng bộ nhớ tổng thể và độ trễ do mở rộng quy mô.


Nếu HashTable hiện tại nhỏ hơn 8 triệu, nó sẽ được chia tỷ lệ theo hệ số 4;

Nếu nó lớn hơn 8M, nó sẽ được chia tỷ lệ theo hệ số 2;

Nếu nó nhỏ hơn 2G, nó sẽ được thu nhỏ khi đầy 50%;

và nếu nó lớn hơn 2G, nó sẽ được thu nhỏ khi đầy 75%.


HashTables mới được tạo sẽ được chia tỷ lệ trước dựa trên lượng dữ liệu mà nó sẽ có. Chúng tôi cũng cung cấp các loại HashTable khác nhau cho các tình huống khác nhau. Ví dụ: đối với tập hợp, bạn có thể áp dụng PHmap.


  1. PODArray

PODArray, như tên gợi ý, là một mảng động của POD. Sự khác biệt giữa nó và std::vector là PODArray không khởi tạo các phần tử. Nó hỗ trợ căn chỉnh bộ nhớ và một số giao diện của std::vector .


Nó được chia tỷ lệ theo hệ số 2. Trong quá trình hủy, thay vì gọi hàm hủy cho từng phần tử, nó sẽ giải phóng bộ nhớ của toàn bộ PODArray. PODArray chủ yếu được sử dụng để lưu chuỗi trong cột và được áp dụng trong nhiều tính toán hàm và lọc biểu thức.

Giao diện bộ nhớ

Là giao diện duy nhất điều phối Arena, PODArray và HashTable, Bộ cấp phát thực thi phân bổ ánh xạ bộ nhớ (MMAP) cho các yêu cầu lớn hơn 64M.


Những video nhỏ hơn 4K sẽ được phân bổ trực tiếp từ hệ thống qua malloc/free; và những thứ ở giữa sẽ được tăng tốc bởi ChunkAllocator bộ nhớ đệm có mục đích chung, giúp tăng hiệu suất 10% theo kết quả đo điểm chuẩn của chúng tôi.


ChunkAllocator sẽ thử và truy xuất một đoạn có kích thước đã chỉ định từ FreeList của lõi hiện tại theo cách không bị khóa; nếu một đoạn như vậy không tồn tại, nó sẽ thử từ các lõi khác theo cách dựa trên khóa; nếu vẫn không thành công, nó sẽ yêu cầu kích thước bộ nhớ đã chỉ định từ hệ thống và đóng gói nó thành một đoạn.


Chúng tôi đã chọn Jemalloc thay vì TCMalloc sau khi trải nghiệm cả hai. Chúng tôi đã thử TCMalloc trong các thử nghiệm đồng thời cao của mình và nhận thấy rằng Spin Lock trong CentralFreeList chiếm 40% tổng thời gian truy vấn.


Vô hiệu hóa "gỡ bỏ bộ nhớ tích cực" giúp mọi thứ tốt hơn, nhưng điều đó mang lại nhiều bộ nhớ sử dụng hơn, vì vậy chúng tôi phải sử dụng một chuỗi riêng lẻ để thường xuyên tái chế bộ đệm. Mặt khác, Jemalloc hoạt động hiệu quả hơn và ổn định hơn trong các truy vấn đồng thời cao.


Sau khi tinh chỉnh cho các tình huống khác, nó mang lại hiệu suất tương tự như TCMalloc nhưng tiêu tốn ít bộ nhớ hơn.

Tái sử dụng bộ nhớ

Tái sử dụng bộ nhớ được thực hiện rộng rãi trên lớp thực thi của Apache Doris. Ví dụ: các khối dữ liệu sẽ được sử dụng lại trong suốt quá trình thực hiện truy vấn. Trong quá trình Xáo trộn, sẽ có hai khối ở đầu Người gửi và chúng hoạt động luân phiên nhau, một khối nhận dữ liệu và khối còn lại trong quá trình vận chuyển RPC.


Khi đọc một máy tính bảng, Doris sẽ sử dụng lại cột vị ngữ, thực hiện đọc theo chu kỳ, lọc, sao chép dữ liệu đã lọc vào khối phía trên, sau đó xóa.


Khi nhập dữ liệu vào bảng Khóa tổng hợp, sau khi MemTable lưu trữ dữ liệu trong bộ nhớ đệm đạt đến một kích thước nhất định, nó sẽ được tổng hợp trước và sau đó sẽ có nhiều dữ liệu hơn được ghi vào.


Tái sử dụng bộ nhớ cũng được thực hiện trong quá trình quét dữ liệu. Trước khi quá trình quét bắt đầu, một số khối trống (tùy thuộc vào số lượng máy quét và luồng) sẽ được phân bổ cho tác vụ quét.


Trong mỗi lần lập lịch máy quét, một trong các khối trống sẽ được chuyển đến lớp lưu trữ để đọc dữ liệu.


Sau khi đọc dữ liệu, khối sẽ được đưa vào hàng đợi của nhà sản xuất để sử dụng cho các toán tử cấp trên trong quá trình tính toán tiếp theo. Khi một toán tử cấp trên đã sao chép dữ liệu tính toán từ khối, khối đó sẽ quay trở lại các khối trống để lập lịch quét tiếp theo.


Chuỗi phân bổ trước các khối miễn phí cũng sẽ chịu trách nhiệm giải phóng chúng sau khi quét dữ liệu, do đó sẽ không có thêm chi phí hoạt động. Số khối trống bằng cách nào đó xác định tính đồng thời của quá trình quét dữ liệu.

Theo dõi bộ nhớ

Apache Doris sử dụng MemTrackers để theo dõi quá trình phân bổ và giải phóng bộ nhớ trong khi phân tích các điểm phát sóng bộ nhớ. MemTrackers lưu giữ hồ sơ của từng truy vấn dữ liệu, nhập dữ liệu, tác vụ nén dữ liệu và kích thước bộ nhớ của từng đối tượng chung, chẳng hạn như Cache và TabletMeta.


Nó hỗ trợ cả đếm thủ công và theo dõi tự động MemHook. Người dùng có thể xem việc sử dụng bộ nhớ thời gian thực trong chương trình phụ trợ Doris trên một trang Web.

Cấu trúc của MemTrackers

Hệ thống MemTracker trước Apache Doris 1.2.0 nằm trong cấu trúc cây phân cấp, bao gồm process_mem_tracker, query_pool_mem_tracker, query_mem_tracker, instance_mem_tracker, ExecNode_mem_tracker, v.v.


MemTrackers của hai lớp lân cận có mối quan hệ cha-con. Do đó, bất kỳ lỗi tính toán nào trong MemTracker con sẽ được tích lũy hết và dẫn đến mức độ khó tin lớn hơn.



Trong Apache Doris 1.2.0 trở lên, chúng tôi đã làm cho cấu trúc của MemTrackers đơn giản hơn nhiều. MemTracker chỉ được chia thành hai loại dựa trên vai trò của chúng: Bộ giới hạn MemTracker và các loại khác.


Bộ giới hạn MemTracker, giám sát việc sử dụng bộ nhớ, là duy nhất trong mọi tác vụ truy vấn/nhập/nén và đối tượng chung; trong khi các MemTracker khác theo dõi các điểm truy cập bộ nhớ trong quá trình thực thi truy vấn, chẳng hạn như HashTables trong các hàm Tham gia/Tập hợp/Sắp xếp/Cửa sổ và dữ liệu trung gian trong quá trình tuần tự hóa, để đưa ra bức tranh về cách bộ nhớ được sử dụng trong các toán tử khác nhau hoặc cung cấp tham chiếu cho việc kiểm soát bộ nhớ trong xả dữ liệu.


Mối quan hệ cha-con giữa Bộ giới hạn MemTracker và các MemTracker khác chỉ được thể hiện trong quá trình in ảnh chụp nhanh. Bạn có thể nghĩ về một mối quan hệ như một liên kết tượng trưng. Chúng không được sử dụng đồng thời và vòng đời của cái này không ảnh hưởng đến vòng đời của cái kia.


Điều này giúp các nhà phát triển hiểu và sử dụng chúng dễ dàng hơn nhiều.


MemTrackers (bao gồm cả MemTracker Limiter và những thứ khác) được đưa vào một nhóm Bản đồ. Chúng cho phép người dùng in ảnh chụp nhanh loại MemTracker tổng thể, ảnh chụp nhanh tác vụ Truy vấn/Tải/Nén và tìm ra Truy vấn/Tải sử dụng nhiều bộ nhớ nhất hoặc sử dụng quá mức bộ nhớ.



MemTracker hoạt động như thế nào

Để tính toán mức sử dụng bộ nhớ của một lần thực thi nhất định, một MemTracker được thêm vào một ngăn xếp trong Thread Local của luồng hiện tại. Bằng cách tải lại malloc/free/realloc trong Jemalloc hoặc TCMalloc, MemHook thu được kích thước thực tế của bộ nhớ được cấp phát hoặc giải phóng và ghi nó vào Thread Local của luồng hiện tại.


Khi thực thi xong, MemTracker có liên quan sẽ bị xóa khỏi ngăn xếp. Ở dưới cùng của ngăn xếp là MemTracker ghi lại việc sử dụng bộ nhớ trong toàn bộ quá trình thực hiện truy vấn/tải.


Bây giờ, hãy để tôi giải thích bằng quy trình thực hiện truy vấn được đơn giản hóa.


  • Sau khi nút phụ trợ Doris bắt đầu, việc sử dụng bộ nhớ của tất cả các luồng sẽ được ghi lại trong Process MemTracker.


  • Khi một truy vấn được gửi, một MemTracker Truy vấn sẽ được thêm vào Ngăn xếp Lưu trữ Cục bộ (TLS) của Chủ đề trong luồng thực thi phân đoạn.


  • Sau khi một ScanNode được lên lịch, một ScanNode MemTracker sẽ được thêm vào Ngăn xếp lưu trữ cục bộ (TLS) của luồng trong luồng thực thi phân đoạn. Sau đó, bất kỳ bộ nhớ nào được phân bổ hoặc giải phóng trong chuỗi này sẽ được ghi vào cả MemTracker Truy vấn và MemTracker ScanNode.


  • Sau khi Máy quét được lên lịch, một MemTracker Truy vấn và MemTracker Máy quét sẽ được thêm vào Ngăn xếp TLS của luồng Máy quét.


  • Khi quá trình quét hoàn tất, tất cả MemTracker trong Ngăn xếp TLS của Chủ đề máy quét sẽ bị xóa. Khi lập lịch trình ScanNode hoàn tất, ScanNode MemTracker sẽ bị xóa khỏi chuỗi thực thi phân đoạn. Sau đó, tương tự, khi một nút tổng hợp được lên lịch, một AggregationNode MemTracker sẽ được thêm vào luồng thực thi đoạn TLS Stack và bị xóa sau khi lập lịch xong.


  • Nếu truy vấn được hoàn thành, Truy vấn MemTracker sẽ bị xóa khỏi luồng thực thi đoạn TLS Stack. Tại thời điểm này, ngăn xếp này sẽ trống. Sau đó, từ QueryProfile, bạn có thể xem mức sử dụng bộ nhớ cao nhất trong toàn bộ quá trình thực thi truy vấn cũng như từng giai đoạn (quét, tổng hợp, v.v.).



Cách sử dụng MemTracker

Trang web phụ trợ Doris thể hiện việc sử dụng bộ nhớ thời gian thực, được chia thành các loại: Truy vấn/Tải/Nén/Toàn cầu. Mức tiêu thụ bộ nhớ hiện tại và mức tiêu thụ cao nhất được hiển thị.



Các loại Toàn cầu bao gồm MemTrackers của Cache và TabletMeta.



Từ các loại Truy vấn, bạn có thể thấy mức tiêu thụ bộ nhớ hiện tại và mức tiêu thụ cao nhất của truy vấn hiện tại và các toán tử liên quan đến truy vấn đó (bạn có thể biết chúng có liên quan như thế nào từ các nhãn). Để biết thống kê bộ nhớ của các truy vấn lịch sử, bạn có thể kiểm tra nhật ký kiểm tra Doris FE hoặc nhật ký BE INFO.



Giới hạn bộ nhớ

Với tính năng theo dõi bộ nhớ được triển khai rộng rãi trong các phần phụ trợ của Doris, chúng tôi đang tiến một bước gần hơn đến việc loại bỏ OOM, nguyên nhân gây ra thời gian ngừng hoạt động của phần phụ trợ và các lỗi truy vấn quy mô lớn. Bước tiếp theo là tối ưu hóa giới hạn bộ nhớ cho các truy vấn và quy trình để kiểm soát việc sử dụng bộ nhớ.

Giới hạn bộ nhớ trên truy vấn

Người dùng có thể đặt giới hạn bộ nhớ cho mọi truy vấn. Nếu vượt quá giới hạn đó trong quá trình thực hiện, truy vấn sẽ bị hủy. Nhưng kể từ phiên bản 1.2, chúng tôi đã cho phép Memory Overcommit, đây là một biện pháp kiểm soát giới hạn bộ nhớ linh hoạt hơn.


Nếu có đủ tài nguyên bộ nhớ, một truy vấn có thể tiêu thụ nhiều bộ nhớ hơn giới hạn mà không bị hủy, vì vậy người dùng không cần phải quan tâm nhiều hơn đến việc sử dụng bộ nhớ; nếu không, truy vấn sẽ đợi cho đến khi không gian bộ nhớ mới được cấp phát. Chỉ khi nào bộ nhớ mới được giải phóng không đủ cho truy vấn thì truy vấn mới bị hủy.


Trong khi ở Apache Doris 2.0, chúng tôi đã nhận ra sự an toàn ngoại lệ cho các truy vấn. Điều đó có nghĩa là bất kỳ sự cấp phát bộ nhớ không đủ nào sẽ ngay lập tức khiến truy vấn bị hủy, điều này sẽ tránh được sự cố khi kiểm tra trạng thái "Hủy" trong các bước tiếp theo.

Giới hạn bộ nhớ trên tiến trình

Trên cơ sở thường xuyên, chương trình phụ trợ Doris truy xuất bộ nhớ vật lý của các quy trình và kích thước bộ nhớ hiện có từ hệ thống. Trong khi đó, nó thu thập ảnh chụp nhanh MemTracker của tất cả các tác vụ Truy vấn/Tải/Nén.


Nếu một quy trình phụ trợ vượt quá giới hạn bộ nhớ hoặc không đủ bộ nhớ, Doris sẽ giải phóng một số dung lượng bộ nhớ bằng cách xóa Bộ nhớ cache và hủy một số truy vấn hoặc tác vụ nhập dữ liệu. Chúng sẽ được thực thi thường xuyên bởi một luồng GC riêng lẻ.



Nếu bộ nhớ xử lý được sử dụng vượt quá SoftMemLimit (81% tổng bộ nhớ hệ thống, theo mặc định) hoặc bộ nhớ hệ thống khả dụng giảm xuống dưới Dấu nước Cảnh báo (dưới 3,2 GB), Minor GC sẽ được kích hoạt.


Tại thời điểm này, việc thực thi truy vấn sẽ bị tạm dừng ở bước cấp phát bộ nhớ, dữ liệu được lưu trong bộ nhớ cache trong các tác vụ nhập dữ liệu sẽ bị buộc xóa và một phần của Bộ đệm ẩn trang dữ liệu và Bộ đệm ẩn phân đoạn lỗi thời sẽ được giải phóng.


Nếu bộ nhớ mới được giải phóng không chiếm 10% bộ nhớ tiến trình, với Memory Overcommit được bật, Doris sẽ bắt đầu hủy các truy vấn là "overcommitters" lớn nhất cho đến khi đạt được mục tiêu 10% hoặc tất cả các truy vấn bị hủy.


Sau đó, Doris sẽ rút ngắn khoảng thời gian kiểm tra bộ nhớ hệ thống và khoảng thời gian GC. Các truy vấn sẽ được tiếp tục sau khi có thêm bộ nhớ.


Nếu bộ nhớ xử lý được tiêu thụ vượt quá MemLimit (90% tổng bộ nhớ hệ thống, theo mặc định) hoặc bộ nhớ hệ thống khả dụng giảm xuống dưới Dấu nước thấp (dưới 1,6 GB), Full GC sẽ được kích hoạt.


Tại thời điểm này, các tác vụ nhập dữ liệu sẽ bị dừng và tất cả Bộ đệm ẩn trang dữ liệu cũng như hầu hết các Bộ đệm ẩn khác sẽ được giải phóng.


Nếu sau tất cả các bước này, bộ nhớ mới giải phóng không chiếm 20% bộ nhớ tiến trình, thì Doris sẽ xem xét tất cả các MemTracker và tìm các truy vấn và tác vụ nhập tốn nhiều bộ nhớ nhất, đồng thời hủy bỏ từng cái một.


Chỉ sau khi đạt được mục tiêu 20%, khoảng thời gian kiểm tra bộ nhớ hệ thống và khoảng thời gian GC mới được kéo dài, đồng thời các tác vụ truy vấn và nhập mới được tiếp tục. (Một thao tác thu gom rác thường mất hàng trăm μs đến hàng chục ms.)

Ảnh hưởng và kết quả

Sau khi tối ưu hóa trong phân bổ bộ nhớ, theo dõi bộ nhớ và giới hạn bộ nhớ, chúng tôi đã tăng đáng kể tính ổn định và hiệu suất đồng thời cao của Apache Doris dưới dạng nền tảng kho dữ liệu phân tích thời gian thực. Hiện tại, sự cố OOM trong phần phụ trợ là một cảnh hiếm gặp.


Ngay cả khi có OOM, người dùng có thể xác định gốc rễ của sự cố dựa trên nhật ký và sau đó khắc phục sự cố đó. Ngoài ra, với giới hạn bộ nhớ linh hoạt hơn đối với các truy vấn và nhập dữ liệu, người dùng không phải tốn thêm công sức để quản lý bộ nhớ khi không gian bộ nhớ đã đủ.


Trong giai đoạn tiếp theo, chúng tôi có kế hoạch đảm bảo hoàn thành các truy vấn trong tình trạng quá tải bộ nhớ, nghĩa là sẽ có ít truy vấn phải hủy hơn do thiếu bộ nhớ.


Chúng tôi đã chia mục tiêu này thành các hướng công việc cụ thể: an toàn ngoại lệ, cách ly bộ nhớ giữa các nhóm tài nguyên và cơ chế xóa dữ liệu trung gian.


Nếu bạn muốn gặp các nhà phát triển của chúng tôi, đây là nơi bạn tìm thấy chúng tôi .