Zero-copy và tối ưu data transfer

Nhiều ứng dụng hiện nay có nhiệm vụ transfer data từ nguồn (Disk, Socket…) mà không cần thiết thay đổi nội dung (VD: webpage asset files, FTP, proxy…). Trong một chương trình Java, phổ biến là lớp `InputStream` nằm trong package `java.io`. Chúng hoạt động bằng cách chia nhỏ data thành những đoạn buffer nhỏ rồi chuyển qua `OutPutStream` để ghi xuống nguồn như Disk hoặc Socket. Một cách cụ thể hơn, đó là khi các ứng dụng web thường transfer những asset file từ server xuống trình duyệt của người dùng. Với những ứng dụng lớn, công việc này làm chi phí overhead tăng khiến cho hệ thống không hoạt động nhanh như mong muốn. Vậy làm sao để tối ưu chúng?
Trước tiên, chúng ta cùng nhau tìm hiểu cơ chế transfer data thông thường bằng đoạn code đơn giản sau đây:
FileInputStream in = new FileInputStream("in.txt");
FileOutputStream out = new FileOutputStream("out.txt");
int c;

while ((c = in.read()) != -1) {
    out.write(c);
}

in.close();
out.close();
Biểu đồ các bước mà nó thực hiện:
Quá trình sẽ trải qua 4 giai đoạn như trên bao gồm:
  • Bước 1: lệnh read() từ chương trình sẽ thực hiện lệnh switch từ user mode sang kernel mode. Sau đó gọi đến DMA (Direct Memory Access) engine để thực hiện bước copy data từ disk đến bộ đệm của kernel
  • Bước 2: data từ bộ đệm của kernel (lấy từ bước 1) sẽ copy vào bộ đệm của user context, đồng thời thực hiện switch từ kernel mode sang user mode.
  • Bước 3: lệnh write() tiếp tục copy data vừa nhận được từ hàm read() tới bộ đệm của kernel, và context switching lại được thực hiện chuyển sang kernel mode.
  • Bước 4: hàm writer() được return, từ kernel mode sẽ chuyển đổi lần cuối về lại user mode. Đồng thời đó, data nằm trong bộ đệm của kernel cũng sẽ copy vào DMA Engine để chuyển tới nguồn đích.
Như đã thấy, quá trình trên cần đến 4 bước context switching và 4 lần data copy vào các bộ đệm. Việc này có thể dẫn tới bottleneck và overhead cost tăng lên đáng kể.
Phương pháp Zero-copy
Khi nhìn vào các bước thực hiện của phương pháp thông thường phía trên, bạn có thể nhận thấy một số bước copy không thực sự cần thiết. Cụ thể đó là thao tác copy ở bước 2 và 3. Nếu ta có thể thực hiện việc copy từ bộ đệm đọc tới thẳng bộ đệm ghi từ kernel mode thì sẽ tiết kiệm được 2 lần copy cũng như thao tác context-switching. Trong Java, phương thức `transferTo()` nằm ở `java.nio.channels.FileChannel` thực hiện chính xác điều này.
Đây là đoạn code sử dụng `transferTo()`:
FileChannel inChannel = new FileInputStream("in.txt").getChannel();
FileChannel outChannel = new FileOutputStream("out.txt").getChannel();

inChannel.transferTo(0, inChannel.size(), outChannel);

inChannel.close();
outChannel.close();
Còn đây là cụ thể các bước chương trình thực hiện:
Như đã thấy, `transferTo()` chỉ thực hiện gọi tới kernel hệ thống để DMA engine copy từ bộ đệm đọc sang bộ đệm ghi rồi sau cùng chuyển tới nguồn. Có nghĩa là nó đã tiết kiệm được một nửa các bước copy và context-switching không cần thiết so với cách thông thường. Trong thực tế, `transferTo()` (cụ thể hơn là JVM) sẽ thực hiện lệnh gọi hệ thống (syscall) dựa trên cách OS hỗ trợ Zero-copy như sendfile() của Linux hay `TransmitFile()` của Windows.
Kết luận:
Hiểu về kernel của OS hoạt động giúp ích nhiều cho ta về hoạt động performance tuning. Zero-copy cung cấp cách thức hoạt động hiệu quả cho chương trình của chúng ta, đặc biệt là trong những hệ thống lớn. Nếu hệ thống của mình cần nhiều đến thao tác transfer data, có lẽ bạn nên cân nhắc sử dụng chúng.
Tham khảo thêm:

Add a Comment

Scroll Up