Trò chuyện đã trở thành một phần không thể thiếu trong cuộc sống hàng ngày của chúng ta. Từ tâm sự với bạn bè, giao tiếp trong công việc cho đến thảo luận mọi chủ đề trên mạng xã hội, các ứng dụng chat đã dần trở nên quen thuộc với hầu hết mọi người. Sự phổ biến rộng rãi của các ứng dụng nhắn tin như Facebook Messenger, WhatsApp, Zalo,… đã thúc đẩy nhu cầu thiết kế và phát triển những hệ thống chat an toàn, hiệu quả và dễ sử dụng.
Xây dựng một hệ thống chat đáng tin cậy và mạnh mẽ không phải là việc đơn giản. Nó đòi hỏi phải hiểu rõ các yêu cầu của người dùng, xem xét các vấn đề về hiệu năng, bảo mật và khả năng mở rộng. Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách thiết kế một hệ thống chat hỗ trợ cả trò chuyện một-một và nhóm, với khả năng mở rộng đáp ứng 50 triệu người dùng hàng ngày. Từ việc xác định các yêu cầu, chọn lựa công nghệ phù hợp đến đề xuất kiến trúc tổng quan và đi sâu vào từng thành phần, chúng ta sẽ khám phá toàn bộ quá trình thiết kế chi tiết của một hệ thống chat.
Bắt đầu thôi nào một hành trình nào.
Trước khi bắt đầu quá trình thiết kế, điều quan trọng là phải xác định rõ mục đích và phạm vi của ứng dụng chat mà chúng ta hướng tới. Thị trường ứng dụng chat vô cùng rộng lớn và phong phú, với nhiều lựa chọn khác nhau để đáp ứng nhu cầu giao tiếp cho hầu hết mọi người. Từ những ứng dụng chat một-một như Facebook Messenger, WeChat hay WhatsApp cho đến các giải pháp chat nhóm chuyên nghiệp dành cho văn phòng như Slack, hay thậm chí là những nền tảng tương tác nhóm lớn và thoại độ trễ thấp chuyên biệt cho game thủ trên Discord 🎮. Mỗi sản phẩm đều có điểm mạnh và điểm yếu riêng, nhằm phục vụ một nhóm đối tượng người dùng cụ thể.
Do vậy, trước khi chúng ta đi sâu vào các chi tiết kỹ thuật, chúng ta cần xác định ứng dụng chat của mình sẽ thuộc phân khúc nào và phục vụ cho mục đích gì 🤔. Liệu đó có phải là một ứng dụng chat cá nhân tiện lợi cho bạn bè, gia đình? Hay là một công cụ hỗ trợ nhóm làm việc? Hoặc nó sẽ phục vụ cộng đồng game thủ yêu cầu trải nghiệm đồng bộ? Khi xác định được mục tiêu rõ ràng, chúng ta có thể xây dựng các yêu cầu và tính năng phù hợp cho ứng dụng. Dưới đây là một vài câu hỏi có thể đặt ra để xác định yêu cầu và phạm vi thiết kế:
BA: Trước khi đi vào chi tiết, hãy xác định xem chúng ta sẽ xây dựng một ứng dụng chat một-một hay nhóm? Hoặc cả hai luôn?
Khách Hàng: Nó nên hỗ trợ cả hai, chat một-một và nhóm.
BA: Đây sẽ là một ứng dụng di động, web hay kết hợp cả hai?
Khách Hàng: Cả hai.
BA: Quy mô của ứng dụng này là như thế nào? Một ứng dụng khởi nghiệp hay quy mô lớn?
Khách Hàng: Nó nên hỗ trợ 50 triệu người dùng hằng ngày.
BA: Đối với chat nhóm, giới hạn số thành viên trong một nhóm là bao nhiêu?
Khách Hàng: Tối đa 100 người.
BA: Tính năng nào quan trọng với ứng dụng chat? Có hỗ trợ tệp đính kèm không?
Khách Hàng: Chat một-một, chat nhóm, chỉ báo trạng thái trực tuyến. Hệ thống chỉ hỗ trợ tin nhắn văn bản.
BA: Có giới hạn kích thước tin nhắn không?
Khách Hàng: Có, độ dài văn bản nên dưới 100.000 ký tự.
BA: Có yêu cầu mã hóa đầu cuối không?
Khách Hàng: Hiện tại không yêu cầu nhưng chúng ta sẽ thảo luận nếu có thời gian.
BA: Chúng ta sẽ lưu trữ lịch sử chat trong bao lâu?
Khách Hàng: Mãi mãi. ⏱⏱⏱
Ở bài viết này, chúng ta sẽ tập trung vào việc thiết kế một ứng dụng chat tương tự như Facebook Messenger, với trọng tâm vào các tính năng sau:
Với những tính năng này, chúng ta sẽ nhắm đến một hệ thống có khả năng phục vụ 50 triệu người hoạt động mỗi ngày.
Để thiết kế được hệ thống trước tiên chúng ta cần phải hiểu được cách mà các clients và servers giao tiếp với nhau. Các clients ở đây có thể là ứng dụng di động hoặc ứng dụng web, điều quan trọng là chúng không thể giao tiếp trực tiếp với nhau. Thay vào đó, mỗi client sẽ được kết nốt tới một chat service, nơi cung cấp toàn bộ các tính năng đã được liệt kê ở trên.
Hãy tập trung vào những tính năng cốt lõi mà chat service cần phải thực hiện:
Khi bắt đầu một cuộc trò chuyện, client sẽ được kết nối tới chat service thông qua giao thức mạng. Trong hầu hết các ứng dụng client/server, luồng hoạt động thường bắt đầu từ phía client. Điều này cũng đúng với phía người gửi trong một ứng dụng chat. Khi người gửi muốn truyền một tin nhắn đến người nhận, họ sẽ sử dụng giao thức HTTP (giao thức web phổ biến nhất).
Cụ thể, client (của người gửi) sẽ mở một kết nối HTTP với chat service. Trên kết nối này, client gửi đi tin nhắn kèm theo yêu cầu chuyển tiếp tin nhắn đó tới người nhận. Việc sử dụng HTTP ở đây là hiệu quả vì giao thức này hỗ trợ keep-alive. Nhờ tính năng này, client có thể duy trì kết nối liên tục với chat service, giảm thiểu số lần phải thiết lập kết nối mới.
HTTP là một lựa chọn phù hợp với phía người gửi tin nhắn. Tuy nhiên, đối với phía nhận, việc sử dụng HTTP gặp phải một số khó khăn. Bởi vì HTTP là giao thức khởi tạo bởi client, nên không đơn giản để server khởi tạo kết nối và gửi tin nhắn cho client. Theo thời gian, nhiều kỹ thuật đã được sử dụng để mô phỏng kết nối được khởi tạo bởi server, bao gồm: polling, long polling và WebSocket.
Polling là kỹ thuật mà client định kỳ gửi yêu cầu đến server để kiểm tra xem có tin nhắn mới hay không. Tuy đơn giản nhưng polling cũng có nhược điểm của nó. Tùy theo tần suất truy vấn, nó có thể gây áp lực đáng kể lên server vì phải phản hồi cho rất nhiều yêu cầu nhưng hầu hết là “không có tin nhắn mới”. 📨
Nhận thấy nhược điểm trên, kỹ thuật Long Polling ra đời. Với long polling, client giữ kết nối mở cho đến khi thực sự có tin nhắn mới hoặc đạt đến ngưỡng timeout. Ngay khi nhận được tin nhắn, client sẽ ngay lập tức gửi một yêu cầu khác đến server, lặp lại quá trình.
Long polling cải thiện hiệu quả so với polling bằng cách giữ kết nối mở trong một khoảng thời gian nhất định chờ đợi tin nhắn mới, thay vì phải liên tục gửi yêu cầu. Tuy nhiên, nó vẫn còn một số hạn chế như sau:
WebSocket là một giao thức truyền thông máy tính (computer communication protocol), giao tiếp hai chiều giữa client và server bằng cách sử dụng một TCP socket để tạo một kết nối hiệu quả và ít tốn kém.
Kết nối WebSocket được khởi tạo bởi client. Ban đầu, kết nối WebSocket khởi đầu là một kết nối HTTP bình thường. Sau đó, client và server sẽ thực hiện một quá trình “bắt tay” (handshake) đặc biệt để nâng cấp kết nối HTTP lên thành kết nối WebSocket. Sau khi nâng cấp thành công, kết nối WebSocket liên tục sẽ được thiết lập. Thông qua kết nối này, server có thể gửi cập nhật dữ liệu (như tin nhắn mới) đến client bất cứ lúc nào mà không cần client phải gửi yêu cầu.
Trước đây, chúng ta đã nói rằng đối với phía người gửi, giao thức HTTP là một lựa chọn tốt. Nhưng vì WebSocket là song hướng, không có lý do kỹ thuật nào không sử dụng nó cho cả việc gửi và nhận.
Bây giờ chúng ta sẽ nói đến thiết kế tổng quan của ứng dụng. Mặc dù WebSocket được chọn làm giao thức giao tiếp chính giữa client và server cho giao tiếp song hướng, nhưng không phải tất cả các thành phần đều phải sử dụng WebSocket. Thực tế, hầu hết các tính năng (đăng ký, đăng nhập, profile, v.v.) của ứng dụng trò chuyện có thể sử dụng phương thức request/response thông qua HTTP. Chúng ta hãy tìm hiểu sâu hơn về các thành phần tổng quan của hệ thống.
Hệ thống sẽ được chia làm ba nhóm chính: stateless services (Dịch vụ không duy trì trạng thái), stateful services (Dịch vụ duy trì trạng thái), và third-party (Dịch vụ tích hợp từ bên thứ ba).
Các stateless service không lưu trữ trạng thái, chúng chỉ xử lý yêu cầu và trả về phản hồi. Ví dụ như authentication, quản lý profile, v.v. Đây là các tính năng phổ biến trong nhiều trang web và ứng dụng.
Các stateless services nằm đằng sau một load balancer. Những services này có thể là monolithic hoặc microservices riêng biệt. Chúng ta không cần phải xây dựng nhiều stateless service này vì có khá nhiều service sẵn có trên thị trường có thể được tích hợp dễ dàng. Phần quan trọng mà chúng ta cần đi sâu đó là service discovery. Nhiệm vụ chính của nó là cung cấp cho client một danh sách các tên miền DNS của các server chat để client có thể kết nối (Phần này sẽ được nói kĩ hơn ở bước 3).
Stateful service duy nhất được sử dụng trong ứng dụng là chat service. Nó được gọi là stateful service vì nó phải duy trì trạng thái kết nối với mỗi client (ứng dụng trò chuyện của người dùng).
Đối với một ứng dụng trò chuyện, thông báo đẩy (push notification) là dịch vụ tích hợp bên thứ ba quan trọng nhất. Nó cho phép thông báo đến người dùng khi có tin nhắn mới, ngay cả khi ứng dụng đang không chạy, đảm bảo người dùng nhận được thông báo kịp thời và liên tục.
Ở quy mô nhỏ, tất cả services có thể chạy trên một máy chủ duy nhất. Ngay cả ở quy mô cao hơn, lý thuyết tất cả các kết nối người dùng vẫn có thể nằm trên một máy chủ đám mây hiện đại, với giới hạn là số lượng kết nối đồng thời mà máy chủ có thể xử lý. Trong kịch bản của chúng ta với 1 triệu người dùng đồng thời, chỉ cần khoảng 10GB bộ nhớ để lưu trữ tất cả các kết nối, với giả định mỗi kết nối yêu cầu 10KB bộ nhớ.
Tuy nhiên, thiết kế đặt tất cả trên một máy chủ duy nhất là một ý tưởng tồi. Việc này đi ngược lại nguyên tắc thiết kế hệ thống phân tán, vì nó sẽ dẫn đến vấn đề điểm lỗi duy nhất (single point of failure) – một trong những lý do lớn nhất khiến thiết kế này bị loại.
Hình minh họa dưới đây cho thấy thiết kế tổng quan của hệ thông.
Trong thiết kế này, client duy trì kết nối WebSocket liên tục với chat server để trao đổi tin nhắn thời gian thực.
Bằng cách chia nhỏ hệ thống thành các thành phần riêng biệt với trách nhiệm cụ thể, nó giúp hệ thống dễ dàng mở rộng hơn và tránh vấn đề điểm lỗi duy nhất. Mỗi thành phần có thể được nhân rộng độc lập theo nhu cầu, đảm bảo hiệu suất và khả năng chịu lỗi cao hơn cho toàn bộ hệ thống.
Sau khi thiết lập các máy chủ, khởi chạy dịch vụ và hoàn tất tích hợp bên thứ ba, chúng ta cần xem xét lớp dữ liệu – thành phần nền tảng quan trọng của hệ thống. Lựa chọn hệ thống lưu trữ phù hợp đóng vai trò quyết định để đáp ứng các yêu cầu về hiệu năng và khả năng mở rộng.
Trong một ứng dụng trò chuyện, có hai loại dữ liệu chính cần được xem xét:
Để lưu trữ lịch sử trò chuyện, cơ sở dữ liệu key-value sẽ là một lựa chọn tuyệt vời với các lý do sau:
Bằng cách kết hợp RDBMS cho dữ liệu thông thường và key-value store cho lịch sử trò chuyện, hệ thống có thể tận dụng ưu điểm của cả hai loại cơ sở dữ liệu, đáp ứng các yêu cầu về hiệu năng, khả năng truy cập ngẫu nhiên dữ liệu, cũng như khả năng mở rộng với quy mô lớn. Lựa chọn phù hợp ở lớp dữ liệu sẽ tạo nền tảng vững chắc cho toàn bộ hệ thống hoạt động ổn định và hiệu quả.
Sau khi quyết định sử dụng cơ sở dữ liệu key-value cho lớp lưu trữ, chúng ta cần xem xét mô hình dữ liệu cho tin nhắn – phần quan trọng nhất của hệ thống trò chuyện.
Trong trò chuyện một-một, mỗi tin nhắn có một trường message_id
duy nhất làm khóa chính, giúp xác định thứ tự của tin nhắn. Không thể dựa vào trường created_at
để sắp xếp thứ tự, vì hai tin nhắn có thể được tạo ra cùng một thời điểm.
Đối với trò chuyện nhóm, khóa chính là cặp (channel_id, message_id)
. channel_id
là khóa phân vùng, vì tất cả tin nhắn trong cùng một nhóm sẽ thuộc về cùng một kênh chat.
Cách tạo message_id
rất quan trọng, vì nó quyết định thứ tự hiển thị của các tin nhắn. Để phục vụ mục đích này, message_id
cần đáp ứng hai yêu cầu:
Để đạt được điều này, chúng ta có thể sử dụng:
auto_increment
trong RDBMS như MySQL (nhưng không khả thi với NoSQL).Service discovery đóng vai trò then chốt trong việc đảm bảo hiệu suất và trải nghiệm người dùng tối ưu cho hệ thống trò chuyện. Với lượng kết nối đồng thời lớn, hệ thống cần phân phối tải trọng một cách hiệu quả giữa các máy chủ để tránh quá tải.
Sử dụng Apache Zookeeper, một giải pháp service discovery mã nguồn mở phổ biến, hệ thống có thể đạt được điều này. Zookeeper giám sát tất cả các máy chủ trò chuyện khả dụng và áp dụng các thuật toán thông minh để chọn ra máy chủ phù hợp nhất cho mỗi kết nối người dùng mới.
Các yếu tố như vị trí địa lý, tải trọng hiện tại và dung lượng của máy chủ được cân nhắc để đảm bảo người dùng được kết nối với máy chủ gần nhất, ít tải nhất, từ đó mang lại trải nghiệm trò chuyện trơn tru và đáp ứng nhanh chóng.
Hiểu được luồng hoạt động từ đầu đến cuối của một hệ thống trò chuyện là điều thú vị. Trong phần này, chúng ta sẽ khám phá luồng trò chuyện một-một, đồng bộ hóa tin nhắn trên nhiều thiết bị và luồng trò chuyện nhóm.
Khi người dùng A nhấn nút gửi, hệ thống trò chuyện khởi động một chuỗi xử lý đồng bộ và phân tán để đảm bảo tin nhắn đến tay người nhận B một cách nhanh chóng và đáng tin cậy. Quá trình này diễn ra như sau:
message_id
từ bộ tạo id. ID này sẽ giúp truy xuất và sắp xếp tin nhắn sau này.Trong thời đại kỹ thuật số ngày nay, người dùng thường sử dụng nhiều thiết bị khác nhau như điện thoại, máy tính bảng và laptop. Do đó, đảm bảo rằng lịch sử trò chuyện và tin nhắn mới được đồng bộ hóa liền mạch trên tất cả các thiết bị là một yêu cầu quan trọng.
Trong trường hợp này, người dùng A có hai thiết bị kết nối: điện thoại và laptop, cả hai đều thiết lập kết nối WebSocket riêng biệt với chat server 1.
Trên mỗi thiết bị, một biến trạng thái quan trọng gọi là cur_max_message_id được duy trì. Biến này theo dõi định danh của tin nhắn mới nhất đã được nhận trên thiết bị tương ứng. Khi một tin nhắn mới đến, hệ thống sẽ kiểm tra hai điều kiện:
Nếu cả hai điều kiện được đáp ứng, tin nhắn sẽ được coi là mới và cần đồng bộ hóa đến tất cả các thiết bị của người dùng đó.
Nhờ có cơ chế cur_max_message_id độc lập trên mỗi thiết bị, việc đồng bộ hóa tin nhắn trở nên đơn giản hơn bao giờ hết. Các thiết bị chỉ cần truy xuất vào hệ thống lưu trữ key-value để lấy tin nhắn mới dựa trên cur_max_message_id hiện tại của chúng.
So với trò chuyện một-một, logic của trò chuyện nhóm phức tạp hơn.
Khi số lượng thành viên tham gia không quá lớn, hệ thống trò chuyện áp dụng một cơ chế đồng bộ hóa tin nhắn đơn giản nhưng hiệu quả để đảm bảo mọi người đều nhận được nội dung trò chuyện mới nhất.
Hãy xem xét một ví dụ cụ thể với một nhóm gồm 3 thành viên: Người dùng A, B và C.
Đầu tiên, khi người dùng A gửi tin nhắn, tin nhắn được sao chép và đẩy vào “hộp thư đến” của từng thành viên còn lại, hay còn gọi là hàng đợi đồng bộ hóa tin nhắn. Trong trường hợp này, tin nhắn sẽ được đưa vào hàng đợi của cả Người dùng B và C.
Thiết kế này mang lại nhiều lợi ích đáng kể. Trước hết, nó đơn giản hóa quy trình đồng bộ hóa vì mỗi người dùng chỉ cần kiểm tra hàng đợi của riêng mình để lấy tin nhắn mới.
Hơn nữa, với số lượng thành viên nhóm nhỏ, chi phí lưu trữ để duy trì các bản sao tin nhắn trong từng hàng đợi là khá nhỏ và có thể đảm bảo hiệu suất.
Tuy nhiên, khi quy mô nhóm trò chuyện lớn lên, thiết kế này sẽ gặp phải những thách thức về hiệu năng và tài nguyên. Do đó, các ứng dụng phổ biến như WeChat đã giới hạn số lượng tối đa thành viên trong một nhóm là 500 người.
Ở phía người nhận, mỗi thành viên sẽ có một hộp thư đến riêng biệt, nơi tập hợp tất cả tin nhắn mới từ các cuộc trò chuyện khác nhau mà họ tham gia. Thiết kế này cho phép người dùng dễ dàng theo dõi và tương tác với nhiều cuộc trò chuyện đồng thời mà không bị nhầm lẫn.
Trạng thái hoạt động là một tính năng phổ biến của các ứng dụng trò chuyện. Thông thường chúng ta sẽ nhìn thấy những chấm tròn màu xanh lá cây nhỏ nhắn bên cạnh tên hoặc ảnh đại diện, báo hiệu rằng người đó đang online.
Phía sau chấm xanh đơn giản này là một hệ thống xử lý phức tạp, liên tục theo dõi và cập nhật trạng thái của từng người dùng.
Khi người dùng đăng nhập vào ứng dụng và thiết lập kết nối WebSocket giữa client và máy chủ thời gian thực, trạng thái trực tuyến của người dùng A và dấu thời gian last_active_at được lưu trong hệ thống lưu trữ key-value. Chỉ báo trạng thái cho thấy người dùng đang trực tuyến sau khi đăng nhập.
Khi người dùng đăng xuất, một quy trình đăng xuất được kích hoạt để đảm bảo trạng thái hiện diện của người dùng được cập nhật chính xác. Trạng thái trực tuyến sẽ được thay đổi thành ngoại tuyến trong hệ thống lưu trữ key-value. Chỉ báo trạng thái cho thấy người dùng đang ngoại tuyến.
Trong thực tế, kết nối mạng không phải lúc nào cũng ổn định. Khi một người dùng bị ngắt kết nối internet, kết nối liên tục giữa máy khách và máy chủ bị mất. Để giải quyết vấn đề này, hệ thống sử dụng một cơ chế “nhịp tim” thông minh. Theo chu kỳ, client trực tuyến sẽ gửi một tín hiệu đến các máy chủ giám sát trạng thái. Nếu không nhận được tín hiệu này trong một khoảng thời gian xác định, máy chủ sẽ đánh dấu người dùng là tạm thời ngoại tuyến.
Tuy nhiên, trạng thái này không ngay lập tức được cập nhật cho bạn bè. Thay vào đó, hệ thống sẽ chờ đợi trong một khoảng thời gian nhất định để xem liệu kết nối có được khôi phục hay không. Điều này giúp tránh trạng thái hiện diện thay đổi liên tục do mất kết nối tạm thời, cải thiện trải nghiệm người dùng.
Nếu kết nối không được khôi phục trong khoảng thời gian đã định, trạng thái “ngoại tuyến” của người dùng sẽ được cập nhật trong hệ thống lưu trữ dữ liệu và hiển thị cho các người dùng khác.
Làm thế nào để bạn bè của người dùng A có thể biết được trạng thái hoạt động của người dùng này. Đây là nơi mô hình public-subscribe được áp dụng.
Với mỗi cặp bạn bè, hệ thống duy trì một kênh riêng biệt. Khi trạng thái hoạt động của người dùng A thay đổi, sự kiện này được phát đi trên tất cả các kênh liên quan đến người dùng A, chẳng hạn như kênh A-B để thông báo cho người dùng B.
Các bạn bè đã đăng ký theo dõi kênh tương ứng sẽ nhận được thông báo cập nhật ngay lập tức thông qua kết nối WebSocket thời gian thực, cho phép họ luôn nắm bắt được trạng thái hoạt động của người dùng A.
Mô hình này hoạt động hiệu quả với các nhóm người dùng nhỏ. Tuy nhiên, khi quy mô nhóm tăng lên, việc thông báo đến tất cả thành viên có thể trở nên tốn kém về mặt tài nguyên và hiệu suất. Do đó, với các nhóm lớn, trạng thái hoạt động thường chỉ được tải lại khi cần thiết.
Qua bài viết này, chúng ta đã khám phá một kiến trúc của một hệ thống chat, hỗ trợ cả trò chuyện một-một và trò chuyện nhóm nhỏ. Với WebSocket đóng vai trò then chốt, giao tiếp thời gian thực giữa máy khách và máy chủ được đảm bảo, mang đến trải nghiệm trò chuyện trực tuyến mượt mà. 😍😍😍
Hệ thống bao gồm các thành phần chính: Chat server gửi và nhận tin nhắn; Presence server giám sát và cập nhật trạng thái trực tuyến; PN Server gửi thông báo đến thiết bị; Key-value store lưu giữ lịch sử trò chuyện; và API Server cung cấp chức năng khác.
Nguồn tham khảo:
You need to login in order to like this post: click here
YOU MIGHT ALSO LIKE