Hướng dẫn fastapi mongodb - fastapi mongodb
Câu chuyện về chiếc API cực kỳ đơn giản đã được phù phép để trở nên phức tạp và fancy. Show
Vừa rồi mình mới được sếp giao cho 1 task vô cùng đơn giản: Tạo 1 service api với duy nhất 1 endpoint là Get item by ID. Tưởng chừng như đây là bài toán tạo API CRUD cho các bạn sinh viên mới ra trường, mình hăm hở lao vào code với tâm thế cửa trên. Chỉ sau khoảng 2 tiếng ngồi setup môi trường, tìm thư viện, code,... mình đã có 1 chiếc project sử dụng KoaJS với 1 endpoint đúng như yêu cầu. Và mọi chuyện tới đây là kết thúc.Get item by ID. Tưởng chừng như đây là bài toán tạo API CRUD cho các bạn sinh viên mới ra trường, mình hăm hở lao vào code với tâm thế cửa trên. Chỉ sau khoảng 2 tiếng ngồi setup môi trường, tìm thư viện, code,... mình đã có 1 chiếc project sử dụng KoaJS với 1 endpoint đúng như yêu cầu. Và mọi chuyện tới đây là kết thúc. First things firstẤy vậy mà. Nếu câu chuyện đã kết thúc sớm như vậy thì chắc mình sẽ chẳng ở đây chém gió với các chế làm gì đúng không? Chúng ta hãy tạm nghỉ giải lao ít phút trước khi bắt đầu ngụp lặn trong cái ao sâu tới nửa mét này nhé. Mình là Minh Monmen, 1 devops chân chính sùng bái vẻ đẹp của sự đơn giản, ưa chuộng việc đu ít mai seo khi tìm hiểu 1 vấn đề, nhưng lại thích đứng trên vai người khổng lồ khi bắt tay vào giải quyết vấn đề đó. Hôm nay mình rất hân hạnh được đồng hành cùng các bạn trong câu chuyện cá chép hóa rồ này. Rất mong được các bạn ủng hộ và cổ vũ để có động lực tiếp tục chém gió trên giang hồ.Minh Monmen, 1 devops chân chính sùng bái vẻ đẹp của sự đơn giản, ưa chuộng việc đu ít mai seo khi tìm hiểu 1 vấn đề, nhưng lại thích đứng trên vai người khổng lồ khi bắt tay vào giải quyết vấn đề đó. Hôm nay mình rất hân hạnh được đồng hành cùng các bạn trong câu chuyện cá chép hóa rồ này. Rất mong được các bạn ủng hộ và cổ vũ để có động lực tiếp tục chém gió trên giang hồ. Đi vào chủ đề chính của chúng ta: chiếc API chính được dùng để Get item by ID.Get item by ID. Thoạt nghe thì các bạn sẽ chẳng thấy nó có gì đáng phải suy ngẫm, tuy nhiên đằng sau chiếc API đơn giản này lại là những yêu cầu hết sức phức tạp đến từ vị trí của người thiết kế hệ thống. Nhưng đó là câu chuyện phía sau, ta hãy cùng điểm qua nhanh 1 số kiến thức nền tảng để có thể hiểu được bài viết này:
Nhào zô.
ContextTại sao mình lại phải làm 1 service riêng biệt chỉ chứa duy nhất 1 API Get item by ID? Well, đó là 1 câu chuyện dài, bắt nguồn từ kiến trúc micro-services.Get item by ID? Well, đó là 1 câu chuyện dài, bắt nguồn từ kiến trúc micro-services. Trong hệ thống micro-services nói chung thường sẽ xuất hiện những service chứa thông tin được dùng chung rất nhiều trên toàn hệ thống (như Account, Product Catalog,...). Đây là những service trọng yếu của hệ thống, không chỉ xử lý business logic của bản thân nó mà còn là nơi tra cứu data của rất nhiều service khác. Chính vì vậy lượng request liên quan tới read của nó sẽ nhiều hơn phần write rất nhiều, dẫn tới bọn mình đã phải tiếp cận vấn đề khác đi 1 chút như sau:Account, Product Catalog,...). Đây là những service trọng yếu của hệ thống, không chỉ xử lý business logic của bản thân nó mà còn là nơi tra cứu data của rất nhiều service khác. Chính vì vậy lượng request liên quan tới read của nó sẽ nhiều hơn phần write rất nhiều, dẫn tới bọn mình đã phải tiếp cận vấn đề khác đi 1 chút như sau: Bài toán optimize cho phần read internal là 1 bài toán khá phức tạp, chính vì vậy bọn mình đã quyết định tách phần read internal ra thành 1 service riêng. Service này sẽ chỉ phục vụ với tư cách là nguồn dữ liệu cho các service khác và tập trung vào tối ưu phần read. Việc tách này giúp mình có thể thoải mái lựa chọn giải pháp hơn và tránh ảnh hưởng tới service chứa rất nhiều business rule hiện tại. Yêu cầu dành cho service read internal:
Chúng ta có thể bỏ qua việc optimize ở tầng Database do những bảng dữ liệu này tương đối bé (cỡ vài triệu rows) và query trực tiếp bằng Primary Key. Thứ chúng ta để ý ở đây là tối ưu phía ứng dụng mà thôi.Primary Key. Thứ chúng ta để ý ở đây là tối ưu phía ứng dụng mà thôi. The first attemptBan đầu service này được mình viết bằng NodeJS với kiến trúc rất đơn giản sau: Benchmark qua performance của hệ thống được kết quả sau:
Như các bạn có thể thấy, TP95 rơi vào khoảng 48ms với throughput 2000 RPS. Cũng là 1 con số khá OK nếu như mình muốn sử dụng luôn codebase từ service đang chạy. Cách xử lý này không dùng cache, do vậy sẽ gây gánh nặng lên DB. Tuy nhiên ở mức tải thấp thì MongoDB hoàn toàn cân được. Việc không dùng cache cũng khiến cho data trả về cho các request luôn là data mới nhất.TP95 rơi vào khoảng 48ms với throughput 2000 RPS. Cũng là 1 con số khá OK nếu như mình muốn sử dụng luôn codebase từ service đang chạy. Cách xử lý này không dùng cache, do vậy sẽ gây gánh nặng lên DB. Tuy nhiên ở mức tải thấp thì MongoDB hoàn toàn cân được. Việc không dùng cache cũng khiến cho data trả về cho các request luôn là data mới nhất. The second attemptMọi chuyện diễn ra hoàn toàn ổn với chiếc API ở trên và mọi thứ chạy trơn tru trong vòng vài tuần. Tuy nhiên mình có ghi nhận vào 1 số thời gian cao điểm thì latency của API này có vọt lên khá lớn (khoảng vài trăm ms) làm các service phụ thuộc vào nó bị chậm đi. Không chấp nhận kết quả chỉ dừng ở con số 2000rps, mình bắt tay vào nghiên cứu tối ưu để tăng hiệu năng của service này. Mặc dù NodeJS đáp ứng tốt hầu hết các loại service và workload mình đã làm, tuy nhiên do đây là 1 trong những service đặc biệt đã được tách riêng do đó việc áp dụng một công nghệ khác là hoàn toàn có thể.2000rps, mình bắt tay vào nghiên cứu tối ưu để tăng hiệu năng của service này. Mặc dù NodeJS đáp ứng tốt hầu hết các loại service và workload mình đã làm, tuy nhiên do đây là 1 trong những service đặc biệt đã được tách riêng do đó việc áp dụng một công nghệ khác là hoàn toàn có thể. 1 yếu tố khác cần cân nhắc đó là tài nguyên. Ứng dụng NodeJS của mình khi chạy chiếm khá nhiều tài nguyên, ví dụ với mỗi instance chiếm 100~200MB ram thì việc chạy 10~15 instance (để đảm bảo throughput của hệ thống) sẽ chiếm kha khá tài nguyên. Để có 1 bước ngoặt thì buộc chúng ta phải rẽ ngang. Và mình đã chuyển qua viết lại service bằng Golang. Do đây là service có logic đơn giản nhất, chỉ cần nhanh nên việc áp dụng hẳn 1 ngôn ngữ mới vào xử lý khá dễ dàng và phù hợp.Golang. Do đây là service có logic đơn giản nhất, chỉ cần nhanh nên việc áp dụng hẳn 1 ngôn ngữ mới vào xử lý khá dễ dàng và phù hợp. Vẫn với mô hình như trên, không cache và gọi trực tiếp tới DB, chiếc API sử dụng Golang của mình ra đời với kết quả benchmark như sau:
Not bad, huh? Chiếc API mới của mình đã có throughput lớn gấp 2.5 lần và TP95 giảm đi 2.5 lần. Tuy nhiên tới đây chắc các bạn sẽ đều thắc mắc: Nếu đây chỉ là 1 bài viết khoe rằng Golang nhanh hơn NodeJS thôi thì có gì mà phải viết dài vậy? Bài viết cũng chả có tý gì gọi là chất xám của tác giả luôn.throughput lớn gấp 2.5 lần và TP95 giảm đi 2.5 lần. Tuy nhiên tới đây chắc các bạn sẽ đều thắc mắc: Nếu đây chỉ là 1 bài viết khoe rằng Golang nhanh hơn NodeJS thôi thì có gì mà phải viết dài vậy? Bài viết cũng chả có tý gì gọi là chất xám của tác giả luôn. Đừng vội thất vọng, những thứ hay ho còn ở phía sau. The third attemptKhông hài lòng với chính mình, mình bắt đầu nghiên cứu các giải pháp để tối ưu hơn nữa kết quả này. Và ý tưởng lóe lên đầu tiên chính là sử dụng caching. Tất nhiên là khi sử dụng caching thì mình sẽ phải giải quyết 1 số vấn đề sau:caching. Tất nhiên là khi sử dụng caching thì mình sẽ phải giải quyết 1 số vấn đề sau:
Tôi là ai, đây là đâu?Việc đầu tiên ta cần làm đó là hiểu được rõ mình đang làm gì. Nghe thì có vẻ hiển nhiên tuy nhiên các bạn đừng coi thường bước này. Đây là bước để các bạn có thể hiểu rõ được hệ thống của mình, các đặc điểm dữ liệu, các yêu cầu đối với response. Mình có thể tóm tắt bằng 1 vài câu hỏi nhanh như sau:
Caching hiệu quả với thuật toán LFU của RistrettoSau khi có thông tin về đặc điểm cũng như traffic hệ thống thì mình bắt tay vào tìm kiếm thư viện với các cơ chế cache phù hợp. Và Ristretto là một thư viện sáng giá đáp ứng đầy đủ yêu cầu của mình:
Các bạn có thể đọc thêm về từng bước xây dựng cũng như benchmark với các cache lib khác trong 2 bài blog này: The State of Cache in Go và Introducing Ristretto: A High-Performance Go Cache
Tới đây, sau khi implement xong phần cache thì về cơ bản là mình đã xong việc. Đây là kết quả benchmark sau khi request gửi tới đã được cache hoàn toàn:
Rất ấn tượng phải không? Mình đã tăng throughput của service từ 5k rps lên 40k rps với latency giảm đi 10 lần với các item được cache.5k rps lên 40k rps với latency giảm đi 10 lần với các item được cache. Mặc dù implement Ristretto vào project rất dễ, tốc độ của API cũng được cải thiện rất nhiều lần. Tuy nhiên đây chưa phải là trạm dừng chân của chúng ta. Sử dụng cache thôi không khiến cho API của bạn bất khả chiến bại. Có rất nhiều yếu tố cần được xem xét để có thể sử dụng cache 1 cách hiệu quả:
Quá trình tinh chỉnh các thông số trên của mình khá mất thời gian, tuy nhiên có 1 số kinh nghiệm rút ra để các bạn tham khảo:
Số lượng cache item và số item bị xóa sẽ phụ thuộc hoàn toàn vào đặc điểm traffic và hệ thống của các bạn, do vậy để xác định được các con số này chẳng có cách nào khác ngoài việc monitor hệ thống của mình, tìm hiểu xem trong 1 ngày có bao nhiêu item được request, tần suất xuất hiện như thế nào, cân đối với lượng RAM cho mỗi instance để quyết định. Giải quyết bài toán độ trễ dữ liệu với MongoDB Change StreamsĐộ trễ dữ liệu luôn là 1 bài toán rất đau đầu với mọi hệ thống cache. Mình nhớ có lần đã từng đọc 1 bác nào đó phát biểu:
Nghe cũng hợp lý phết đúng không? Ở phía trên khi chưa có cơ chế invalidate cache thì mình chỉ sử dụng expire time để invalidate cache thôi. Đây là cách dễ nhất để quản lý độ trễ dữ liệu. Tuy nhiên việc giảm expire time xuống sẽ ảnh hưởng trực tiếp tới Cache Hit Ratio. Thêm vào đó những item ít thay đổi cũng bị invalidate trong khi những item thường xuyên thay đổi thì vẫn phải chấp nhận độ trễ dữ liệu. Dựa vào đặc điểm dữ liệu là update không thường xuyên / ít so với tần suất đọc, mình có mạnh dạn sử dụng 1 tính năng rất xịn xò của MongoDB đó là Change Streams. Qua đó thay vì việc mình lắng nghe các sự kiện của ứng dụng để invalidate cache thì sẽ lắng nghe trực tiếp event của database thông qua Change Streams.Change Streams. Qua đó thay vì việc mình lắng nghe các sự kiện của ứng dụng để invalidate cache thì sẽ lắng nghe trực tiếp event của database thông qua Change Streams. Nôm na là khi collection của các bạn xuất hiện các action thêm sửa xóa thì Change Streams sẽ trả cho bạn 1 event liên quan tới những document có thay đổi. Mình dựa vào event này để invalidate cache của item đó trong ứng dụng. Cách này có 1 số đặc điểm mà các bạn cần lưu ý:
Vậy là cơ chế cache hoàn chỉnh của mình sẽ là: Nhờ vào cơ chế invalidate này, mình có thể tăng thời gian cache lên tới 1-2 ngày, từ đó giúp tăng Cache Hit Ratio lên tối đa. (>90%) Tổng kếtMột số thành quả mình thu được:
Hết rồi. Nếu có câu hỏi gì vui lòng comment nhé. Làm cách nào để bắt đầu một dự án bằng fastapiBắt đầu bằng cách tạo một thư mục mới để giữ dự án của bạn có tên là "Fastapi-mongo": Tiếp theo, tạo và kích hoạt môi trường ảo: Hãy thoải mái trao đổi VirtualEnv và Pip cho thơ hoặc PipenV.
Trình điều khiển cơ sở dữ liệu tốt nhất cho MongoDB là gì?Nó cũng khá hữu ích khi sử dụng GUI cho ví dụ MongoDB của bạn.Điều này cho phép bạn duyệt các cơ sở dữ liệu, bộ sưu tập và hồ sơ, có thể hoàn toàn hữu ích khi gỡ lỗi ứng dụng Fastapi của bạn.Tôi khuyên dùng Robo 3T - nó miễn phí và dễ sử dụng.Pymongo là trình điều khiển cơ sở dữ liệu Python chính thức cho MongoDB.
Trình điều khiển cơ sở dữ liệu tốt nhất cho Fastapi là gì?Điều này cho phép bạn duyệt các cơ sở dữ liệu, bộ sưu tập và hồ sơ, có thể hoàn toàn hữu ích khi gỡ lỗi ứng dụng Fastapi của bạn.Tôi khuyên dùng Robo 3T - nó miễn phí và dễ sử dụng.Pymongo là trình điều khiển cơ sở dữ liệu Python chính thức cho MongoDB.Sau đó, bạn có thể kết nối với máy chủ cơ sở dữ liệu MongoDB của bạn thông qua Mongoclient.Ví dụ:
MongoDB là gì và nó hoạt động như thế nào?Theo Wikipedia, MongoDB là một chương trình cơ sở dữ liệu định hướng tài liệu đa nền tảng.Được phân loại là chương trình cơ sở dữ liệu NoQuery, MongoDB sử dụng các tài liệu giống JSON với các lược đồ tùy chọn. |