Quy trình hoạt động của mmap như sau 1 - bóng đá anh

/imgposts/yefxebgx.jpg

Đây là một chủ đề khá phức tạp và có thể được chia thành hai phần để giải thích rõ ràng hơn. Phần đầu tiên sẽ tập trung vào khái niệm "zero-copy" (sao chép không), vốn không phải là khái niệm mới nhưng đóng vai trò quan trọng trong việc tối ưu hiệu suất lưu trữ thông điệp của RocketMQ.

Trong hệ thống RocketMQ, khối lượng lớn nhất về lưu trữ chính là tin nhắn được lưu trữ dưới dạng CommitLog. Ngoài ra còn có ConsumeQueue, IndexFile và một số tệp bắn cá ăn xu online khác. Mỗi file CommitLog có kích thước 1GB làm đơn vị lưu trữ cơ bản. Khi một file đã đầy, hệ thống sẽ tự động tạo ra file tiếp theo. Vậy làm thế nào để cải thiện hiệu suất đọc/ghi của file 1GB này? Đó chính là sử dụng kỹ thuật mmap.

Trong cách thức truyền thống, mỗi lần thực hiện các hoạt động như read hoặc write đều yêu cầu các cuộc gọi hệ thống (system call) và liên tục chuyển đổi giữa trạng thái người dùng (user-space) và trạng thái nhân (kernel-space). Quá trình này bao gồm nhiều bước sao chép dữ liệu giữa bộ đệm nhân và bộ đệm người dùng.

12

| ``` read(file, tmp_buf, len);write(socket, tmp_buf, len);


Hình trên minh họa rõ ràng rằng dữ liệu cần phải di chuyển qua lại giữa trạng thái người dùng và nhân nhiều lần:

>   1. Đầu tiên, khi gọi hàm read(), chương trình chuyển từ trạng thái người dùng sang trạng thái nhân. Sau đó, mô-đun DMA sẽ đọc dữ liệu từ ổ đĩa và lưu vào bộ đệm nhân. Đây là lần sao chép thứ nhất.
>   2. Tiếp theo, dữ liệu được sao chép từ bộ đệm nhân sang bộ đệm người dùng, kết thúc quá trình gọi hàm read(). Đồng thời, chương trình cũng chuyển từ trạng thái nhân trở về trạng thái người dùng. Đây là lần sao chép thứ hai.
>   3. Sau đó, khi thực hiện write(), chương trình lại chuyển từ trạng thái người dùng sang trạng thái nhân. Dữ liệu từ bộ đệm người dùng được sao chép vào bộ đệm nhân và sau đó được chuyển tiếp đến bộ đệm socket. Đây là lần sao chép thứ ba.
>   4. Cuối cùng, sau khi hoàn tất write(), chương trình quay trở lại trạng thái người dùng. Dữ liệu sau đó được DMA chuyển tiếp đến bộ điều khiển giao thức. Đây là lần sao chép cuối cùng.

Từ những phân tích trên, ta thấy rằng chi phí cho các thao tác đọc/viết mặc định là rất cao. Để giải quyết vấn đề này, các hệ thống trung gian hiệu năng cao như RocketMQ đã áp dụng công nghệ zero-copy, cụ thể là sử dụng mmap.

### [MMAP]

MMAP dựa trên kỹ thuật ánh xạ bộ nhớ của hệ điều hành (OS) để ánh xạ file trực tiếp vào không gian địa chỉ bộ nhớ của trạng thái người dùng. Điều này biến các thao tác đọc/ghi file thành các thao tác trực tiếp với bộ nhớ, giúp tăng tốc độ đáng kể.

> MMAP ánh xạ file vào vùng bộ nhớ ảo của không gian người dùng mà không cần sao chép dữ liệu từ bộ đệm nhân sang bộ đệm người dùng. Vị trí của file trong bộ nhớ ảo tương ứng với địa chỉ có thể truy cập giống như khi thao tác với bộ nhớ thông thường. Cách tiếp cận này giảm thiểu số lần sao chép dữ liệu giữa bộ đệm nhân và không gian người dùng, dẫn đến hiệu suất cao hơn.

12

| ```
tmp_buf = mmap(file, len);write(socket, tmp_buf, len);

Quy trình hoạt động của mmap như sau:

  1. Gọi hàm mmap() khiến nội dung file được mô-đun DMA sao chép vào bộ đệm nhân. Bộ đệm này được chia sẻ giữa kernel và tiến trình người dùng mà không cần bất kỳ lần sao chép nào giữa kernel và bộ nhớ người dùng. nohu95 2. Khi thực hiện write(), kernel sẽ sao chép dữ liệu từ bộ đệm nhân ban đầu sang bộ đệm kernel liên quan đến socket.
  2. Lần sao chép thứ ba xảy ra khi mô-đun DMA chuyển dữ liệu từ bộ đệm socket kernel sang bộ điều khiển giao thức.

Bằng cách sử dụng mmap thay vì read(), chúng ta đã giảm đi một nửa số lần sao chép dữ liệu trong kernel. Điều này đặc biệt hữu ích khi phải xử lý một lượng dữ liệu lớn. Tuy nhiên, phương pháp này cũng có một số nhược điểm tiềm ẩn. Ví dụ, nếu bạn thực hiện ánh xạ bộ nhớ cho một file rồi trong một tiến trình khác cắt bỏ file này, lệnh write() của bạn có thể bị gián đoạn bởi tín hiệu SIGBUS. Hành vi mặc định của SIGBUS là giết chết tiến trình và dump core – điều này không hề phù hợp với các máy chủ mạng.

Có hai cách để giải quyết vấn đề này:

  1. Phương án đầu tiên là cài đặt một chương trình xử lý tín hiệu SIGBUS và đơn giản hóa bằng cách trả về. Mặc dù phương pháp này có thể hoạt động tạm thời, nó không phải là một giải pháp triệt để vì SIGBUS báo hiệu một vấn đề nghiêm trọng của tiến trình. Do đó, phương pháp này không được khuyến khích sử dụng.
  2. Phương án thứ hai liên quan đến việc sử dụng cơ chế thuê file của kernel (trong Windows gọi là "opportunistic locks"). Đây là giải pháp đúng đắn. Bằng cách thiết lập thuê file trên file descriptor, bạn có thể yêu cầu kernel cấp một thỏa thuận thuê đọc/ghi. Khi một tiến trình khác cố gắng cắt bỏ file mà bạn đang truyền tải, kernel sẽ gửi một tín hiệu thời gian thực RT_SIGNAL_LEASE để thông báo rằng thỏa thuận thuê đọc/ghi sắp bị hủy bỏ. Trước khi chương trình của bạn truy cập vào một địa chỉ không hợp lệ và bị tín hiệu SIGBUS giết chết, lệnh write() sẽ bị gián đoạn. Giá trị trả về của write() sẽ là số byte đã viết trước khi bị gián đoạn và mã lỗi sẽ được đặt thành thành công.

Dưới đây là ví dụ về mã nguồn để thiết lập thỏa thuận thuê với kernel:

123456789

| ``` if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("Không thể thiết lập tín hiệu thuê từ kernel");
return -1;
}
/* Loại thuê có thể là F_RDLCK hoặc F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)) {
perror("Không thể thiết lập loại thuê từ kernel");
return -1;
}


Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về cách mà RocketMQ sử dụng mmap để tối ưu hóa hiệu suất lưu trữ tin nhắn.