Đặt Vấn Đề
Hè đến, nhu cầu du lịch tăng cao, kéo theo nhu cầu về đặt nhà nghỉ, khách sạn cũng tăng cao. Đặt phòng theo cách truyền thống sẽ có quy trình như sau: chúng ta tìm kiếm khách sạn, nhà nghỉ trên báo chí, google, sau đó gọi điện để check và đặt phòng. Khi thực hiện theo cách truyền thống, du khách sẽ gặp một số vấn đề như sau:
- Khó tìm được phòng đúng với yêu cầu (giá, vị trí, cơ sở vật chất, …). Lý do thứ nhất là chưa có những bộ lọc (filter) chuyên dụng. Thứ hai là nhiều nhà nghỉ, khách sạn chưa marketing tốt, chưa tiếp cận được nhiều khách.
- Quy trình rườm rà, mất nhiều thời gian là một cản trở.
Bên cạnh đó, vấn đề đối với phía nhà nghỉ, khách sạn là khi không trong mùa du lịch, họ cần quảng cáo những chiến dịch giảm giá để kích cầu du lịch.
Những nền tảng đặt phòng như Airbnb, Agoda, Booking.com, … sinh ra nhằm giải quyết những vấn đề trên. Bài này mình và các bạn sẽ cùng nhau thiết kế hệ thống Airbnb.
Đại bàng đầu trắng (Bald Eagle) là biểu tượng và là linh vật quốc gia của Hoa Kỳ. Chúng là loài chim lớn, có sải cánh lớn tới 2m. Đại bàng đầu trắng có bộ kỹ năng của những tay thợ săn siêu hạng. Bên cạnh đó, chúng còn nổi tiếng với hành vi khám phá, bao quát được vùng lãnh thổ rộng lớn để tìm kiếm thức ăn hoặc tìm chỗ làm tổ. Khi mùa đông tới, chúng di cư từ Bắc Mỹ xuống phía nam của lục địa với một quảng đường rất dài để trú đông.
Phân Tích Yêu Cầu & Phạm Vi
Airbnb là một hệ thống lớn, chứa rất nhiều logic nghiệp vụ phức tạp như cách tính giá phòng theo mùa, hoàn trả (refund), giảm giá (coupon), portal quản lý cho chủ/nhân viên khách sạn, … Bài viết này mình chỉ tập trung phân tích một số năng quan trọng như sau:
Yêu Cầu Tính Năng
Đầu đọc:
- Tìm kiếm và xem thông tin khách sạn
- Kiểm tra còn phòng trong khoảng thời gian từ ngày X đến ngày Y
- Tra cứu lịch sử đặt phòng
Đầu ghi:
- Đặt phòng
Yêu Cầu Phi Tính Năng
Giờ ta làm một số phép tính để nắm được tính chất của hệ thống.
Giả sử:
- Hệ thống của chúng ta phục vụ 10000 khách sạn, trung bình mỗi khách sạn có 100 phòng → có 1 triệu phòng tất cả.
- Trung bình 70% phòng được đặt và mỗi phòng đặt trong 3 ngày → Số lượng yêu cầu đặt phòng trong 1 ngày = (1 triệu x 0.7) / 3 = 233333 (reservations)
- Yêu cầu đặt phòng trong 1 giây = 233333 / (24 x 60 x 60) ~ 2.7 làm tròn lên ~ 3 yêu cầu đặt phòng trong 1 giây
- Để đặt phòng gồm 3 bước chính:
- Tìm kiếm và xem thông tin khách sạn
- Xem thông tin thanh toán
- Đặt phòng
- Tại mỗi bước có 10% khách sẽ thực hiện bước tiếp theo. Do đó, QPS (Query Per Second) tại bước “Xem thông tin thanh toán" = 3 x 10 = 30. QPS tại bước “Tìm kiếm và xem thông tin khách sạn” = 30 x 10 = 300.
Ta có thể thấy, đây là một hệ thống read-heavy, write-less (đọc nhiều hơn ghi). Với giả sử này, quy mô của hệ thống chưa lớn nhưng để thiết kế có tính mở rộng, có thể chịu tải vào mùa du lịch. Cũng như phục vụ mục đích nghiên cứu thì mình đưa ra một thiết như sau:
Thiết Kế Tổng Quát
Kiểu kiến trúc microservices được sử dụng cho hệ thống đặt phòng vì nhằm khắc phục những hạn chế của kiểu kiến trúc monolithic. Airbnb đã chuyển (migrate) từ monolithic vào năm 2018. Với lý do là khi hệ thống phình to ra, nó trở nên khó maintain, không ổn định và không tin cậy (a single point of failure).
Thiết kế gồm 4 layer chính:
- API Gateway: Kiểm tra quyền và điều hướng request.
- Data Access Layer: Do dữ liệu bị phân mảng ở layer dưới, ta cần tổng hợp dữ liệu ở tầng này cho những tính năng, use case cụ thể. Ví dụ: để xem thông tin khách sạn, Listing Presentation Service sẽ lấy thông tin khách sạn từ Hotel Service, lấy thông tin đánh giá ở Review Service, … rồi tổng hợp lại trả về Front End hiển thị.
- Platform Layer: chứa các statefule service tương tác với các vùng dữ liệu tương ứng. Trên thực tế, sẽ có rất nhiều service khác nữa. Chúng ta cần dành thời gian để review, quy hoạch, tránh đẻ ra nhiều service, phân mảnh về mặt logic và dữ liệu khiến cho hệ thống phức tạp và dữ liệu không thống nhất (inconsistency).
- Database: dữ liệu được quản lý bởi các team và stateful service tương ứng.
- Đối với Reservation Service, Pricing Service, ta nên sử dụng cơ sở dữ liệu có quan hệ (RDBMS) như MySQL. Vì 3 lý do sau:
- RDBMS hoạt động tốt với read-heavy và write less.
- RDBMS đảm bảo tính ACID [4] cần thiết cho chức năng đặt phòng.
- Do data model đều rất rõ ràng nên dễ dàng sử dụng RDBMS.
- Đối với các service khác như Hotel Service, Review Service có thể sử dụng database khác phù hợp với use case như search, filter, …
- Đối với Reservation Service, Pricing Service, ta nên sử dụng cơ sở dữ liệu có quan hệ (RDBMS) như MySQL. Vì 3 lý do sau:
Thiết Kế Chi Tiết
Giờ chúng ta đi sâu hơn, tìm hiểu một số vấn đề về mặt kỹ thuật.
Nhà Đông Con
Vấn đề dễ thấy nhất đầu tiên, service có thể được “đẻ" ra nhiều ở cả Platform Layer và Data Access Layer nhưng việc phát triển và triển khai 1 service mới sẽ tối nhiều effort (công sức). Để giảm tối thiểu effort cho quá trình này, Airbnb đã tìm ra những phần công việc chung (common workload) để dựng một số framework in-house nhằm tự động hoá quá trình phát triển và triển khai.
- Đối với các service ở tầng Data Access, có nhiều đoạn code chung như lấy dự liệu từ tầng Platform (do API ở tầng Platform đều được thiết kế theo 1 chuẩn), data transformation, kiểm tra quyền, … từ đó tạo ra 1 framework dùng để generate code từ các annotation.
- Đối với deployment, 1 framework được tạo ra dựa trên tư tưởng của GitOps [9]. Quản lý cấu hình resource, môi trường, alerts và service phụ thuộc của service cần triển khai ở trong 1 thư mục Git. Ngoài ra, framework cung cấp tính năng command line dùng để tự động sinh cấu hình và deploy service ở các môi trường khác nhau.
Để tối ưu việc đọc dữ liệu, ở tầng Data Access, thứ nhất ta có thể cache lại kết quả của những request về listing khách sạn, … Ngoài ra, việc gọm request (batching) từ tầng Data Access sẽ hạn chế được số lượng request được đẩy xuống tầng Platform. Ví dụ, ta có request đầu tiên R1 tìm khách sạn có id là H1 findHotelById(H1)
. Request R1 sẽ chưa được đẩy xuống tầng Platform ngay. Sau đó request R2 đến, tìm khách sạn H2. Trước đó, ta cần xác định timeout của mỗi batch. Đến thời điểm timeout, 2 request được gọm lại thành 1 request findHotelsByIds(H1, H2)
và đẩy xuống tầng Platform.
Bạn có thể xem chi tiết giải pháp ở reference [1], [2].
Nấu Thừa Còn Hơn Thiếu Ăn
Trên thực tế, khách sẽ đặt phòng theo loại phòng, chứ không đặt theo tên phòng hay ID của phòng. Do đó ta sẽ 2 bảng về số lượng phòng trống (room_inventory) và yêu cầu đặt trước (reservation) như sau:
Bảng room_inventory
Column | Key |
---|---|
hotel_id | PK |
room_type_id | PK |
date | PK |
total_inventory | |
total_reserved |
Bảng reservation
Column | Key |
---|---|
reservation_id | PK |
hotel_id | PK |
room_type_id | PK |
start_date | |
end_date | |
user_id | |
status |
Khi 1 yêu cầu đặt phòng được thực hiện thành công, hệ thống cần tạo ra 1 bảng ghi ở bảng reservation, đồng thời cập nhật số phòng đã đặt ở bảng room_inventory
. 2 bảng này sẽ được đặt cùng 1 database schema để chúng tận dụng được transaction của RDBMS và tránh không xử lý distributed transaction giữa các service.
Để thuận tiện cho việc kiểm tra còn phòng trong khoảng thời gian từ ngày X đến ngày Y trong tương lai. Sau khi 1 khách sạn được khởi tạo, hệ thống tạo ra số bảng ghi trong 2 năm cho tất cả loại phòng của khách sạn đó trong bảng room_inventory
. Giả sử, trung bình mỗi khách sạn có 15 loại phòng. Suy ra, số bảng ghi cho 2 năm trong bảng room_inventory
= 10000 x 15 x 2 x 365 = 109,500,000. Con số này có thể tăng lên theo mỗi năm thế nên ta có thể đánh partition theo hotel_id cho bảng room_inventory
để tối ưu performance.
Việc tạo trước các bản ghi sẽ giúp cơ chế xử lý concurrency (sẽ được đề cập ở phần tới) đơn giản hơn. Lưu ý, khi khách sạc cập nhật loại phòng hoặc hết kỳ 2 năm, ta sẽ sinh job tạo hoặc sửa các bản ghi tương ứng ở bảng room_inventory
.
Có một fact trong các ngành như khách sạn hay hàng không, đó là người ta hay áp dụng chiến lược cho phép đặt quá (overbooking) số lượng sẵn có. Mục đích để tối ưu lợi nhuận từ những trường hợp huỷ phòng, huỷ vé. Nghĩa là khách sạn X có 100 phòng, nhưng hệ thống lại cho phép đặt quá 10%, nói cách khác là có thể đặt tối đa 110 phòng của khách sạn X. Trong trường hợp cũng có 10% trường hợp huỷ phòng thì khách sạn X vẫn sẽ phục vụ hết công suất 100%. Chiến lược này có ưu nhược điểm, phía khách sạn phải cân nhắc, dựa vào dữ liệu thông kế để đưa ra phần trăm overbooking hợp lý (các bạn có thể xem thêm ở reference [5],[6]). Do đó, hệ thống chúng ta cần hỗ trợ cấu hình phần trăm overbooking này.
Như vậy, ta có logic kiểm tra còn phòng trong code như sau:
if (total_reversed + num_of_rooms_to_reverse <= total_inventory * (1 + overbooking))
Ăn Tranh
Ta có 2 vấn đề về concurrency cần được giải quyết:
- Một user clicks nút đặt phòng nhiều lần cùng thời điểm
- Nhiều user đặt cùng 1 loại phòng tại cùng thời điểm
Để giải quyết vấn đề 1, ta có thể thêm Idempotency Key. Idempotency Key có thể là request_id
được sinh ở Frontend, hoặc là reversation_id
được sinh từ Backend. Trước đó, cột Idempotency key đã được khai báo ràng buộc unique trong bảng reservation
. Như vậy, khi có 2 request có cùng request_id
đến tầng Database thì 1 trong 2 request (request đến sau) sẽ bị reject do đã tồn tại bản ghi unique trước đó.
Tư tưởng để giải quyết được vấn đề 2 là cơ chế locking. Ở tầng Database, ta có 3 kỹ thuật ở thực hiện cơ chế này: Pessimistic Locking, Optimistic Locking và CHECK Constraint.
Mình xin phép dẫn reference về 3 kỹ thuật vì bài viết đã khá dài rồi.
Các bạn xem thêm về Pessimistic Locking và Optimistic Locking tại reference [7], CHECK Constraint tại reference [8] đã trình bày đúng và chi tiết.
Mình tổng hợp lại ưu nhược điểm của 3 kỹ thuật trên như sau:
Ưu Điểm | Nhược Điểm | |
---|---|---|
Pessimistic Locking | + Ngăn được việc update vùng dữ liệu đang được update hoặc đã được update + Dễ implement + Phù hợp với yêu cầu về tính consistency cao |
- Có thể sinh ra deadlock nếu tầng app viết không đúng logic - Ảnh hưởng tới hiệu năng của DB nếu nhiều transaction bị lock quá lâu → khó scale |
Optimistic Locking | + Ngăn được việc update với dữ liệu không cũ, không phù hợp + Dễ implement + Không cần lock bản ghi. Khi tần suất conflicts thấp thì Optimistic Locking ít ảnh hưởng hơn tới performance của DB hơn so với Pessimistic Locking |
- Khi có nhiều request cùng lúc, chỉ có 1 request thành công, còn lại thất bại → trải nghiệm người dùng không tốt - Khi có số lượng concurrency lớn, performance của DB vẫn bị ảnh hưởng. |
CHECK Constraint | + Dễ implement + Tương tự với Optimistic Locking, phù hợp với tần suất conflicts thấp |
- Tương tự với Optimistic Locking, khi tần suất conflicts cao thì performance giảm đáng kể - Không thể version-control như code - Không phải DB nào cũng support tính năng này |
Tuỳ thuộc vào scale và yêu cầu của hệ thống để bạn chọn kỹ thuật locking cho phù hợp. Với trường hợp này, sần suất conflict của hệ thống thấp nên Optimistic Locking là 1 sự lựa chọn hợp lý.
Ngoài ra, nhóm mình có 1 bài viết khác đề cập tới vấn đề concurrency: Thiết Kế Hệ Thống Bán Vé - https://viblo.asia/p/thiet-ke-he-thong-ban-ve-ticketing-system-design-GyZJZnjZJjm
Các bạn tham khảo thêm nhé.
Tổng kết
Chúng ta đã cùng nhau thiết kế được 1 hệ thống Airbnb ở mức đơn giản, nắm được một số nghiệp vụ và kỹ thuật về:
- Phát triển và triển khai nhiều service
- Nghiệp vụ về overbooking
- Một số kỹ thuật cơ bản giải quyết 2 vấn đề concurrency
Cám ơn anh em đã đọc bài viết 🙏
Nếu anh em thấy hay thì cho mình xin 1 upvote và 1 share nhé.
Cám ơn anh em rất nhiều 🙏
🏢 System Design VN: https://fb.com/groups/systemdesign.vn
Tham Khảo
[1] Airbnb at Scale: From Monolith to Microservices - https://www.infoq.com/presentations/airbnb-scalability-transition/?topicPageSponsorship=25253fe5-e346-4907-afa1-87e0c77904ae
[2] The Human Side of Airbnb’s Microservice Architecture - https://www.youtube.com/watch?v=yGOtTd-l_3E
[3] Book: System Design Interview - Alex Xu & Sahn Lam
[4] ACID - https://www.ibm.com/docs/en/cics-ts/5.4?topic=processing-acid-properties-transactions
[5] What is an overbooking strategy in hotels and what are its advantages? - https://www.mews.com/en/blog/hotel-overbooking-strategy
[6] Why do airlines sell too many tickets? - https://www.youtube.com/watch?v=ZFNstNKgEDI
[7] Optimistic vs. Pessimistic Locking - https://vladmihalcea.com/optimistic-vs-pessimistic-locking/
[8] CHECK Constraint - https://vladmihalcea.com/mysql-custom-sql-check-constraints/
[9] What is GitOps? - https://www.redhat.com/en/topics/devops/what-is-gitops