Facebook Live Commenting – Behind the scenes and Demo implement

Nhân tiện gần đây có cậu em mới hỏi về cách Facebook hiển thị live comment, nên có dành chút thời gian tìm hiểu thêm và note lại đây để chia sẻ với các bạn. Nếu ai chưa rõ live comment ở đây là gì thì đây là cách mà facebook hiển thị realtime khi có một comment mới cho tất cả các post nằm trong phần đang hiển thị trên newfeed của bạn.

Bài viết sẽ gồm hai phần, phần đầu sẽ phân tích lại case study thực tế của Facebook, problem họ đã gặp phải và solution mà các Facebook engineer đã đưa ra để giải quyết vấn đề. Phần hai sẽ đưa ra một implement đơn giản ở quy mô nhỏ hơn sử dụng các công cụ có sẵn.

Pushing vs Polling Data

Mỗi phút server Facebook nhận về khoảng 100 triệu đoạn dữ liệu, có thể là post, image/view, comment… Trong đấy sẽ có khoảng 650.000 là comment và tất cả cần phải được hiển thị đến đúng người xem một cách nhanh nhất. Đây là một thách thức không hề đơn giản với ngay cả các kĩ sư Facebook. Để đáp ứng yêu cầu mới này, họ đã phải build một hệ thống khác hoàn toàn với những gì đã phải xử lý trước đó.

Cách tiếp cận ban đầu mà các kĩ sư Facebook sử dụng là poll-based. Khi user scroll trên newfeed, js lưu lại các post đang nằm trong tầm hiển thị. Sau mỗi khoảng thời gian t, thực hiện ajax gọi tới API để check xem có comment nào mới không theo id của bài viết. Bằng cách giảm thời gian t ta có thể tiến tới mức gần với real time.

Tuy nhiên, rất nhanh chóng họ đã nhận ra rằng cách tiếp cận này rất khó để scale. Để tạo ra trải nghiệm khi comment thực sự đáng giá, đòi hỏi các comment phải được hiển thị một cách nhanh nhất đến đúng user mong muốn. Để user phải chờ quá lâu thật sự là một sự phiền phức và gây ra trải nghiệm rất tệ. Các kĩ sư Facebook đã nhận ra rằng, ít nhất t phải nhỏ hơn 5s để tạo ra một trải nghiệm chấp nhận được (mặc dù như thế vẫn là rất chậm). Bạn hãy tưởng tượng với số lượng người dùng tại một thời điểm của Facebook, cứ 5s lại có một request để check comment mới, rất nhanh chóng thì server của họ sẽ “sập”, dù có là Facebook đi chăng nữa. Vì thế sẽ phải cần một cách tiếp cận khác là push-based.

>> Kế hoạch thâu tóm bất cứ công ty startup nào cạnh tranh với Facebook

>> Khiếu nại, nhận phản hồi từ Facebook Ads bị khoá mới nhất năm 2021

Ý tưởng cơ bản của cách tiếp cận này là server Facebook sẽ chủ động push các comment về cho browser. Để các comment này tới đúng được với các user mong muốn, sẽ cần phải biết user nào đang xem nội dung mà vừa có comment mới. Rõ ràng là họ sẽ cần phải có một hệ thống có thể track lại nội dung user đã xem để trả lời câu hỏi “ai đang xem cái gì” ?

Write Locally, Read Globally

Lưu trữ mối liên kết một-một giữa nội dung và người đọc thì rất dễ dàng, nhưng giữ 16 triệu liên kết một giây thì lại là một vấn đề hoàn toàn khác. Tại thời điểm đó, hệ thống của Facebook được xây dựng để tối ưu cho nhu cầu read rất lớn và cho khả năng read rất nhanh so với write. Điều này không có gì là khó hiểu khi user tiêu thụ nội dung nhiều hơn rất nhiều so với những gì họ tạo ra. Tuy nhiên hệ thống đang cần xây dựng lại hoàn toàn ngược lại. Hệ thống mới cần xây dựng sẽ phải có khả năng write rất nhanh và nhiều hơn so với read. Mỗi khi user scroll trên newfeed, các nội dung liên tục được hiện ra và hoặc bị trôi đi. Mỗi nội dung được hiển thị hay bị ẩn với user đều cần phải được lưu trữ lại (dù chỉ là lướt qua), mỗi hành động này đều yêu cầu nhiều thao tác write. Chỉ khi có comment mới mới cần có một thao tác read (để tìm user đang xem bài viết). Rõ ràng tốc độ lướt newfeed và đọc thông tin của user sẽ nhanh hơn rất nhiều so với tốc độ tạo ra một comment mới.

Trong hệ thống hiện tại, write được thực hiện trên một database sau đó được replicate asynchronously sang các datacenters khác ở tất cả các regions trên toàn thế giới. Tuy nhiên, hãy nhớ rằng read được thực hiện nhiều hơn nhiều so với write, nên user sẽ đọc nội dung từ datacenter tùy thuộc vào region của họ. Sẽ có thể mất một khoảng thời gian nhỏ để nội dung mới được consistency trên toàn bộ các database nhưng đó là một vấn đề khác mà sẽ không đi sâu hơn trong bài viết này. Cách tiếp cận này gọi là: “read locally, write globally”.

Hệ thống đang cần xây dựng lại có một cách tiếp cận ngược lại, gọi là: “write locally, read globally”. Bởi vì cần write nhiều hơn, thao tác write sẽ được thực hiện local (không replicate) trên một datacenter. Khi read dữ liệu được collect từ tất cả các datacenters sau đó tổng hợp lại và tạo ra kết quả cuối cùng. Điều này có nghĩa là trong thực tế, mỗi khi một comment mới được tạo ra, hệ thống cần thực hiện multiple read trên các tất cả các datacenters từ các regions khác nhau. Tuy nhiên nó hoạt động vì như đã nói tốc độ tạo ra comment mới thấp hơn nhiều so với tốc độ đọc thông tin. Reading globally giúp Facebook không phải replicate một lượng lớn dữ liệu ra tất cả các datacenters, tiết kiệm băng thông đường dài và đắt tiền.

Demo implement

Xong phần lí thuyết nhàm chán rồi, giờ ta cùng bắt tay vào thử implement một hệ thống tương tự, dĩ nhiên là ở một quy mô nhỏ hơn Facebook nhiều rồi. Hệ thống của chúng ta sẽ chỉ có 1 cụm cluster duy nhất chứ không có nhiều datacenter như Facebook. Tuy ở quy mô nhỏ hơn, yêu cầu cho hệ thống mà chúng ta xây dựng cũng không hề đơn giản.

Hệ thống cần xây dựng phải đáp ứng được các yêu cầu:

  • Có khả năng lưu trữ lại toàn bộ view log theo thời gian thực, đây sẽ là cơ sở để biết user nào đang đọc bài viết vừa có thêm comment. Quá trình write này phải cực nhanh, và write rất nhiều.
  • Có cơ chế để app chính notify cho biết mỗi khi có comment mới. Và hệ thống cũng cần phải tìm ra các user đang đọc một bài viết nhanh nhất có thể.

Architecture

Facebook Live Commenting – Behind the scenes and Demo implement

Dựa vào các yêu cầu trên, tôi lựa chọn một số công cụ sau để xây dựng hệ thống của mình. Kiến trúc hệ thống như hình trên:

  • Một Message Queue (MQ) là trung tâm của hệ thống. Kafka được chọn vì performance tuyệt vời của nó. Cá nhân tôi thấy Kafka là best Message Queue nếu không cần thiết phải replay lại cho mỗi message.
  • Để đáp ứng khả năng lưu trữ view log cần write cực nhanh, không gì write nhanh bằng append trực tiếp vào file. Log cuối cùng của một user trên một nội dung sẽ cho biết user đó có đang xem nội dung đó không. Tuy nhiên tự implement theo hướng này khá khó, và ta không thể biết là cần phải đọc bao nhiêu log là đủ để có thể collect được tất cả các user đang xem một bài viết. Ở đây tôi sẽ sử dụng Cassandra, Cassandra có tốc độ write cực tốt (append vào commitlog, thời gian write luôn là hằng số, không phụ thuộc độ lớn của database), đồng thời tốc độ read cũng khá tốt.

Work Follow

  • Mỗi khi user scroll trên newfeed, js track thông tin và send log về server, log được send qua Kafka và lưu vào Cassandra.
  • Mỗi khi có comment mới, nó sẽ được lưu vào database chính đồng thời một message chứa toàn bộ thông tin về comment được send vào Kafka. Lưu ý là toàn bộ thông tin về comment như: id bài viết chứa nó, user comment, content, time… vì ta sẽ không query lại trong database để lấy các thông tin này nữa.
  • Module xử lý chính nhận message từ Kafka, query trong Cassandra để tìm ra tất cả các user đang xem bài viết vừa có comment mới. Kết quả sẽ cho ta một list id của các user.
  • Việc còn lại chỉ là send một message vào Kafka chứa thông tin comment + toàn bộ các user tìm được và để cho module khác push comment đấy về cho client. Việc push comment về cho client là một vấn đề nằm ngoài phạm vi bài viết này, các bạn có hứng thú có thể tham khảo kĩ thuật reverse ajax.

Kafka

Không có gì để nói nhiều, đơn giản ta sử dụng 3 topics cho 3 mục đích khác nhau. logs cho write view log, new_comments cho notify khi có comment mới và push_comments để đẩy nội dung comment và thông tin user cần nhận tới module push.

Cassandra

Vì Cassandra cho khả năng write luôn là hằng số, ta chỉ cần tối ưu cho read. Ở đây ta cần trả lời câu hỏi: “user nào đang xem bài viết x” ? Ta thiết kế table view_logs để lưu trữ view log như sau (ở đây tôi sử dụng Cassandra 3.x):

create table view_logs (
post_id text,
is_visible boolean,
user_id int,
last_update timestamp,
primary key(post_id, is_visible)
)

Mỗi khi một nội dung hiển thị hoặc không còn được hiển thị trên browser của user, js sẽ track và send log về server của chúng ta. Do sử dụng  làm partition key nên log mới sẽ luôn đè log cũ, ta sẽ luôn biết được trạng thái cuối cùng của một user trên một bài viết.

Khi cần biết user nào đang đọc một bài viết, ta chỉ cần query:

select * from view_logs where post_id = x and is_visible = true

Tất nhiên là hệ thống của chúng ta chỉ nằm trong 1 cluster, không có nhiều datacenter cũng như ở nhiều region khác nhau để có thể áp dụng lí thuyết “write locally, read globally” của Facebook. Tuy nhiên ta có cách tiếp cận gần tương tự với Cassandra: sử dụng ConsistencyLevel.ONE cho write và ConsistencyLevel.ALL cho read. Các bạn thử để lại comment xem như thế có hợp lí không nhé ?

Một vài hạn chế và cải tiến

Một vài vấn đề mà ta có thể gặp phải với hệ thống này:

– Do vấn đề khi truyền dữ liệu qua mạng, ta không thể bảo đảm thứ tự view log đúng với những gì nó xảy ra. Ví dụ một user X kéo qua bài viết A rồi lại kéo lại, nghĩa là ta có 2 thao tác write trong đó thao tác bài viết A được hiển thị xuất hiện sau và nó phải là trạng thái cuối cùng của user X với bài viết A. Tuy nhiên trong thực tế có thể xảy ra, thao tác user kéo qua bài viết lại được gửi tới sau, dẫn tới trong view log khi query ta không nhận được user X đang xem bài viết A. Trường hợp này thực ra cũng ít khi xảy ra, và thực ra thì nếu nó có xảy ra thì cũng không có gì quá nghiêm trọng. Để bảo đảm thứ tự chính xác tuyệt đối thì khi send log về phía client gửi kèm timestamp tại thời điểm send. Phía server ta sử dụng tính năng USING TIMESTAMP của Cassandra để bảo đảm log với timestamp lớn hơn sẽ luôn là log cuối cùng.

– Cassandra có thể là điểm gây chậm nhất của toàn hệ thống. Để tối ưu hơn, ta hoàn toàn có thể delay một chút việc đọc từ Kafka các comment mới được tạo ra một khoảng thời gian nhỏ mà user không thể nhận ra (ví dụ 100ms). Với các nội dung có sự tương tác rất lớn, tại một thời điểm có thể có tới hàng trăm comment. Nếu trong khoảng thời gian 100ms đọc từ Kafka cho biết rằng có 1000 comments mới nhưng trong đó có 100 comment là từ cùng một bài viết, đơn giản ta có thể tiết kiệm được 99 lần query vào Cassandra và chỉ cần send về cho phía client một lần 99 comments thay vì 99 lần một comment đơn lẻ -> tiết kiệm kha khá phải không nào ?

Fun

Một điểm thú vị là cũng giống như New Feed, nút Like, thì tính năng Live Commenting này cũng là kết quả từ một cuộc thi Hackathon, bá đạo không nào ? và có vẻ như Facebook đã hốt cả nhóm thi này về làm các core engineer để xây dựng tính năng này cho họ, các bạn có thấy con đường vào làm cho Facebook gần hơn tí nào chưa ^^

Fivestar: 
No votes yet