Đây là một nhu cầu cơ bản của developer: bạn đọc từ file hoặc một api danh sách các số nguyên. Bạn cần lưu nó vào bộ nhớ rồi làm abc xyz tiếp.
Quá dễ nhỉ, trước đây, với Java, mình hay làm thế này:
import java.util.*;
...
final int size = 1_000_000; // giả sử list size là 1 triệu
final List<Integer> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
int value = randomInt();
list.add(value);
}
Nhưng rồi mình nhận ra, một Data Engineer không bao giờ làm thế.
Vì sao vậy?
Thử ước lượng, một số nguyên là 4 bytes, chúng ta lưu 1 triệu số => Mất 4MB Ram để lưu trữ.
Đơn giản vậy thì đã tốt, hãy thử đo memory thực tế dòng code này chiếm dụng:
final long bytes = MemoryMeasurer.measureBytes(list); // đếm số bytes
System.out.println(bytes);
Kết quả: 20_000_000. Tức là 20M Ram.
Thử với một mapping giữa 2 số nguyên 4 bytes và 8 bytes, cứ tưởng sẽ mất cỡ 12M Ram, nhưng có vẻ không phải.
final Map<Long, Integer> map = new HashMap<>(size);
for (int i = 0; i < size; i++) {
long key = (long) randomInt();
int value = randomInt();
map.put(key, value);
}
final long bytes = MemoryMeasurer.measureBytes(map);
System.out.println(bytes);
Kết quả: 80_000_000. Tức là 80M Ram.
Chúng ta mất hơn 5 lần bộ nhớ so với tính toán. Có nghĩa là nếu làm big data, chúng ta phải mất 5 máy tính thay vì 1 so với dự tính?
Vì sao ra nông nỗi?
Ban đầu mình đổ tội cho ngôn ngữ Java, nhưng mình nhầm, ngoài các ngôn ngữ bậc thấp, C/C++ hoặc ngôn ngữ chuyên biệt như Scala, thì Python, Php, JS, … tất cả đều như vậy.
Phải có gì đó không ổn trong cách thiết kế của các ngôn ngữ này.
Giải pháp
Rất may là mỗi ngôn ngữ có vẻ như có giải pháp cho việc này.
Mình code Java, nên hiện nay mình dùng thư viện fast util chứ không bao giờ xài các thư viện mặc định của Java.
Hãy xem sự khác biệt nhé, đoạn code sau mình lưu cùng một lượng dữ liệu vào thư viện mặc định của java và fast util, và đo lượng memory mỗi thư viện sẽ chiếm dụng:
Java ArrayList vs fast-util IntArrayList
public static void testList() {
final int size = 1_000_000;
System.out.println("Test memory java DEFAULT LIST and FAST LIST, test size = " + size);
final List<Integer> list = new ArrayList<>(size);
final List<Integer> fastList = new IntArrayList(size);
for (int i = 0; i < size; i++) {
int value = randomInt();
list.add(value);
fastList.add(value);
}
final long listBytes = MemoryMeasurer.measureBytes(list) / 1000;
final long fastListBytes = MemoryMeasurer.measureBytes(fastList) / 1000;
System.out.println("Default list = " + listBytes + " KB");
System.out.println("Fast list = " + fastListBytes + " KB (" + ((100.0 * fastListBytes) / listBytes) + "%)");
}
Kết quả: IntArrayList chiếm dụng memory chỉ bằng 1/5 thư viện mặc định của Java.
Test memory java DEFAULT LIST and FAST LIST, test size = 1000000
Default list = 20000 KB
Fast list = 4000 KB (20.0%)
Java HashMap vs fast-util OpenHashMap
public static void testNativeMap() {
final int size = 1_000_000;
System.out.println("Test memory java DEFAULT MAP and FAST MAP with NATIVE TYPE, test size = " + size);
final Map<Long, Integer> map = new HashMap<>(size);
final Map<Long, Integer> fastMap = new Long2IntOpenHashMap(size);
for (int i = 0; i < size; i++) {
long key = (long) randomInt();
int value = randomInt();
map.put(key, value);
fastMap.put(key, value);
}
final long defaultBytes = MemoryMeasurer.measureBytes(map) / 1000;
final long fastBytes = MemoryMeasurer.measureBytes(fastMap) / 1000;
System.out.println("Default map = " + defaultBytes + " KB");
System.out.println("Fast map = " + fastBytes + " KB (" + (100.0 * fastBytes / defaultBytes) + "%)");
}
Kết quả: FastUtil chiếm dụng memory bằng 1/3 so với thư viện mặc định Java
Test memory java DEFAULT MAP and FAST MAP with NATIVE TYPE, test size = 1000000
Default map = 80380 KB
Fast map = 27263 KB (33.91764120427967%)
Kết luận
Vậy là, nếu làm Data Engineer, bạn đừng bao giờ dùng những thư viện mặc định để lưu trữ dữ liệu trong RAM nhé. Hãy tìm những thư viện chuyên dụng như kiểu fast-util cho Java.
Nếu không, chi phí server/máy tính của bạn sẽ đội lên theo cấp số nhân.
Tại BeeCost.Com, nhờ sử dụng FastUtil mà với 1 máy chủ dữ liệu giá $25/tháng, chúng mình theo dõi 100 triệu sản phẩm online, xử lý 2 tỷ giá tiền sản phẩm mỗi ngày, giúp người dùng mua sắm tiết kiệm hơn.
Ít tài nguyên vậy liệu bạn có nghi ngờ về tốc độ xử lý của BeeCost??
Phải thử đi bạn mới thấy sự khác biệt nhé !!
Thực hành ngay.
Các bạn hãy sử dụng nguyên tắc này để thực hành lại các ví dụ của mình xem sao nhé.
Đối với các bạn sử dụng Java, thì đây là hướng dẫn để các bạn có code chạy lại ví dụ mình đã chạy. Trong bài này mình đưa ra 2 ví dụ về List và Map, project code mở của chúng mình còn một ví dụ thứ 3 để bạn biết cách sử dụng thành thạo fast-util.
Step 1: Download code
Step 2: Build code
Step 3: Trong thư mục code, hãy build lại và chạy class FastUtilExample:
Ví dụ đây là lệnh tại máy tính của mình:
cd ~/workspace/data_engineering/
bin/java.sh -javaagent:lib/object-explorer.jar de.FastUtilExample
Output:
Test memory java DEFAULT LIST and FAST LIST, test size = 1000000
Default list = 20000 KB
Fast list = 4000 KB (20.0%)
- - - - - - - -
Test memory java DEFAULT MAP and FAST MAP with NATIVE TYPE, test size = 1000000
Default map = 80380 KB
Fast map = 27263 KB (33.91764120427967%)
Chúc các bạn sử dụng blog này hiệu quả. Đừng coi thường những dòng code cơ sở này nhé. Nên nhớ là BeeCost.Com, nhờ sử dụng FastUtil mà với 1 máy chủ dữ liệu giá $25/tháng, chúng mình theo dõi 100 triệu sản phẩm online, xử lý 2 tỷ giá tiền sản phẩm mỗi ngày.
Mình còn nợ các bạn
Mình vẫn chưa trả lời vấn đề vì sao mà các thư viện mặc định lại chiếm dụng memory nhiều tới vậy. Bằng kinh nghiệm của mình, đây là một một trong những rào cản của một Data Engineering khi phải tiếp xúc với lượng dữ liệu lớn.
Bài tiếp theo của BeeCost Engineering Blog sẽ đào sâu vấn đề này. Hãy chờ chúng mình nhé!
Bài gốc tại BeeCost’s Github.