Cải thiện hiệu năng Facebook Android với FlatBuffers
Ở Facebook, người dùng có thể nắm bắt thông tin từ gia đình, bạn bè qua việc đọc các status và xem ảnh. Ở phía backend, chúng ta lưu tất cả dữ liệu tạo nên các kết nối trên mạng xã hội. Ở phía mobile, chúng ta không thể download các entire về, vậy chúng ta tải 1 node và một số kết nối của nó và lưu dưới dạng tree ở local.
Ảnh phía dưới mô tả cách kết nối. Trong ví dụ, Jonh tạo 1 story và rồi bạn bè của anh ta like, comment. Bên tay trái của ảnh là đồ thị để mô tả các quan hệ ở Facebook. Khi ở Android truy cập story, chúng ta lấy về một cấu trúc cây bắt đầu với story, bao gồm các thông tin về actor, feedback và các đính kèm (xem ở phần bền phải của ảnh).
Một điểm mấu chốt chúng tôi xác định là làm sao để biểu diễn và lưu trữ dữ liệu ở app. Nó không khả dụng để đưa dữ liệu vào các bảng khác nhau trong SQLite bởi vì có rất nhiều kiểu truy xuất các node và các liên quan từ phía backed. Chúng ta lưu trực tiếp nó theo cấu trúc dạng cây. Một lựa chọn khác là lưu nó dạng Json nhưng mà thời gian deserialize từ Json sang Java quá tốn tài nguyên như sau:
- Tốc độ parsing. Tốn đến 35 ms để parse từ JSON (20kb — đây là size thông thường của 1 response từ Facebook), thời gian giữa 2 frame là 16.6ms. Với cách này thì sẽ bị drop frame khi mà load stories từ disk cache.
- Khởi tạo Parser. Một JSON Parse cần build một trường ánh xạ trước khi nó có thực hiện parse, nó tốn 100ms đến 200ms, nó làm chậm quá trình khởi động của ứng dụng.
- Bộ dọn rác GC: Một lượng các objects được tạo ra trong quá trình parse JSON, và theo theo dump từ bộ nhớ thì có khoảng 100KB được allocated khi mà parse JSON nặng 20KB từ đó GC phải hoạt động.
Chúng tôi muốn tìm một cách lưu trữ tốt hơn nhằm tăng tốc ứng dụng Facebook.
FlatBuffers
Trong quá trình tìm kiếm các cách thay thế, chúng tôi tìm thấy FlatBuffers, một mã nguồn mở từ Google. FlatBuffers là một bước tiến của phương thức bộ đệm, nó bao gồm cả object metadata, cho phép truy cập trực tiếp từ các thành phần con mà không cần phải deserialize cả object (trong trường hợp này là một tree).
Tưởng tượng rằng chúng ta có một class đơn giản Person với các trường dữ liệu của nó: name, friendship status, spouse, và danh sách bạn bè. Các trường spouse và friends cũng bao gồm các đối tượng person, và nó tạo thành 1 cây. Đây là một hình mình họa đơn giản của của 1 đối tượng trong FlatBuffer cho một người tên John và vợ anh ấy Mary.
class Person {
String name;
int friendshipStatus;
Person spouse;
List<Person>friends;
}
Trong layout trên, bạn cần chú ý rằng:
- Mỗi đối tượng chia thành 2 phần: Phần metadata(vtable) bên trái và phần dữ liệu thật ở bên phải được tách bởi vị trí pivot.
- Mỗi trường tương ứng với một vị trí trong vtable, nó lưu trữ offset của thông tin thật của trường đó. Ví dụ, ô đầu tiên của vtable của Jonh có giá trị 1, nó xác định rằng tên của Jonh được lưu bằng 1 byte ở bên phải vị trí pivot.
- Tương tự với các trường khác, offset ở vtable sẽ trỏ đến vị trí pivot của các object con. Ví dụ, ô thứ 3 ở vtable của Jonh trỏ đến pivot của Mary.
- Để xác định rằng không có giá trị để biểu diễn, chúng ta có thể sử dụng một offset 0 trong vtable.
Đoạn code sau sẽ biểu diễn các chúng ta tìm kiếm tên của vợ Jonh trong cấu trúc nếu trên.
// Root object position is normally stored at beginning of flatbuffer.
int johnPosition = FlatBufferHelper.getRootObjectPosition(flatBuffer);
int maryPosition = FlatBufferHelper.getChildObjectPosition(
flatBuffer,
johnPosition, // parent object position
2 /* field number for spouse field */);
String maryName = FlatBufferHelper.getString(
flatBuffer,
johnPosition, // parent object position
2 /* field number for name field */);
Chú ý rằng không có việc tạo đối tượng trung gian, tiết kiệm việc phân bộ nhớ tạm thời. Chúng ta có thể tối việc này hơn nữa bởi việc lưu dữ liệu FlatBuffer trực tiếp trong một file và ánh xạ nó lên memory. Điều đó có nghĩa rằng chúng ta chỉ load những phần của file mà chúng ta cần đọc, hơn nữa nó còn làm giảm thiểu lưu trữ bộ nhớ chung.
Ngoài ra, sẽ không cần phải deserialize đối tượng cây trước khi đọc các trường. Nó giảm thiểu độ trễ giữa phần lưu trữ và phần UI, và nó tăng hiệu năng nói chúng.
Mutation on FlatBuffers
Thi thoảng chúng ta cần thay đổi giá trị bên trong FlatBuffers. Khi mà FlatBuffer là bất biến theo thiết kế, chính vì vậy mà không có cách nào trực tiếp thay đổi giá trị. Giải pháp là chúng ta theo dõi các thay đổi side by side với FlatBuffer gốc.
Mỗi phần của dữ liệu trong FlatBuffer có thể được định danh duy nhất bởi vị trí tuyệt đối của nó với FlatBuffer. Chúng tôi hỗ trợ sự thay đổi (mutation) nên bạn không cần phải tải lại một câu chuyện để có một thay đổi nhỏ — như là một thay đổi trong friendship status. Để ví dụ, đây là 2 hình dung khái niệm của việc làm sao track theo dõi của 2 thay đổi:
John’s friendship status is pointed to by a vtable slot at absolute index 2 of the FlatBuffer. To change John’s status, we just need to record that data corresponding to absolute index 2 is now 1 (meaning a friend) instead of 2 (meaning not a friend, but friendship request is sent).
- Friendship status của Jonh có vị trí trong vtable là 2. Để thay đổi friendship status của Jonh, chúng ta chỉ cần ghi lại rằng dữ liệu tương ứng ở vị trí 2 là 1(nghĩa là friend) thay vì là 2 (request kết bạn được gửi).
- Tên của Mary trong vtable ở vị trí 13. Để đổi tên của Mry, chúng ta chỉ cần ghi lại một chuỗi giá trị chuỗi mới tương ứng ở vị trí 13.
At the end, we could pack all mutations into mutation buffer. Mutation buffer is made up of two parts: mutation index and mutation data. Mutation index records mapping from absolute index in base buffer to position of new data. Mutation data stores the new data in FlatBuffer format.
Cuối cùng, chúng ta có thể đóng gói toàn bộ các thay đổi vào trong 1 buffer chưa dữ liệu thay đổi. Buffer này được tạo thành 2 phần: phần index thay đổi và phần data thay đổi. Phần index ghi lại ánh xạ từ vị trị cố định trong buffer gốc đến vị trí dữ liệu mới. Phần data lưu trữ dữ liệu mới ở dạng FlatBuffer.
Khi query một phần của dữ liệu trong FlatBuffers, chúng ta tìm vị trí cố định của dữ liệu, xem trong bộ đệm thay đổi xem có thay đổi nào xảy ra hay không và trả về nó, nếu không chúng ta sẽ trả về dữ liệu trong bộ đệm cơ sở.
Flat Models
FlatBuffers có thể được sử dụng không chỉ cho lưu trữ, nó có thể cho networking, và như là định dạng bộ nhớ trong trong ứng dụng. Điều này loại bỏ bất kì sự chuyển đổi dữ liệu nào từ response server đên UI. Nó cho phép chúng ta hướng đến một mô hình Flat Models rõ ràng hơn, nó loại bỏ sự phức tạp giữa tầng UI và tầng lưu trữ dữ liệu.
Khi JSON được sử dụng như là định dạng lưu trữ, chúng ta cần thêm bộ nhớ các cho việc deserialization. Chúng ta thêm một tầng logic giữa UI và tầng lưu trữ. Xem hình dưới.
Mặc dù kiến trúc 3 tầng trên cũng đã rất phổ biến ở iOS và deskop, nhưng nó có thể là vấn đề với Android:
- Một bộ nhớ cache thông thường nghĩa là chúng ta có thể giữa nhiều trong bộ nhớ hơn cần thiết cho việc hiển thị lên UI. Nhiều thiết bị Android trên thị trường cho phép không quá 48MB bộ nhớ cho một ứng dụng. Khi bạn thêm quá nhiều việc cho bộ dọn rác Java, nó có thể gây ra vấn đề hiệu năng.
- Logic ứng dụng cần thiết phải giải quyết vấn đề bộ nhớ cache, UI, lưu trữ, nhưng thông thường code liên quan đến UI và lưu trữ diễn ra ở một luồng khác. Giữ mô hình luồng đơn giản trong một ứng dụng lớn có thể khó khăn.
- UI thông thường nhận dữ liệu từ nhiều nguồn, như là dữ liệu cách ở bộ nhớ, dữ liệu mới từ mạng, dữ liệu thay đổi cục bộ từ logic ứng dụng… Nó yêu cầu UI phải xử lý với nhiều kịch bản dữ liệu thay đổi và dẫn tới quá UI vẽ không kịp.
Với Flat Models, tầng UI và tầng lưu trữ có thể được tích hợp dễ dàng như là diagram sau đây.
- UI được xây dựng trực tiếp trên bộ nhớ bằng cursor trên Android và vì lưu trữ đến giao diện người dùng là đường nhanh nhất trong hầu hết các ứng dụng Android, điều này có thể giúp giữ cho giao diện người dùng phản ứng nhanh.
- Phần Logic ứng dụng và phần mạng đã được di chuyển xuống dưới lớp lưu trữ, cho phép tất cả logic ở đó diễn ra trên một background thread và đảm bảo rằng kết quả được phản ánh trong lưu trữ trước tiên. Sau đó, UI có thể được thông báo để vẽ lại thông qua việc sử dụng thông báo content provider của Android.
- Kiến trúc này cho phép phân tách rõ ràng giữa các lớp logic UI và logic ứng dụng — và chúng ta có thể đơn giản hóa logic cho từng lớp. Các thành phần UI chỉ cần phản ánh trạng thái lưu trữ và logic ứng dụng chỉ cần ghi thông tin cuối cùng (chính xác) vào lớp lưu trữ. Các lớp logic và UI chạy trên các thread khác nhau và chúng không bao giờ phải giao tiếp trực tiếp với nhau.
Conclusion
FlatBuffers là một dạng dữ liệu loại bỏ đi việc phải chuyển đổi dữ liệu giữa lưu trữ và UI. Trong việc áp dụng nó, chúng ta đã cải tiến kiến trúc trong ứng dụng như là Flat Models. Thay đổi mở rộng chúng ta đã xây dựng dựa trên FlatBuffers cho phép chúng ta theo dõi dữ liệu server, các thay đổi, và trạng thái local trong một cấu trúc đơn, nó đã cho phép chúng ta đơn gian hóa mô hình dữ liệu và mở ra một API thống nhất cho các thành phần UI.
Trong 6 tháng gần đây, chúng tôi đã chuyển đổi phần lớn Facebook trên Android sang sử dụng FlatBuffers như là định dạng lưu trữ. Một vài cải tiến hiệu năng có thể kể đến:
- Thời gian tải từ bộ nhớ cach trên đĩa cứng giảm từ 35ms xuống còn 4ms cho 1 story.
- Bộ nhớ cấp phát tạm thời giảm 75%.
- Thời gian khởi động chết giảm 10–15%.
- Chúng tôi giảm bộ nhớ lưu trữ 15%.
Nó rất tuyệt để sử dụng làm định dạng dữ liệu cho phép con người tốn 1 chút thời gian đọc update từ bạn và xem ảnh từ gia đình.
Cảm ơn FlatBuffers!
Nguồn: https://code.fb.com/android/improving-facebook-s-performance-on-android-with-flatbuffers/
Đánh giá cá nhân
Giải pháp này không khả dụng cho hầu hết các ứng dụng của chúng ta bây giờ. Điều kiện áp dụng là cả Server và client đều phải sử dụng cơ sở dữ liệu Graph.
Điều học được ở trong giải pháp nãy là:
- Cách giải quyết bài toán cập nhật dữ liệu giữa client -server
- Tư tưởng của FlatBuffer trong lưu trữ
- Tư tưởng của Flat Model trong phân tách các component.