Chương 1: Tổng quan về thiết kế hệ thốngThiết kế hệ thống là quá trình định nghĩa các thành phần và cách tích hợp chúng, các giao diện lập trình ứng dụng APIs, và mô hình dữ liệu data mo
Tổng quan về thiết kế hệ thống
Thiết kế trong các hệ thống nhỏ và trong các hệ thống lớn
1.1.1 Thiết kế trong hệ thống nhỏ
Với các hệ thống nhỏ và tương đối đơn giản, thiết kế và kiến trúc của hệ thống không phải là một vấn đề quá phức tạp, do bản thân các hệ thống này tương đối dễ tổ chức, dễ vận hành, dễ
Với các hệ thống này, Với các hệ thống này, có rất nhiều mô hình phát triển phần mềm đủ đáp ứng yêu cầu: Kiến trúc client-server, kiến trúc layer, kiến trúc monolithic,…
Hình 3: Kiến trúc Client-Server
Một điểm chung của các loại kiến trúc này đều là mô hình hệ thống tập trung vì vậy nó sẽ có các vấn đề sau:
Về mặt “hiệu suất”, các hạn chế về vật lý của phần cứng như nâng cấp CPU, nâng cấp RAM, nâng ổ cứng,… đặt ra một số giới hạn đối một máy chủ Với các hệ thống có quy mô lớn hơn, mô hình nghiệp vụ phức tạp hơn, nhiều người sử dụng hơn, các mô hình kiến trúc kể trên không đủ khả năng để cung cấp dịch vụ kịp thời, chính xác và ổn định Hơn nữa, việc nâng cao hiệu suất của một máy chủ duy nhất trở nên vô cùng đắt đỏ sau một ngưỡng nhất định.
Về mặt “mở rộng”, việc lưu trữ và xử lý dữ liệu đều đóng góp cho các hệ thống phần mềm về những giá trị nhất định Vì vậy, Khi lượng khách hàng của hệ thống tăng lên, hệ thống cần xử lý thêm lưu lượng và lưu trữ lượng dữ liệu lớn hơn Tuy nhiên, hệ thống bao gồm một máy chủ duy nhất chỉ có thể mở rộng đến một mức nhất định.
Về mặt “sẵn sàng”, Hiện nay, hầu hết các dịch vụ trực tuyến cần hoạt động liên tục (24/7), đây là một thách thức lớn đối với các hệ thống Khi một dịch vụ khẳng định có tính “sẵn sàng” 5-9, thì thường hoạt động 99.999% thời gian Điều này do giới hạn phần cứng của một server không thể nào đáp ứng được một nghìn, mười nghìn, thậm chí là cả triệu yêu cầu tại một thời điểm.
1.1.2 Thiết kế trong hệ thống lớn Để giải quyết các vấn đề của những hệ thống nhỏ, việc thiết kế hệ thống lớn cần phải áp dụng những kiến trúc phức tạp, đặc biệt hơn để đáp ứng nhu cầu chịu tải lớn, đảm bảo độ ổn định, độ chính xác và có khả năng chịu lỗi Một hệ thống có các nhu cầu trên thông thường sẽ sử dụng nhiều server, mỗi server song song chạy chương trình và nhiều cơ sở dữ liệu để đáp ứng nhu cầu lưu trữ.
Thiết kế hệ thống hiện đại với Khối Xây Dựng (Building Blocks)
Trong thiết kế hệ hiện đại, những thành phần thiết kế cơ bản như cân bằng tải (Load balancer) sẽ được tách ra thành các khối xây dựng cơ bản (Building block). Điều này phục vụ cho 2 mục đích:
Thứ nhất, có thể thảo luận về tất cả khối xây và các vấn đề thiết kế của chúng.
Thứ hai, khi giải quyết một vấn đề thiết kế, có thể tập trung vào các khía cạnh cụ thể của vấn đề, đề cập đến các khối xây dựng nhất định và cách sử dụng chúng Điều này giúp loại bỏ các cuộc thảo luận trùng lặp về các yếu tố thiết kế thường xuyên xảy ra.
Chi tiết 16 khối xây dựng:
1 Hệ thống tên miền (Domain Name System: DNS): Khối này tập trung vào cách thiết kế các hệ thống đặt tên phân cấp và phân tán cho các máy tính được kết nối với Internet thông qua các giao thức Internet khác nhau.
2 Bộ cân bằng tải (Load Balancers): Khối này được sử dụng để phân phối cân bằng các yêu cầu đến từ khách hàng đến một nhóm máy chủ có sẵn Nó cũng giảm tải và có thể bỏ qua các máy chủ khi gặp sự cố.
3 Cơ sở dữ liệu (Databases): Khối này cho phép lưu trữ, truy xuất, sửa đổi và xóa dữ liệu liên quan đến các thủ tục xử lý dữ liệu khác nhau như là các loại cơ sở dữ liệu, sao chép, phân vùng và phân tích của cơ sở dữ liệu phân tán.
4 Bộ lưu trữ Khóa-Giá trị (Key-Value store): Khối này là cơ sở dữ liệu phi quan hệ, cho phép lưu trữ dữ liệu dưới dạng cặp khóa-giá trị.
5 Mạng phân phối nội dung (Content Delivery Network: CDN): Khối này được sử dụng để lưu trữ nội dung lan truyền như video, hình ảnh, âm thanh và trang web Nó hiệu quả trong việc phân phối nội dung cho người dùng cuối mà vẫn giảm thiểu độ trễ và gánh nặng cho các dữ liệu trung tâm.
6 Bộ tạo số thứ tự (Sequencer): Khối này được dùng để tạo số ID duy nhất.
7 Bảng giám sát dịch vụ (Service Monitoring): Khối này quan trọng trong các hệ thống phân tán chúng giúp phân tích hệ thống và cảnh báo cho các bên liên quan nếu xảy ra vấn đề.
8 Bộ đệm phân tán (Distributed Caching): Khối này là một hệ thống đệm phân tán trong đó nhiều máy chủ đệm phối hợp để lưu trữ dữ liệu được truy cập thường xuyên.
Các nguyên lý thiết kế hệ thống
Scalability là khả năng mở rộng (scaling) của hệ thống (system), quy trình (process) hay mạng lưới (network), với nhu cầu gia tăng về số lượng công việc tăng theo thời gian của mô hình kinh doanh (business model).
Mô hình kinh doanh có thể mở rộng quy mô vì nhiều lý do như:
• Gia tăng khối lượng dữ liệu lưu trữ (data storage)
• Gia tăng về số lượng yêu cầu và khối lượng công việc (process/request)
Ví dụ: số lượng truy cập hay đặt hàng của một hệ thống thương mại điện tử Và yêu cầu của sự mở rộng phải đạt được nhu cầu này mà không làm giảm hiệu suất, nói chung Scalability là đáp ứng được sử mở rộng hay giảm theo kích thước của hệ thống theo thời gian.
Dưới đây là các khía cạnh mở rộng khác nhau về kích thước của một hệ thống:
Tính mở rộng về kích thước (Size Scalability): Đây là khả năng của hệ thống để tăng cường kích thước của nó mà không cần sửa đổi cấu trúc hoặc mã nguồn Ví dụ, một trang web có thể thêm nhiều người dùng cùng một lúc mà không gây ra sự giảm sút đáng kể trong tốc độ và hiệu suất của trang web.
Tính mở rộng về Quản lý (Administrative Scalability): Khi hệ thống có tính mở rộng quản lý, nhiều tổ chức hoặc người dùng có thể sử dụng cùng một hệ thống mà không cần sự phức tạp trong việc quản lý và duy trì Ví dụ, một hệ thống lưu trữ đám mây có thể phục vụ nhiều tổ chức và người dùng mà không gây ra sự rối rắm trong việc quản lý tài khoản và dịch vụ.
Tính mở rộng về Địa lý (Geographical Scalability): Nó đề cập đến việc hệ thống có thể phục vụ một vùng địa lý rộng, chẳng hạn như một quốc gia hoặc toàn cầu, mà không gây ra độ trễ hoặc giảm hiệu quả đáng kể trong hiệu suất Ví dụ, các dịch vụ trực tuyến như trang web thương mại điện tử cần phải có khả năng phục vụ khách hàng từ khắp nơi trên thế giới mà không gây ra độ trễ lớn trong việc tải trang hoặc giao dịch.
Những cách để thực hiện tính mở rộng của một hệ thống
Có hai dạng scaling là mở rộng theo chiều ngang (vertical scaling) và mở rộng theo chiều dọc (horizontal scaling).
Tính mở rộng theo chiều dọc (Vertical scaling) – Scaling Up: là cách mở rộng server hiện tại bằng cách nâng cấp độ mạnh (power) bằng cách nâng cấp CPU, Ram, Storage, v.v… Vertical- scaling cho phép chúng ta mở rộng năng xuất của phần cứng và phần mềm, nhưng thường bị giới hạn bởi giới hạn về cấu hình vật lý hiện đại hay độ trễ khi “chẳng may” Server bị downtime để nâng cấp hay deploy hệ thống Chi phí cho việc vertical scaling thường đắt đỏ.
Tính mở rộng theo chiều ngang (Horizontal Scalability) - Scaling Out: Horizontal scalability là một phương pháp mở rộng hệ thống bằng cách tăng số lượng máy chủ hoặc nodes trong mạng Nó còn được gọi là "scaling out." Phương pháp này đối phó với việc gia tăng tải lên hệ thống bằng cách thêm nhiều máy chủ hoặc nodes khác nhau vào mạng Horizontal scalability tập trung vào việc mở rộng hệ thống mà không cần tăng cường cấu trúc của máy chủ hiện tại.
Song, horizontal scaling cũng phức tạp hóa mô hình kiến trúc của hệ thống do phải điều phối hoạt động của nhiều server riêng biệt và vận hành như một bộ máy duy nhất Kiến trúc này được biết đến với tên gọi Hệ thống phân tán (Distributed system) và là cơ sở cho ra đời một họ các mô hình kiến trúc sau này như Distributed monolith, Microservices, cùng với đó là rất nhiều vấn đề cực kỳ phức tạp và khó giải quyết khác.
Khi nhắc đến scalability, hầu hết ta luôn muốn nói đến khả năng horizontal scaling của hệ thống.
Tính khả dụng (availability) là một chỉ số đo lường phần trăm thời gian mà một dịch vụ hoặc một hệ thống nào đó có thể truy cập được bởi khách hàng và hoạt động trong điều kiện bình thường Nó đo lường mức độ hoạt động và xử lý của hệ thống ở trạng thái bình thường trong suốt một khoảng thời gian nhất định Chẳng hạn, nếu một dịch vụ có khả dụng 100%, điều đó có nghĩa rằng dịch vụ đó luôn sẵn sàng hoạt động, xử lý và phản hồi các yêu cầu.
Cách tính toán tính khả dụng
Trong toán học, tính khả dụng A được biểu thức dưới dạng tỉ lệ Tỉ lệ A càng cao càng tốt Chúng ta cũng có thể biểu thức điều này bằng một công thức:
Chúng ta đo lường mức độ khả dụng (availability) bằng cách sử dụng số lượng "9" Cụ thể, số lượng "9" thể hiện mức độ chính xác của khả dụng và cho biết bao nhiêu thời gian ngừng hoạt động được phép trong khoảng thời gian nhất định.
Dưới đây là một bảng thể hiện mức độ khả dụng tương ứng với số lượng "9" và thời gian
Mỗi nhà cung cấp dịch vụ có thể có cách tính toán khả dụng riêng biệt dựa trên thời gian bắt đầu đo lường, cách họ xử lý các sự cố hoặc gián đoạn trong dịch vụ, và cách họ xem xét thời gian ngừng hoạt động Điều này có nghĩa là khi bạn đánh giá hoặc so sánh các nhà cung cấp dịch vụ, bạn nên nắm rõ cách mà họ tính toán và báo cáo mức độ khả dụng của họ để có một cái nhìn chính xác về độ tin cậy của dịch vụ của họ. Để tăng cường tính khả dụng của một hệ thống, chúng ta có những phương pháp sau đây: a Xóa bỏ những tác nhân bị hỏng duy nhất Để loại bỏ các tác nhân bị hỏng, cần đảm bảo tính dự phòng ở mọi cấp độ hệ thống Một tác nhân hỏng duy nhất là một thành phần trong hệ thống của bạn gây gián đoạn dịch vụ nếu nó gặp sự cố.
Mọi phần quan trọng có trách nhiệm thực thi của một hệ thống cơ bản phải có một bản sao dự phòng để xử lý nếu có sự cố xảy ra đối với thành phần chính Có các mức độ khác nhau về tính dự phòng:
Mô hình N+1 bao gồm số lượng thiết bị (được gọi là "N") cần thiết để duy trì hệ thống hoạt động, cộng với một bản sao dự phòng độc lập cho mỗi thành phần quan trọng Các thành phần thường là “active” và “passive” (bản sao dự phòng sẵn sàng, chờ đợi để tiếp quản khi xảy ra sự cố) hoặc “active” và “active” (bản sao dự phòng hoạt động ngay cả khi thành phần chính hoạt động với khi không có sự cố).
Mô hình N+2 tương tự như N+1, nhưng mô hình này đảm bảo hệ thống có thể chịu được sự cố của hai thành phần giống nhau Mô hình N+2 đủ để duy trì hoạt động của hầu hết các tổ chức ở mức độ "sẵn sàng cao".
Load balancer
Load balancer
Load balancer (cân bằng tải) là một thành phần quan trọng trong kiến trúc hệ thống phân tán, được sử dụng để phân phối công việc vào các máy chủ hoặc các thành phần xử lý khác nhau trong một hệ thống Mục tiêu của load balancer là tối ưu hóa hiệu suất, tăng tính sẵn sàng và đảm bảo ổn định cho hệ thống.
Phân loại
Có nhiều loại load balancer khác nhau, bao gồm:
Hardware load balancer (HLB) là một thiết bị phần cứng có hệ điều hành chuyên dụng giúp phân phối lưu lượng truy cập ứng dụng web trên một cụm máy chủ ứng dụng Để đảm bảo hiệu suất tối ưu, hardware load balancer phân phối lưu lượng theo quy tắc tùy chỉnh để máy chủ ứng dụng không bị quá tải HLB hoạt động ở tầng 4 (Transport Layer) hoặc tầng 7 (Application Layer) trong mô hình OSI
Softwarer load balancer (SLB) là các ứng dụng phần mềm chạy trên máy chủ và thực hiện chức năng cân bằng tải Các ví dụ phổ biến bao gồm Nginx, HAProxy và Apache HTTP Server
Sự khác biệt rõ ràng nhất giữa HLB và SLB là HLB yêu cầu các thiết bị phần cứng, trong khi SLB chỉ cần được cài đặt trên máy chủ hoặc máy ảo Một sự khác biệt quan trọng khác giữa HLB và SLB nằm ở khả năng mở rộng Khi lưu lượng mạng tăng lên, các trung tâm dữ liệu phải cung cấp đủ bộ cân bằng tải để đáp ứng nhu cầu cao điểm Đối với nhiều doanh nghiệp, điều này có nghĩa là hầu hết các HLB sẽ không hoạt động cho đến thời điểm có lưu lượng mạng cao Mặt khác, SLB có thể mở rộng quy mô một cách linh hoạt để đáp ứng nhu cầu Cho dù lưu lượng truy cập mạng thấp hay cao, SLB có thể tự động điều chỉnh quy mô theo thời gian thực một cách đơn giản, loại bỏ chi phí khi cung cấp các HLB quá mức và sự gia tăng lưu lượng truy cập bất ngờ Trên thực tế, các HLB không tương thích với môi trường cloud, trong khi SLB lại tương thích với nền tảng đám mây
Load balancer có thể được phân loại dựa trên tầng xử lý mà chúng hoạt động trong kiến trúc mạng Có hai phân loại chính cho load balancer trên tầng xử lý:
Load balancer sử dụng thông tin ở tầng vận chuyển làm cơ sở để quyết định cách phân phối các yêu cầu của máy khách trên một nhóm máy chủ dựa trên thông tin về địa chỉ IP nguồn và đích cũng như các port được ghi trên header của packet mà không xem xét nội dung của packet Khi load balancer nhận được yêu cầu và đưa ra quyết định cân bằng tải, nó cũng thực hiện Network Address Translation (NAT) trên gói yêu cầu, thay đổi địa chỉ IP đích từ địa chỉ
IP đích của gói thành địa chỉ của máy chủ được chọn trên mạng nội bộ Tương tự, trước khi chuyển tiếp phản hồi của máy chủ đến máy khách, load balancer sẽ thay đổi địa chỉ nguồn được ghi trong header của gói từ địa chỉ IP của máy chủ thành địa chỉ IP của chính nó
Transport Load Balancer là một phương pháp phổ biến để xử lý lưu lượng khi phần cứng thông thường chưa mạnh mẽ như hiện nay và sự tương tác giữa máy khách và máy chủ ứng dụng ít phức tạp hơn nhiều Nó đòi hỏi ít tính toán hơn so với các phương pháp cân bằng tải phức tạp hơn (chẳng hạn như Application Load Balancer), nhưng CPU và bộ nhớ hiện nay đủ nhanh và rẻ nên lợi thế về hiệu suất của Transport Load Balancer trở nên không đáng kể hoặc không còn phù hợp trong hầu hết các tình huống
Application Load Balancer (ALB) hoạt động ở tầng ứng dụng trong mô hình OSI, sử dụng các giao thức như HTTP và SMTP để đưa ra quyết định dựa trên nội dung của request Thay vì chỉ chuyển tiếp, bộ cân bằng tải sẽ ngắt luồng mạng, thực hiện giải mã nếu cần, kiểm tra và đưa ra quyết định định tuyến dựa trên nội dung request, khởi tạo kết nối TCP mới đến máy chủ thích hợp và ghi yêu cầu vào máy chủ ALB thường được triển khai trong các môi trường đám mây như Amazon Web Services (AWS) thông qua dịch vụ Elastic Load Balancer (ELB) Nó cung cấp một giải pháp linh hoạt và mạnh mẽ để cân bằng tải và ổn định hoạt động của các ứng dụng web.
Thuật toán
Load balancer có thể được phân loại dựa trên các thuật toán cân bằng tải mà chúng sử dụng để quyết định cách phân phối yêu cầu Dưới đây là một số phân loại phổ biến của load balancer dựa trên thuật toán:
Thuật toán này phân phối yêu cầu theo cách lần lượt đến các máy chủ xử lý tải theo một thứ tự xác định Mỗi yêu cầu mới sẽ được chuyển tiếp đến máy chủ tiếp theo trong danh sách Đây là một thuật toán đơn giản và công bằng, nhưng không xem xét tải của các máy chủ
Weighted Round Robin Đây là thuật toán mở rộng của thuật toán Round Robin Đối với Round Robin, server phải xử lí khối lượng request là ngang nhau Do đó, server với khả năng xử lí thấp hơn có thể sẽ bị overload và nhanh chóng quá tải trong khi server mạnh hơn thì đang nhàn rỗi huật toán này gán một trọng số (weight) cho mỗi máy chủ, xác định tỷ lệ phân phối tải giữa các máy chủ Máy chủ có trọng số cao sẽ nhận được nhiều yêu cầu hơn so với máy chủ có trọng số thấp
Thuật toán này chọn máy chủ có số kết nối hiện tại ít nhất để chuyển tiếp yêu cầu tới Điều này giúp phân phối tải một cách công bằng hơn dựa trên tình trạng thực tế của máy chủ
Thuật toán này chọn máy chủ có thời gian phản hồi (response time) thấp nhất để chuyển tiếp yêu cầu tới Thời gian đáp ứng được xác định bằng khoảng thời gian giữa gửi một gói tin đến server và thời điểm nhận được gói tin trả lời Việc gửi và nhận này do Load Balancer đảm nhiệm
Sử dụng địa chỉ IP nguồn và đích của lưu lượng truy cập để tính toán một giá trị băm và gán kết nối đến một máy chủ cụ thể dựa trên giá trị băm này
Server-side load balancing và client-side load balancing đều là các phương pháp phân phối lưu lượng truy cập trên nhiều máy chủ, nhưng chúng khác nhau về cách thức hoạt động:
• Server-side load balancing: Server-side load balancing được thực hiện bởi một thiết bị hoặc ứng dụng được đặt trước các máy chủ backend Load balancer sẽ nhận các yêu cầu từ client và sử dụng một thuật toán để chọn một máy chủ backend để xử lý yêu cầu Sau đó, load balancer sẽ chuyển tiếp yêu cầu đến máy chủ backend được chọn và trả về kết quả cho client
• Client-side load balancing: Client-side load balancing được thực hiện bởi client
Client sẽ nhận thông tin về các máy chủ backend từ một dịch vụ danh mục (registry service) và sử dụng một thuật toán để chọn một máy chủ backend để gửi yêu cầu Sau đó, client sẽ gửi yêu cầu trực tiếp đến máy chủ backend được chọn và nhận kết quả trả về
Cả hai loại load balancing đều có ưu và nhược điểm riêng Server-side load balancing đơn giản hơn và dễ triển khai hơn, nhưng có thể tạo ra bottleneck và tăng độ trễ Client-side load balancing linh hoạt hơn và giảm độ trễ, nhưng phức tạp hơn và đòi hỏi client phải có khả năng thực hiện load balancing
2.5 Tăng availability của hệ thống
Single point of failure (SPOF) là một điểm trong hệ thống mà nếu nó gặp sự cố, toàn bộ hệ thống sẽ ngừng hoạt động Việc có một SPOF trong hệ thống có thể dẫn đến nhiều vấn đề, bao gồm:
• Mất dữ liệu: Nếu SPOF là một thiết bị lưu trữ dữ liệu, sự cố có thể dẫn đến mất dữ liệu quan trọng
• Thời gian chết: Khi SPOF gặp sự cố, toàn bộ hệ thống sẽ ngừng hoạt động, dẫn đến thời gian chết và ảnh hưởng đến trải nghiệm người dùng
• Mất doanh thu: Thời gian chết có thể dẫn đến mất doanh thu, đặc biệt đối với các doanh nghiệp phụ thuộc vào hệ thống để bán hàng hoặc cung cấp dịch vụ
• Mất uy tín: Sự cố liên tục có thể làm giảm uy tín của doanh nghiệp và ảnh hưởng đến lòng tin của khách hàng Để giảm thiểu rủi ro từ SPOF, ta có thể áp dụng một số phương pháp để tạo ra bản sao của các thành phần trong hệ thống rồi load balance giữa các instance đó
Ta có thể khắc phục SPOF cho database sử dụng Sharding và replication Sharding và replication là hai phương pháp phân tán dữ liệu và tài nguyên trong các hệ thống lớn, không chỉ áp dụng cho cơ sở dữ liệu mà còn cho các dịch vụ khác
• Sharding: Sharding là một phương pháp phân chia dữ liệu hoặc tài nguyên lớn thành nhiều phần nhỏ hơn và lưu trữ chúng trên nhiều máy chủ khác nhau Mỗi máy chủ sẽ lưu trữ một phần của dữ liệu hoặc tài nguyên và chỉ xử lý các yêu cầu liên quan đến phần đó Sharding giúp tăng khả năng mở rộng và cải thiện hiệu suất của hệ thống bằng cách giảm gánh nặng cho từng máy chủ và cho phép xử lý song song các request
Tăng availability của hệ thống
Single point of failure (SPOF) là một điểm trong hệ thống mà nếu nó gặp sự cố, toàn bộ hệ thống sẽ ngừng hoạt động Việc có một SPOF trong hệ thống có thể dẫn đến nhiều vấn đề, bao gồm:
• Mất dữ liệu: Nếu SPOF là một thiết bị lưu trữ dữ liệu, sự cố có thể dẫn đến mất dữ liệu quan trọng
• Thời gian chết: Khi SPOF gặp sự cố, toàn bộ hệ thống sẽ ngừng hoạt động, dẫn đến thời gian chết và ảnh hưởng đến trải nghiệm người dùng
• Mất doanh thu: Thời gian chết có thể dẫn đến mất doanh thu, đặc biệt đối với các doanh nghiệp phụ thuộc vào hệ thống để bán hàng hoặc cung cấp dịch vụ
• Mất uy tín: Sự cố liên tục có thể làm giảm uy tín của doanh nghiệp và ảnh hưởng đến lòng tin của khách hàng Để giảm thiểu rủi ro từ SPOF, ta có thể áp dụng một số phương pháp để tạo ra bản sao của các thành phần trong hệ thống rồi load balance giữa các instance đó
Ta có thể khắc phục SPOF cho database sử dụng Sharding và replication Sharding và replication là hai phương pháp phân tán dữ liệu và tài nguyên trong các hệ thống lớn, không chỉ áp dụng cho cơ sở dữ liệu mà còn cho các dịch vụ khác
• Sharding: Sharding là một phương pháp phân chia dữ liệu hoặc tài nguyên lớn thành nhiều phần nhỏ hơn và lưu trữ chúng trên nhiều máy chủ khác nhau Mỗi máy chủ sẽ lưu trữ một phần của dữ liệu hoặc tài nguyên và chỉ xử lý các yêu cầu liên quan đến phần đó Sharding giúp tăng khả năng mở rộng và cải thiện hiệu suất của hệ thống bằng cách giảm gánh nặng cho từng máy chủ và cho phép xử lý song song các request
• Replication: Replication là một phương pháp sao chép dữ liệu hoặc tài nguyên từ một máy chủ sang một hoặc nhiều máy chủ khác Mục đích của replication là tăng độ tin cậy và khả dụng của hệ thống bằng cách cung cấp các bản sao dự phòng của dữ liệu hoặc tài nguyên Khi một máy chủ gặp sự cố, các máy chủ khác có thể tiếp tục hoạt động và đảm bảo availability của dịch vụ
Load balancer có vai trò quan trọng trong việc tăng tính sẵn sàng (availability) của hệ thống
• Phân phối tải: Load balancer phân phối tải đến các máy chủ hoặc tài nguyên khác nhau Bằng cách chia sẻ công việc và tải trọng, load balancer đảm bảo rằng không có máy chủ nào bị quá tải, giúp tránh tình trạng hệ thống bị chậm hoặc gặp sự cố Nếu một máy chủ gặp vấn đề, load balancer có thể tự động loại bỏ nó khỏi danh sách máy chủ hoạt động, từ đó giữ cho hệ thống vẫn hoạt động một cách liên tục
• Kiểm soát lưu lượng: Load balancer có thể giới hạn số lượng yêu cầu được chấp nhận từ phía client hoặc từ một nguồn lưu lượng nhất định Điều này giúp đảm bảo rằng hệ thống không bị quá tải do lưu lượng truy cập đột ngột tăng cao hoặc tấn công từ phía người dùng
• Giám sát và phát hiện sự cố: Load balancer thường được tích hợp với các tính năng giám sát để theo dõi trạng thái và hiệu suất của các máy chủ hoặc tài nguyên Nếu một máy chủ hoặc tài nguyên trở nên không hoạt động hoặc gặp sự cố, load balancer có thể phát hiện và tự động loại bỏ nó khỏi quá trình phân phối tải, giữ cho hệ thống hoạt động bình thường
• Định tuyến thông minh: Một số load balancer có khả năng định tuyến thông minh dựa trên các tiêu chí như tình trạng máy chủ, vị trí địa lý, hoặc khối lượng công việc Điều này cho phép load balancer đưa ra quyết định phân phối tải dựa trên yếu tố cụ thể để tối ưu hóa hiệu suất và tính sẵn sàng của hệ thống
• Cân bằng tải động: Load balancer có thể tự động điều chỉnh phân phối tải dựa trên tình trạng hoạt động của các máy chủ hoặc tài nguyên Nếu một máy chủ hoặc tài nguyên
Caching
Tổng quan về caching
Load balancing giúp hệ thống mở rộng theo chiều ngang bằng cách ngày càng tăng số lượng các máy chủ, nhưng Caching lại là cách để sử dụng tài nguyên (resource) hiệu quả hơn từ đó resource cần cung cấp giảm đi, nhằm tiết kiệm resource và giảm chi phí của hệ thống Để làm được điều này, hệ thống sẽ cung cấp một vùng nhớ đệm (cache) để chứa những dữ liệu được request nhiều lần mà không thường xuyên thay đổi vào đó, từ đó hệ thống sẽ lấy dữ liệu từ bộ nhớ cache mà không truy cập vào bộ nhớ chính Điều này giúp bộ nhớ chính sẽ được giảm tải đi từ đó năng lực của nó tăng lên
Bộ nhớ cache được sử dụng rất rộng rãi trong mọi thành phần của hệ thống như trong phần cứng (hardware) như RAM/CPU Cache, hệ điều hành (operating systems), trình duyệt web (web browsers), web applications v.v… Bộ nhớ cache giống như một bộ nhớ ngắn hạn (short- term memory) nó có giới hạn về kích cỡ, nhưng thường nhanh hơn rất nhiều lần bộ nhớ chính
Về kiến trúc phần mềm thì Cache có thể tồn tại ở hầu hết các thành phần, nhưng chủ yếu nó tồn tại ở phần thao tác với dữ liệu như Database Layer, giúp trả về dữ liệu nhanh hơn mà không cần tốn chi phí để truy cập vào Database, hay ở phần Front-end để Cache những resource tĩnh như js, css, image file bằng cách dùng CDN.
Các pattern trong caching
Local cache là một cơ chế lưu trữ dữ liệu tạm thời được sử dụng trong các ứng dụng phần mềm để lưu trữ dữ liệu được truy cập thường xuyên hoặc được sử dụng gần đây ở một vị trí gần hơn và truy cập nhanh hơn nguồn dữ liệu ban đầu Mục đích chính của bộ đệm cục bộ là cải thiện hiệu suất và khả năng phản hồi của ứng dụng bằng cách giảm thời gian và tài nguyên cần thiết để tìm nạp dữ liệu từ nguồn chậm hơn hoặc từ xa, chẳng hạn như cơ sở dữ liệu, dịch vụ web hoặc hệ thống tệp
Nếu bạn đang xây dựng một ứng dụng web được truy cập từ trình duyệt, bạn có thể sử dụng lưu trữ cục bộ để lưu trữ dữ liệu key-value trong trình duyệt của người dùng
Ví dụ, sau khi người dùng xác thực vào dịch vụ của bạn, bạn có thể lưu trữ một số thông tin về
ID và hồ sơ của người dùng được sử dụng để truy cập dịch vụ để tăng tốc hiển thị ứng dụng của bạn khi xem lại lần sau
Lợi ích của Local browser caching là tính đơn giản của nó, vì API lưu trữ cục bộ đã được bao gồm trong các trình duyệt web hiện đại Ngoài ra, không cần lo lắng về việc cung cấp cache trước hoặc sợ không có đủ không gian, vì bạn đang thuê không gian trên máy tính của người dùng để lưu trữ dữ liệu này
Nhược điểm của Local browser caching là nó chỉ hữu ích trong trường hợp cụ thể Dữ liệu đã được lưu trữ không áp dụng cho người dùng khi họ sử dụng thiết bị khác hoặc thậm chí sử dụng một phiên bản trình duyệt khác trên thiết bị đó Hơn nữa, không có cơ chế cho nguồn dữ liệu backend của bạn để tự động vô hiệu hóa các mục trong local cache nếu dữ liệu gốc đã thay đổi
Với Local backend caching , các backend server instances của bạn có thể lưu trữ các network response hoặc dữ liệu trung gian từ các hệ thống khác Dữ liệu này thường được lưu trữ trong bộ nhớ trong ứng dụng của bạn, chẳng hạn như trong các bản đồ key-value trong ngôn ngữ lập trình của bạn Khi backend server instances của bạn cần truy cập dữ liệu đó, nó sẽ trước tiên kiểm tra đối tượng trong bộ nhớ, sau đó sử dụng nguồn dữ liệu chính nếu giá trị đã lưu trữ không có Ưu điểm của chiến lược này nằm trong sự dễ sử dụng và tính đơn giản của nó Nếu bạn có dữ liệu thường xuyên truy cập và tồn tại trong thời gian dài, bạn có thể nhanh chóng lưu trữ nó trên từng server instances mà không cần xây dựng và vận hành cơ sở hạ tầng bổ sung Điều này có thể hoạt động tốt cho dữ liệu cấu hình hoặc dữ liệu chậm
Nhược điểm của chiến lược lưu trữ này là nó ít hiệu quả hơn so với các phương pháp lưu trữ từ xa Mỗi backend instance sẽ có bộ nhớ cache riêng biệt Nếu bạn có một tập dữ liệu rộng lớn cần lưu trữ, và bạn chỉ lưu trữ nó sau khi nó đã được yêu cầu một lần trên phiên bản đó, cache hit rate của bạn có thể khá thấp
Một distributed cache là một cache có dữ liệu của nó được phân tán trên một số nút trong một (a) cụm, (b) trên một số cụm khác nhau hoặc (c) trên một số trung tâm dữ liệu khác nhau trên khắp thế giới
Tại sao chúng ta lại cần Distributed cache?
Scalability, High Availability, Fault-tolerance
Các doanh nghiệp không thể chấp nhận dịch vụ của họ bị ngừng hoạt động Hãy nghĩ đến các dịch vụ y tế, thị trường chứng khoán, quân đội Chúng không có khả năng ngừng hoạt động Chúng được phân tán trên nhiều nút với một lượng dự phòng khá đáng tin cậy
Một số ví dụ về các hệ thống distributed cache phổ biến bao gồm Memcached, Redis và
• Có thể đáp ứng các giao dịch khối lượng lớn với độ trễ thấp
• Tính khả dụng cao ngay cả khi bất kỳ nút nào bị lỗi, nút phụ sẽ bắt đầu phục vụ các yêu cầu
• Đảm bảo tính nhất quán của dữ liệu
• Khả năng quản lý cao và khả năng chịu lỗi
• Khi có sự phân bố trong tự nhiên, chi phí cơ sở hạ tầng tăng lên
• Không khả dụng nếu toàn bộ trung tâm dữ liệu khu vực ngừng hoạt động, điều này rất hiếm
• Bằng cách sử dụng bộ đệm được quản lý của nhà cung cấp dịch vụ đám mây, chúng tôi sẽ đặt tính khả dụng và bảo mật dữ liệu của dữ liệu được lưu trong bộ nhớ đệm vào tay các nhà cung cấp dịch vụ bên thứ ba
Reverse proxy cache (hoặc reverse caching proxy) là một thành phần hệ thống hoặc dịch vụ máy chủ được đặt ở trước máy chủ ứng dụng hoặc máy chủ web chính Chức năng chính của reverse proxy cache là lưu trữ tạm thời (caching) các tài nguyên web hoặc ứng dụng trên các máy chủ để cung cấp dữ liệu nhanh hơn cho các yêu cầu từ người dùng cuối
Một số phần mềm hoặc công nghệ phổ biến được sử dụng để triển khai reverse proxy cache bao gồm Nginx, Apache Traffic Server, và Varnish Ưu điểm: Khả năng chịu lỗi Giảm chi phí không cần thiết cho các vi dịch vụ cơ bản
Nhược điểm: Khó bảo trì, gateway là một thành phần rất quan trọng của kiến trúc
Sidecar cache là một mô hình kiến trúc trong đó một phần mềm hoặc dịch vụ gọi là "sidecar" được sử dụng để quản lý cache dữ liệu cho ứng dụng hoặc dịch vụ chính Sidecar cache thường là một phần của kiến trúc microservices, nơi một ứng dụng được chia thành nhiều dịch vụ nhỏ, và sidecar cache được sử dụng để lưu trữ tạm thời dữ liệu cần thiết cho các dịch vụ này Sidecar cache có thể chạy trên cùng một máy chủ hoặc container với dịch vụ chính hoặc chạy trên các nút riêng lẻ trong mô hình phân tán Ưu điểm:
• Độ trễ thấp vì sidecar hoạt động như bộ đệm cục bộ
• Service instance và cache data được tách biệt với nhau khi chúng đang chạy trong một vùng chứa riêng biệt
• Cache data sẽ không chiếm hết bộ nhớ phiên bản ứng dụng
• Chỉ có thể được thực hiện với nền tảng được đóng gói
• Nếu Pod khởi động lại do bất kỳ sự cố nào, cả vùng chứa sẽ được khởi động lại và dữ liệu được lưu trong bộ nhớ đệm sẽ bị mất
Rever-Proxy Side-Car Cache là phiên bản kết hợp của Reverse-proxy và sidecar cache, được tạo ra bằng cách khởi chạy container reverse-proxy và container cache dữ liệu trong cùng một pod
Trong cơ chế này, chỉ có các phản hồi API mới được lưu trữ trong bộ nhớ cache của sidecar, và sẽ được phục vụ cho các yêu cầu lặp lại
Nó sẽ kế thừa tất cả các ưu điểm và nhược điểm của reverse proxy và sidecar cache
3.2.5 Làm trống cache (Cache eviction)
Các pattern truy cập trong caching
Là cơ chế cache thường được sử dụng nhất, mô hình hoạt động của nó như sau:
Cơ chế này thường được dùng cho trường hợp đọc nhiều và ghi ít Ngoài ra còn phụ thuộc vào việc dữ liệu trả về có thay đổi hay không (truy vấn theo primary key thì thường hiếm khi thay đổi)
Kĩ thuật thường dùng là memcache
Lợi ích đem lại ở đây đó là khả năng phục hồi dữ liệu khi cache gặp lỗi (chỉ cần truy vấn ngược về DB là xong)
• Cập nhật vào cache các state create/ update của DB (sử dụng logic code)
• Cơ chế refresh cache (LRU hay LFU)
Mô hình Read-through Cache khá giống với cache-aside Nhưng thay vì application phải kết nối với cache và database, giờ đây application chỉ cần giao tiếp với cache Còn cache sẽ tự lấy dữ liệu ở chính nó hoặc xuống database lấy dữ liệu Với trường hợp này, cache chính là database chính của ứng dụng, nó đóng vai trò rất rất quan trọng Với cache-aside, việc cache bị chết thì ứng dụng vẫn chạy được, nhưng với read-through cache, nếu cache chết thì ứng dụng chết.
Mô hình Read-through Cache Ưu điểm:
• Application không cần quan tâm tới trường hợp cache miss Mọi thứ cứ để cache server lo hết
• Phải tìm được ứng dụng, platform đóng vai trò cache phù hợp
• Khó điều khiển thời gian hết hạn của cache
• Có nhiều dữ liệu cũ, dữ liệu không đồng nhất với database trong cache
Với chiến lược này, data sẽ được lưu xuống cache, cache sẽ lưu dữ liệu vào database
Khi một request write tới:
• Dữ liệu sẽ được lưu vào cache
• Cache sẽ gửi yêu cầu lưu dữ liệu vào database ngay lập tức Ưu điểm:
• Không bao giờ xảy ra trường hợp cache miss, bởi vì dữ liệu luôn được lưu vào cache trước khi lưu vào database
• Không xảy ra trường hợp dữ liệu không khớp với database
• Dữ liệu luôn đồng nhất nếu chúng ta kết hợp Write through cache và Read through cache Đây sẽ là một đôi song kiếm hợp bích
• Hầu hết các dữ liệu trên cache đều là dữ liệu đọc một lần, vậy nên việc ghi qua cache sẽ dẫn tới rất nhiều dữ liệu tồn tại trên cache không cần thiết
• Dữ liệu lưu trên cache nhiều ngang ngữa database, dẫn tới tốn nhiều resource không cần thiết
• Quá trình lưu dữ liệu thường sẽ lâu vì phải chờ lưu xuống cache và database
Khi nào dùng Write through cache? Với cái tên write through cache thì chúng ta cũng có thể đoán được rằng chiến lược này dùng cho trường hợp write-heavy workloads
Write back nhìn sơ khá giống với write through cache Tuy nhiên, ở write back, cache không lưu dữ liệu xuống database ngay khi nhận request từ application Cache sẽ đồng bộ dữ liệu xuống database định kì theo thời gian, hoặc theo số lượng dữ liệu được insert/update Chúng ta có thể hiểu đơn giản write back việc write batch process Để hiểu đơn giản hơn, chúng ta có thể đơn giản hóa sự khác biệt của write through cache và write back đó là:
• Write through cache: lưu dữ liệu từ cache xuống database một cách đồng bộ
• Write back: lưu dữ liệu từ cache xuống database bất đồng bộ
Khi một request write tới:
• Dữ liệu sẽ được lưu vào cache
• Sau một khoảng thời gian, cache sẽ gửi yêu cầu lưu dữ liệu vào database Ưu điểm:
• Giảm tải áp lực write xuống database, từ đó sẽ giảm được chi phí và các vấn đề liên quan tới database
• Nếu kết hợp giữa Write back và Read through cache, chúng ta có một hệ thống tốt cho cả read heavy workloads và write heavy workloads
• Dữ liệu có thể không đồng nhất giữa cache và database nếu như cache chưa kịp đồng bộ dữ liệu về database
• Nếu cache server bị chết, hệ thống sẽ bị chết, chúng ta có thể bị mất vĩnh viễn dữ liệu mà cache chưa kịp đồng bộ vào database
Khi nào dùng Write back? Chiến lược này phù hợp cho hệ thống write-heavy workloads
Write around là quá trình lưu trực tiếp dữ liệu từ application vào database và đọc dữ liệu theo một trong hai chiến lược: cache aside hoặc read through cache Ưu điểm:
• Không gây quá tải cache
Microservices
Miêu tả microservice
Nhiều ứng dụng dựa trên web vừa và nhỏ được xây dựng bằng kiến trúc monolithic (nguyên khối) Trong kiến trúc nguyên khối, một ứng dụng được phân phối dưới dạng một ứng dụng phần mềm có thể triển khai được Tất cả logic nghiệp vụ và cơ sở dữ liệu đều được đóng gói cùng nhau thành một ứng dụng duy nhất và được triển khai trên một máy chủ ứng dụng
Mặc dù các ứng dụng nguyên khối đôi khi được mô tả theo nghĩa tiêu cực bởi những người ủng hộ kiến trúc microservices, đây thường là một lựa chọn tuyệt vời Ứng dụng nguyên khối dễ xây dựng và triển khai hơn các kiến trúc phức tạp hơn như n-tier hoặc microservice Nếu use case của bạn được xác định rõ ràng và khó có thể thay đổi thì đó có thể là một quyết định đúng đắn để bắt đầu với kiến trúc monolithic Tuy nhiên, khi một ứng dụng bắt đầu trở lên lớn và phức tạp hơn, ứng dụng monolithic có thể trở nên khó quản lý Mỗi thay đổi lên một ứng dụng monolithic có thể có một tầng ảnh hưởng đến các phần khác của ứng dụng, điều này có thể khiến nó tốn thời gian và tốn nhiều chi phí hơn, đặc biệt là trong môi trường production Kiến trúc microservice mang lại tiềm năng về tính linh hoạt và khả năng bảo trì tốt hơn Định nghĩa của tôi về kiến trúc microservice được lấy cảm hứng từ Martin Abbott và cuốn sách xuất sắc của Michael Fisher, The Art of Scalability (Addison-Wesley, 2015) mô tả một mô hình có khả năng mở rộng ba chiều: the scale cube:
Mô hình cho thấy 3 cách để mở rộng một ứng dụng: X, Y và Z
Mở rộng theo trục X bằng cách cân bằng tải trên nhiều instances
Mở rộng theo trục X là cách phổ biến để mở rộng một ứng dụng nguyên khối Bạn chạy nhiều phiên bản của ứng dụng đằng sau một cân bằng tải Bộ cân bằng tải phân phối các yêu cầu giữa N phiên bản giống hệt nhau của ứng dụng Đây là một cách tuyệt vời để nâng cao khả năng chịu tải và tính sẵn sàng của một ứng dụng
Mở rộng theo trục Z điều hướng request dựa trên tính chất của nó
Mở rộng theo trục Z cũng chạy nhiều phiên bản của ứng dụng nguyên khối, nhưng không giống như trục X, mỗi phiên bản chỉ chịu trách nhiệm cho một tập hợp con dữ liệu Bộ định tuyến ở phía trước các phiên bản sử dụng thuộc tính của request để định tuyến nó đến instance thích hợp Ví dụ, một ứng dụng có thể định tuyến các yêu cầu sử dụng userId
Trong ví dụ này, mỗi phiên bản ứng dụng chịu trách nhiệm về một tập người dùng Các bộ định tuyến sử dụng userId được chỉ định bởi Authorization header để chọn một trong N phiên bản giống hệt nhau của ứng dụng Mở rộng theo trục Z là một cách tuyệt vời để mở rộng ứng dụng để xử lý khối lượng giao dịch và dữ liệu ngày càng tăng
Mở rộng theo trục Y phân tách ứng dụng thành các service
Mở rộng theo trục X và Z cải thiện khả năng chịu tải và tính khả dụng của ứng dụng nhưng không giải quyết vấn đề trong việc phát triển và độ phức tạp ngày càng tăng của ứng dụng Để giải quyết những vấn đề đó, bạn cần áp dụng mở rộng theo trục Y
Hình ảnh trên cho thấy cách mở rộng theo trục Y: chia nhỏ ứng dụng monolithic thành tập các service
Service là một ứng dụng nhỏ thực hiện chức năng tập trung vào một phạm vi hẹp, chẳng hạn như quản lý đơn hàng, quản lý khách hàng, v.v Một service được mở rộng theo trục X, mặc dù một số service cũng có thể sử dụng tỷ lệ trục Z Ví dụ: Order service bao gồm một tập hợp các service được cân bằng tải
Khái niệm microservice bắt đầu xuất hiện trong cộng đồng phát triển phần mềm như là một câu trả lời cho rất nhiều thử thách vướng phải (cả về mặt kỹ thuật lẫn tổ chức) khi mở rộng một ứng dụng monolithic lớn Microservice là một ứng dụng nhỏ, ràng buộc thấp và phân tán Microservice cho phép bạn phân tách một ứng dụng lớn nó thành các thành phần dễ quản lý với phạm vi được xác định dựa trên chức năng mà nó phụ trách trong hệ thống Các khái niệm chính bạn cần nắm bắt khi nghĩ về microservice là decomposing và unbundling
Hình ảnh trên cho thấy mỗi nhóm sở hữu mã nguồn và cơ sở hạ tầng của service của mình như thế nào Họ có thể xây dựng, triển khai và thử nghiệm độc lập với nhau vì mã nguồn, kho lưu trữ mã nguồn và cơ sở hạ tầng (máy chủ ứng dụng và cơ sở dữ liệu) hoàn toàn độc lập với các phần khác của ứng dụng
Tại sao chúng ta cần phải thay đổi cách phát triển ứng dụng ?
• Độ phức tạp của các ứng dụng ngày càng lớn: các ứng dụng “tách biệt” giao tiếp với một cơ sở dữ liệu duy nhất và không tích hợp với các ứng dụng khác không còn là tiêu chuẩn nữa Ứng dụng ngày nay cần giao tiếp với nhiều dịch vụ và cơ sở dữ liệu, không ở chỉ bên trong các trung tâm dữ liệu của công ty mà còn với dịch vụ của các bên thứ ba
• Khách hàng mong muốn bản phát hành nhanh: Khách hàng không còn muốn chờ đợi lần tiếp theo phát hành hàng năm một gói phần mềm Thay vào đó, họ mong đợi những tính năng trong một phần mềm được tách nhóm để chức năng mới có thể được phát hành nhanh chóng trong vài tuần (hoặc thậm chí vài ngày)
• Khách hàng cũng yêu cầu hiệu suất đáng tin cậy và khả năng mở rộng: Các ứng dụng toàn cầu làm cho cực kỳ khó dự đoán có bao nhiêu giao dịch sẽ được xử lý bởi một ứng dụng và khi nào khối lượng giao dịch đó sẽ đạt được Các ứng dụng cần mở rộng quy mô nhanh chóng trên nhiều máy chủ, sau đó thu nhỏ lại khi lưu lượng truy cập không còn cao
• Khách hàng mong đợi ứng dụng của họ luôn sẵn sàng: Các ứng dụng của phải có khả năng phục hồi cao Sự cố ở một phần của ứng dụng sẽ không ảnh hưởng đến toàn bộ ứng dụng ứng dụng Để đáp ứng những mong đợi này, chúng ta cần phải chia ứng dụng thành các dịch vụ nhỏ có thể được xây dựng và triển khai độc lập Nếu chúng ta “phân tách” các ứng dụng của mình thành các dịch vụ nhỏ hơn và di chuyển những thứ này ra khỏi một kiến trúc nguyên khối duy nhất, chúng ta có thể xây dựng các hệ thống:
• Flexible: Các dịch vụ tách rời có thể được kết hợp lại một cách nhanh chóng để phát hành chức năng mới Đơn vị mã nguồn mà chúng ta phải làm việc càng nhỏ, việc thay đổi càng ít phức tạp và tiết kiệm thời gian trong việc test và deploy
• Resilient: Các dịch vụ tách rời có nghĩa là một ứng dụng không còn là một “ball of mud,” trong đó sự sụp đổ ở một phần của ứng dụng sẽ gây ra sự sụp đổ của toàn bộ ứng dụng
Phương thức giao tiếp giữa các service
Inter-process communication (IPC) là một cơ chế cho phép các tiến trình giao tiếp với nhau và đồng bộ hóa hành động của chúng Có rất nhiều công nghệ IPC khác nhau để bạn lựa chọn Service có thể sử dụng cơ chế giao tiếp dựa trên yêu cầu/phản hồi đồng bộ, chẳng hạn như dựa trên HTTP REST hoặc gRPC Ngoài ra, nó có thể sử dụng giao tiếp không đồng bộ, dựa trên tin nhắn sử dụng các cơ chế như AMQP hoặc STOMP Ngoài ra còn có nhiều loại các định dạng tin nhắn khác nhau Các service có thể sử dụng các định dạng dựa trên văn bản, có thể đọc được như JSON hoặc XML Ngoài ra, có thể sử dụng định dạng nhị phân hiệu quả hơn như Avro hoặc Protocol Buffers
Có nhiều phong cách tương tác giữa clients và services Chúng ta có thể phân ra làm hai loại Khía cạnh đầu tiên là liệu sự tương tác one-to-one hay one-to-many:
• One-to-one: Mỗi yêu cầu của chỉ được xử lý bởi một service
• One-to-many: Mỗi yêu cầu được xử lý bởi nhiều service
Khía cạnh thứ hai là sự tương tác đồng bộ hay không đồng bộ:
• Synchronous: client mong đợi phản hồi lập tức từ service và thậm chí có thể chặn các tác vụ khác trong khi nó chờ đợi
• Asynchronous: Client không chặn các tác vụ khác và phản hồi, nếu có, không nhất thiết phải được gửi ngay lập tức
Sau đây là những kiểu tương tác one-to-one khác nhau:
• Request/response: Client gọi một request tới service và chờ đợi response Client có thể bị block trong khi đang chờ đợi Đây là một kiểu tương tác thường dẫn tới kết quả là các service liên kết quá chặt chẽ với nhau
• Asynchronous request/response: Client gửi request đến service, service này sẽ trả lời không đồng bộ Client sẽ không bị block trong khi chờ đợi, vì service có thể không gửi phản hồi trong một thời gian dài
• One-way notifications: Client sends gửi request tới service nhưng không mong đợi phản hồi
Sau đây là những kiểu tương tác one-to-many khác nhau:
• Publish/subscribe: Client publish một thông báo hoặc một tin nhắn được sử dụng bởi những service đăng ký nhận thông báo hoặc tin nhắn đó
• Publish/async responses: Client publish một thông báo yêu cầu và sau đó đợi một khoảng thời gian nhất định để nhận được phản hồi từ các service quan tâm
4.2.1 Giao tiếp đồng bộ dựa trên Remote procedure invocation pattern
Khi sử dụng cơ chế IPC dựa trên Remote procedure invocation pattern (RPI), client sẽ gửi một yêu cầu đến một service và service sẽ xử lý yêu cầu đó và gửi lại phản hồi Một số client có thể bị block để chờ phản hồi, những client khác có thể có kiến trúc reactive hoặc non blocking
Hình trên cho thấy RPI hoạt động như thế nào Logic nghiệp vụ ở client gọi proxy interface, được triển khai bởi một RPI proxy adapter class Proxy RPI đưa ra yêu cầu cho service Yêu cầu được xử lý bởi RPI server adpter class, lớp này gọi logic nghiệp vụ của service thông qua một interface Sau đó nó sẽ gửi lại phản hồi tới proxy RPI, trả về kết quả cho logic nghiệp vụ cho client Proxy interface thường đóng gói giao thức truyền thông ở phía dưới Có rất nhiều giao thức có thể được lựa chọn, thông thường là REST và gPRC
REST (Representational State Transfer) là một kiểu kiến trúc phần mềm được sử dụng rộng rãi cho việc giao tiếp giữa các dịch vụ Các dịch vụ sử dụng các API (Application Programming Interface) RESTful để truyền thông tin và thực hiện các yêu cầu HTTP như GET, POST, PUT và DELETE để trao đổi dữ liệu
Có rất nhiều lợi ích khi sử dụng REST:
• Đơn giản và quen thuộc
• Bạn có thể kiểm tra HTTP API từ bên trong trình duyệt bằng cách sử dụng Postman hoặc từ dòng lệnh bằng cách sử dụng Curl
• Nó trực tiếp hỗ trợ kiểu giao tiếp request/response
• Nó không yêu cầu một nhà môi giới trung gian, giúp đơn giản hóa kiến trúc của hệ thống
Hạn chế khi sử dụng REST:
• Nó chỉ hỗ trợ kiểu giao tiếp request/response
• Giảm tính khả dụng Bởi vì client và service giao tiếp trực tiếp không thông qua trung gian nên cả hai phải chạy song song trong suốt quá trình trao đổi dữ liệu
• Client phải biết chính xác URLs của service
Một thách thức khi sử dụng REST là vì HTTP chỉ cung cấp một số lượng giới hạn các method, nên không phải lúc nào cũng dễ dàng để thiết kế một REST API hỗ trợ nhiều hoạt động cập nhật cùng lúc Một công nghệ IPC (Remote Procedure Call) giải quyết vấn đề này là gRPC, một framework cho việc viết các client và server chạy trên nhiều ngôn ngữ khác nhau gRPC là một giao thức dựa trên tin nhắn nhị phân, client và server trao đổi các tin nhắn nhị phân theo định dạng Protocol Buffers bằng cách sử dụng HTTP/2
Lợi ích khi sử dụng gRPC:
• Dễ dàng thiết kế các API hỗ trợ cập nhật nhiều thứ cùng lúc
• Hiệu quả trong việc trao đổi lượng lớn dữ liệu
• Hỗ trợ streaming hai chiều
Hạn chế khi sử dụng gRPC:
• Bất tiện hơn cho client sử dụng Javascript hơn là sử dụng API dựa trên REST với dữ liệu JSON
• Các firewall cũ có thể không hỗ trợ HTTP/2
4.2.2 Giao tiếp bất đồng bộ sử dụng messaging pattern
Khi sử dụng messaging, các service giao tiếp với nhau thông qua việc trao đổi message bất đồng bộ Một ứng dụng dựa trên messaging thông thường sẽ có một message broker đóng vai trò trung gian giữa các service mặc dù có một số trường hợp các service sẽ giao tiếp trực tiếp với nhau Service client sẽ yêu cầu tới service khác bằng cách gửi một message Nếu client mong đợi một phản hồi, service sẽ trả lời bằng cách gửi một message độc lập khác tới client Bởi vì chúng đang giao tiếp một cách bất đồng bộ, client sẽ không block và chờ đợi reply
Mô hình messaging được định nghĩa lần đầu trong cuốn sách Enterprise Integration Patterns (Addison-Wesley Professional, 2003) bởi Gregor Hohpe và Bobby Woolf Trong mô hình này, message được trao đổi qua các message channel Sender (một ứng dụng hoặc service) gửi message tới một channel và receiver (ứng dụng hoặc dịch vụ) đọc tin nhắn từ một channel Dưới đây là một số loại message:
• Document: Message chỉ chứa data
• Command: Tương đương với một RPC request Nó xác định thao tác để thực hiện
• Event: Một message cho biết có điều gì đó đáng chú ý đã xảy ra ở sender Một sự kiện thường là một sự kiện thể hiện sự thay đổi trạng thái của đối tượng
Logic nghiệp vụ ở sender kích hoạt sending port interface, được cài đặt bởi một message sender adapter Message sender gửi message tới receiver thông qua một message channel Message channel là một abstraction của hệ thống hạ tầng messaging Message handler adapter ở receiver được kích hoạt để xử lý message Nó kích hoạt receiving port interface được cài đặt bởi logic nghiệp vụ của receiver
Có 2 loại message channel: point to point channel và publish-subcribe channel
• Point to point channel chuyển message tới chính xác một consumer đang đọc message từ channel Service thường sử dụng channel này cho kiểu giao tiếp one-to-one Trường hợp sử dụng ví dụ như command message
Khuyết điểm của microservice
Không có công nghệ nào là giải pháp toàn diện và kiến trúc microservice có một số hạn chế như sau:
• Tìm ra đúng tập các services là một thách thức: Một thách thức khi sử dụng kiến trúc microservice là không có một quy trình cụ thể, thuật toán được xác định rõ ràng để phân tách một hệ thống thành các services Nếu bạn phân tách một hệ thống không chính xác, bạn sẽ xây dựng một khối kiến trúc nguyên khối phân tán (distributed monolith), một hệ thống bao gồm các service phải được triển khai cùng nhau
Distributed monolith có những nhược điểm của cả kiến trúc monolith và kiến trúc microservice
• Hệ thống phân tán là kiến trúc phức tạp: Một vấn đề khác khi sử dụng kiến trúc microservice là các nhà phát triển phải giải quyết những thách thức riêng khi làm việc với một hệ thống phân tán Service phải được thiết kế để có thể xử lý lỗi cục bộ và xử lý trong trường hợp giao tiếp với các service bị lỗi hoặc có độ trễ cao Mỗi service có một database riêng là một thách thức trong việc quản lý transaction hoặc truy vấn trên nhiều service
• Triển khai tính năng trên nhiều service cần sự phối hợp cẩn thận: Một thách thức khác khi sử dụng kiến trúc microservice là việc triển khai các tính năng trải rộng trên nhiều service đòi hỏi sự phối hợp cẩn thận giữa các nhóm phát triển khác nhau
• Chi phí: Việc triển khai và vận hành một hệ thống microservices có thể đắt hơn so với một ứng dụng monolithic do yêu cầu nhiều tài nguyên hơn
• Yêu cầu nhân lực nhiều hơn: Khi triển khai một hệ thống microservice thì ta cần phải có một đội ngũ các DevOps làm việc với nhau để có thể đưa hệ thống từ development lên production
Tuy nhiên, nếu được thiết kế và triển khai đúng cách, microservices có thể mang lại nhiều lợi ích cho doanh nghiệp, chẳng hạn như khả năng mở rộng linh hoạt, availability cao và khả năng phát triển nhanh chóng Do đó, việc lựa chọn giữa microservices và kiến trúc monolithic sẽ phụ thuộc vào yêu cầu và mục tiêu của doanh nghiệp.
Service discovery & API Gateway
Microservice routing pattern
Microservice routing pattern cách mà một client muốn sử dụng một service trong hệ thống microservice và hệ thống microservice sẽ phải tìm ra service và định tuyến client tới đúng service đó Trên nền tảng cloud, có thể có hàng trăm instance của service đang chạy Để đảm bảo tính bảo mật, cần phải trừu tượng hóa địa chỉ IP của các service này và chỉ nên có một điểm truy cập duy nhất cho các yêu cầu tới service Các pattern sau đây sẽ trả lời cho câu hỏi đó:
• Service discovery: Với service discovery và tính năng chính của nó service registry, bạn có thể làm cho hệ thống microservice của mình discoverable cho nên các client có thể tìm thấy chúng mà không cần phải hardcoded vị trí của các service vào trong ứng dụng
• Service routing: Với API gateway chúng ta có thể cung cấp một điểm truy cập duy nhất cho toàn bộ service để các chính sách bảo mật và quy tắc định tuyển được áp dụng một cách thống nhất trong toàn bộ hệ thống.
Phân loại service discovery
Có hai cách chính để triển khai service discovery:
• Các service và client của chúng tương tác trực tiếp với service registry
• Cơ sở hạ tầng cho việc deployment kiêm luôn chức năng của service discovery
5.2.1 Application - level Service Discovery Pattern
Một cách để triển khai service discovery là cho các service và client tương tác trực tiếp với service registry Hình dưới cho thấy cách thức hoạt động của nó Một instance của service đăng ký địa chỉ mạng của nó với service registry Một client gọi một service bằng cách trước tiên truy vấn service registry để lấy danh sách các service instance sau đó gửi request đến một trong những instance đó
Cách tiếp cận này là sự kết hợp của hai mẫu thiết kế Đầu tiên là Self registration pattern Một service instance gọi API đăng ký của service registry để đăng ký địa chỉ mạng của nó Nó cũng có thể cung cấp một health check URL Health check URL là API mà service registry gọi định kỳ để xác minh rằng serivce instance khỏe mạnh và sẵn sàng đáp ứng các yêu cầu
Service registry có thể yêu cầu một dịch vụ ví dụ định kỳ gọi API "heartbeat“ để ngăn chặn việc đăng ký của nó không hết hạn
Mẫu thứ hai là Client-side discovery pattern Khi một client muốn gọi một service, nó sẽ truy vấn service registry để lấy danh sách các service instance Để cải thiện hiệu suất, client có thể lưu trữ danh sách các service instance Sau đó, client sẽ sử dụng thuật toán cân bằng tải, chẳng hạn như thuật toán quay vòng hoặc ngẫu nhiên, để chọn instance Sau đó, nó gửi request tới instance được chọn
Application-level Service Discovery Pattern đã được Netflix và Pivotal phổ biến Netflix đã phát triển một số phần mềm mã nguồn mở: Eureka, một service registry có tính sẵn sàng cao và Ribbon, HTTP client hỗ trợ Eureka client Spring Cloud được phát triển bởi Pivotal, một framework dựa trên Spring giúp việc sử dụng các công cụ của Netflix trở nên cực kỳ dễ dàng Các service phát triển bằng Spring Cloud tự động đăng ký với Eureka và tự động sử dụng Eureka làm service discovery Ưu điểm:
• Giải quyết trường hợp các service được deploy trên nhiều platform khác nhau: Ví dụ bạn có một vài service được deploy trên Kubernetes và phần còn lại chạy trên các môi trường cũ hơn Application-level service discovery sử dụng Eureka hoạt động trên cả 2 môi trường này trong khi service discovery của Kubernetes chỉ hoạt động trên môi trường Kubernetes
• Cần thư viện service discovery phù hợp với mọi ngôn ngữ và framwork bạn sử dụng: Spring Cloud chỉ hỗ trợ các lập trình viên Spring Nếu bạn sử dụng ngôn ngữ khác ngoài Java như là NodeJS hoặc GoLang, bạn sẽ phải tìm service discovery khác
• Bạn phải chịu trách nhiệm thiết lập và quản lý service registry Do đó, tốt hơn hết bạn nên sử dụng cơ chế service discovery được cung cấp bởi deployment infrastructure
5.2.2 Platform - provided Service Discovery Pattern
Nhiều nền tảng triển khai hiện đại như Docker và Kubernetes có cơ chế service registry và service discovery được tích hợp sẵn Nền tảng triển khai cung cấp cho mỗi service DNS name, địa chỉ IP ảo (Virtual IP - VIP) DNS name được phân giải thành địa chỉ VIP Một client thực hiện một yêu cầu tới địa chỉ DNS/VIP và nền tảng triển khai sẽ tự động định tuyến yêu cầu tới một trong các instance có sẵn Kết quả là service registration, service discovery và định tuyến yêu cầu hoàn toàn được xử lý bởi nền tảng triển khai
Hình bên dưới cho thấy các hoạt động của nó Nền tảng triển khai bao gồm service registry nắm địa chỉ IP của các service đã triển khai Trong ví dụ này, client truy cập Order service bằng DNS name “order-service“ được phân giải thành địa chỉ IP ảo 10.1.3.4 Các nền tảng triển khai tự động cân bằng tải các yêu cầu trên 3 instance của Order service
Cách tiếp cận này là sự kết hợp của 2 mẫu thiết kế:
• Third party registration pattern: Thay vì service tự đăng ký với service registry, một bên thứ ba gọi là registrar, một phần của deployment platform, xử lý việc đăng ký cho các service
• Server-side discovery pattern: Thay vì client truy vấn service registry, nó sẽ gửi yêu cầu tới DNS name và được xử lý bởi request router, request router truy vấn service registry và tự cân bằng tải các yêu cầu Ưu điểm:
• Tất cả khia cạnh của service discovery được xử lý hoàn toàn bởi nền tảng triển khai do đó cơ chế service discovery luôn có sẵn cho tất cả service và client bất kể ngôn ngữ và framework được sử dụng
• Chỉ hỗ trợ cho các service được triển khai bằng nền tảng đó Ví dụ service discovery của Kubernetes chỉ hỗ trợ các service được triển khai trên Kubernetes Bất chấp hạn chế này người ta vẫn khuyên dùng Platform-provided Service Discovery bất cứ khi nào có thể.
Phân loại hình thức register
Trong Service Discovery, có hai cách chính để đăng ký một service với registry:
Khi sử dụng Self-Registration Pattern, service instance có trách nhiệm đăng ký và hủy đăng ký với service registry Ngoài ra, nếu được yêu cầu, service instance sẽ gửi heartbeat request để ngăn việc đăng ký của nó hết hạn Sơ đồ sau đây cho thấy cấu trúc của pattern này
Một ví dụ điển hình cho cách tiếp cận này là Netflix OSS Eureka Client Eureka Client xử lý tất cả các khía cạnh của việc đăng ký và hủy đăng ký Dự án Spring Cloud, triển khai nhiều pattern khác nhau bao gồm service discovery, giúp việc đăng ký tự động với Eureka trở nên dễ dàng
Mô hình này có nhiều lợi ích và hạn chế khác nhau Một lợi ích là nó tương đối đơn giản và không yêu cầu bất kỳ thành phần hệ thống nào khác Tuy nhiên, một nhược điểm lớn là nó gắn chặt các service với discovery service Bạn phải triển khai việc đăng ký lên tất cả các service bạn sử dụng
Khi sử dụng Third-Party Registration Pattern, các instance không chịu trách nhiệm tự đăng ký với service registry Thay vào đó, một thành phần hệ thống khác được gọi là service registrar sẽ xử lý việc đăng ký Service registrar theo dõi các thay đổi đối với tập hợp các instance đang chạy bằng cách thăm dò môi trường triển khai hoặc đăng ký sự kiện Khi nhận thấy một instance mới, nó sẽ đăng ký instannce đó với service registry Service registrar cũng hủy đăng
Một ví dụ về service registrar là dự án mã nguồn mở Registrator Nó tự động đăng ký và hủy đăng ký các instance được triển khai dưới dạng Docker container Registrator hỗ trợ một số service registrar, bao gồm etcd và Consul Một ví dụ khác về Registrator là NetflixOSS Prana Chủ yếu dành cho các service được viết bằng ngôn ngữ không được biên dịch bằng JVM Registrator là một thành phần tích hợp của môi trường triển khai EC2 instance được tạo ra bởi Autoscaling Group có thể được đăng ký tự động với ELB (Elastic Load Balancer) Các service chạy bởi Kubernetes được tự động đăng ký và có thể được tìm thấy thông qua service discovery
Third-Party Registration Pattern có nhiều lợi ích và hạn chế khác nhau Lợi ích chính là các service được tách rời khỏi service registry Bạn không cần triển khai logic đăng ký dịch vụ cho từng ngôn ngữ lập trình và framework mà nhà phát triển của bạn sử dụng Thay vào đó, việc đăng ký được xử lý theo cách tập trung trong một service chuyên dụng Một nhược điểm của mẫu này là trừ khi nó được tích hợp vào môi trường triển khai, nếu không nó vẫn là một thành phần hệ thống có tính sẵn sàng cao khác mà bạn cần thiết lập và quản lý.
Sử dụng service
Cách thức đầu tiên ta có thể sử dụng service là sử dụng trực tiếp thông qua service discovery Nhưng điều này là không nên vì để làm như thế này thì ta cần phải expose các địa chỉ IP của service và dẫn đến một lỗ hổng trong bảo mật Mặt khác các request sẽ trở nên phức tạp hơn vì ta sẽ cần phải tổng hợp lại các thông tin mà ta thu thập từ nhiều service khác nhau, ta phải biết service nào cung cấp thông tin nào
Một cách khác là ta dùng composite UI, khi này ta sẽ thiết kế giao diện dựa trên các service trong kiến trúc microservice ở backend Thay vì ta có một monolithic UI thì bây giờ ta sẽ ánh xạ kiến trúc của backend và thiết kế quanh các service Việc thiết kế giao diện này sẽ yêu cầu nhiều kỹ năng hơn từ người lập trình nhưng sẽ giúp phân tách nhiệm vụ truy vấn dữ liệu ra Tuy nhiên cách này vẫn cần phải expose địa chỉ IP, ta có thể giải quyết bằng cách áp dụng một API gateway
API gateway, một service đóng vai trò là điểm truy cập duy nhất cho các API request vào ứng dụng từ bên ngoài tường lửa Nó tương tự như Facade pattern trong các mẫu thiết kế hướng đối tượng Giống như một mặt tiền của ngôi nhà, API gateway đóng gói kiến trúc bên trong nhiệm khác như xác thực, giám sát và giới hạn số lượng request Hình dưới thể hiện mối quan hệ giữa các client, API gateway, và các service
API gateway chịu trách nhiệm định tuyến yêu cầu và chuyển đổi giao thức Tất cả các API request từ máy khách bên ngoài trước tiên sẽ chuyển đến API gateway sau đó nó định tuyến một số yêu cầu đến service thích hợp API gateway xử lý các request sử dụng API compositon pattern và bằng cách gọi nhiều dịch vụ và tổng hợp các kết quả Nó cũng có thể chuyển đổi các giao thức thân thiện với client như là HTTP và Websocket và giao thức không thân thiện với client được sử dụng bởi các service
Request Routing: Khi nhận một request, API gateway sử dụng routing map và xác định service nào để điều hướng request tới Chức năng này giống với reverse proxy được cung cấp bởi các web server như là Nginx
API Composition: API Gateway thường thực hiện nhiều chức năng hơn chỉ là reverse proxy
API Gateway cũng có thể triển khai API Composition Ví dụ như hình dưới: mobile client tạo một request tới API Gateway API gateway cung cấp mọi endpoint duy nhất cho phép mobile client có thể lấy được toàn bộ thông tin cần thiết Client chỉ cần tạo một request getOrderDetails() tới API Gateway để lấy toàn bộ thông tin thay vì phải tạo 4 request getOrder(), getDelivery(), getBill(), getTicket()
Protocol Translation: API Gateway cũng có thể thực chuyển đổi giao thức Nó có thể cung cấp RESTful API cho các máy khách bên ngoài, mặc dù các service sử dụng hỗn hợp các giao thức nội bộ, bao gồm REST và gRPC
Edge Function: Mặc dù trách nhiệm chính của cổng API là API routing và API Composition, nó cũng có thể thực hiện các edge function - một chức năng được triển khai ở rìa của ứng dụng Ví dụ về các edge function mà ứng dụng có thể triển khai bao gồm:
• Authentication: Xác thực ai là người tạo request
• Authorization: Xác thực client có đủ thẩm quyền để thực hiện thao tác
• Rate limiting: Giới hạn số lượng request mỗi giây
• Caching: Ghi nhớ response vào bộ nhớ đệm để giảm số lần gọi tới các service
• Metrics collection: Thu thập số liệu về việc sử dụng API cho mục đích phân tích
• Request logging: Ghi log request
TLS termination: Giúp tăng tốc độ giao tiếp giữa các service trong cùng datacenter rồi re-
Kiến trúc của API Gateway
Kiến trúc của nó được thể hiện trong hình dưới, bao gồm hai layer: API layer và common layer API layer bao gồm một hoặc nhiều mô-đun API độc lập Mỗi mô-đun API thực hiện một API cho một khách hàng cụ thể Common layer triển khai chức năng chia sẻ, bao gồm các edge function như authentication
Trong ví dụ này, API gateway có ba mô-đun API:
• Mobile API: Triển khai API cho khách hàng di động
• Browser API: Triển khai API cho ứng dụng JavaScript chạy trên trình duyệt
• Public API: Triển khai API cho các nhà phát triển bên thứ ba
API mô-đun định tuyến các request theo hai cách:
• Chuyển tiếp các API tới các service tương ứng mà không thay đổi URL
• Đọc các quy tắc định tuyến trong một file configuration
Có những mô-đun API triển khai phức tạp hơn bằng cách sử dụng API composition - xử lý request bằng cách gọi tới nhiều service và tổng hợp kết quả
Sử dụng Backends For Frontends Pattern
Một vấn đề liên quan đến API gateway là sự mờ nhạt trong việc phân định trách nhiệm Nhiều nhóm đóng góp vào cùng một mã nguồn Một nhóm quản lý API gateway chịu trách nhiệm về hoạt động của nó Mặc dù không tệ như một SOA ESB (Enterprise Service Bus), sự mờ nhạt trong trách nhiệm này là trái với triết lý kiến trúc microservice "nếu bạn xây dựng nó, bạn chịu trách nhiệm về nó" (“if you build it, you own it”)
Các client team sở hữu mô-đun API của họ Họ có thể thay đổi API mô-đun và không cần yêu cầu API Gateway team cho phép thực hiện các thay đổi
Giải pháp là có một API gateway cho mỗi client gọi là Backends for Frontends (BFF), được tiờn phong bởi Phil Calỗado (http://philcalcado.com/ ) và đồng nghiệp của ụng tại
SoundCloud Như hình dưới cho thấy, mỗi mô-đun API trở thành một API Gateway độc lập được phát triển và vận hành bởi một nhóm client duy nhất
Backends for Frontends pattern xác định API Gateway riêng cho từng client Mỗi client sở hữu API Gateway của họ API Gateway team sở hữu commons layer
Lý thuyết cho rằng, các API Gateway khác nhau có thể được phát triển bằng cách sử dụng các công nghệ khác nhau Tuy nhiên, điều này có nguy cơ lặp lại mã nguồn cho các chức năng chung, chẳng hạn như edge function Lý tưởng nhất là tất cả các API gateway sử dụng cùng một tech satck Các chức năng chung được thực hiện bởi một thư viện chia sẻ do nhóm API gateway thực hiện
Ngoài việc xác định rõ trách nhiệm, mô hình BFF còn có những lợi ích khác Các mô-đun API được cô lập với nhau, điều này cải thiện tính tin cậy Một API hoạt động không đúng cách không thể dễ dàng ảnh hưởng đến các API khác Lợi ích khác của mô hình BFF là mỗi API có khả năng mở rộng độc lập Mô hình BFF cũng giảm thời gian khởi động vì mỗi API Gateway là một ứng dụng nhỏ, đơn giản hơn
Envoy
Envoy Proxy là một L3/L4 và L7 proxy có thể được dùng như edge proxy, được thiết kế cho các ứng dụng cloud Được phát triển tại Lyft, Envoy là một proxy được viết bằng C++ với hiệu suất cao được thiết kế cho các hệ thống distributed và ứng dụng khác
Envoy được xây dựng trên những kiến thức có được từ các nền tảng đi trước như NGINX, HAProxy, envoy được deploy với các application và ẩn đi sự phức tạp của networking Khi ta áp dụng Envoy, ta có thể dễ dàng thu thập metrics để monitor hệ thống
Có thể ứng dụng làm API gateway nhờ các tính năng chủ yếu:
Envoy bao gồm nhiều thành phần khác nhau để cung cấp các tính năng phong phú và linh hoạt Một số thành phần chính của Envoy Proxy bao gồm:
• Listeners: Là các thành phần lắng nghe các kết nối đến từ client và chuyển chúng đến các filter chain thích hợp
• Filters: Là các thành phần xử lý dữ liệu truyền qua Envoy Có nhiều loại filter khác nhau, bao gồm network filter, HTTP filter và listener filter
• Filter chains: Là một chuỗi các filter được sắp xếp theo thứ tự để xử lý dữ liệu truyền qua Envoy Mỗi listener có thể có nhiều filter chain khác nhau và một filter chain mặc định tùy chọn
• Routes: Là các quy tắc redirect request từ client đến các cluster thích hợp
• Clusters: Là một nhóm các máy chủ cung cấp cùng một dịch vụ Envoy sử dụng thông tin về cluster để redirect request từ client đến máy chủ thích hợp
• Hosts: Là các máy chủ trong một cluster Envoy sử dụng thông tin về host để redirect request từ client đến máy chủ thích hợp
Envoy sử dụng protocol buffer để định nghĩa các thuộc tính có thể được configure (listener, route, endpoint, filter, ), qua đó cho phép tùy ý extend các configuration đó
Các configuration có thể được fetch từ một management server bằng gRPC/HTTP polling và áp dụng ở runtime
Ví dụ về file yaml để configure Envoy:
Distributed transactions
Tổng quan về transaction
Transaction là một tiến trình xử lý có xác định điểm đầu và điểm cuối, được chia nhỏ thành các operation (phép thực thi) , tiến trình được thực thi một cách tuần tự và độc lập các operation đó theo nguyên tắc hoặc tất cả đều thành công hoặc một operation thất bại thì toàn bộ tiến trình thất bại Nếu việc thực thi một operation nào đó bị fail (hỏng) đồng nghĩa với việc dữ liệu phải rollback (trở lại) trạng thái ban đầu
Ví dụ : Ngân hàng thực hiện chuyển tiền từ tài khoản A sang tài khoản B, cần thực hiện hai công việc : trừ tiền của A, tăng tiền của B Hai công việc này hoặc cả hai thành công hoặc không có công việc nào thành công (nếu một công việc vì lý do nào đó không thực hiện thành công thì trạng thái ban đầu trước khi chuyển tiền phải được khôi phục để bảo toàn dữ liệu) Khi đó việc chuyển tiền cần được đặt vào một giao dịch (Transaction)
• Atomicity - đảm bảo rằng mọi operation đều được thực hiện thành công Nếu không thì transaction sẽ bị hủy bỏ tại điểm xảy ra lỗi và mọi operation phía trước nó được roll back về trạng thái ban đầu
• Consistency - đảm bảo CSDL chỉ thay đổi khi transaction được commit thành công
• Isolation - các transaction được thực thi độc lập và "vô hình" với nhau (không tác động lẫn nhau)
Two-phase commit (2PC)
Cần có 1 component Coordinator để quản lý local transaction của các service
Trong 2PC, một server tham gia transaction sẽ được chọn bởi client để đảm nhiệm vai trò điều phối toàn bộ transaction đó, server này gọi là coordinator Coordinator quản lý nhiều transaction và dùng log để ghi nhận các thay đổi về trạng thái của transaction
Giả sử chúng ta có 2 services A và B
• Coordinator request tới A: o Begin Transaction o Thay dổi db, nếu có lỗi thì response ERROR o Response OK
• Coordinator request tới B: o Begin Transaction o Thay dổi db, nếu có lỗi thì response ERROR o Response OK
Coordinator sẽ đợi response từ các service, lúc này sẽ có 2 trường hợp:
• Nếu có 1 service bị lỗi: coordinator sẽ gửi request yêu cầu các service rollback
• Nếu tất cả đều OK: coordinator sẽ gửi request yêu cầu các service commit transaction Để tăng fault tolerance, ta có thể sử dụng Write-ahead log lưu trên ổ cứng và sử dụng để lưu trạng thái, tiến độ của các transaction để phòng trường hợp coordinator sập Bằng cách đó, các transaction có thể được phục hồi từ WAL khi có một server mới được chọn làm coordinator Các cluster cũng có thể sử dụng WAL và áp dụng Replicated log (Chương 8, Consensus) để lưu trạng thái của các transaction xảy ra trên các server thuộc cluster đó và đồng bộ các thay đổi giữa các node trong cùng một cluster Thêm vào đó, các server cũng cần có tính idempotent khi xử lý các yêu cầu từ coordinator, để đáp ứng việc coordinator mới sau khi phục hồi các transaction từ WAL có thể gửi lại các yêu cầu prepare/commit
Coordinator và các cluster sử dụng Write-ahead log để lưu trạng thái và tiến độ của các transaction Ưu điểm:
• Khi thực hiện transaction, nó sẽ block exceuting record đảm bảo tính global isolation Các transaction khác trên cùng service phải chờ tới khi transaction hiện tại được commit
• Latency: Coordinator cần chờ reply từ tất cả các services để quyết định tiếp theo cần làm gì Các transaction bắt buộc phải lock (pessimistic lock) nếu thực hiện trên cùng data
• Coordinator: bản thân coordinator cũng là một mắt xích yếu trong 2PC Nếu coordinator gặp sự cố, toàn bộ các transactions sẽ bị block cho đến khi coordinator phục hồi
• Transaction dependency: các local transaction sẽ phụ thuộc vào nhau Các transaction cần chờ được commit hoặc rollback cho đến khi transaction cuối cùng phản hồi, có thể dẫn tới resource leak
• Eventually consistence: mặc dù 2PC là một giải pháp cực kì tốt nếu muốn có strong consistency nhưng sự thật thì không hẳn là really consistence Có độ trễ nhất định giữa các commit của services, có thể một thoáng tích tắc nào đó refresh page thấy tiền đã bị trừ nhưng chưa lên order.
Three-phase commit (3PC)
Với three-phase commit bất kì một service nào trong microservice architecture (MSA) cũng có thể trở thành Coordinator để phù hợp với việc nếu một service gặp sự cố thì sẽ có Coordinator khác thay thế và tiếp tục công việc Một Coordinator mới lên cần contact với các service khác để biết được state hiện tại của transaction là gì để quyết định phase tiếp theo là gì
• Prepare Phase: Tương tự 2PC
• Commit Phase: Tương tự 2PC
Three-phase commit có thêm một pha giữa pha prepare và pha commit là pha prepared to commit Pha này có mục đích thông báo quyết định của coordinator đến các server tham gia transaction Khi đến pha prepare to commit, coordinator gửi yêu cầu preCommit đến các server, lúc này các server sẽ biết được quyết định của coordinator nhưng chưa thực hiện commit Bằng cách đó, thuật toán đảm bảo tất cả các server node đều biết được quyết định của coordinator trước khi có node nào ngừng hoạt động sau khi đã lỡ commit hoặc abort Ta có thể đảm bảo cả cluster hoạt động như một bộ máy thống nhất
Các node server khi nhận thấy coordinator không gửi yêu cầu Commit Initialized sau khi chúng đã phản hồi xác nhận prepare to commit sẽ hiểu rằng coordinator đã sập và tự động abort, rollback transaction Tương tự với trường hợp coordinator không nhận được phản hồi prepare to commit từ một node server
Tuy nhiên nó không giải quyết bài toán blocking với synchronous nên gần không được khuyến khích trong MSA Nhưng synchronous chính là lí do giúp 2PC hay 3PC có tính consistence cao.
Saga
Kiến trúc Saga cung cấp khả năng quản lý giao dịch bằng cách sử dụng một chuỗi các giao dịch cục bộ Giao dịch cục bộ là đơn vị công việc được thực hiện bởi Saga participant Mọi hoạt động là một phần của Saga đều có thể được rollback bằng một giao dịch đền bù
Saga Execution Coordinator (SEC) là component trung tâm để triển khai Saga Flow, chứa Saga log ghi lại chuỗi các sự kiện của hệ thống phân tán Khi bất cứ lỗi nào xảy ra, SEC sẽ đọc Saga log xác định component bị ảnh hưởng và chuỗi giao dịch đền bù nào nên được thực hiện Khi SEC gặp lỗi, nó có thể đọc Saga log khi khởi động lại
Có 2 cách triển khai Saga: choreography và orchestration
Mỗi service sẽ publish một event để các service khác xử lý tiếp Trong Saga, Choreography thành công nếu tất cả service hoàn thành giao dịch cục bộ của chúng và không có bất cứ service nào gặp lỗi
Trong trường hợp gặp lỗi, service sẽ báo cáo lỗi với SEC và SEC có nhiệm vụ thực hiện giao dịch bồi thường tương ứng
Nếu việc thực hiện giao dịch bồi thường bị lỗi, SEC có trách nhiệm retry cho tới khi thành công Choreography phù hợp với greenfield applicaiton development
Có một orchestration quản lý toàn bộ trạng thái của các transaction, các service không tự gọi với nhau mà phải thông qua orchestration
Orchestration pattern phù hợp với brownfield microservice application development architecture - hệ thống microservie đã có sẵn và muốn triển khai Saga
Một vấn đề quan trọng trong Saga là đảm bảo bước thực hiện local transaction và bước gửi message được thực hiện một cách đơn nhất (atomic), để không có tình trạng service ngừng hoạt động sau khi thực hiện local transaction nhưng lại trước khi gửi message event, dẫn tới mất nhất quán giữa service đó - cũng như các service đã thực hiện local transaction trước nó - và các service còn lại trong saga Để đảm bảo yêu cầu này, ta có thể áp dụng các phương pháp sau:
Một service gửi message bằng cách insert nó vào bảng OUTBOX như một phần của local transaction Message Relay đọc bảng OUTBOX và gửi message tới mesage broker
• Transactional outbox: ghi message vào một table trong database (table outbox), table này dùng để ghi nhớ các message được phát đi bởi service Lúc này, ta hoàn toàn có thể gói gọn các thao tác của local transaction và thao tác ghi message trong cùng một database transaction Table outbox hoạt động như một message queue tạm thời
Message relay là một thành phần trong hệ thống đọc outbox table và publish message tới message broker Có 2 cách để gửi message tới message broker
Polling publisher: Nếu ứng dụng sử dụng cơ sở dữ liệu quan hệ, một cách rất đơn giản để publish message được insert vào bảng OUTBOX là để MessageRelay dò các message chưa được publish Nó sẽ định kỳ truy vấn bảng:
SELECT * FROM OUTBOX ORDERED BY ASC
Tiếp theo, MessageRelay publish message đó tới message broker cuối cùng, nó xóa những tin nhắn đó khỏi OUTBOX:
Việc kiểm tra định kỳ cơ sở dữ liệu là một phương pháp đơn giản và hoạt động tương đối tốt ở quy mô thấp Nhược điểm là việc kiểm tra cơ sở dữ liệu thường xuyên có thể tốn kém Nếu sử dụng phương pháp này với NoSQL, nó có thể không hiệu quả bởi chúng ta không thể truy vấn vào OUTBOX table như với SQL mà phải truy vấn trực tiếp các bussines entity Bởi vì những hạn chế này mà người ta thường sử dụng cách thứ hai Transaction Log Tailing Pattern
Transaction Log Tailing: Một giải pháp khác là MessageRelay theo dõi database transaction log (commit log) Mỗi cập nhật được commit được biểu diễn bởi một entry trong transaction log Một transaction log miner có thể đọc transaction log và publish mỗi thay đổi dưới dạng một message đến message broker
Phương pháp này có thể được sử dụng để publish các message được insert vào một bảng OUTBOX trong hệ quản trị cơ sở dữ liệu quan hệ hoặc các mesage được thêm vào bản ghi trong cơ sở dữ liệu NoSQL
Một số ví dụ về việc sử dụng phương pháp này:
Debezium (http://debezium.io) Open source project publishes những thay đổi của database tới Apache Kafka
LinkedIn Databus (https://github.com/linkedin/databus)%E2%80%94An Open source project that theo dõi Oracle transaction log và publishe chúng dưới dạng event
DynamoDB streams: DynamoDB streams chứa chuỗi thay đổi theo thứ tự thời gian (tạo, cập nhật và xóa) được thực hiện đối với các mục trong bảng
DynamoDB trong 24 giờ qua Ứng dụng có thể đọc những thay đổi đó và publish chúng dưới dạng event
Eventuate Tram (https://github.com/eventuate-tram/eventuate-tram- core)%E2%80%94Your Open source project sử dụng giao thức MySQL binlog, Postgres WAL hoặc đọc các thay đổi được thực hiện đối với OUTBOX table và publish chúng tới Apache Kafka
• Event sourcing: là một kiến trúc lưu trữ trong đó database không lưu các domain object ở trạng thái hiện tại mà thay vào đó là lưu các sự kiện (event) đã xảy ra với các domain object đó Từ đó, ta có thể tái tạo một domain object tại một thời điểm bất kỳ trong suốt quá trình sống của hệ thống bằng cách thực hiện lần lượt các event đã diễn ra trên domain object đó Có một số framework hỗ trợ xây dựng hệ thống event sourcing như Eventuate Tram, Eventuate Local Service có thể subscribe vào event store để nhận được thông báo khi có event được thêm vào store, qua đó xử lý một cách tương ứng.
So sánh Two-phase commit/Three-phase commit và Saga
Two-phase commit (2PC) và Saga đều được dùng để đảm bảo tính nhất quán của dữ liệu trong hệ thống phân tán
• 2PC là một thuật toán dùng để đảm bảo rằng mọi node trong hệ thống phân tán đều
• Giao thức 2PC sẽ hữu ích trong trường hợp mọi node tham gia giao dịch phân tán phải commit hoặc rollback transaction cùng lúc
• Đảm bảo tính atomicity và consistency của transaction nhưng có thể dẫn tới blocking và hiệu năng giảm
• Là một microservice design pattern dùng cho việc quản lý những long-lived transactions trong hệ thống phân tán
• Saga là một chuỗi các local transaction mà mỗi transaction sẽ cập nhật trạng thái của service đó, mỗi service có một database riêng
• Nếu như local transaction thất bại, saga sẽ kích hoạt giao dịch đền bù mà loại bỏ thay đổi của transaction bị thất bại
• Saga hữu ích trong trường hợp transaction quá lớn để một 2PC có thể quản lý.
Consensus
Vấn đề Consensus
Một vấn đề cốt lõi trong các hệ thống phân tán là đảm bảo tính nhất quán về dữ liệu giữa các node trong cluster server (cluster), cũng như đảm bảo độ tin cậy, tính sẵn sàng của toàn hệ thống Để đáp ứng các tiêu chí trên đòi hỏi các node trong hệ thống đạt được sự thống nhất về trạng thái Vấn đề về sự thống nhất giữa các node gọi là Consensus, cụ thể vấn đề thống nhất về trạng thái để đảm bảo tính nhất quán gọi là State machine replication State machine replication được ứng dụng vào các pattern như Replicated log, Write-ahead log,
7 1.1 Định nghĩa về vấn đề Consensus
Giả sử chúng ta có một hệ thống phân tán gồm k nút (n1, n2, …, nk), trong đó mỗi nút có thể đề xuất một giá trị khác nhau.
Vấn đề thống nhất (Consensus problem) là vấn đề làm cho tất cả các nút này đồng ý về một giá trị duy nhất. Đồng thời, các tính chất sau phải được thỏa mãn:
• Kết thúc (Termination): Mọi nút không bị lỗi cuối cùng phải quyết định
• Thỏa thuận (Agreement): Quyết định cuối cùng của mọi nút không bị lỗi phải giống nhau
• Độ chính xác (Validity): Giá trị được đồng thuận phải được đề xuất trước đó bởi một trong các nút
7 1.2 Các use - case của sự thống nhất a Bầu chọn leader Đây là một vấn đề phổ biến khi các nút trong một hệ thống phân tán cần bầu chọn một nút từ trong số họ để hoạt động như người điều phối (leader) và điều phối hoạt động của toàn bộ hệ thống.
Một ví dụ về vấn đề này là single-master replication schema.
Schema này dựa trên việc một nút được chỉ định là chính (primary), sẽ chịu trách nhiệm thực hiện các hoạt động cập nhật dữ liệu Các nút khác, được chỉ định là phụ (secondaries), sẽ thực hiện các hoạt động tương tự.
Tuy nhiên, trước tiên hệ thống cần chọn nút chính thông qua một quy trình gọi là bầu chọn leader Bởi vì tất cả các nút thực tế đều đồng ý về một giá trị duy nhất, danh tính của leader, vấn đề này có thể dễ dàng được mô hình hóa như một vấn đề thống nhất. b Khóa phân phối
Một vấn đề phổ biến khác là khóa phân tán (distributed locking) Hầu hết các hệ thống phân tán tiếp nhận đồng thời nhiều yêu cầu và cần thực hiện kiểm soát đồng thời để ngăn chặn sự không nhất quán dữ liệu do sự can thiệp giữa các yêu cầu này.
Một trong các phương pháp kiểm soát đồng thời này là sử dụng khoá (locking) Tuy nhiên, việc sử dụng khoá trong hệ thống phân tán đem lại nhiều trường hợp rủi ro.
Tất nhiên, khoá phân tán cũng có thể được mô hình hóa như một vấn đề thống nhất, trong đó các nút của hệ thống đồng ý về một giá trị duy nhất, đó là nút nắm giữ khoá. c Truyền tải nguyên tử
Một vấn đề thường được đề cập là việc truyền tải nguyên tử (atomic broadcast), liên quan đến việc cho phép một tập hợp các nút truyền tải các thông điệp đồng thời, trong khi đảm bảo rằng
Thuật toán Raft
Raft đặt ra khái niệm về máy trạng thái sao chép (replicated state machine) và nhật ký sao chép (replicated log of commands) là những thực thể hàng đầu (first-class citizens) trong hệ thống.
Nó yêu cầu một tập hợp các nút tạo thành nhóm thống nhất (consensus group), được gọi là cụm Raft (Raft cluster) Mỗi nút có thể trong ba trạng thái sau:
• Ứng cử viên (Candidate) a Trạng thái nút (Node states)
Cluster có một node leader, gọi là leader, sẽ gửi heartbeat (nhịp tim) đến tất cả các node khác trong cluster, các node này gọi là những follower, để thông báo cho chúng về sự hiện diện của một leader Mỗi follower có một timer, được reset về một khoảng thời gian ngẫu nhiên khi nhận được heartbeat từ leader Chỉ có thể có một leader tại một thời điểm Một server có thể ở một trong ba trạng thái: follower, candidate (candidate), leader.
Trạng thái các nút trong Raft b Nhiệm kỳ (terms) Để ngăn chặn việc xuất hiện đồng thời hai người điều phối, Raft có một khái niệm về nhiệm kỳ
Thời gian được chia thành các nhiệm kỳ, một nhiệm kỳ được đánh số bằng các số nguyên liên tiếp Mỗi nhiệm kỳ bắt đầu bằng một cuộc bầu cử trong đó một hoặc nhiều ứng cử viên (candidate) cố gắng trở thành người điều phối (leader). Để trở thành người điều phối (leader), ứng cử viên cần nhận được phiếu bầu từ đa số các nút Bên cạnh đó, Mỗi nút bầu cho tối đa một ứng cử viên trong một nhiệm kỳ theo nguyên tắc cơ bản "first-come-first-served" Do đó, tối đa chỉ có một nút có thể thắng cuộc trong cuộc bầu cử cho một nhiệm kỳ cụ thể quyết định bầu cử của mỗi server được lưu giữ trên disk để tránh bầu lại do sự cố
Nếu một ứng cử viên thắng trong cuộc bầu cử, nó sẽ trở thành người điều phối trong suốt phần còn lại của nhiệm kỳ đó Bất kỳ người điều phối từ các nhiệm kỳ trước đây sẽ không thể sao chép các mục nhật ký (log entries) mới đến các nhóm vì những người bỏ phiếu cho người điều phối mới sẽ từ chối các yêu cầu của người điều phối cũ, và cuối cùng nó sẽ phát hiện mình đã bị thay thế.
Nếu không có ứng cử viên nào đạt có được đa số phiếu trong một nhiệm kỳ, nhiệm kỳ này sẽ kết thúc mà không có người điều phối, và một nhiệm kỳ mới (với một cuộc bầu cử mới) bắt đầu ngay sau đó.
7 2.2 Giao tiếp giữa các nút Raft a Cơ chế giao tiếp
Các nút giao tiếp thông qua cuộc gọi thủ tục từ xa (Remote Procedure calls – RPCs), và Raft có hai loại RPC cơ bản:
• RequestVote: Được gửi bởi ứng cử viên trong quá trình bầu cử
• AppendEntries: Được gửi bởi người điều phối (leader) để sao chép các log entries và cung cấp một tín hiệu "heartbeat" (tim đập)
Các lệnh được lưu trữ trong một nhật ký – “log” được sao chép cho tất cả các nút trong cụm.Các log entries được đánh số thứ tự, và chúng chứa nhiệm kỳ mà chúng được tạo ra và các lệnh liên quan cho state machine.
Một mục được xem là đã committed nếu nó có thể được áp dụng vào state machine của các nút Raft đảm bảo rằng các committed entries là bền vững và cuối cùng sẽ được thực hiện bởi tất cả các state machine có sẵn, đồng thời đảm bảo rằng không có mục khác sẽ được committed cho cùng một chỉ mục (index) Bên cạnh đó, nó cũng đảm bảo rằng tất cả các mục trước đó đã committed Trạng thái này về cơ bản thông báo rằng thống nhất đã được đạt trong mục này.
Mỗi server trong cluster duy trì một write-ahead log chỉ được thêm được sử dụng để lưu trữ các lệnh mà server nhận được, mỗi entry log lưu trữ nhiệm kỳ nhận được lệnh và lệnh (thay đổi trạng thái) Log được lưu trên disk để chống lỗi.
Như đã đề cập trước đây, người điều phối có trách nhiệm nhận các lệnh từ “khách hàng” và sao chép chúng qua các cụm Quá trình diễn ra theo thứ tự sau:
• Khi người điều phối nhận được một lệnh mới, nó ghi thêm mục vào nhật ký riêng của mình và sau đó gửi một yêu cầu AppendEntries đồng thời đến các nút khác, nó sẽ lặp lại khi nó không nhận được phản hồi trong một khoảng thời gian từ một trong số các node chưa phản hồi
• Nếu entry được sao chép thành công trên đa số follower Người điều phối sẽ đánh dấu nó là đã commit, thực hiện lệnh và trả lại kết quả cho client
• Người điều phối áp dụng lệnh vào state machine của mình và thông báo cho các người theo (follower) rằng họ cũng có thể commit bằng cách kèm thông tin cần thiết trong các committed entries trong các thông điệp AppendEntries tiếp theo
Khi gửi yêu cầu AppendEntries cho follower, leader cũng bao gồm cặp [nhiệm kỳ, index] của entry cuối cùng của nó Nếu cặp [nhiệm kỳ, index] không khớp với entry cuối cùng của follower, nó sẽ từ chối yêu cầu và gửi lại phản hồi không thành công Leader sẽ quay lui log về entry trước và gửi các cặp [nhiệm kỳ, index] tương ứng cho đến khi khớp với entry cuối cùng của follower hoặc đã đi đến đầu của log, trong trường hợp đó, log của follower hoàn toàn trống.
Thực tế đây là trường hợp đẹp nhất khi không có bất kỳ sự cố hoặc sự sai sót gì xảy ra, nhưng thực tế thì ngược lại hoàn toàn. b Sự phân kỳ giữa các nút
Trong quá trình sự cố của người điều phối và người theo, có thể quan sát được sự phân kỳ giữa các nút khác nhau.
Sự phân kỳ tạm thời của các node logs
Querying in microservice
Querying using the API composition pattern
Khi các truy vấn lấy dữ liệu từ một service duy nhất, việc triển khai những truy vấn này thường rất đơn giản Tuy nhiên có rất nhiều trường hợp một truy vấn lấy dữ liệu từ nhiều service giả sử như findOrder() được miêu tả dưới đây
Hoạt động findOrder() truy xuất một đơn hàng dựa trên khóa chính của nó Nó nhận một orderId làm tham số và trả về một đối tượng OrderDetails, chứa thông tin về đơn hàng Như được thể hiện trong dưới, hoạt động này được gọi bởi một module frontend, chẳng hạn như một thiết bị di động hoặc một ứng dụng web
Nếu như sử dụng kiến trúc monolithic, vì dữ liệu của nó đặt tại một cơ sở dữ liệu duy nhất, ứng dụng có thể dễ dàng lấy thông tin đơn hàng bằng cách thực thi một câu lệnh SELECT duy nhất kết hợp thông tin từ các bảng khác nhau Ngược lại, trong kiến trúc microservices, dữ liệu phân tán qua các service sau:
• Order Service: Thông tin cơ bản về đơn hàng, bao gồm chi tiết và trạng thái
• Kitchen Service: Trạng thái của đơn hàng từ góc độ của nhà hàng và thời gian ước tính món ăn sẵn sàng để lấy đi
• Delivery Service: Trạng thái giao hàng, thông tin giao hàng dự kiến và vị trí hiện tại của đơn hàng
• Accounting Service: Trạng thái thanh toán của đơn hàng
Bất kỳ khách hàng nào cần chi tiết đơn hàng đều phải yêu cầu từ tất cả các service này
8.1.2 Overview of the API composition pattern
API composition pattern triển khai một hoạt động truy vấn bằng cách gọi các service sở hữu dữ liệu và kết hợp kết quả Hình dưới mô tả cấu trúc của mô hình này, với hai loại service cấu thành:
• API composer: Thực hiện hoạt động truy vấn bằng cách truy vấn các provider service
• Provider service: Service sở hữu một số dữ liệu mà truy vấn muốn trả về
Hình trên mô tả ba provider service API composer thực hiện truy vấn bằng cách lấy dữ liệu từ các provider service và kết hợp kết quả API composer có thể là một client, chẳng hạn như một ứng dụng web, cần dữ liệu để hiển thị trang web Hoặc nó có thể là một service, chẳng hạn như API gateway và Backends for frontends Khả năng sử dụng pattern này phụ thuộc vào nhiều yếu tố Ngay cả khi các Provider service cung cấp các API để nhận các dữ liệu cần thiết, API composer có thể phải thực hiện các thao tác kết hợp những dataset lớn in-memory không hiệu quả Sau này, bạn sẽ thấy các ví dụ về các hoạt động truy vấn mà không thể triển khai bằng pattern này Tuy nhiên, may mắn là có nhiều tình huống mà pattern này có thể áp dụng
8.1.3 Implementing the findOrder() query operation using the API composition pattern Ở đây chúng ta sẽ xem xét một ví dụ, findOrder() là một thao tác truy vấn đơn giản dựa trên khóa chính Sẽ là phù hơp nếu chúng ta mong đợi mỗi Provider service đều cung cấp một API lấy các dữ liệu cần thiết dựa trên orderId Do đó, findOrder() là một ứng viên xuất sắc để triển khai bằng API compositon pattern API composer sẽ gửi request tới cả bốn service và tổng hợp các kết quả Hình dưới đây mô tả thiết kế của Find Order Composer
Trong ví dụ này API composer là một service cung cấp query dưới dạng một REST endpoint Các Provider service cũng triển khai REST API Tuy nhiên ý tưởng cũng tương tự như vậy nếu các service sử dụng một dạng interprocess comnunication protocol khác ví dụ như gRPC thay vì REST Find Order Composer triển khai một REST endpoint GET /order/{orderId} Các Provider service còn lại cũng có một REST endpoint dựa trên orderId để lấy ra dữ liệu đúng với order đó Như bạn có thể thấy, API compositon khá đơn giản Hãy xem xét một vài vấn đề thiết kế bạn cần giải quyết khi áp dụng mô hình này
Khi sử dụng pattern này, bạn sẽ phải đối mặt với hai vấn đề về mặt thiết kế:
• Thành phần nào trong hệ thống sẽ đóng vai trò API composer
• Làm thế nào để viết logic tổng hợp một cách hiệu quả
WHO PLAYS THE ROLE OF THE API COMPOSER?
Thông thường chúng ta sẽ có ba lựa chọn Lựa chọn đầu tiên là để client làm API composer như mô tả trong hình dưới
Client như là web application được chạy trên cùng một mạng LAN với các service có thể query data trực tiếp từ chúng và tổng hợp lại Tuy nhiên cách này có thể không khả thi với client nằm bên ngoài firewall và phải query tới service bằng mạng có độ trễ cao
Lựa chọn thứ hai chính là API gateway - nơi là external API của hệ thống sẽ đóng vai trò là API composer Lựa chọn này hợp lý khi mà query operation là một phần của external API của ứng dụng Thay vì routing request tới một trong các service, API gateway triển khai composition logic Cách tiếp cận này cho phép những client nằm bên ngoài firewall như là mobile device có thể lấy data từ nhiều service một cách hiệu quả thông qua một API duy nhất
Implementing API composition in the API gateway The API queries the provider services to retrieve the data, combines the results, and returns a response to the client
Lựa chọn thứ ba chính là sử dụng một service độc lập (stand-alone service)
Implement a query operation used by multiple clients and services as a standalone service
Bạn nên sử dụng lựa chọn này cho các query operation dùng trong nội bộ bởi nhiều service
Nó cũng có thể dùng cho các external query mà logic tổng hợp của nó quá phức tạp để là một phần của API gateway
API COMPOSERS SHOULD USE A REACTIVE PROGRAMMING MODEL
Khi phát triển một hệ thống phân tán, việc giảm thiểu độ trễ là một mối bận tâm luôn hiện hữu Khi có thể, API composer nên gọi các provider service một cách song song để giảm thiểu thời gian phản hồi cho một hoạt động truy vấn Ví dụ, Find Order Aggregator nên gọi cùng một lúc bốn service vì không có phụ thuộc giữa các lần gọi Tuy nhiên, đôi khi, API composer cần kết quả của một provider service để gọi một service khác Trong trường hợp này, nó sẽ cần gọi một số (nhưng hy vọng là không phải tất cả) các provider service theo tuần tự
Logic để thực hiện một sự kết hợp hiệu quả giữa các lầm gọi service theo tuần tự và đồng thời có thể phức tạp Để API composer có thể được bảo trì cũng như có hiệu suất và khả năng mở rộng cao, nó nên sử dụng một reactive design dựa trên Java CompletableFutures, RxJava observables, hoặc công nghệ tương đương khác
8.1.5 The benefits and drawbacks of the API composition pattern
Pattern này đơn giản và thường là thứ đầu tiên chúng ta nghĩ tới khi triển khai các query operation trong microservice tuy nhiên nó có một số nhược điểm như sau
Trong ứng dụng monolithic, client có thể lấy được data chỉ với một request thường là một lần query database Tuy nhiên khi triển khai pattern này đòi hỏi nhiều request và database query Kết quả là tài nguyên tính toán và mạng tăng lên, chi phí hoạt động của ứng dụng cũng sẽ tăng theo
Một nhược điểm tiếp theo của pattern này là giảm tính sẵn sàng của hệ thống Tính sẵn sàng của một hoạt động giảm dần theo số lượng service tham gia bởi vì nó đòi hỏi ít nhất sự tham gia của ba service, API composer và hai provider service Ví dụ, giả sử tính sẵn sàng của một service là 99.5 % thì tính sẵn sàng của findOrder() - gọi tới 4 provider service là 99.5 % ^ (4 + 1) = 97.5 %
Có một số chiến lược giúp tăng tính sẵn sàng của hệ thống Chiến lược đầu tiên là cho API composer trả về data được cache từ lần gọi trước khi provider service không khả dụng để tăng performance Một chiến lược khác là cho API composer trả về data chưa hoàn chỉnh Nếu một service không khả dụng, API composer có thể bỏ qua data từ service đó bởi vì UI vẫn có thể hiển thị những thông tin hữu ích từ các service khác
LACK OF TRANSACTIONAL DATA CONSISTENCY
Using the CQRS pattern
Nhiều ứng dụng enterprise sử dụng RDBMS là transactional system of record và một text search database như là Elasticsearch hoặc Solr dùng cho text search query Một số ứng dụng giữ các database đồng bộ với nhau bằng cách ghi dữ liệu vào cả hai một cách đồng thời Một số khác copy dữ liệu định kỳ từ RDBMS sang text searh engine Các ứng dụng với kiến trúc này sẽ kích hoạt sức mạnh của multiple databases: các thuộc tính của RDBMS transaction và khả năng truy vấn của text search engine
Pattern: Command query responsibility segregation
Implement a query that needs data from several services by using events to maintain a read-only view that replicates data from the services
CQRS chính là một dạng tổng quát của loại kiến trúc này Nó duy trì một hay nhiều database view - không chỉ text search database - mà triển khai một hoặc nhiều application query Để hiểu tại sao pattern này lại hữu ích, hãy xem xét một số query mà không thể triển khai API composition một cách hiệu quả
API composition là một phương pháp tốt để triển khai nhiều query phải lấy dữ liệu từ nhiều service Thật không may, đây chỉ là một phần giải pháp cho vấn đề truy vấn trong kiến trúc microservices bởi vì có nhiều truy vấn mà API composition không thể triển khai một cách hiệu quả
IMPLEMENTING THE FINDORDERHISTORY() QUERY OPERATION findOrderHistory() trả về lịch sử order của một customer, nó nó một vài parameter:
• customerId: định danh của một customer
• pagination: trang kết quả cần trả về
• filter: tiêu chí lọc bao gồm max age của order, optional order status, optional keyword phù hợp với restaurant name và menu items
Query này trả về một đối tượng OrderHistory chứa các order phù hợp được sắp xếp theo thứ tự tăng dần về thời gian Nó được gọi bởi module triển khai Order History view View này hiển thị mỗi order, bao gồm order number, order status, tổng giá trị đơn hàng và thời gian giao hàng dự kiến
Nhìn bề ngoài, query này giống với findOrder() Sự khác biệt duy nhất là nó trả về nhiều order thay vì chỉ một Có vẻ như API composer chỉ cần thực hiện cùng một truy vấn đối với provider service và kết hợp kết quả Thật không may, điều đó không đơn giản như vậy
Bởi vì không phải tất cả các service đều lưu trữ các thuộc tính được sử dụng để lọc hoặc sắp xếp Ví dụ, một trong các tiêu chí lọc của thao tác findOrderHistory() là một từ khóa phù hợp với một mục trong menu Chỉ có hai service là Order Service và Kitchen Service lưu trữ các mục trong menu của một order Cả Delivery Service và Accounting Service đều không lưu trữ các mục trong menu, vì vậy không thể lọc dữ liệu của chúng bằng từ khóa này Tương tự, cả Kitchen Service và Delivery Service đều không thể sắp xếp theo thuộc tính orderCreationDate
Một ví dụ đơn giản hơn chúng ta muốn tìm các order và lọc theo status của chúng Có hai provider service lần lượt là OrderService và KitchenService OrderService service thì có chưas status nên chúng ta có thể lọc các order dễ dàng còn KitchenService chỉ chứa orderId tham chiếu tới OrderService cho nên không có cách nào lọc các dữ liệu phù hợp ở KitchenService được
Có hai cách mà một API composer có thể giải quyết vấn đề này Một giải pháp là cho API composer thực hiện một in-memory join, như được hiển thị trong hình dưới Nó truy xuất tất cả các order của một customer từ Delivery Service và Accounting Service và thực hiện một join với các order được truy xuất từ Order Service và Kitchen Service
Nhược điểm của phương pháp này là nó có thể đòi hỏi API composer phải truy xuất và kết hợp các bộ dữ liệu lớn, điều này là không hiệu quả Giải pháp khác là để API composer truy xuất các order phù hợp từ Order Service và Kitchen Service và sau đó yêu cầu các order từ các service khác theo ID Nhưng điều này chỉ khả thi nếu những service đó có một API fetch hàng loạt Request order một cách riêng lẻ có thể không hiệu quả do lưu lượng mạng lớn Các truy vấn như findOrderHistory() đòi hỏi API composer phải làm lại chức năng query engine của một RDBMS Developer chỉ nên viết chức năng kinh doanh, không phải một query engine
Như bạn vừa thấy, việc triển khai các truy vấn trả về dữ liệu từ nhiều service có thể khó khăn Nhưng ngay cả các truy trong một service cũng có thể khó triển khai
Hãy xem xét ví dụ của truy vấn findAvailableRestaurants() Truy vấn này tìm kiếm các nhà hàng có sẵn để giao đến một địa chỉ cụ thể vào một thời điểm nhất định Cốt lõi của truy vấn này là một tìm kiếm địa lý (dựa trên vị trí) để tìm các nhà hàng nằm trong một khoảng cách nhất định từ địa chỉ giao hàng Đây là một phần quan trọng của quá trình đặt hàng và được gọi bởi mô-đun giao diện người dùng hiển thị các nhà hàng có sẵn
Thách thức chính khi triển khai truy vấn này là thực hiện geospatial query hiệu quả Cách bạn triển khai truy vấn findAvailableRestaurants() phụ thuộc vào khả năng của cơ sở dữ liệu lưu trữ thông tin nhà hàng Ví dụ, triển khai truy vấn findAvailableRestaurants() sử dụng
MongoDB và các geospatial extension của Postgres và MySQL là khá đơn giản
Nếu ứng dụng lưu trữ thông tin nhà hàng trong một loại cơ sở dữ liệu khác, việc triển khai truy vấn findAvailableRestaurant() sẽ phức tạp hơn Nó phải duy trì một bản sao của dữ liệu nhà hàng dưới một dạng được thiết kế để hỗ trợ truy vấn địa lý ví dụ như Geospatial Indexing Library for DynamoDB (https://github.com/awslabs/dynamodb-geo) Hoặc, ứng dụng có thể lưu trữ một bản sao của dữ liệu nhà hàng trong một loại cơ sở dữ liệu hoàn toàn khác, tình huống rất giống như việc sử dụng text search database cho text query Thách thức khi sử dụng bản sao (replicas) là duy trì cập nhật chúng mỗi khi dữ liệu gốc thay đổi Như bạn sẽ tìm hiểu dưới đây, CQRS giải quyết vấn đề đồng bộ hóa bản sao
THE NEED TO SEPARATE CONCERNS
Một lý do khác khiến việc triển khai các truy vấn trở nên phức tạp là đôi khi service sở hữu dữ liệu không nên là người triển khai truy vấn đó Truy vấn findAvailableRestaurants() thu thập dữ liệu thuộc sở hữu của Restaurant Service, nghe có vẻ là hợp lý để Restaurant Service triển khai truy vấn này Tuy nhiên, việc sở hữu dữ liệu không phải là yếu tố duy nhất cần xem xét
Bạn cũng cần xem xét việc phân tách các mối bận tâm và tránh làm quá tải service với quá nhiều trách nhiệm Ví dụ, trách nhiệm chính của nhóm phát triển Restaurant Service là hỗ trợ quản lý nhà hàng của họ Điều này khá khác biệt so với việc triển khai một truy vấn có thể có lượng truy cập cao và quan trọng Hơn nữa, nếu họ phải chịu trách nhiệm cho truy vấn findAvailableRestaurants(), nhóm sẽ liên tục lo lắng về việc triển khai một thay đổi có thể ngăn chặn người tiêu dùng đặt hàng
Designing CQRS views
Một module view trong CQRS có những API bao gồm một hoặc nhiều query Nó thực hiện ký theo dõi các sự kiện được publish bởi một hoặc nhiều service Như hình dưới thể hiện, một module view bao gồm một view database và ba submodule
Data access module thực hiện logic truy cập cơ sở dữ liệu Event handlers module xử lý sự kiện và Query API sử dụng Data access module để cập nhật và truy vấn cơ sở dữ liệu Event handlers module đăng ký theo dõi các sự kiện và cập nhật cơ sở dữ liệu Query API module thực hiện API truy vấn
Bạn phải đưa ra một số quyết định thiết kế quan trọng khi phát triển một view module:
• Bạn phải chọn một cơ sở dữ liệu và thiết kế schema
• Khi thiết kế data access module, bạn phải giải quyết nhiều vấn đề, bao gồm đảm bảo rằng các cập nhật là idempotent và xử lý các cập nhật đồng thời
• Khi triển khai một view mới trong một ứng dụng hiện tại hoặc thay đổi schema của một ứng dụng hiện tại, bạn phải triển khai một cơ chế để xây dựng hoặc xây dựng lại view một cách hiệu quả
• Bạn phải quyết định cách client có thể đối phó với replication lag như đã mô tả trước đó
Không lâu trước đây, chỉ có một loại cơ sở dữ liệu thống trị tất cả: hệ thống quản lý cơ sở dữ liệu quan hệ (RDBMS) dựa trên SQL Tuy nhiên, khi Web trở nên phổ biến, nhiều công ty phát hiện ra rằng một RDBMS không thể đáp ứng được yêu cầu về quy mô của họ trên Web Điều này dẫn đến việc tạo ra những cơ sở dữ liệu không quan hệ (NoSQL) Một cơ sở dữ liệu NoSQL thường bị giới hạn về các loại transaction và query Đối với một số trường hợp cụ thể, những cơ sở dữ liệu này có một số ưu điểm so với cơ sở dữ liệu SQL, bao gồm mô hình dữ liệu linh hoạt hơn và hiệu suất và khả năng mở rộng tốt hơn
Một cơ sở dữ liệu NoSQL thường là lựa chọn tốt cho một CQRS view, có thể tận dụng các điểm mạnh và bỏ qua các điểm yếu của nó Một CQRS view hưởng lợi từ mô hình dữ liệu phong phú và hiệu suất của một cơ sở dữ liệu NoSQL Nó không bị ảnh hưởng bởi những hạn chế của một cơ sở dữ liệu NoSQL vì nó chỉ sử dụng giao dịch đơn giản và thực hiện một tập hợp cố định các truy vấn
Tuy nhiên, đôi khi cũng phù hợp khi triển khai một CQRS view bằng cách sử dụng cơ sở dữ liệu SQL Một hệ quản trị cơ sở dữ liệu quan hệ (RDBMS) hiện đại chạy trên phần cứng hiện đại có hiệu năng xuất sắc Những developer, database administrator và các IT operations thông thường quen thuộc với cơ sở dữ liệu SQL hơn là cơ sở dữ liệu NoSQL Như đã đề cập trước đó, cơ sở dữ liệu SQL thường có các tiện ích mở rộng cho các tính năng không quan hệ như là geospatial datatypes
Như bạn có thể thấy trong bảng dưới, có rất nhiều lựa chọn khác nhau Và để làm cho quyết định trở nên khó khăn hơn, sự khác biệt giữa các loại cơ sở dữ liệu khác nhau đang bắt đầu mờ nhạt Ví dụ, MySQL, một hệ quản trị cơ sở dữ liệu quan hệ, hỗ trợ tuyệt vời cho JSON, đó là một trong những điểm mạnh của MongoDB, một cơ sở dữ liệu hướng document kiểu JSON Bây giờ sau khi đã thảo luận về các loại cơ sở dữ liệu khác nhau mà bạn có thể sử dụng để triển khai một CQRS view, hãy nhìn vào vấn đề làm thế nào để cập nhật một view một cách hiệu quả
Ngoài việc triển khai các truy vấn một cách hiệu quả, view data model cũng phải triển khai các hoạt động cập nhật một cách hiệu quả thông qua event handler Thông thường, một event handler sẽ cập nhật hoặc xóa một bản ghi trong view database bằng cách sử dụng khóa chính Đôi khi, tuy nhiên, nó sẽ cần cập nhật hoặc xóa một bản ghi bằng cách sử dụng tương đương của một khóa ngoại Hãy xem xét ví dụ event handler cho các sự kiện của Delivery* Nếu có một mối quan hệ 1 - 1 giữa Order và Delivery, thì Delivery.id sẽ tương đương với Order.id như vậy các event handler của Delivery* có thể dễ dàng cập nhật bản ghi cơ sở dữ liệu của Order
Nhưng giả sử một Delivery có khóa chính riêng hoặc có mối quan hệ 1-n giữa một Order và một Delivery Một số sự kiện Delivery*, như sự kiện Delivery-Created, sẽ chứa orderId
Nhưng các sự kiện khác, như sự kiện DeliveryPickedUp, có thể không có thông tin này Trong tình huống này, event handler cho DeliveryPickedUp sẽ cần cập nhật bản ghi của đơn hàng bằng cách sử dụng deliveryId
Một số loại cơ sở dữ liệu hỗ trợ hiệu quả các hoạt động cập nhật dựa trên khóa ngoại Ví dụ, nếu bạn đang sử dụng một hệ quản trị cơ sở dữ liệu quan hệ (RDBMS) hoặc MongoDB, bạn có thể tạo một index trên các cột cần thiết Tuy nhiên, các cập nhật dựa trên khóa ngoại không phải lúc nào cũng đơn giản khi sử dụng các cơ sở dữ liệu NoSQL khác Ứng dụng sẽ cần duy trì một loại ánh xạ cụ thể của cơ sở dữ liệu từ khóa ngoại đến khóa chính để xác định bản ghi nào cần cập nhật Ví dụ, một ứng dụng sử dụng DynamoDB, chỉ hỗ trợ cập nhật và xóa dựa trên khóa chính, phải trước tiên truy vấn một chỉ mục phụ của DynamoDB (sẽ thảo luận sau) để xác định các khóa chính của các mục cần cập nhật hoặc xóa
Event handler và API query module không trực tiếp truy cập vào cơ sở dữ liệu Thay vào đó, chúng sử dụng một data access module, bao gồm một đối tượng truy cập dữ liệu (DAO) và các lớp trợ giúp của nó DAO có một số trách nhiệm Nó thực hiện các hoạt động cập nhật được gọi bởi các event handler và các hoạt động truy vấn được gọi bởi query module DAO thực hiện ánh xạ giữa các loại dữ liệu được sử dụng bởi ngôn ngữ lập trình và API cơ sở dữ liệu
Nó cũng phải xử lý các cập nhật đồng thời và đảm bảo rằng các cập nhật là idempotent
Hãy xem xét những vấn đề này, bắt đầu bằng cách làm thế nào để xử lý các cập nhật đồng thời
HANDLING CONCURRENCY Đôi khi, một DAO phải xử lý khả năng của nhiều cập nhật đồng thời đối với cùng một bản ghi cơ sở dữ liệu Nếu một view đăng ký theo dõi các sự kiện được xuất bản bởi một loại aggregate duy nhất, sẽ không có vấn đề về đồng thời Nhưng nếu một view đăng ký theo dõi các sự kiện được xuất bản bởi nhiều loại aggregate, thì có khả năng mà nhiều event handler có thể cập nhật cùng một bản ghi
Như đã đề cập trước đó, event handler có thể được gọi với cùng một sự kiện nhiều lần Điều này thường không phải là một vấn đề nếu một xử lý sự kiện phía truy vấn là idempotent Một event handler là idempotent nếu xử lý các sự kiện trùng lặp vẫn cho ra kết quả giống nhau Trong trường hợp xấu nhất, view database tạm thời sẽ out-date Ví dụ, một event handler thao tác với Order History view có thể được gọi với chuỗi sự kiện (mặc dù nó khá là không thường xuyên) như trong hình dưới: Delivery-PickedUp, DeliveryDelivered, DeliveryPickedUp và
Deployment
Virtual Machine & Container
Các container và máy ảo là các công nghệ ảo hóa tài nguyên rất tương đồng Ảo hóa là quá trình trong đó một tài nguyên duy nhất của hệ thống như RAM, CPU, ổ đĩa hoặc mạng có thể được 'ảo hóa' và biểu diễn dưới dạng nhiều tài nguyên Điểm khác biệt chính giữa container và máy ảo là máy ảo ảo hóa toàn bộ máy tính xuống tới lớp phần cứng, trong khi container chỉ ảo hóa các lớp phần mềm ở mức độ trên hệ điều hành
Các container là các gói phần mềm nhẹ chứa tất cả các phụ thuộc cần thiết để thực thi ứng dụng phần mềm nằm bên trong Những phụ thuộc này bao gồm các thư viện hệ thống, các gói mã nguồn bên thứ ba từ bên ngoài, và các ứng dụng cấp hệ điều hành khác Các phụ thuộc bao gồm trong một container tồn tại ở mức độ cao hơn hệ điều hành Ưu điểm:
• Bởi vì các container rất nhẹ và chỉ bao gồm phần mềm cấp cao nên chúng có thể sửa đổi rất nhanh phần mềm phổ biến như cơ sở dữ liệu hoặc hệ thống messaging và có thể được tải và thực thi ngay lập tức, giúp tiết kiệm thời gian cho các nhà phát triển
• Các container chia sẻ cùng một hệ thống phần cứng cơ bản dưới tầng hệ điều hành, có thể xảy ra tình huống một lỗ hổng bảo mật trong một container có thể thoát khỏi container và ảnh hưởng đến phần cứng chung Hầu hết các container runtime phổ biến có các kho lưu trữ công cộng chứa các container đã được xây trước Có một rủi ro bảo mật khi sử dụng một trong những hình ảnh công cộngimage này vì chúng có thể chứa các lỗ hổng bảo mật hoặc có thể bị tấn công bởi các kẻ xấu
Các container engine phổ biến:
Các máy ảo là các gói phần mềm nặng có chức năng mô phỏng hoàn toàn các thiết bị phần cứng cấp thấp như CPU, ổ đĩa và các thiết bị mạng Máy ảo cũng có thể bao gồm các phần mềm bổ sung để chạy trên phần cứng được mô phỏng Sự kết hợp của những gói phần cứng và phần mềm này tạo ra một bản sao hoàn chỉnh và hoạt động đầy đủ của một hệ thống máy tính Ưu điểm:
• Bảo mật: Máy ảo hoạt động hoàn toàn tách biệt như các hệ thống độc lập Điều này có nghĩa là chúng được bảo vệ khỏi những lỗ hổng hoặc sự can thiệp từ các máy ảo khác đang chạy trên cùng một máy chủ Ngay cả khi một máy ảo bị tấn công, nó vẫn được cô lập và không thể ảnh hưởng đến các máy ảo lân cận khác
• Máy ảo cung cấp môi trường phát triển linh hoạt hơn Khác với container thường cố đinh đối với các phụ thuộc và cấu hình, máy ảo có thể cài đặt thêm các ứng dụng sau khi đã xác định cụ thể các thông số phần cứng
• Máy ảo tốn nhiều thời gian để xây dựng và tái khởi động vì chúng bao gồm một hệ thống đầy đủ
• Máy ảo có thể chiếm nhiều dung lượng lưu trữ Chúng có thể nhanh chóng tăng kích thước lên vài gigabyte Điều này có thể dẫn đến việc thiếu dung lượng ổ đĩa trên máy chủ máy ảo
Các nhà cung cấp máy ảo phổ biến:
Lựa chọn nào là tốt hơn ?
Nếu bạn có một yêu cầu cụ thể về phần cứng hoặc bạn đang phát triển ứng dụng trên một nền tảng phần cứng và mục tiêu của bạn là nhắm tới một nền tảng khác ví dụ như Windows & MacOS, bạn cần phải sử dụng máy ảo Hầu hết các yêu cầu “software only“ đều có thể được đáp ứng khi sử dụng container.
Deploy patterns
Một cách triển khai các dịch vụ microservices của bạn là sử dụng Multiple Service Instances per Host pattern Khi sử dụng mẫu này, bạn cấp phát một hoặc nhiều máy chủ vật lý hoặc ảo và chạy nhiều service trên mỗi máy chủ Đây là phương pháp triển khai truyền thống của ứng dụng Mỗi instance chạy trên một cổng xác định trên một hoặc nhiều máy chủ
Có 2 host và mỗi host chạy một instance của 3 service A, B và C
Có 2 biến thể của pattern này
• Mỗi service instance là một process độc lập trên host đó VD: mỗi service sẽ chạy trên JVM riêng của nó,
• Các service instance chạy đồng thời trên cùng một process VD: Tomcat server chạy nhiều web application
Các ưu điểm của pattern này:
• Tận dụng tài nguyên hiệu quả hơn so với Once Instance per Host.pattern
• Tồn tại nguy cơ xung đột yêu cầu tài nguyên giữa các instance
• Nguy cơ xung đột các phụ thuộc (dependency) với nhau
• Khó khăn trong việc giới hạn tài nguyên được sử dụng bởi mỗi instance
• Nếu nhiều instance được triển khai trong cùng một process, việc giám sát việc sử dụng tài nguyên của mỗi instance trở nên khó khăn Đồng thời, việc cô lập từng instance cũng không thực hiện được
Một cách khác để triển khai microservices của là mẫu Once instance per host pattern Khi sử dụng pattern này, bạn chạy mỗi instance độc lập trên một máy chủ riêng Có hai cách triển khai khác nhau của pattern này: Service Instance per Virtual Machine và Service Instance per Container
9.2.2.1 Service Instance per Virtual Machine
Khi bạn sử dụng Service Instance per Virtual Machine pattern, bạn đóng gói mỗi service thành virtual machine (VM) image như Amazon EC2 AMI Mỗi service instance là một VM (ví dụ như một EC2 instance) được khởi chạy bằng cách sử dụng VM image Sơ đồ dưới đây mô tả cấu trúc của pattern này: Đây là phương pháp chính được Netflix sử dụng để triển khai dịch vụ xem phim trực tuyến của mình Netflix đóng gói mỗi service của mình thành EC2 AMI bằng cách sử dụng công cụ Aminator Mỗi service instance đang chạy là một EC2 instance
Có nhiều công cụ khác nhau mà bạn có thể sử dụng để xây dựng máy ảo của riêng bạn Bạn có thể cấu hình máy chủ tích hợp liên tục (CI server) (ví dụ: Jenkins) dùng Aminator để đóng gói dịch vụ của bạn thành EC2 AMI Packer.io là một lựa chọn khác để tạo ra VM image tự động Khác với Aminator, Pakcer.io hỗ trợ nhiều công nghệ ảo hóa khác nhau bao gồm EC2,
DigitalOcean, VirtualBox và VMware Ưu điểm:
• Việc mở rộng service bằng cách tăng số lượng instance là dễ dàng Amazon
Autoscaling Groups có thể tự động thực hiện điều này dựa trên tải của service
• Máy ảo đóng gói chi tiết về công nghệ được sử dụng để xây dựng service Tất cả các service được khởi động và dừng lại theo cách hoàn toàn giống nhau
• Mỗi phiên bản dịch vụ được cô lập riêng biệt
• Máy ảo áp đặt giới hạn về CPU và bộ nhớ mà mỗi instance sử dụng
• Các giải pháp IaaS như AWS cung cấp một cơ sở hạ tầng ổn định và đa chức năng để triển khai và quản lý máy ảo Ví dụ: Elastic Load Balancer, Autoscaling groups Nhược điểm:
• Xây dựng VM image chậm và tốn thời gian
Khi sử dụng mẫu Service Instance per Container, mỗi phiên bản dịch vụ chạy trong một container riêng Container là một cơ chế ảo hóa ở cấp hệ điều hành Một container bao gồm riêng và hệ thống tệp riêng Bạn có thể giới hạn tài nguyên bộ nhớ và CPU của một container Một số công cụ triển khai container cũng hỗ trợ giới hạn I/O Các công nghệ container ví dụ như Docker và Solaris Zones Để sử dụng pattern này, bạn đóng gói service của mình thành một container image Container image là một hệ thống tệp bao gồm các ứng dụng và thư viện cần thiết để chạy service Một số container image bao gồm toàn bộ hệ thống tệp gốc của Linux Ví dụ, để triển khai một service Java, bạn xây dựng một image chứa môi trường chạy Java, có thể là máy chủ Apache Tomcat và ứng dụng Java đã biên dịch của bạn
Sau khi đóng gói service của bạn thành container image, bạn khởi chạy một hoặc nhiều container Thông thường, bạn chạy nhiều container trên mỗi máy chủ vật lý hoặc ảo Bạn có thể sử dụng một trình quản lý cluster như Kubernetes hoặc Marathon để quản lý các container của bạn Trình quản lý cluster xem các máy chủ như một nguồn tài nguyên và quyết định đặt mỗi container ở đâu dựa trên tài nguyên yêu cầu của container và tài nguyên có sẵn trên mỗi máy chủ Ưu điểm:
• Dễ dàng mở rộng và thu nhỏ service bằng cách thay đổi số lượng container
• Container đóng gói chi tiết về công nghệ được sử dụng để xây dựng service Tất cả các service đều được khởi động và dừng lại theo cách hoàn toàn giống nhau
• Mỗi service instance được cô lập riêng biệt
• Một container áp đặt giới hạn về CPU và bộ nhớ mà mỗi service instance sử dụng
• Containers rất nhanh chóng để xây dựng và khởi động Ví dụ, đóng gói một ứng dụng thành một container Docker nhanh gấp 100 lần so với đóng gói thành một AMI
Containers Docker cũng khởi động nhanh hơn rất nhiều so với một máy ảo vì chỉ có quá trình ứng dụng được khởi động, không phải toàn bộ hệ điều hành
• Cơ sở hạ tầng để triển khai container không phong phú như cơ sở hạ tầng để triển khai máy ảo
Sử dụng một cơ sở hạ tầng triển khai ẩn đi bất kỳ khái niệm về máy chủ (ví dụ như tài nguyên được dành trước hoặc được cấp phát trước) - máy chủ vật lý hoặc ảo, hoặc các container Cơ sở hạ tầng này lấy mã nguồn service của bạn và chạy nó Bạn sẽ bị tính phí cho mỗi yêu cầu dựa trên tài nguyên tiêu thụ Để triển khai dịch vụ của bạn bằng cách sử dụng phương pháp này, bạn đóng gói mã nguồn (ví dụ như một tập tin ZIP), tải lên cơ sở hạ tầng triển khai và mô tả các đặc điểm hiệu suất mong muốn
Cơ sở hạ tầng triển khai là một tiện ích do nhà cung cấp đám mây vận hành Thông thường, nó sử dụng container hoặc máy ảo để cô lập các service Tuy nhiên, các chi tiết này được ẩn đi Bạn cũng như bất kỳ ai khác trong tổ chức của bạn không cần phải quản lý bất kỳ cơ sở hạ tầng cấp thấp nào như hệ điều hành, máy ảo, v.v
Có một số môi trường triển khai serverless khác nhau:
Chúng cung cấp các chức năng tương tự như nhau, nhưng AWS Lambda có bộ tính năng phong phú nhất AWS Lambda function là một thành phần không có trạng thái được gọi để xử lý các sự kiện Để tạo một AWS Lambda function, bạn đóng gói mã code NodeJS, Java hoặc Python cho service của bạn vào một tập tin ZIP và tải lên AWS Lambda Bạn cũng chỉ định tên của hàm xử lý sự kiện cũng như các giới hạn tài nguyên
Docker và Docker Swarm
Docker là một nền tảng mã nguồn mở cho phép xây dựng, vận chuyển và chạy các ứng dụng trong các container Docker cho phép tách các ứng dụng khỏi cơ sở hạ tầng để có thể cung cấp phần mềm nhanh chóng Với Docker, ta có thể quản lý cơ sở hạ tầng của mình theo cách giống như ta quản lý các ứng dụng của mình
Một số tính năng chính của Docker bao gồm:
• Mã nguồn mở: Docker cho phép cộng đồng đóng góp và cải tiến nó
• Đóng gói ứng dụng: Docker cho phép đóng gói và vận chuyển các ứng dụng của mình một cách đơn giản, nhẹ và nhất quán
• Vòng đời phát triển nhanh và hiệu quả: Docker giúp rút ngắn thời gian phát triển bằng cách cho phép các lập trình viên làm việc trong các môi trường giống nhau sử dụng các local container cung cấp các ứng dụng và dependency cần thiết
• Phân tách trách nhiệm: Docker cho phép phân tách trách nhiệm giữa các nhóm khác nhau
• Bảo mật: Docker cung cấp các tính năng bảo mật để bảo vệ các ứng dụng và dữ liệu của
• Khả năng mở rộng: Docker cho phép mở rộng các ứng dụng của mình một cách linh hoạt thông qua việc cập nhật các image một cách nhanh chóng
Quá trình đóng gói một ứng dụng thành một image trong Docker sẽ tạo ra một image chứa tất cả các thành phần cần thiết để chạy ứng dụng đó, bao gồm mã nguồn, thư viện, biến môi trường và các tệp cấu hình Điều này cho phép triển khai ứng dụng của mình một cách dễ dàng và nhất quán trên bất kỳ máy chủ nào có Docker được cài đặt Để đóng gói một ứng dụng thành một image Docker, ta cần thực hiện các bước sau:
• Tạo một tệp Dockerfile trong thư mục chứa mã nguồn của ứng dụng Tệp Dockerfile này sẽ chứa các chỉ thị để xây dựng image Docker
• Trong tệp Dockerfile, chỉ định image mà ta muốn sử dụng làm base cho image của cần build Image base này có thể là một image hệ điều hành hoặc một image có sẵn từ kho lưu trữ Docker Hub (https://hub.docker.com/_/openjdk )
• Sao chép mã nguồn và các tệp cấu hình của ứng dụng vào image
• Cài đặt các thư viện cần thiết cho ứng dụng
• Định nghĩa biến môi trường và các thông số khác cho ứng dụng
• Khai báo lệnh để chạy ứng dụng khi container được khởi chạy từ image
Docker Swarm là công cụ tạo ra một clustering Docker Cho phép ta có thể kết nối các docker host với nhau tạo thành một cụm các máy, khi tạo được hệ thống Docker Swarm thì chúng ta có thể quản lý và chạy các dịch vụ trên hệ thống này một cách dẽ dàng Giả sử ta có các hệ thống docker chạy trên các vps khác nhau thì ta có kể kết nối chúng tạo thành một cụm docker
Chúng ta cần dùng Docker Swarm khi project của bạn cần phát triển, quản lý, deploy trên nhiều nhiều host thì đó là lúc cần dùng đến Docker Swarm
• Cluster management integrated with Docker Engine : Sử dụng bộ Docker Engine CLI để tạo swarm một cách dễ dàng
• Decentralized design: Docker Swarm được thiết kế dạng phân cấp Thay vì xử lý sự khác biệt giữa các roles của node tại thời điểm triển khai, Docker xử lý bất kỳ tác vụ nào khi runtime Ta có thể triển khai node managers và worker bằng Docker Engine
• Declarative service model: Docker Engine sử dụng phương thức khai báo để cho phép bạn define trạng thái mong muốn của các dịch vụ khác nhau trong stack ứng dụng của bạn
• Scaling: với mỗi service có thể khai báo số lượng task mà ta muốn chạy, Scale up, down replicas của 1 service một cách dễ dàng
• Desired state reconciliation: Swarm đảm bảo 1 service hoạt động ổn định bằng cách tự động thay 1 replicas crash bằng 1 replicas mới cho các worker đang run
• Multi-host networking: Swarm manager có thể tự động gán IP cho mỗi service khi nó khởi tạo và cập nhật application
• Service discovery: Swarm manager node gán mỗi service trong swarm một DNS server riêng Do đó bạn có thể truy xuất thông qua DNS này
• Load balancing: Có thể expose các port cho các services tới load balance tích hợp cân bằng tải sử dujgn thuật toán thuật toán Round-robin
• Secure by default: Các service giao tiếp với nhau sử dụng giao thức bảo mật TLS
• Rolling updates: ASwarm giúp update image của service một cách hoàn toàn tự động Swarm manager giúp bạn kiểm soát độ trễ giữa service deploy tới các node khác nhau và bạn có thể rolling back bất cứ lúc nào
Kiến trúc của Docker Swarm
Kiến trúc Swarm bao gồm một tập hợp các node có ít nhất một nút chính (Manager-Leader) và một số node worker có thể là máy ảo hoặc vật lý
• Swarm: là một cluster của một node trong chế độ Swarm, thay vì phải chạy các container bằng câu lệnh thì ta sẽ thiết lập các services để phân bổ các bản replicas tới các node
• Manager Node: Là node nhận các define service từ user, quản lý theo dõi các service và tác vụ đang chạy trong Swarm, điều phối và chỉ định các node worker làm việc
• Worker Node: là một máy vật lý hay máy ảo chạy các tác vụ được chỉ định bới node manager
• Task: là các Docker containers thực thi các lệnh bạn đã định nghĩa trong service Tác vụ này sẽ do node Manager phân bổ xuống, và sau khi việc phân bổ này task không thể chuyển sang một worker khác Nếu task thất bại, node Manager sẽ chỉ định một phiên bản mới của tác vụ đó cho một node có sẵn khác trong Swarm
• Service: Một service xác định image của container và số lượng các replicas (bản sao) mong muốn khởi chạy trong Swarm
So sánh Kubernetes vs Docker Swarm
Tính năng Kubernetes Docker Swarm
Cài đặt Phức tạp Đơn giản
Cân bằng tải Không tự động Tự động
Khả năng mở rộng Hoạt động chậm Hoạt động nhanh
Cụm Khó thiết lập Dễ dàng thiết lập
Thiết lập container Triển khai thông qua hình thức thiết lập lại Triển khai trực tiếp Ghi nhật ký và giám sát Công cụ tích hợp để quản lý cả hai quy trình Các công cụ không cần thiết để ghi nhật ký và giám sát
Khối lượng dữ liệu Được chia sẻ với các container từ cùng một nhóm Có thể được chia sẻ với mọi container
Kubernetes và Helm
Kubernetes là một nền tảng nguồn mở, khả chuyển, có thể mở rộng để quản lý các ứng dụng được đóng gói và các service, giúp thuận lợi trong việc cấu hình và tự động hoá việc triển khai ứng dụng Kubernetes là một hệ sinh thái lớn và phát triển nhanh chóng Các dịch vụ, sự hỗ trợ và công cụ có sẵn rộng rãi
Tên gọi Kubernetes có nguồn gốc từ tiếng Hy Lạp, có ý nghĩa là người lái tàu hoặc hoa tiêu Google mã nguồn mở Kubernetes từ năm 2014 Kubernetes xây dựng dựa trên một thập kỷ rưỡi kinh nghiệm mà Google có được với việc vận hành một khối lượng lớn workload trong thực tế, kết hợp với các ý tưởng và thực tiễn tốt nhất từ cộng đồng
Chúng ta hãy xem tại sao Kubernetes rất hữu ích bằng cách quay ngược thời gian
Thời đại triển khai theo cách truyền thống: Ban đầu, các ứng dụng được chạy trên các máy chủ vật lý Không có cách nào để xác định ranh giới tài nguyên cho các ứng dụng trong máy chủ vật lý và điều này gây ra sự cố phân bổ tài nguyên Ví dụ, nếu nhiều ứng dụng cùng chạy trên một máy chủ vật lý, có thể có những trường hợp một ứng dụng sẽ chiếm phần lớn tài nguyên hơn và kết quả là các ứng dụng khác sẽ hoạt động kém đi Một giải pháp cho điều này sẽ là chạy từng ứng dụng trên một máy chủ vật lý khác nhau Nhưng giải pháp này không tối ưu vì tài nguyên không được sử dụng đúng mức và rất tốn kém cho các tổ chức để có thể duy trì nhiều máy chủ vật lý như vậy
Thời đại triển khai ảo hóa: Như một giải pháp, ảo hóa đã được giới thiệu Nó cho phép bạn chạy nhiều Máy ảo (VM) trên CPU của một máy chủ vật lý Ảo hóa cho phép các ứng dụng được cô lập giữa các VM và cung cấp mức độ bảo mật vì thông tin của một ứng dụng không thể được truy cập tự do bởi một ứng dụng khác Ảo hóa cho phép sử dụng tốt hơn các tài nguyên trong một máy chủ vật lý và cho phép khả năng mở rộng tốt hơn vì một ứng dụng có thể được thêm hoặc cập nhật dễ dàng, giảm chi phí phần cứng và hơn thế nữa Với ảo hóa, bạn có thể có một tập hợp các tài nguyên vật lý dưới dạng một cụm các máy ảo sẵn dùng
Mỗi VM là một máy tính chạy tất cả các thành phần, bao gồm cả hệ điều hành riêng của nó, bên trên phần cứng được ảo hóa
Thời đại triển khai Container: Các container tương tự như VM, nhưng chúng có tính cô lập để chia sẻ Hệ điều hành (HĐH) giữa các ứng dụng Do đó, container được coi là nhẹ
(lightweight) Tương tự như VM, một container có hệ thống tệp (filesystem), CPU, bộ nhớ, process space, v.v Khi chúng được tách rời khỏi cơ sở hạ tầng bên dưới, chúng có thể khả chuyển (portable) trên cloud hoặc các bản phân phối Hệ điều hành
Các container đã trở nên phổ biến vì chúng có thêm nhiều lợi ích, chẳng hạn như:
• Tạo mới và triển khai ứng dụng Agile: gia tăng tính dễ dàng và hiệu quả của việc tạo các container image so với việc sử dụng VM image
• Phát triển, tích hợp và triển khai liên tục: cung cấp khả năng build và triển khai container image thường xuyên và đáng tin cậy với việc rollbacks dễ dàng, nhanh chóng
• Phân biệt giữa Dev và Ops: tạo các images của các application container tại thời điểm build/release thay vì thời gian triển khai, do đó phân tách các ứng dụng khỏi hạ tầng
• Khả năng quan sát không chỉ hiển thị thông tin và các metric ở mức Hệ điều hành, mà còn cả application health và các tín hiệu khác
• Tính nhất quán về môi trường trong suốt quá trình phát triển, testing và trong production: Chạy tương tự trên laptop như trên cloud
• Tính khả chuyển trên cloud và các bản phân phối HĐH: Chạy trên Ubuntu, RHEL, CoreOS, on-premises, Google Kubernetes Engine và bất kì nơi nào khác
• Quản lý tập trung ứng dụng: Tăng mức độ trừu tượng từ việc chạy một Hệ điều hành trên phần cứng ảo hóa sang chạy một ứng dụng trên một HĐH bằng logical resources
• Các micro-services phân tán, elastic: ứng dụng được phân tách thành các phần nhỏ hơn, độc lập và thể được triển khai và quản lý một cách linh hoạt - chứ không phải một app nguyên khối (monolithic)
• Cô lập các tài nguyên: dự đoán hiệu năng ứng dụng
• Sử dụng tài nguyên: hiệu quả
Kubernetes cung cấp cho bạn một framework để chạy các hệ thoosng phân tán một cách mạnh mẽ Nó đảm nhiệm việc nhân rộng và chuyển đổi dự phòng cho ứng dụng của bạn, cung cấp các mẫu deployment và hơn thế nữa Ví dụ, Kubernetes có thể dễ dàng quản lý một triển khai canary cho hệ thống của bạn
Kubernetes cung cấp cho bạn:
Service discovery và cân bằng tải
Kubernetes có thể expose một container sử dụng DNS hoặc địa chỉ IP của riêng nó Nếu lượng traffic truy cập đến một container cao, Kubernetes có thể cân bằng tải và phân phối lưu lượng mạng (network traffic) để việc triển khai được ổn định Điều phối bộ nhớ
Kubernetes cho phép bạn tự động mount một hệ thống lưu trữ mà bạn chọn, như local storages, public cloud providers, v.v
Tự động rollouts và rollbacks
Bạn có thể mô tả trạng thái mong muốn cho các container được triển khai dùng Kubernetes và nó có thể thay đổi trạng thái thực tế sang trạng thái mong muốn với tần suất được kiểm soát
Ví dụ, bạn có thể tự động hoá Kubernetes để tạo mới các container cho việc triển khai của bạn, xoá các container hiện có và áp dụng tất cả các resource của chúng vào container mới Đóng gói tự động
Bạn cung cấp cho Kubernetes một cluster gồm các node mà nó có thể sử dụng để chạy các tác vụ được đóng gói (containerized task) Bạn cho Kubernetes biết mỗi container cần bao nhiêu CPU và bộ nhớ (RAM) Kubernetes có thể điều phối các container đến các node để tận dụng tốt nhất các resource của bạn
Consul
Consul được tạo ra để cải thiện việc bảo mật và deploy distributed system Consul sẽ abstract đi các cluster và gộp chung lại thành một service mesh và cung cấp các tính năng như:
Giải quyết bài toán khó mà K8S gặp phải là multi-cluster communication, sử dụng Service mesh và Mesh gateway Có thể được deploy lên K8S mà không cần phải thay đổi code của ứng dụng Sử dụng sidecar injection để áp dụng TLS mà không cần phải thay đổi code.
Khi được deploy, consul sẽ inject vào các Pods của K8S hai container, một là consul agent và hai là envoy proxy:
• Consul agent sẽ đảm bảo việc register service với service registry cho service discovery và là gateway để các container có thể sử dụng các dịch vụ mà consul cung cấp
• Envoy proxy sẽ được inject vào chung với agent để khi một muốn giao tiếp với một container nằm ở pod khác thì sẽ thông qua proxy Khi này ta có thể encrypt message mà bản thân ứng dụng trong container không cần phải biết gì cả Proxy có thể hoạt động trong chế độ transparent, khi này ứng dụng hoàn toàn không hề biết về sự tồn tại của proxy nhưng mọi request đều sẽ được redirect sử dụng IP table Để điều khiển các agent thì consul có một thành phần được deploy vào trong K8S là consul servers, sẽ chỉ có một leader và nhiều follower Consul server là bộ não của toàn hệ thống.
Prometheus + Grafana
Việc monitor các thành phần của hệ thống quan trọng, nhất là khi có vấn đề xảy ra, thay vì phải manually đi tìm ra nguồn gốc của lỗi thì ta deploy một hệ thống để lấy các thông số của hệ thống rồi tổng hợp nó lại
• Pull metrics: Monitor tools sẽ chủ động lấy metrics từ các máy chủ, các máy chủ cần phải được cài đặt một agent để có thể thu thập số liệu Vd: Prometheus
• Push metrics: Các máy chủ sẽ chủ động gửi số liệu đến Monitor tools theo định kỳ Có thể dẫn đến high traffic trong local network Vd: influxDB
Prometheus là một hệ thống giám sát mã nguồn mở được phát triển ban đầu bởi SoundCloud và sau đó được chuyển giao cho Cloud Native Computing Foundation (CNCF) Nó được thiết kế để thu thập, lưu trữ và truy xuất dữ liệu về các chỉ số hoạt động và hiệu suất của các hệ thống và ứng dụng
Prometheus sử dụng mô hình pull-based, trong đó các máy chủ giám sát (prometheus server) định kỳ truy vấn các endpoints (exporters) trên các ứng dụng, máy chủ, hoặc hệ thống để thu thập dữ liệu về các chỉ số như CPU sử dụng, tài nguyên hệ thống, thời gian phản hồi, và nhiều hơn nữa Dữ liệu này được lưu trữ trong cơ sở dữ liệu của Prometheus và có thể được truy xuất thông qua giao diện truy vấn linh hoạt
Prometheus cung cấp một ngôn ngữ truy vấn mạnh mẽ (PromQL) cho phép người dùng tạo các biểu thức truy vấn phức tạp để phân tích và trực quan hóa dữ liệu giám sát Nó cũng cung cấp giao diện đồ họa (Prometheus Graph) để hiển thị các biểu đồ và đồ thị thời gian
Ngoài ra, Prometheus tích hợp tốt với cộng đồng Kubernetes và có thể được triển khai như một giải pháp giám sát cho cụm Kubernetes Nó cung cấp tích hợp sẵn với các thành phần khác của hệ sinh thái Cloud Native như Grafana (công cụ trực quan hóa dữ liệu) và
Alertmanager (quản lý cảnh báo)
Prometheus đã trở thành một công cụ giám sát phổ biến trong cộng đồng DevOps và được sử dụng rộng rãi để giám sát các hệ thống và ứng dụng hiện đại
Grafana là một công cụ trực quan hóa dữ liệu mã nguồn mở và phổ biến được sử dụng để hiển thị và giám sát các dữ liệu thống kê và hiệu suất từ nhiều nguồn khác nhau Với giao diện người dùng đơn giản và linh hoạt, Grafana cho phép người dùng tạo biểu đồ, đồ thị, bảng điều khiển tùy chỉnh và báo cáo để theo dõi, phân tích và hiểu rõ hơn về dữ liệu giám sát
Grafana hỗ trợ nhiều nguồn dữ liệu, bao gồm các cơ sở dữ liệu thống kê như Prometheus, InfluxDB, Graphite, Elasticsearch, cũng như các dịch vụ đám mây như Amazon CloudWatch, Microsoft Azure Monitor, Google Cloud Monitoring, và nhiều nguồn dữ liệu khác Người dùng có thể kết hợp các nguồn dữ liệu này để xây dựng bảng điều khiển đa nguồn dữ liệu và theo dõi các chỉ số quan trọng từ các nguồn khác nhau trong một nơi duy nhất
Grafana cung cấp nhiều tùy chọn trực quan hóa, bao gồm biểu đồ đường, biểu đồ cột, biểu đồ hình tròn, đồ thị thời gian, bản đồ địa lý và nhiều hơn nữa Người dùng có thể tùy chỉnh giao diện và trình bày bằng cách sử dụng các biểu thức, hàm tính toán và các tùy chọn định dạng để tạo ra các biểu đồ và bảng điều khiển đẹp và dễ hiểu
Grafana cũng hỗ trợ chức năng báo cáo và cảnh báo, cho phép người dùng tạo các báo cáo tự động và cấu hình các cảnh báo dựa trên ngưỡng và quy tắc nhất định Khi các cảnh báo được kích hoạt, Grafana có thể gửi thông báo qua email, Slack, PagerDuty và các kênh khác để cung cấp thông báo kịp thời về các vấn đề trong hệ thống giám sát
Grafana đã trở thành một công cụ quan trọng trong cộng đồng DevOps và được sử dụng rộng rãi để trực quan hóa và hiển thị dữ liệu giám sát từ các nguồn khác nhau, giúp người dùng hiểu rõ hơn về hiệu suất và hoạt động của hệ thống và ứng dụng của mình.
Elasticsearch+ Logstash + Kibana
ELK stack là một bộ công cụ mã nguồn mở phổ biến được sử dụng cho việc xử lý và phân tích log trên hệ thống ELK là viết tắt của Elasticsearch, Logstash và Kibana, các công cụ chính trong bộ stack này
• Elasticsearch: Elasticsearch là một hệ thống tìm kiếm và phân tích dữ liệu phân tán
Nó được sử dụng để lưu trữ và tìm kiếm các log và dữ liệu có cấu trúc khác
Elasticsearch cung cấp khả năng tìm kiếm nhanh chóng và phân tích dữ liệu theo thời gian thực, cho phép người dùng truy vấn và tìm kiếm log dễ dàng
• Logstash: Logstash là một công cụ xử lý và chuyển đổi log Nó có khả năng thu thập, xử lý và chuyển đổi log từ nhiều nguồn khác nhau Logstash có thể tiếp nhận log từ các nguồn như máy chủ, ứng dụng, thiết bị mạng và các công cụ giám sát khác Sau đó, nó xử lý và chuẩn hóa log trước khi gửi chúng đến Elasticsearch để lưu trữ và tìm kiếm
• Kibana: Kibana là một công cụ trực quan hóa dữ liệu và truy vấn cho Elasticsearch Nó cung cấp giao diện người dùng đồ họa để truy vấn, phân tích và hiển thị dữ liệu log Kibana cho phép người dùng tạo biểu đồ, đồ thị, bảng điều khiển tùy chỉnh và các trực quan hóa khác để hiển thị thông tin từ log Nó cũng hỗ trợ tìm kiếm và lọc dữ liệu log theo nhiều tiêu chí khác nhau
Elasticsearch là một hệ thống tìm kiếm và phân tích dữ liệu phân tán mã nguồn mở Nó được xây dựng dựa trên nền tảng Apache Lucene, một thư viện tìm kiếm thông tin mạnh mẽ
Elasticsearch được thiết kế để xử lý và tìm kiếm dữ liệu có cấu trúc và không có cấu trúc từ nhiều nguồn khác nhau với tốc độ cao và khả năng mở rộng
Dưới đây là một số đặc điểm và khả năng của Elasticsearch:
• Phân tán và khả năng mở rộng: Elasticsearch được thiết kế để hoạt động trong môi trường phân tán, cho phép nó chia nhỏ và phân phối dữ liệu trên nhiều nút trong một cụm Điều này giúp cải thiện hiệu suất và khả năng mở rộng của hệ thống khi dữ liệu tăng lên
• Tìm kiếm và truy vấn mạnh mẽ: Elasticsearch cung cấp một ngôn ngữ truy vấn linh hoạt và mạnh mẽ để tìm kiếm và truy xuất dữ liệu Nó hỗ trợ các truy vấn phức tạp như tìm kiếm văn bản đầy đủ, tìm kiếm phù hợp (matching), tìm kiếm dựa trên điều kiện, tìm kiếm địa lý và nhiều hơn nữa
• Xử lý và phân tích dữ liệu thời gian thực: Elasticsearch được tối ưu để xử lý và tìm kiếm các dữ liệu thời gian thực, như log hệ thống, dữ liệu giao dịch và thông tin theo thời gian Nó hỗ trợ lưu trữ và truy vấn dữ liệu theo thời gian, cho phép tìm kiếm và phân tích dữ liệu trong thời gian gần như thời gian thực
• Tích hợp với các công cụ khác: Elasticsearch có khả năng tích hợp với nhiều công cụ và ngôn ngữ lập trình khác nhau Nó cung cấp các API RESTful để tương tác với dữ liệu và cũng có thể tích hợp với các công cụ khác như Logstash (xử lý và chuyển đổi log), Kibana (trực quan hóa dữ liệu) và Beats (xử lý và gửi dữ liệu)
• Tính nhất quán và độ tin cậy cao: Elasticsearch sử dụng cơ chế nhất quán phân tán để đảm bảo dữ liệu được sao chép và phân tán đến các nút trong cụm Điều này giúp đảm bảo tính nhất quán và độ tin cậy cao cho dữ liệu lưu trữ
Elasticsearch đã trở thành một công cụ quan trọng trong việc tìm kiếm, phân tích dữ liệu và giám sát hệ thống Nó được sử dụng rộng rãi trong các ứng dụng web, ứng dụng di động, phân tích log, phân tích thời gian thực và nhiều lĩnh vực khác nữa
Logstash là một công cụ mã nguồn mở thuộc Elastic Stack, được sử dụng để thu thập, xử lý và chuyển đổi log và dữ liệu từ nhiều nguồn khác nhau Nó giúp quản lý dữ liệu log một cách dễ dàng và hiệu quả, và cung cấp khả năng chuẩn hóa và phân tích log trước khi lưu trữ và tìm kiếm bằng Elasticsearch
Dưới đây là một số đặc điểm và chức năng của Logstash:
• Thu thập dữ liệu đa nguồn: Logstash hỗ trợ thu thập dữ liệu từ nhiều nguồn khác nhau như log hệ thống, cơ sở dữ liệu, máy chủ, thiết bị mạng, và nhiều nguồn dữ liệu khác
Nó cung cấp các plugin đa dạng để kết nối và thu thập dữ liệu từ các nguồn này
• Xử lý và chuyển đổi log: Logstash cho phép xử lý và chuyển đổi log từ các nguồn thu thập Bằng cách sử dụng các bộ lọc (filters), bạn có thể chuẩn hóa, phân tích cú pháp, chuyển đổi định dạng và thực hiện các thao tác khác trên log trước khi gửi chúng đến Elasticsearch hoặc các đích khác
Xây dựng hệ thống với Spring Boot
Spring Boot Starter
Spring Boot Starter là một tính năng quan trọng của Spring Boot, giúp đơn giản hóa việc cấu hình và triển khai ứng dụng Spring Nó cung cấp các dependency tự động để tích hợp các module và framework khác vào ứng dụng Spring Boot
Cách cài đặt: Để sử dụng Spring Boot Starter, bạn có thể thêm dependency tương ứng vào file pom.xml (cho Maven) hoặc build.gradle (cho Gradle) của dự án Spring Boot cung cấp nhiều Starter khác nhau tùy thuộc vào nhu cầu sử dụng của bạn Ví dụ, để sử dụng Starter cho Spring Web, thêm đoạn mã sau vào file cấu hình Maven:
org.springframework.boot
spring-boot-starter-web
Cách sử dụng cơ bản:
Sau khi cài đặt, bạn có thể sử dụng các dependency của Spring Boot Starter trong ứng dụng của mình Spring Boot Starter giúp tự động cấu hình và tích hợp các module và framework liên quan, giúp bạn tập trung vào việc phát triển ứng dụng mà không cần lo lắng về việc cấu hình chi tiết
Ví dụ, nếu bạn đã thêm Spring Boot Starter cho Spring Web, bạn có thể bắt đầu viết các
RESTful API bằng cách tạo các Controller và các phương thức xử lý yêu cầu HTTP Dưới đây là một ví dụ đơn giản:
@GetMapping("/hello") public String hello() { return "Hello, World!";
} một Controller và @GetMapping để xác định phương thức xử lý yêu cầu GET tới đường dẫn
"/hello" Khi ứng dụng chạy, bạn có thể truy cập đường dẫn http://localhost:8080/hello để nhận được chuỗi "Hello, World!"
Với Spring Boot Starter, bạn cũng có thể sử dụng các dependency khác như Spring Data JPA cho truy vấn cơ sở dữ liệu, Spring Security cho bảo mật ứng dụng, Spring Cloud cho phát triển ứng dụng phân tán, và nhiều hơn nữa Tùy thuộc vào nhu cầu của dự án, bạn có thể thêm các Starter tương ứng để tận dụng các tính năng mà Spring Boot cung cấp một cách dễ dàng và nhanh chóng.
Spring Data JPA
Spring Data JPA là một phần mở rộng của Spring Framework giúp đơn giản hóa việc truy cập và tương tác với cơ sở dữ liệu quan hệ thông qua JPA (Java Persistence API) Nó cung cấp một cách tiếp cận linh hoạt và mạnh mẽ để làm việc với cơ sở dữ liệu trong ứng dụng Spring
Cách cài đặt: Để sử dụng Spring Data JPA, bạn cần thêm dependency tương ứng vào file pom.xml (cho Maven) hoặc build.gradle (cho Gradle) của dự án Dưới đây là ví dụ cách cài đặt Spring Data JPA thông qua Maven:
org.springframework.boot
spring-boot-starter-data-jpa
Cách sử dụng cơ bản:
Sau khi cài đặt, bạn có thể sử dụng các tính năng của Spring Data JPA để thao tác với cơ sở dữ liệu quan hệ một cách dễ dàng và hiệu quả Dưới đây là một ví dụ về cách sử dụng Spring Data JPA trong ứng dụng Spring Boot: Định nghĩa Entity (đối tượng): Đầu tiên, bạn cần định nghĩa một Entity (đối tượng) để ánh xạ với bảng trong cơ sở dữ liệu Ví dụ, chúng ta có một Entity User đơn giản như sau:
@GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email;
Tiếp theo, bạn cần tạo một Repository để thực hiện các thao tác truy vấn với cơ sở dữ liệu Repository sẽ tự động cung cấp các phương thức CRUD (Create, Read, Update, Delete) cơ bản Ví dụ, chúng ta tạo một UserRepository như sau:
@Repository public interface UserRepository extends JpaRepository {
Sử dụng Repository trong Service hoặc Controller:
Sau khi tạo Repository, bạn có thể sử dụng nó trong các Service hoặc Controller để thực hiện các thao tác với cơ sở dữ liệu Ví dụ, chúng ta tạo một UserService để quản lý người dùng:
@Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository;
} public User saveUser(User user) { return userRepository.save(user);
} public List getAllUsers() { return userRepository.findAll();
Spring cache
Spring Cache là một tính năng của Spring Framework cho phép bạn lưu trữ kết quả của các phương thức trong bộ nhớ tạm thời (cache) Khi cùng một phương thức được gọi với cùng một đầu vào, Spring sẽ trả về kết quả từ cache mà không cần thực hiện lại phương thức, giúp cải thiện hiệu suất của ứng dụng
Cách cài đặt: Để sử dụng Spring Cache, bạn cần thêm dependency tương ứng vào file pom.xml (cho Maven) hoặc build.gradle (cho Gradle) của dự án Dưới đây là ví dụ cách cài đặt Spring Cache thông qua Maven:
org.springframework.boot
spring-boot-starter-cache
Cách sử dụng cơ bản:
Sau khi cài đặt, bạn có thể sử dụng các annotation của Spring Cache để đánh dấu các phương thức cần được cache
Bật tính năng cache: Đầu tiên, bạn cần bật tính năng cache trong ứng dụng Spring Boot bằng cách thêm
@EnableCaching vào một class cấu hình:
} Đánh dấu phương thức cần được cache:
Tiếp theo, bạn có thể đánh dấu các phương thức cần được cache bằng cách sử dụng các annotation @Cacheable, @CachePut, @CacheEvict, và @Caching Dưới đây là ví dụ về cách sử dụng @Cacheable:
@Cacheable("products") public Product getProductById(Long id) {
// Lấy sản phẩm từ cơ sở dữ liệu
Trong ví dụ trên, phương thức getProductById() sẽ được cache với tên "products" Khi phương thức này được gọi lần đầu tiên với một id, kết quả sẽ được lưu vào cache Khi phương thức được gọi lại với cùng một id, kết quả sẽ được trả về từ cache mà không cần thực hiện lại phương thức
Bạn cũng có thể xóa cache bằng cách sử dụng annotation @CacheEvict Ví dụ:
@CacheEvict("products") public void clearProductCache() {
Trong ví dụ trên, phương thức clearProductCache() sẽ xóa cache với tên "products" Khi phương thức này được gọi, cache "products" sẽ bị xóa và phương thức getProductById() sẽ được thực hiện lại khi được gọi tiếp theo
Tóm lại, Spring Cache là một tính năng mạnh mẽ giúp cải thiện hiệu suất của ứng dụng bằng cách lưu trữ kết quả của các phương thức trong cache Bằng cách sử dụng các annotation như
@Cacheable và @CacheEvict, bạn có thể dễ dàng áp dụng caching vào các phương thức của ứng dụng Spring.
Spring Cloud Stream
Spring Cloud Stream là một dự án trong hệ sinh thái Spring Cloud, cung cấp một cách tiếp cận dễ dàng và linh hoạt để xây dựng ứng dụng dựa trên kiến trúc hướng sự kiện (event-driven architecture) Nó giúp đơn giản hóa việc truyền tin nhắn giữa các thành phần của ứng dụng, ví dụ như xử lý sự kiện, gửi và nhận tin nhắn qua các message broker như Apache Kafka hoặc RabbitMQ
Cách cài đặt: Để sử dụng Spring Cloud Stream, bạn cần thêm dependency tương ứng vào file pom.xml (cho Maven) hoặc build.gradle (cho Gradle) của dự án Dưới đây là ví dụ cách cài đặt Spring Cloud Stream thông qua Maven
org.springframework.cloud
spring-cloud-starter-stream-{message-broker}
Trong đó, {message-broker} là tên của message broker mà bạn muốn sử dụng, ví dụ như kafka hoặc rabbit
Cách sử dụng cơ bản:
Sau khi cài đặt, bạn có thể sử dụng các annotation và interfaces của Spring Cloud Stream để xác định và xử lý các kênh (channels) và các tin nhắn Định nghĩa interface kênh: Đầu tiên, bạn cần định nghĩa các interface kênh để xác định cách gửi và nhận tin nhắn
Interface kênh có thể được chia thành hai loại: Source (đại diện cho việc gửi tin nhắn) và Sink (đại diện cho việc nhận tin nhắn) Ví dụ: public interface MySource { public interface MySink {
Trong ví dụ trên, MySource là một interface kênh để gửi tin nhắn, và MySink là một interface kênh để nhận tin nhắn Kênh gửi tin nhắn được gắn với tên "myOutput", và kênh nhận tin nhắn được gắn với tên "myInput"
Tiếp theo, bạn có thể sử dụng các annotation @StreamListener để xác định các phương thức xử lý tin nhắn trên các kênh Ví dụ:
@EnableBinding(MySink.class) public class MyMessageListener {
@StreamListener("myInput") public void handleMessage(String message) {
System.out.println("Received message: " + message);
Trong ví dụ trên, phương thức handleMessage() sẽ được gọi khi có tin nhắn được nhận trên kênh "myInput" Nội dung của tin nhắn được truyền vào phương thức như một tham số
Gửi tin nhắn: Để gửi tin nhắn, bạn có thể sử dụng MessageChannel để gửi tin nhắn thông qua kênh gửi tin nhắn Ví dụ:
@EnableBinding(MySource.class) public class MyMessageSender { private final MySource mySource; public MyMessageSender(MySource mySource) { this.mySource = mySource;
} public void sendMessage(String message) { mySource.output().send(MessageBuilder.withPayload(message).build());
Trong ví dụ trên, MyMessageSender sử dụng MySource để lấy kênh gửi tin nhắn và gửi tin nhắn thông qua phương thức sendMessage() Đó là một khái quát về cách sử dụng Spring Cloud Stream Spring Cloud Stream cung cấp rất nhiều tính năng khác nhau để xử lý tin nhắn, như truyền tin nhắn theo batch, xử lý lỗi, phân tán và nhiều hơn nữa Bạn có thể tìm hiểu thêm trong tài liệu chính thức của Spring Cloud
Stream để biết chi tiết về các tính năng và cách sử dụng chúng.
Spring Data Elasticsearch
Spring Data Elasticsearch là một phần của dự án Spring Data, cung cấp các tính năng để tương tác với Elasticsearch - một hệ thống tìm kiếm và phân tích phân tán mã nguồn mở Spring Data Elasticsearch giúp đơn giản hóa việc lưu trữ, truy vấn và tìm kiếm dữ liệu trong
Elasticsearch thông qua các repository và các API cung cấp sẵn
Cách cài đặt: Để sử dụng Spring Data Elasticsearch, bạn cần thêm dependency tương ứng vào file pom.xml (cho Maven) hoặc build.gradle (cho Gradle) của dự án Dưới đây là ví dụ cách cài đặt Spring Data Elasticsearch thông qua Maven:
org.springframework.boot
spring-boot-starter-data-elasticsearch
Trước khi sử dụng Spring Data Elasticsearch, bạn cần thiết lập cấu hình kết nối với
Elasticsearch Bạn có thể cung cấp các thông tin cấu hình trong tệp application.properties hoặc application.yml của ứng dụng Spring Boot Dưới đây là một ví dụ về cấu hình kết nối
Elasticsearch trong application.properties: spring.data.elasticsearch.cluster-nodes=localhost:9200
Cách sử dụng cơ bản:
Sau khi cài đặt và cấu hình, bạn có thể sử dụng Spring Data Elasticsearch để thực hiện các hoạt động lưu trữ, truy vấn và tìm kiếm dữ liệu trong Elasticsearch Định nghĩa một Elasticsearch entity: Đầu tiên, bạn cần định nghĩa một Elasticsearch entity, đại diện cho một tài liệu trong
Elasticsearch Entity này tương ứng với một chỉ mục và một loại dữ liệu trong Elasticsearch
@Document(indexName = "books", type = "book") public class Book {
@Id private String id; private String title; private String author;
Trong ví dụ trên, Book là một Elasticsearch entity đại diện cho các cuốn sách trong loại dữ liệu là "book"
Sau khi định nghĩa Elasticsearch entity, bạn có thể sử dụng Elasticsearch repository để thực hiện các hoạt động lưu trữ và truy vấn dữ liệu Ví dụ: public interface BookRepository extends ElasticsearchRepository {
List findByTitle(String title);
Trong ví dụ trên, BookRepository là một Elasticsearch repository cho entity Book Nó kế thừa từ ElasticsearchRepository và cung cấp các phương thức chức năng như lưu trữ, tìm kiếm và xóa dữ liệu Phương thức findByTitle() cho phép tìm kiếm các cuốn sách theo tiêu đề
Ngoài việc sử dụng repository, bạn cũng có thể sử dụng Elasticsearch operations để thực hiện các truy vấn Elasticsearch linh hoạt hơn Ví dụ:
@Autowired private ElasticsearchOperations elasticsearchOperations; public List searchBooks(String keyword) {
QueryBuilder query = QueryBuilders.matchQuery("title", keyword);
SearchHits hits = elasticsearchOperations.search(new NativeSearchQuery(query), Book.class); return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
Trong ví dụ trên, ElasticsearchOperations được autowired vào bean và sử dụng để thực hiện truy vấn Elasticsearchlinh hoạt hơn Phương thức searchBooks() sử dụng
ElasticsearchOperations để tạo và thực thi một truy vấn Elasticsearch linh hoạt và trả về danh sách các cuốn sách phù hợp với từ khóa tìm kiếm Đó là một khái quát về cách sử dụng Spring Data Elasticsearch Spring Data Elasticsearch cung cấp rất nhiều tính năng khác nhau để tương tác với Elasticsearch, bao gồm tìm kiếm theo từ khóa, tìm kiếm phức tạp, phân trang, sắp xếp và nhiều hơn nữa Bạn có thể tìm hiểu thêm trong tài liệu chính thức của Spring Data Elasticsearch để biết chi tiết về các tính năng và cách sử dụng chúng.
Spring Boot với docker
Spring Boot và Docker là hai công nghệ mạnh mẽ thường được sử dụng cùng nhau để xây dựng và triển khai ứng dụng Spring Boot giúp bạn phát triển ứng dụng Java nhanh chóng và dễ dàng, trong khi Docker cung cấp một nền tảng để đóng gói và chạy ứng dụng trong môi trường container độc lập
Dưới đây là các bước cơ bản để tích hợp Spring Boot với Docker:
Bước 1: Xây dựng ứng dụng Spring Boot
Hãy bắt đầu bằng việc xây dựng ứng dụng Spring Boot của bạn Bạn có thể sử dụng Spring
Initializr để tạo một dự án Spring Boot cơ bản hoặc sử dụng một dự án Spring Boot hiện có của bạn
Dockerfile là một tệp cấu hình để xây dựng hình ảnh Docker cho ứng dụng của bạn Tạo một tệp có tên Dockerfile trong thư mục gốc của dự án Spring Boot và thêm các chỉ thị cần thiết Dưới đây là một ví dụ cơ bản của Dockerfile:
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Trong ví dụ trên, chúng ta sử dụng hình ảnh openjdk:11-jdk-slim làm cơ sở và sao chép tệp JAR của ứng dụng vào trong hình ảnh ENTRYPOINT chỉ định lệnh để chạy ứng dụng khi container được khởi chạy
Bước 3: Xây dựng hình ảnh Docker Để xây dựng hình ảnh Docker, hãy mở terminal và điều hướng đến thư mục chứa Dockerfile
Sử dụng lệnh sau để xây dựng hình ảnh Docker: docker build -t
Trong đó, là tên bạn muốn đặt cho hình ảnh Docker Dấu chấm cuối cùng nói cho Docker biết rằng Dockerfile nằm trong thư mục hiện tại
Bước 4: Chạy ứng dụng trong container Docker
Sau khi xây dựng hình ảnh Docker, bạn có thể chạy ứng dụng trong một container Docker Sử dụng lệnh sau: docker run -p :
Trong đó, là cổng trên máy host mà bạn muốn liên kết với ứng dụng Spring Boot, và là cổng ứng dụng đang lắng nghe trong container là tên hình ảnh Docker bạn đã xây dựng trong bước trước
Sau khi chạy lệnh trên, ứng dụng Spring Boot của bạn sẽ chạy trong một container Docker độc lập.
Spring Security
Spring Security là một framework bảo mật mạnh mẽ cho ứng dụng Java Nó cung cấp các tính năng xác thực (authentication), phân quyền (authorization), quản lý phiên (session management), bảo vệ chống tấn công (protection against common attacks) và nhiều tính năng bảo mật khác Spring Security được tích hợp mạnh mẽ với các dự án Spring khác như Spring
Cách cài đặt: Để sử dụng Spring Security trong dự án của bạn, bạn cần thêm dependency của Spring
Security vào file pom.xml (nếu bạn đang sử dụng Maven) hoặc file build.gradle (nếu bạn đang sử dụng Gradle) Dưới đây là ví dụ về cách thêm dependency Spring Security vào Maven:
org.springframework.boot
spring-boot-starter-security
Sau khi thêm dependency, Maven hoặc Gradle sẽ tải xuống và cài đặt Spring Security vào dự án của bạn
Cách sử dụng cơ bản của Spring Security:
Tạo một lớp cấu hình (configuration class) và đánh dấu nó bằng @EnableWebSecurity để bật tính năng bảo mật của Spring Security
Ghi đè phương thức configure(HttpSecurity http) để cấu hình các quy tắc bảo mật, ví dụ như quy tắc truy cập vào các URL, phân quyền, xác thực, v.v import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationMana gerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAda pter;
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth
password("{noop}password") // Không mã hóa mật khẩu để đơn giản hóa ví dụ roles("USER");
@Override protected void configure(HttpSecurity http) throws Exception { http
antMatchers("/public/**").permitAll() // Các URL công khai
anyRequest().authenticated() // Các URL cần xác thực
permitAll() // Cho phép truy cập trang đăng nhập
permitAll(); // Cho phép đăng xuất
Xác thực và phân quyền: Định nghĩa một lớp UserDetailsService để cung cấp thông tin người dùng và mật khẩu từ nguồn dữ liệu như cơ sở dữ liệu hoặc bất kỳ nguồn dữ liệu nào khác
Sử dụng lớp UserDetailsService để xác thực người dùng Bạn có thể sử dụng các cơ chế xác thực sẵn có như JDBC Authentication, LDAP Authentication, UserDetailsService, v.v import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationMana gerBuilder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration public class UserDetailsServiceImpl implements UserDetailsService {
@Override public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException { if ("user".equals(username)) { return User.builder()
} else { throw new UsernameNotFoundException("User not found: " + username);
@Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance();
Bảo vệ các tài nguyên: quyền cho phương thức import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/api") public class MyController {
@GetMapping("/public") public String publicEndpoint() { return "public";
@Secured("ROLE_USER") // Yêu cầu quyền ROLE_USER để truy cập public String privateEndpoint() { return "private";
@PreAuthorize("hasRole('ADMIN')") // Yêu cầu quyền ADMIN để truy cập public String adminEndpoint() { return "admin";
Tùy chỉnh và mở rộng:
Spring Security cung cấp nhiều tính năng và khả năng tùy chỉnh khác nhau Bạn có thể tùy chỉnh cách xác thực, phân quyền, quản lý phiên, xử lý lỗi, v.v bằng cách sử dụng các cấu hình và cơ chế mở rộng của Spring Security.
Spring Boot JSR303
Spring Boot hỗ trợ việc sử dụng JSR 303 (Bean Validation) để kiểm tra và xác thực dữ liệu trong ứng dụng JSR 303 cung cấp một tập hợp các annotation để áp dụng các quy tắc kiểm tra dữ liệu trên các trường của đối tượng Để sử dụng JSR 303 trong Spring Boot, bạn cần thực hiện các bước sau:
Thêm dependency vào file pom.xml (hoặc build.gradle nếu sử dụng Gradle) để bao gồm thư viện Bean Validation:
org.springframework.boot
spring-boot-starter-validation
Định nghĩa một đối tượng (POJO) chứa các trường cần kiểm tra dữ liệu Áp dụng các annotation JSR 303 để xác định quy tắc kiểm tra dữ liệu cho từng trường import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; public class User {
@NotBlank(message = "Tên không được để trống") private String name;
@NotBlank(message = "Email không được để trống")
@Email(message = "Email không hợp lệ") private String email;
@NotBlank(message = "Mật khẩu không được để trống")
@Size(min = 6, message = "Mật khẩu phải có ít nhất 6 ký tự") private String password;
Trong ví dụ trên, chúng ta sử dụng các annotation như @NotBlank, @Email, và @Size để xác định các quy tắc kiểm tra dữ liệu cho trường tương ứng
Trong Controller, sử dụng annotation @Valid trước đối tượng cần kiểm tra dữ liệu Khi một yêu cầu được gửi đến, Spring Boot sẽ tự động kiểm tra dữ liệu và trả về các lỗi nếu có import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;
@PostMapping("/users") public void createUser(@Valid @RequestBody User user) {
// Xử lý tạo người dùng
Trong ví dụ trên, chúng ta sử dụng annotation @Valid trước tham số user trong phương thức createUser() Điều này yêu cầu Spring Boot kiểm tra dữ liệu của đối tượng user dựa trên các quy tắc trong lớp User
Nếu có lỗi xảy ra trong quá trình kiểm tra dữ liệu, Spring Boot sẽ tự động tạo ra một đối tượng BindingResult chứa thông tin về các lỗi Bạn có thể sử dụng BindingResult để kiểm tra và xử lý các lỗi đó.
Spring Cloud Open Feign
Spring Cloud OpenFeign là một thư viện trong Spring Cloud giúp xây dựng và sử dụng dễ dàng các dịch vụ RESTful trong môi trường microservices
Thêm dependency vào file pom.xml (hoặc build.gradle nếu sử dụng Gradle) để bao gồm thư viện Spring Cloud OpenFeign:
org.springframework.cloud
spring-cloud-starter-openfeign
Đánh dấu một interface làm đại diện cho dịch vụ RESTful mà bạn muốn gọi Sử dụng các annotation như @FeignClient, @GetMapping, @PostMapping, vv để xác định các phương thức và URL tương ứng import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "example-service") public interface ExampleServiceClient {
Trong ví dụ trên, chúng ta định nghĩa một interface ExampleServiceClient với một phương thức getExample() Annotation @FeignClient xác định tên của dịch vụ mà chúng ta muốn gọi Annotation @GetMapping xác định phương thức HTTP và URL tương ứng
Cấu hình Feign Client trong ứng dụng Spring Boot bằng cách thêm annotation
@EnableFeignClients trên lớp cấu hình chính import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients public class MyApp { public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
Sử dụng Feign Client để gọi dịch vụ RESTful Bạn có thể inject Feign Client vào các bean khác trong ứng dụng và sử dụng nó như một thành phần bình thường import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
@Service public class MyService { private final ExampleServiceClient exampleServiceClient;
@Autowired public MyService(ExampleServiceClient exampleServiceClient) { this.exampleServiceClient = exampleServiceClient;
} public String callExampleService() { return exampleServiceClient.getExample();
Trong ví dụ trên, chúng ta inject ExampleServiceClient vào MyService và sử dụng phương thức getExample() để gọi dịch vụ RESTful
Spring Cloud OpenFeign cung cấp các tính năng như xử lý lỗi, cấu hình timeout, load balancing và nhiều tính năng khác để làm việc dễ dàng với các dịch vụ RESTful trong môi trường microservices Bạn có thể tìm hiểu thêm về các tính năng và cấu hình chi tiết trong tài liệu chính thức của Spring Cloud OpenFeign.
Spring Cloud Resilience4j
Spring Cloud Resilience4j là một thư viện trong Spring Cloud giúp xây dựng các hệ thống bền vững (resilient) trong môi trường microservices Nó cung cấp các cơ chế để xử lý lỗi, giới hạn tải, và khôi phục trong trường hợp các dịch vụ gặp sự cố Để sử dụng Spring Cloud Resilience4j, bạn cần thực hiện các bước sau:
Thêm dependency vào file pom.xml (hoặc build.gradle nếu sử dụng Gradle) để bao gồm thư viện Spring Cloud Resilience4j:
org.springframework.cloud
spring-cloud-starter-resilience4j
Cấu hình các khả năng của Resilience4j trong ứng dụng Spring Boot bằng cách sử dụng các annotation như @CircuitBreaker, @RateLimiter, @Retry, vv
Ví dụ, bạn có thể đánh dấu một phương thức để sử dụng khả năng Circuit Breaker: import org.springframework.stereotype.Service; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@CircuitBreaker(name = "backendA", fallbackMethod = "fallbackMethod") public String myMethod() {
// Gọi dịch vụ hoặc thực hiện công việc
// Xử lý khi xảy ra lỗi
Trong ví dụ trên, chúng ta sử dụng annotation @CircuitBreaker để đánh dấu phương thức myMethod() sử dụng khả năng Circuit Breaker Nếu có lỗi xảy ra trong phương thức,
Resilience4j sẽ thực hiện các hành động được xác định trong phương thức fallbackMethod()
Bạn cũng có thể sử dụng các annotation khác như @RateLimiter để giới hạn tải, @Retry để thử lại nếu có lỗi, và @Bulkhead để giới hạn số lượng đồng thời Các annotation này giúp xây dựng hệ thống ổn định và bền vững trong môi trường microservices
Cấu hình các thuộc tính và quy tắc của Resilience4j trong tệp cấu hình ứng dụng (ví dụ: application.yaml hoặc application.properties) Bạn có thể cấu hình các giá trị như thời gian chờ, số lần thử lại, ngưỡng tải, vv
Ví dụ cấu hình Circuit Breaker trong application.yaml: resilience4j: circuitbreaker: configs: default: waitDurationInOpenState: 5000 failureRateThreshold: 50 ringBufferSizeInHalfOpenState: 2 ringBufferSizeInClosedState: 2 instances: backendA: baseConfig: default
Trong ví dụ trên, chúng ta cấu hình một khả năng Circuit Breaker với các thuộc tính như thời gian chờ trong trạng thái mở, ngưỡng tỷ lệ lỗi, kích thước bộ nhớ đệm vòng trong trạng thái mở và trạng thái đóng Chúng ta cũng định nghĩa một instance của Circuit Breaker với tên
"backendA" và cấu hình mặc định.
Spring Cloud Consul
Spring Cloud Consul là một dự án trong Spring Cloud giúp tích hợp và sử dụng Consul trong các ứng dụng dựa trên Spring Boot Consul là một hệ thống quản lý dịch vụ và khám phá dịch vụ mã nguồn mở được phát triển bởi HashiCorp
Spring Cloud Consul cung cấp các tính năng sau:
Service Discovery: Consul cho phép đăng ký và khám phá các dịch vụ Spring Cloud Consul sử dụng Consul để quản lý và khám phá các dịch vụ trong một mạng microservices Bằng cách sử dụng các annotation như @EnableDiscoveryClient, bạn có thể đánh dấu ứng dụng Spring Boot để nó có thể đăng ký và tìm kiếm các dịch vụ bằng Consul
Distributed Configuration: Consul cung cấp tính năng lưu trữ cấu hình phân tán Spring Cloud Consul cho phép ứng dụng đọc cấu hình từ Consul Bằng cách sử dụng các annotation như
@EnableConfigServer, bạn có thể xây dựng một Config Server trong ứng dụng Spring Boot để lấy cấu hình từ Consul
Distributed Locks: Consul hỗ trợ locks phân tán để đảm bảo đồng bộ hóa hoạt động giữa các ứng dụng Spring Cloud Consul cung cấp các khả năng lock phân tán thông qua các annotation như @DistributedLock và @DistributedUnlock
Health Checking: Consul cho phép kiểm tra sức khỏe của các dịch vụ Spring Cloud Consul sử dụng Consul để kiểm tra sức khỏe của các ứng dụng Spring Boot và báo cáo trạng thái sức khỏe cho Consul thông qua các API hoặc các annotation như @HealthIndicator
Routing and Load Balancing: Spring Cloud Consul tích hợp Consul với Ribbon, một thư viện cân bằng tải trong Spring Cloud Điều này cho phép việc cân bằng tải giữa các phiên bản và các ứng dụng khác nhau dựa trên thông tin từ Consul Để sử dụng Spring Cloud Consul, bạn cần thêm dependency vào file pom.xml (hoặc build.gradle nếu sử dụng Gradle) để bao gồm thư viện Spring Cloud Consul:
org.springframework.cloud
spring-cloud-starter-consul-discovery
Spring Cloud Vault
Spring Cloud Vault là một dự án trong Spring Cloud giúp tích hợp và sử dụng Vault trong các ứng dụng dựa trên Spring Boot Vault là một hệ thống quản lý bảo mật mã nguồn mở được phát triển bởi HashiCorp
Spring Cloud Vault cung cấp các tính năng sau:
Secure Configuration: Vault cung cấp tính năng lưu trữ và quản lý bảo mật cấu hình Spring Cloud Vault cho phép ứng dụng đọc cấu hình bảo mật từ Vault Bằng cách sử dụng các annotation như @EnableVaultConfig, bạn có thể xây dựng một ứng dụng Spring Boot để lấy cấu hình từ Vault
Dynamic Secrets: Vault cung cấp tính năng tạo và quản lý secrets động Spring Cloud Vault cung cấp tích hợp với Vault để tạo và quản lý secrets động trong ứng dụng Spring Boot Bằng cách sử dụng các annotation như @VaultPropertySource, bạn có thể đánh dấu các bean trong ứng dụng để lấy secrets từ Vault và sử dụng chúng trong ứng dụng
Encryption and Decryption: Vault cung cấp tính năng mã hóa và giải mã dữ liệu Spring Cloud Vault cho phép ứng dụng mã hóa và giải mã dữ liệu bằng cách sử dụng Vault Bằng cách sử dụng các annotation như @VaultPropertySource, bạn có thể mã hóa dữ liệu trong tệp cấu hình và giải mã nó khi đọc từ Vault
Token Management: Vault sử dụng tokens để xác thực và quyền truy cập Spring Cloud Vault cung cấp tích hợp với Vault để quản lý và sử dụng các tokens trong ứng dụng Spring Boot Bằng cách sử dụng các cấu hình và API của Spring Cloud Vault, bạn có thể xác thực và sử dụng tokens trong quá trình giao tiếp với Vault Để sử dụng Spring Cloud Vault, bạn cần thêm dependency vào file pom.xml (hoặc build.gradle nếu sử dụng Gradle) để bao gồm thư viện Spring Cloud Vault:
org.springframework.cloud
spring-cloud-starter-vault-config
Spring State Machine
Spring State Machine là một dự án trong Spring Framework cung cấp một cách tiếp cận linh hoạt để xây dựng và quản lý máy trạng thái trong các ứng dụng dựa trên Spring Nó cung cấp một tập hợp các API và công cụ để mô hình hóa và thực hiện logic máy trạng thái
Spring State Machine cung cấp các tính năng và khả năng sau:
Mô hình hóa máy trạng thái: Spring State Machine cho phép bạn định nghĩa các trạng thái, sự kiện và chuyển đổi giữa các trạng thái trong ứng dụng của mình Bạn có thể sử dụng code hoặc cấu hình XML để xác định cấu trúc máy trạng thái
Quản lý chuyển đổi: Spring State Machine quản lý việc chuyển đổi giữa các trạng thái dựa trên sự kiện và điều kiện xác định Bạn có thể định nghĩa các hành động và điều kiện để kiểm soát chuyển đổi trạng thái
Trạng thái và sự kiện phân cấp: Spring State Machine hỗ trợ cấu trúc phân cấp cho trạng thái và sự kiện Điều này cho phép bạn xác định các trạng thái và sự kiện con trong các trạng thái và sự kiện cha, tạo ra một cấu trúc phân cấp dễ quản lý
Lắng nghe và phản ứng: Spring State Machine cung cấp khả năng lắng nghe và phản ứng với các sự kiện trong máy trạng thái Bạn có thể định nghĩa các hành động để thực hiện khi một sự kiện xảy ra hoặc khi máy trạng thái chuyển đổi sang một trạng thái mới
Tích hợp dễ dàng: Spring State Machine tích hợp tốt với các thành phần khác trong Spring Framework như Spring Boot, Spring Data và Spring Integration Bạn có thể sử dụng Spring State Machine trong các ứng dụng Spring hiện có một cách dễ dàng Để sử dụng Spring State Machine, bạn cần thêm dependency vào file pom.xml (hoặc build.gradle nếu sử dụng Gradle) để bao gồm thư viện Spring State Machine:
org.springframework.boot
spring-boot-starter-statemachine
Dưới đây là một ví dụ đơn giản về việc sử dụng Spring State Machine để quản lý trạng thái trong một ứng dụng Spring Boot:
Hãy tạo enum để định nghĩa các trạng thái và sự kiện: public enum States {
Sau đó, tạo một bean để cấu hình và quản lý trạng thái:
@EnableStateMachine public class StateMachineConfig extends StateMachineConfigurerAdapter {
@Override public void configure(StateMachineStateConfigurer states) throws Exception { states
states(EnumSet.allOf(States.class));
@Override public void configure(StateMachineTransitionConfigurer transitions) throws Exception { transitions
source(States.STATE1).target(States.STATE2).event(Events.EVENT1) and()
source(States.STATE2).target(States.STATE3).event(Events.EVENT2) and()
source(States.STATE3).target(States.STATE1).event(Events.EVENT3); }
Sau đó, tạo một bean để xử lý các sự kiện và hành động trong state machine:
@Autowired private StateMachine stateMachine; public void handleEvent(Events event) { stateMachine.sendEvent(event);
@OnTransition(target = "STATE1") public void onState1() {
System.out.println("Entered STATE1");
@OnTransition(target = "STATE2") public void onState2() {
System.out.println("Entered STATE2");
Cuối cùng, trong ứng dụng Spring Boot chính, bạn có thể sử dụng StateMachineHandler để xử lý các sự kiện và chuyển đổi trạng thái:
@SpringBootApplication public class StateMachineApplication implements CommandLineRunner {
@Autowired private StateMachineHandler stateMachineHandler; public static void main(String[] args) {
SpringApplication.run(StateMachineApplication.class, args);
@Override public void run(String args) throws Exception { stateMachineHandler.handleEvent(Events.EVENT1); stateMachineHandler.handleEvent(Events.EVENT2); stateMachineHandler.handleEvent(Events.EVENT3);
Khi bạn chạy ứng dụng, bạn sẽ thấy đầu ra tương ứng với các trạng thái được nhập vào:
Demo và tổng kết
Demo
Demo của nhóm là một hệ thống microservice E-commerce sử dụng các công nghệ:
• Spring Boot 3: Authorization Server (OAuth 2), Statemachine
• Spring Cloud Gateway, Open Feign, Stream, Resilient4j
• Elastic stack: Elasticsearch, Logstash, Kibana, Filebeat
Nhóm cũng demo Saga Pattern mô hình hóa dưới dạng state machine và sử dụng Spring Statemachine để làm saga orchestration
Ngoài ra nhóm cũng demo CQRS pattern thực hiện replicate dữ liệu bằng event thông qua RabbitMQ
Tổng kết
Thông qua đồ án, nhóm đã đạt được một số kết quả như sau:
• Các nguyên lý trong thiết kế hệ thống
Tuy nhiên nhóm vẫn còn nhiều chủ đề khác chưa tìm hiểu như:
• Thư viện eventuate tram hỗ trợ event driven
Tuy rằng còn nhiều thiếu sót, nhưng trong quá trình tìm hiểu về thiết kế và xây dựng hệ thống nhóm đã học được rất nhiều kiến thức hữu ít, được sử dụng các công nghệ phức tạp và dùng chúng cho việc xây dựng một hệ thống E-Commerce demo Với các kiến thức nền tảng này, nhóm sẽ tiếp tục tìm tòi và học hỏi thêm các kiến thức chuyên sâu hơn về việc xây dựng hệ thống để có thể áp dụng vào các dự án thực tế.
Link github
https://github.com/QuangDuong2903/Genesis