Trong thế giới công nghệ ngày nay, tốc độ là chìa khóa của sự thành công. Tại LinkedIn, nơi hàng tỷ yêu cầu từ người dùng được xử lý mỗi ngày trên nhiều nền tảng khác nhau, các kĩ sư tại Linkedin luôn tìm cách để cải thiện trải nghiệm người dùng. Một trong những yếu tố quan trọng nhất là thời gian tải trang - yếu tố có ảnh hưởng trực tiếp đến sự hài lòng của người dùng và hiệu suất của nền tảng. Để cải thiện trải nghiệm người dùng và giảm thiểu thời gian phản hồi, họ đã tích hợp một giải pháp mã hóa mới vào Rest.li, giúp giảm đáng kể độ trễ và tối ưu hóa việc sử dụng tài nguyên.

Tags: #system design, #protocol buffers, #linkedin

Trong thế giới công nghệ ngày nay, tốc độ là chìa khóa của sự thành công. Tại LinkedIn, nơi hàng tỷ yêu cầu từ người dùng được xử lý mỗi ngày trên nhiều nền tảng khác nhau, các kĩ sư tại Linkedin luôn tìm cách để cải thiện trải nghiệm người dùng. Một trong những yếu tố quan trọng nhất là thời gian tải trang - yếu tố có ảnh hưởng trực tiếp đến sự hài lòng của người dùng và hiệu suất của nền tảng. Để cải thiện trải nghiệm người dùng và giảm thiểu thời gian phản hồi, họ đã tích hợp một giải pháp mã hóa mới vào Rest.li, giúp giảm đáng kể độ trễ và tối ưu hóa việc sử dụng tài nguyên.

💡 Rest.li: Là một framework mã nguồn mở, được LinkedIn phát triển và sử dụng rộng rãi để xây dựng các microservices nhằm đáp ứng các yêu cầu của người dùng. Rest.li đóng vai trò quan trọng trong việc xây dựng các microservices , giúp tạo ra các kiến trúc RESTful mạnh mẽ và có khả năng mở rộng.

Khi JSON tạo ra các thách thức

Khi mới ra đời, Rest.li sử dụng JSON làm định dạng tuần tự hóa mặc định. JSON đã phục vụ tốt trong một thời gian dài. Nó dễ đọc cho người lập trình, được hỗ trợ rộng rãi trong nhiều ngôn ngữ lập trình. Nhưng dần dần, JSON bắt đầu bộc lộ những hạn chế của mình:

Cồng kềnh: JSON là một định dạng văn bản, nghĩa là nó thường khá dài dòng và chiếm nhiều không gian hơn cần thiết. Điều này dẫn đến việc sử dụng nhiều băng thông mạng hơn và thời gian phản hồi chậm hơn. Mặc dù kích thước có thể được giảm bớt bằng cách sử dụng các thuật toán nén như gzip, nhưng việc nén và giải nén gây tiêu tốn thêm tài nguyên phần cứng, và cũng có thể gây tốn kém và không khả dụng trong một số môi trường.
Chậm: Quá trình serialize và deserialize JSON không hiệu quả như mong muốn, đặc biệt là khi làm việc với các ngôn ngữ có garbage collection như Java và Python.

Họ đã bắt đầu đi tìm kiếm một giải pháp giải quyết được các thách thức này, một số tiêu chí họ đặt ra cho “ứng viên” thay thế được JSON là:

Kích thước payload nhỏ gọn: để tiết kiệm băng thông mạng và giảm độ trễ.
Hiệu suất serialize và deserialize cao: Quá trình chuyển đổi dữ liệu phải diễn ra nhanh chóng và sử dụng ít tài nguyên hệ thống. Độ trễ sẽ giảm, giúp hệ thống phản hồi nhanh hơn. Đồng thời, thông lượng sẽ tăng, giúp hệ thống xử lý được nhiều yêu cầu hơn trong cùng một khoảng thời gian.
Hỗ trợ nhiều ngôn ngữ lập trình: Rest.li được sử dụng trong nhiều ngôn ngữ lập trình tại LinkedIn (Java, Kotlin, Scala, ObjC, Swift, JavaScript, Python, Go), và họ muốn giải pháp thay thế hỗ trợ tất cả các ngôn ngữ này.
Dễ dàng tích hợp vào cơ chế serialize hiện có của Rest.li: họ muốn có thể tiếp tục sử dụng mô hình dữ liệu PDL và thực hiện quá trình di chuyển một cách dần dần với ít gián đoạn nhất.

Sau khi đánh giá kỹ lưỡng nhiều ứng viên như Flatbuffers, Cap'n'Proto, SMILE, MessagePack, CBOR, và Kryo, họ đã xác định Protobuf là lựa chọn tốt nhất vì nó hoạt động hiệu quả nhất với các tiêu chí trên.

Tích hợp Protobuf vào Rest.li

Hãy tưởng tượng bạn đang cố gắng ghép một mảnh ghép hình tròn vào một lỗ vuông. Đó chính xác là thách thức mà chúng tôi phải đối mặt khi tích hợp Protobuf vào Rest.li. Tại sao ư? Bởi vì Rest.li và Protobuf có cách "suy nghĩ" rất khác nhau về dữ liệu.Rest.li là một kẻ tự do. Nó không quan tâm đến schema và chỉ làm việc với DataMaps và DataLists. Nó sử dụng PDL (Protocol Definition Language) để mô hình hóa dữ liệu, nhưng PDL không có khái niệm về số thứ tự của trường dữ liệu. Ngược lại, Protobuf là một người thích trật tự. Nó cần dữ liệu được định nghĩa rõ ràng và mỗi trường dữ liệu phải có một số thứ tự cụ thể thường được xác định rõ trong Proto Schema.Để giải quyết vấn đề này, họ đã tạo ra "bảng ký hiệu" (symbol tables) - một bản đồ hai chiều kết nối các fields/value enum trong PDL với các số nguyên. Đây chính là chìa khóa để họ có thể sử dụng Protobuf mà không cần thay đổi cách Rest.li hoạt động.

1.Tại thời điểm runtime, ta biết loại đối tượng trong DataMap/DataList.
2.Kết hợp thông tin này với bảng ký hiệu, ta có thể tạo ra một schema Protobuf "ảo" ngay lúc chạy.
3.Sau đó, ta sử dụng các thư viện Protobuf có sẵn để serialize/deserialize dữ liệu.

Thách thức tiếp theo mà họ phải đối mặt là làm thế nào để tạo ra các bảng ký hiệu (symbol tables). Để giảm bớt công việc cho các dịch vụ backend, họ đã chọn phương pháp tạo và trao đổi bảng ký hiệu trong thời gian chạy (runtime), như được minh họa dưới đây:

Họ đã áp dụng hai chiến lược khác nhau để quản lý bảng ký hiệu, tùy thuộc vào loại ứng dụng:

Cho các dịch vụ backend: Phương pháp động

Trong quá trình khởi động dịch vụ (Service BootUp)

Quét các schema PDL: Dịch vụ sẽ quét tất cả các schema được định nghĩa (PDL - Pegasus Data Language schemas) cho các endpoint mà dịch vụ này hỗ trợ. Quá trình này tuân theo một thứ tự xác định, đảm bảo rằng tất cả các phiên bản của cùng một dịch vụ sẽ tạo ra bảng ký hiệu giống hệt nhau.
Tạo bảng ký hiệu: Các trường dữ liệu (fields) và giá trị enum từ các schema này sẽ được ánh xạ thành một danh sách các ký hiệu (symbols list). Mỗi tên trường hoặc giá trị sẽ được gán một chỉ số (index) duy nhất trong bảng ký hiệu. Bảng ký hiệu này sau đó được lưu trữ dưới dạng một mảng, và trong quá trình truyền tải dữ liệu, các tên trường sẽ được thay thế bằng các chỉ số tương ứng để tối ưu hóa kích thước payload. Dịch vụ cung cấp bảng ký hiệu này thông qua một endpoint đặc biệt gọi là symbolTable.

Giao tiếp giữa Client và Server

Kiểm tra bảng ký hiệu trong bộ nhớ cache:

Khi client cần giao tiếp với server, nó sẽ kiểm tra bảng ký hiệu tương ứng trong bộ nhớ cache của mình.
Nếu không tìm thấy, client sẽ yêu cầu bảng ký hiệu từ server thông qua endpoint symbolTable. Bảng ký hiệu nhận được sẽ được lưu trữ vào bộ nhớ cache để sử dụng trong các yêu cầu sau này, giảm thiểu số lần yêu cầu đến server.
Yêu cầu và phản hồi (Request and Response) được mã hóa:

Cả yêu cầu từ client và phản hồi từ server đều được mã hóa bằng cách sử dụng các ký hiệu thay vì tên trường thực tế, và định dạng được sử dụng là ProtoBuf.
MIME type của nội dung (content-type) bao gồm thông tin về bảng ký hiệu được sử dụng, giúp quá trình giải mã trở nên dễ dàng hơn. Ví dụ: Content-Type: application/x-protobuf2; symbol-table=mySymbolTable-v1

Phương pháp này rất linh hoạt và tự động, giúp tối ưu hóa việc truyền tải dữ liệu bằng cách giảm kích thước payload và đảm bảo đồng bộ hóa giữa các phiên bản của dịch vụ. Tuy nhiên, do cần phải yêu cầu bảng ký hiệu từ server trong lần khởi động đầu tiên, phương pháp này có thể gây ra độ trễ ban đầu. Điều này có thể không phù hợp với các ứng dụng web/mobile yêu cầu thời gian khởi động nhanh và trải nghiệm người dùng mượt mà.

Cho ứng dụng web/mobile: Phương pháp tĩnh

Đây là cách tiếp cận "chuẩn bị trước", tương tự như việc mang theo một cuốn từ điển khi bạn đi du lịch. Phương pháp này giúp giảm thiểu độ trễ và áp lực cho server trong quá trình triển khai các phiên bản mới của API.

Tạo bảng ký hiệu trong quá trình build: Thay vì tạo bảng ký hiệu một cách động khi dịch vụ khởi động (dẫn đến khả năng gây độ trễ hoặc tạo lưu lượng đột biến khi triển khai phiên bản mới), bảng ký hiệu được tạo sẵn trong quá trình build của hệ thống. Trong mỗi lần build, hệ thống sẽ tự động quét các schema của API và tạo ra bảng ký hiệu tương ứng.
Cập nhật thận trọng (append-only): Khi có thay đổi trong API và một phiên bản mới được build, plugin trong hệ thống build sẽ tự động cập nhật bảng ký hiệu. Cụ thể, các ký hiệu mới sẽ được thêm vào cuối danh sách, nhưng các ký hiệu cũ không bị xóa hoặc thay đổi. Điều này đảm bảo tính tương thích ngược (backward compatibility), giúp các phiên bản trước đó của client vẫn hoạt động bình thường với các bảng ký hiệu cũ.
Phân phối dưới dạng artifact có phiên bản: Bảng ký hiệu sau khi được tạo ra sẽ được xuất bản và lưu trữ dưới dạng một artifact có phiên bản cụ thể. Artifact này sau đó được client tải xuống và sử dụng trong quá trình build ứng dụng. Việc này đảm bảo rằng client luôn có bảng ký hiệu chính xác và tương thích với phiên bản API mà nó sẽ giao tiếp.

Phương pháp này giúp đảm bảo hiệu năng ổn định và giảm thiểu rủi ro về độ trễ hoặc sự cố trong quá trình khởi động hoặc nâng cấp ứng dụng, đặc biệt hữu ích khi ứng dụng cần giao tiếp với các API có thể thay đổi thường xuyên.

Những cải tiến từ Protobuf

Việc tích hợp Protobuf vào Rest.li đã giúp giảm độ trễ lên đến 60% đối với các dịch vụ có payload lớn. Con số này không chỉ là số liệu thống kê - chúng đại diện cho trải nghiệm nhanh hơn, mượt mà hơn cho hàng triệu người dùng LinkedIn trên toàn cầu. Dưới đây là biểu đồ so sánh độ trễ giữa Protobuf và JSON khi các server chịu tải nặng.

Kết luận

Việc sử dụng Protobuf thay vì JSON đã mang lại những cải tiến hiệu suất đáng kể, tạo ra những khoản tiết kiệm thực sự ở quy mô kỹ thuật của LinkedIn. Quá trình triển khai thay đổi này bằng các kỹ thuật như bảng ký hiệu (symbol tables) và cấu hình theo từng giai đoạn đã giúp cho việc chuyển đổi trở lên dễ dàng. Nhờ đó, họ đã đạt được những lợi ích này với rất ít gián đoạn đối với hoạt động kỹ thuật và kinh doanh.

SYDEXA
We learn, we share, we grow together!

About

  • Sydexa

Resources

  • Docs
  • Sydexa Hub

Contact

  • For Work
  • Report

Members

  • Sign in
  • Sign up
  • Portal
@ Sydexa 2024. All copyrights reserved