Xin chào mọi người, chắc hẳn các bạn đã từng nghe qua thuật ngữ ETL và tự hỏi rằng ETL là gì? Đối với tester muốn test ETL cần phải làm gì? Quy trình ETL ra sao? Tại sao phải ETL nhỉ? Và test ETL khác gì so với test app, test web? Trước đây, mình cũng thế, đặt ra hàng vạn câu hỏi vì sao rồi đi tìm hiểu, sau quá trình học và có chút kinh nghiệm thực tế, hôm nay mình xin giới thiệu đôi chút kiến thức về ETL testing, tổng quan về quy trình ETL và test ETL khác gì so với test app, web.
1. Khái niệm
1.1. ETL Thử nghiệm/Kiểm thử ETL là gì?
Kiểm thử ETL là quá trình kiểm thử được thực hiện để đảm bảo dữ liệu được tải từ nguồn đến đích sau khi chuyển đổi là chính xác, là việc xác minh dữ liệu ở các giai đoạn trung gian đang được sử dụng giữa nguồn và đích. ETL là từ viết tắt của Extract-Transform-Load.
1.2. Kiểm tra kho dữ liệu (data warehouse) là gì?
Kiểm tra kho dữ liệu là một phương pháp kiểm tra trong đó dữ liệu bên trong kho dữ liệu được kiểm tra tính toàn vẹn, độ tin cậy, độ chính xác và tính nhất quán để tuân thủ khung dữ liệu của công ty. Mục đích chính của thử nghiệm kho dữ liệu là để đảm bảo rằng dữ liệu được tích hợp bên trong kho dữ liệu đủ tin cậy để một công ty đưa ra quyết định.
1.3. ETL là gì? ETL hoạt động như nào?
ETL là viết tắt của Extract-Transform-Load, là một quy trình trích xuất dữ liệu từ các hệ thống nguồn khác nhau, sau đó chuyển đổi dữ liệu (như áp dụng phép tính, phép nối, v.v.) Và cuối cùng tải dữ liệu vào hệ thống Kho dữ liệu.
Trích xuất, chuyển đổi và tải (ETL) hoạt động bằng cách di chuyển dữ liệu từ hệ thống gốc đến hệ thống đích trong các chu kỳ định kỳ. Quy trình ETL hoạt động theo ba bước:
Extract: Trích xuất dữ liệu có liên quan từ cơ sở dữ liệu nguồn
Transform: Chuyển đổi dữ liệu để phù hợp hơn cho việc phân tích
Load: Tải dữ liệu vào cơ sở dữ liệu đích
1.4 Tại sao chúng ta phải ETL dữ liệu?
Nếu chúng ta vẫn để nguyên các dữ liệu trên các database của các dữ liệu nguồn, chúng ta vẫn làm được các báo cáo phân tích, … Vậy tại sao chúng ta phải ETL dữ liệu làm gì?
Như đã nói trên, bạn dùng ETL dữ liệu để chuyển mục đích, và tối ưu hóa mục đích sử dụng dữ liệu của các phần mềm từ ghi nhận các nghiệp vụ phát sinh hàng ngày, sang mục đích khai thác, vận hành, và phân tích các dữ liệu này để các nhà quản trị tìm ra các cơ may phát triển, các hoạt động kinh doanh mới đề vận hành doanh nghiệp – và đây chính là mục đích của ETL, và là nguyên nhân bạn cần công cụ này – chuyển đổi công năng sử dụng dữ liệu để cung cấp cho nhà quản trị.
2. Quy trình kiểm thử ETL
Tương tự như các Quy trình kiểm thử khác, ETL cũng trả qua các giai đoạn khác nhau. Các giai đoạn của quá trình kiểm thử ETL như sau:
3. ETL Tools
Trên thị trường, có rất nhiều tools ETL, nhưng dưới đây là vài tool nổi bật nhất mọi người hay dùng:
4. Mình đã sử dụng AWS trong ETL testing như thế nào?
Như ở trên, chúng ta đã hiểu, ETL testing là kiểm tra để đảm bảo dữ liệu được tải từ nguồn đến đích sau khi chuyển đổi là chính xác. Lý thuyết là vậy, còn thực hành sẽ như nào nhỉ?
Thật khó để mình có thể chia sẻ hết kinh nghiệm trong quá trình tìm hiểu, học và kiểm thử ETL trong bài viết này, nhưng mình sẽ lấy 1 Ví dụ để mô tả một cách dễ hiểu nhất những gì 1 tester cần làm trong quá trình kiểm thử ETL. Từ đó, các bạn dễ hình dung, hiểu hơn về ETL testing và có thể áp dụng trong tương lai.
Ví dụ: Dưới đây là luồng di chuyển dữ liệu từ hệ thống nguồn (Stream data source) đến hệ thống đích (S3 Data Lake Target) trên AWS. Tester sẽ cần làm gì để test dữ liệu từ Source lên S3?
B1. Bạn sẽ cần chuẩn bị dữ liệu thô (VD: file csv, file parquet, …) để up lên source.
B2. Vậy làm cách nào để cho data chạy từ source lên hệ thống đích được nhỉ?
Bạn sẽ cần phải khởi chạy ETL job.
B3. Sau khi chạy, chúng ta sẽ kiểm tra job ETL đã chạy thành công chưa?
B4. Sau khi job chạy thành công, kiểm tra hệ thống đích (VD: S3 Data Lake Target) có tạo bảng như mong đợi?
B5. Kiểm tra dữ liệu bảng đích.
VD: Số lượng bản ghi, số lượng column, tên column, data type từng bản ghi, data trong từng column, … Kiểm tra, theo dõi cách ghi/thay đổi dữ liệu trong bảng đích đã đúng yêu cầu hay chưa.
5. ETL testing giống và khác gì so với test mobile, test web?
5.1. Giống nhau:
Trước hết, để test bất kỳ cái gì chúng ta đều phải đọc và hiểu tài liệu đặc tả. Lên kế hoạch kiểm thử và estimate thời gian kiểm thử.
Thiết kế test case, đảm bảo test đủ các trường hợp có thể xảy ra.
Chuẩn bị data test, môi trường test, …
Mục đích cuối cùng đều là đảm bảo chất lượng sản phẩm, đảm bảo đầu ra đúng với nhu cầu khách hàng.
….
5.2. Khác nhau:
Vậy ETL testing có gì khác biệt so với test web và mobile? Dưới đây là một vài điểm khác biệt mà mình thấy được trong quá trình làm việc với ETL:
Test app, web để kiểm tra giao diện (UI), tương tác và trải nghiệm người dùng (UX) hay các chức năng, giá trị hiển thị, … thì chúng ta sẽ cần so sánh đúng với yêu cầu đặc tả (SRS)/mong muốn của khách hàng. Tức là chúng ta đã có sẵn yêu cầu đầu ra, việc cần làm là kiểm tra tính đúng đắn so với yêu cầu đó.
Test ETL thì chúng ta cần có kiến thức về SQL. Vì thực tế luôn có những chuyển đổi dữ liệu (transform) phức tạp, hoặc transform data từ nhiều nguồn, nhiều khoảng thời gian, … nên để tìm ra được output expect (kết quả đầu ra) là điều không dễ dàng. Do đó, chúng ta cần viết script SQL chuẩn, đúng với tài liệu để có được kết quả đầu ra, từ đó mới có thể so sánh và kiểm tra dữ liệu.
Kết luận
ETL là viết tắt của Trích xuất, Chuyển đổi và Tải (Extract, Transform and Load)
ETL cung cấp phương pháp di chuyển dữ liệu từ nhiều nguồn khác nhau vào kho dữ liệu.
Trong bước trích xuất đầu tiên, dữ liệu được trích xuất từ hệ thống nguồn vào khu vực tổ chức.
Trong bước chuyển đổi, dữ liệu được trích xuất từ nguồn được làm sạch và chuyển đổi.
Tải dữ liệu vào kho dữ liệu đích là bước cuối cùng của quy trình ETL.
Trên đây là những chia sẻ của mình về ETL testing, mong rằng bài viết này sẽ giúp ích được cho các bạn. Nếu mọi người có thắc mắc hay câu hỏi gì đừng ngần ngại comment và cùng nhau giải đáp nhé!
Trong thực tế, sau khi một khách hàng đặt một đơn hàng GrabFood từ ứng dụng Grab, đối tác bán hàng sẽ chuẩn bị đơn hàng. Một đối tác tài xế (Grab Bike) sẽ tiếp nhận đơn hàng và vận chuyển cho khách hàng. Làm cách nào mà nền tảng đặt hàng (Order Platform) có thể xử lý hàng triệu đơn hàng như vậy mỗi ngày !?
Nhìn chung, có thể phân loại các truy vấn mà Order Platform cần xử lý làm 2 loại: transactional queries vs analytical queries. Các truy vấn transactional ví dụ như: Create đơn hàng, Update đơn hàng, Get đơn hàng theo ID, Get các đơn hàng đang được xử lý của một khách hàng,… Những truy vấn này là những truy vấn quan trọng cần xử lý chính xác, hiệu quả. Các truy vấn analytical ví dụ như: Get lịch sử đơn hàng, Get các metrics thống kê về đơn hàng,…
→ Order Platform cần được thiết kế để có thể xử lý một lượng lớn dữ liệu transaction hàng tháng.
Sau khi phân loại các mẫu truy vấn như trên, nhóm Kỹ sư của Grab đã đặt ra 3 mục tiêu thiết kế hệ thống Database để lưu trữ và xử lý như sau:
Stability (Tính ổn định): Giải pháp cơ sở dữ liệu phải đảm bảo phục vụ được khả năng đọc/ghi với thông lượng cao (Queries per Second, hay QPS). Xử lý đơn hàng trực tuyến phải có tính sẵn sàng cao.
Scalability and cost (Khả năng mở rộng và chi phí): Giải pháp phải đáp ứng được sự phát triển nhanh chóng về mặt business, ngoài ra giải pháp phải hiệu quả về chi phí trên quy mô lớn.
Consistency (Tính nhất quán): strong consistency cho các đối với các transactional queries và eventually consistency với các analytical queries.
Với các mục tiêu đó, Grab team đã quyết định thiết kế giải pháp cơ sở dữ liệu sử dụng các loại Database khác nhau, một để xử lý các transactional queries (OLTP) và một để xử lý các analytical queries (OLAP).
Hình 1: Tổng quan giải pháp cơ sở dữ liệu cho Order Platform
Vì các mục đích sử dụng khác nhau nên các cơ sở dữ liệu OLTP và OLAP sẽ lưu trữ data theo các cách khác nhau, với cùng một nguồn data nhưng OLTP sẽ lưu trữ chúng trong thời gian ngắn, ngược lại thì OLAP sẽ lưu trữ data trong thời gian dài hơn nhằm truy xuất các thông tin lịch sử và thống kê với dữ liệu. Trong Hình 1, Grab team sử dụng một Data Ingestion pipeline để có thể lưu trữ giữ liệu nhất quán trong cả 2 cơ sở dữ liệu. Cần lưu ý rằng, pipeline này sẽ hoạt động bằng cách sử dụng các phương pháp xử lý asynchronous (bất đồng bộ) vì chúng ta không cần phải xử lý các truy vấn với OLAP trực tuyến, real-time như OLTP.
Trong bài viết này, tôi sẽ tập trung vào phân tích cách Grab xử lý hàng triệu đơn hàng mỗi ngày, chính vì vậy mà trong các phần tiếp theo, tôi sẽ không đề cập đến cơ sở dữ liệu OLAP. Các bạn nếu có hứng thú, có thể tìm đọc thêm các tài liệu từ Grab Tech Blog.
Với các truy vấn OLTP, Grab team chọn DynamoDB là công nghệ lưu trữ và xử lý dữ liệu. Trong phần 2, tôi sẽ phân tích tại sao DynamoDB lại phù hợp. Phần 3, tôi sẽ phân tích Grab sử dụng DynamoDB như thế nào trong thiết kế mô hình dữ liệu hiệu quả.
Tại sao lại là DynamoDB?
Để trả lời câu hỏi này, tôi sẽ phân tích tính phù hợp của DynamoDB đối với việc đáp ứng các mục tiêu thiết kế được nêu ở phần trước.
Điều đầu tiên để nói về DynamoDB, nó là một cơ sở dữ liệu NoSQL, fully-managed được cung cấp bởi AWS, có tính khả dụng cao (high availability), hiệu năng cao (high performance) với hiệu suất chỉ 1 phần nghìn giây (single-digit millisecond performance), khả năng mở rộng quy mô vô hạn mà không giảm hiệu suất (infinite scaling with no performance degradation) → Stability.
High availability
DynamoDB tự động phân phối dữ liệu và lưu lượng truy cập qua một số lượng máy chủ vừa đủ để xử lý các yêu cầu lưu trữ và thông lượng trong khi vẫn đảm bảo hiệu suất nhanh chóng và nhất quán. Tất cả dữ liệu được lưu trữ sử dụng ổ đĩa SSD và tự động được replicated qua các AZs khác nhau → high availability & high durability. Hơn nữa, chúng ta có thể sử dụng Global Tables để đồng bộ dữ liệu qua các Regions.
Data models
Trước khi nói về hiệu suất, tôi đề cập một chút về các mô hình dữ liệu mà DynamoDB hỗ trợ. Trong DynamoDB, có 2 mô hình dữ liệu đó là key-value store (sử dụng hash table) → cho phép hiệu suất truy vấn O(1) và wide-column store (sử dụng B-tree) → cho phép hiệu suất truy vấn O(log(n)), n là kích thước của 1 collections các phần tử có chung partition key.
DynamoDB cho phép truy cập dữ liệu theo nhiều cách (access patterns) khác nhau với hiệu suất cao → điều quan trọng với DynamoDB đó là chúng ta phải thiết kế mô hình dữ liệu hiệu quả, tôi sẽ tiếp tục đề cập đến điều này trong phần 3 – Grab đã sử dụng DynamoDB thế nào?
Infinite scaling with no performance degradation
DynamoDB có khả năng scale vô hạn là nhờ cơ chế sharding dữ liệu qua nhiều server instances. Partitions là đơn vị lưu trữ cốt lõi của các DynamoDB tables. Khi một HTTP request (ứng dụng giao tiếp với DynamoDB qua HTTP connection thay vì TCP connection như các cơ sở dữ liệu truyền thống khác) tới DynamoDB, request router sẽ lấy partition key trong request đó và áp dụng một hàm băm (hash function) đối với partition key này. Kết quả của hàm băm sẽ chỉ định server nơi mà dữ liệu được lưu trữ, request sau đó sẽ được chuyển tiếp đến server nơi lưu trữ dữ liệu để thực hiện read/write với dữ liệu. Thiết kế này giúp cho DynamoDB có thể bổ sung thêm các storage nodes vô hạn khi dữ liệu scale up.
Trong các phiên bản sớm hơn của DynamoDB, thông lượng được chia sẻ đều giữa các partitions → có thể dẫn đến mất cân bằng truy cập dữ liệu (có partition thì cần truy cập nhiều → không đủ tài nguyên, có partition thì nhu cầu truy cập ít hơn → thừa tài nguyên). Tuy nhiên, DynamoDB team hiện nay đã cho phép adaptive capacity, có nghĩa là thông lượng sẽ tự động được đáp ứng đủ cho các items cần truy cập nhiều, DynamoDB tự động cung cấp capacity cao hơn cho các partitions có nhiều lưu lượng truy cập hơn → DynamoDB có thể đáp ứng thông lượng lên đến 1000 WCUs (write capacity units) và 3000 RCUs (read capacity units). Hay nói một cách khác, DynamoDB có thể đáp ứng đến tối đa 3000 read Queries per second và 1000 write Queries per Second → hiệu suất cao.
Ngoài ra, AWS còn cung cấp một tính năng nữa cho DynamoDB, gọi là DynamoDB Accelerator (DAX) – một fully-managed in-memory cache cho bảng DynamoDB. Mặc dù hiệu suất của DAX có thể không thực sự mạnh như Redis nhưng DAX có thể là một giải pháp caching phù hợp cho phép tốc độ đủ nhanh, ít bảo trì và tiết kiệm chi phí hơn so với các bộ đệm khác.
Tóm lại, với DynamoDB, chúng ta vẫn có thể đảm bảo single-digit millisecond performance khi dữ liệu scale up. Chúng ta có thể mở rộng dữ liệu lên đến 10TB mà vẫn không suy giảm hiệu suất như sử dụng với 10GB. Điều mà không thể được đáp ứng trong các cơ sở dữ liệu quan hệ – ở đó, hiệu suất giảm dần khi dữ liệu scale up.
Consistency
Khi nói về các databases và các hệ thống phân tán, một trong những thuộc tính cần xem xết đó là các cơ chế consistency mà nó chúng hỗ trợ.
Với DynamoDB, có 2 tùy chọn consistency mà cơ sở dữ liệu này hỗ trợ:
Strong consistency
Eventual consistency
Với strong consistency, bất kỳ item nào chúng ta đọc từ DynamoDB sẽ phản ánh tất cả các yêu cầu writes đã xảy ra trước đó, trước khi yêu cầu đọc được thực thi. Ngược lại, với eventual consistency thì các yêu cầu đọc items có thể không phản ánh tất cả các yêu cầu writes xảy ra trước đó.
→ Các transactional queries trong Order Platform hoàn toàn có thể được sử dụng với strong consistency trong DynamoDB.
Cost effective
DynamoDB cung cấp một mô hình định giá linh hoạt. Với hầu hết các cơ sở dữ liệu, chi phí dựa trên 1 lượng capacities nhất định cho máy chủ bao gồm CPU, RAM, Hard Disks,… → điều này không tối ưu vì chúng ta sẽ phải trả tiền cho tài nguyên thay vì trả cho workload ta sử dụng (bao nhiêu truy vấn mỗi giây/queries per second)
Ngược lại, DynamoDB được định giá trực tiếp dựa trên workload mà ứng dụng cần, chúng ta chỉ định thông lượng dựa trên WCU (write capacity units) và RCU (read capacity units). Một RCU cung cấp 1 strongly-consistent read per second hoặc 2 eventually-consistent reads per second, lên đến 4KB kích thước. Một WCU cho phép 1 write per second, lên đến 1KB kích thước → Chi phí cho Read và Write là riêng biệt → Có thể tối ưu 1 trong 2 độc lập dựa trên nhu cầu ứng dụng.
Điều thú vị với DynamoDB là chúng ta có thể điều chỉnh thông lượng WCU và RCU nếu cần. Ví dụ như vào ban đêm hoặc cuối tuần, chúng ta có thể giảm thông lượng để tiết kiệm chi phí.
Nếu chúng ta không estimate được thông lượng của ứng dụng → DynamoDB cung cấp tùy chọn On-Demand capacity mode (chi phí cao hơn Provisioned capacity mode) → Có thể sử dụng On-Demand capacity mode trong quá trình phát triển, thực thi load testing để đưa ra Provisioned capacity phù hợp → tiết kiệm chi phí.
Năm 2017, AWS công bố một tính năng gọi là Time-to-live (TTL) cho DynamoDB, cho phép DynamoDB tự động xóa các item khi chúng ta chỉ muốn lưu trữ chúng cho 1 thời gian ngắn → điều này phù hợp với nhu cầu sử dụng DynamoDB của Grab, khi mà Grab team đã thiết kế một Data Ingestion Pipeline để lưu trữ dữ liệu lâu dài trong cơ sở dữ liệu OLAP. Việc lưu trữ dữ liệu trong DynamoDB trong thời gian ngắn còn cho phép hiệu quả chi phí cũng như đảm bảo hiệu suất luôn ổn định cao → đáp ứng được các transactional queries hàng ngày.
Sử dụng DynamoDB như thế nào?
Single-table design
Khi mô hình hóa dữ liệu với DynamoDB, chúng ta nên sử dụng càng ít bảng càng tốt, lý tưởng nhất là chỉ sử dụng 1 bảng cho toàn bộ ứng dụng → single-table design. Tại sao lại như vậy !?
Với các cơ sở dữ liệu quan hệ, thông thường chúng ta sẽ tạo 1 bảng ứng với mỗi entity (thực thể) trong ứng dụng. Các bảng có thể liên kết với nhau thông qua foreign keys → qua đó dữ liệu có thể được truy vấn qua các bảng sử dụng các phép toán joins. Các phép joins này khá tốn kém, khi mà nó phải scan nhiều bảng, so sánh các giá trị,… và trả về các kết quả.
DynamoDB được xây dựng cho các use cases với hiệu suất truy vấn cao, quy mô dữ liệu lớn, như Order Platform của Grab → Các phép toán joins là không phù hợp với các use cases như vậy. Thay vì cải thiện các phép toán joins trên quy mô dữ liệu lớn, DynamoDB giải quyết vấn đề bằng cách loại bỏ hoàn toàn việc sử dụng joins trong truy vấn. Loại bỏ bằng cách nào !?
Cơ chế pre-join trong DynamoDB có thể cho phép liên kết dữ liệu tương tự như các cơ sở dữ liệu quan hệ. Cơ chế này sử dụng các item collections. Một item collection là tập các items trong bảng chia sẻ chung partition key. Như vậy, nhờ pre-join mà DynamoDB cho phép thiết kế cơ sở dữ liệu ứng dụng chỉ với 1 bảng. Tất nhiên, có những hạn chế nhất định với single-table design, tuy nhiên trong phạm vi bài viết này tôi sẽ không đi sâu vào phân tích những hạn chế đó. Trong các phần sau, tôi muốn làm nổi bật thiết kế của Grab sử dụng single-table design như thế nào để xử lý các transactional queries với các đơn hàng.
Data modeling
Trong DynamoDB, khi thiết kế mô hình dữ liệu, chúng ta phải xác định Primary keys của bảng. DynamoDB cung cấp 2 loại Primary keys khác nhau:
Simple primary keys: chỉ bao gồm một thuộc tính, gọi là partition key.
Composite primary keys: là sự kết hợp của partition key và một thuộc tính khác, gọi là sort key. Đôi khi, chúng ta có thể gọi partition key là hash key, sort key là range key.
Để thiết kế cơ sở dữ liệu hiệu quả, trước hết chúng ta cần hình dung về mô hình dữ liệu của Order Platform. Order Platform cần thực hiện các truy vấn với đơn hàng (order), các đơn hàng liên quan đến khách hàng hay người đặt hàng, mỗi đơn hàng sẽ có 1 trong 3 trạng thái: ongoing, completed, hoặc canceled → Khá đơn giản và phù hợp với single-table design trong DynamoDB. Hình dưới đây cho thấy thiết kế này của Grab.
Hình 2: Minh họa bảng DynamoDB lưu trữ các đơn hàng của Order Platform
Trong hình 2 ở trên, Grab team đã đưa ra một minh họa về bảng DynamoDB nơi mà lưu trữ các đơn hàng, với Partition key là mã đơn hàng (order_id) (Lưu ý rằng hình trên chỉ mang tính chất minh họa, không phải là toàn bộ thiết kế của Order Platform).
Thiết kế này cho phép các transactional queries với đơn hàng như Get, Create, Update được thực hiện với hiệu suất cao, strong consistency. Bởi cấu trúc dữ liệu sử dụng hash table → các truy vấn sử dụng order_id sẽ có độ phức tạp thời gian là O(1).
Với các truy vấn liên quan đến 1 đơn hàng cụ thể, chúng ta có thể sử dụng order_id (partition key). Vậy với các truy vấn liên quan đến 1 khách hàng, ví dụ như lấy danh sách các đơn hàng ở trạng thái ongoing của khách hàng Alice như trong Hình 2, chúng ta có thể tiếp tục sử dụng mô hình dữ liệu trên không !?
→ Câu trả lời là có. Đến đây, chúng ta phải sử dụng một tính năng hữu ích khác được cung cấp bởi DynamoDB, đó là Global secondary index.
Secondary indexes
Primary keys có thể giới hạn các access patterns, như trong trường hợp của Grab, khi sử dụng primary keys với partition key là order_id thì chúng ta không thể tiếp tục truy vấn với 1 khách hàng cụ thể trong bảng đơn hàng. Để giải quyết hạn chế này, DynamoDB đưa ra một khái niệm gọi là secondary indexes. Secondary indexes cho phép chúng ta re-shape lại dữ liệu sang 1 cấu trúc khác để phục vụ các truy vấn với access pattern khác so với thiết kế ban đầu.
Khi tạo secondary index, chúng ta cần chỉ định key schema của index, key schema này tương tự như Primary keys của bảng, có thể chỉ cần partition key hoặc kết hợp partition key và sort key. Có 2 loại secondary indexes trong DynamoDB:
Local secondary indexes (LSI)
Global secondary indexes (GSI)
Một LSI sử dụng chung partition key như primary key của bảng nhưng với một sort key khác đi → điều này cho phép các truy vấn theo range khác với access pattern ban đầu.
Ngược lại, với GSI, chúng ta có thể chọn bất cứ thuộc tính nào làm partition key và sort key.
→ Trong trường hợp của Grab, với thiết kế ban đầu như Hình 2, bởi LSI không thay đổi partition key → không phù hợp với các truy vấn theo 1 khách hàng cụ thể → Grab team đã sử dụng GSI để giải quyết vấn đề này.
Ví dụ với một truy vấn Get tất cả các đơn hàng trong trạng thái Ongoing của khách hàng có pax_id là Alice. Nếu chúng ta sử dụng GSI chỉ với partition key là pax_id thì việc truy vấn dựa trên pax_id (trường hợp này là Alice) sẽ trả về tất cả các đơn hàng của Alice → Filter trên các kết quả đó để trích xuất ra các đơn hàng của Alice có trạng thái Ongoing. Như vậy, nếu như Alice có nhiều đơn hàng thì việc Filter như vậy sẽ ảnh hưởng đến độ trễ truy vấn nói riêng và hiệu năng tổng thể của hệ thống nói chung. Giải quyết bằng cách nào !?
→ Sử dụng Sparse indexes
Hình 3: GSI cho bảng đơn hàng
GSI được Grab team thiết kế sử dụng ID của khách hàng (pax_id_gsi) làm partition key và thời gian tạo đơn hàng (created_at) làm sort key, sparse index cho phép tại bất kỳ thời điểm nào, bảng GSI chỉ lưu trữ các đơn hàng với trạng thái Ongoing (Khi một đơn hàng chuyển từ Ongoing → Completed thì nó sẽ tự động bị xóa khỏi GSI) → Cho phép độ trễ truy vấn thấp, hiệu suất tốt hơn và hiệu quả chi phí.
Một vài lưu ý khi sử dụng GSI với DynamoDB:
Với GSI, chúng ta cần provision thêm throughput. RCU và WCU được sử dụng riêng biệt so với bảng ban đầu.
Dữ liệu được replicated từ bảng ban đầu sang GSI theo cơ chế asynchronous → chỉ cho phép eventual consistency.
Time to live (TTL)
Vì mục đích chỉ lưu trữ giữ liệu trong DynamoDB trong một thời gian ngắn (chúng ta có thể thấy điều này trên ứng dụng Grab Food, các đơn hàng sau một thời gian ~ 3 tháng sẽ không còn thấy trong lịch sử nữa :v) → Grab team sử dụng tính năng TTL được cung cấp bởi DynamoDB → cho phép tự động xóa các items khi expired. Để sử dụng TTL hiệu quả, Grab team chỉ thêm TTL với các items mới được add vào bảng, xóa các items không có thuộc tính TTL theo cách thủ công và chạy các script để xóa các items có TTL đã out of date khá lâu → cho phép tính năng TTL trên bảng với kích thước đủ nhỏ → hiệu quả khi DynamoDB tự động thực hiện scan bảng và xóa các items đã expired.
Data ingestion pipeline
Hình 4: Data ingestion pipeline
Grab team sử dụng Kafka để đồng bộ dữ liệu giữa 2 cơ sở dữ liệu OLTP và OLAP. Sử dụng Kafka như thế nào, các bạn có thể đọc thêm từ Grab tech blog như trong tài liệu tham khảo [1]. Trong phạm vi bài viết này, tôi không phân tích về lựa chọn này. Tuy nhiên, nếu trong tương lai có thể, tôi muốn phân tích về tính khả thi của giải pháp sử dụng DynamoDB Streams để xử lý và đồng bộ dữ liệu giữa các cơ sở dữ liệu OLTP và OLAP.
Kết luận
Trong bài viết này, tôi đã dựa trên một case study là Order Platform của Grab Food để phân tích một số tính năng, nguyên lý của DynamoDB phù hợp với các ứng dụng cần OLTP. DynamoDB đã cho phép Order Platform đạt được tính ổn định, tính khả dụng cao, hiệu suất cao cùng với khả năng mở rộng quy mô vô hạn. Ngoài ra, tôi cũng giới thiệu các cơ chế để cải thiện hiệu suất, chi phí hiệu quả mà Grab team sử dụng như GSI, TTL. Data ingestion pipeline được giới thiệu như là 1 biện pháp mà Grab sử dụng để đồng bộ dữ liệu sang cơ sở dữ liệu OLAP phục vụ các truy vấn thống kê cũng như đảm bảo khả năng mở rộng nghiệp vụ ứng dụng về sau.
Elasticsearch là một công cụ phân tích và tìm kiếm phân tán theo thời gian thực. Nó cho phép khám phá dữ liệu với tốc độ và quy mô chưa từng có trước đây. Nó được sử dụng trong tìm kiếm toàn văn bản (full-text search), tìm kiếm có cấu trúc, phân tích và kết hợp cả ba. Một số hệ thống nổi tiếng sử dụng Elasticsearch có thể kể đến như: GitHub, StackOverflow, Wikipedia,… Elasticsearch còn là một công cụ tìm kiếm nguồn mở, được xây dựng dựa trên Apache Lucene – một thư viện tìm kiếm toàn văn bản.
Elasticsearch lưu trữ các documents theo mô hình phân tán, các documents là các đối tượng lưu trữ dữ liệu dưới dạng khóa-giá trị (key-value), có thể được chuyển đổi từ định dạng JSON, và thực tế là Elasticsearch nhận các JSON documents để làm đầu vào cho xử lý hoặc để trả về các kết quả cho máy khách. Elasticsearch không chỉ lưu trữ các documents, nó còn lập chỉ mục (indexing) chúng để làm cho chúng có thể tìm kiếm được.
Elasticsearch cung cấp khả năng mở rộng và tính khả dụng cao nhờ bản chất phân tán, nhờ che giấu toàn bộ việc quản lý hạ tầng giúp cho ứng dụng có thể dễ dàng làm việc với Elasticsearch mà không cần phải có một hiểu biết sâu sắc về vận hành hạ tầng cũng như tổ chức dữ liệu trong Elasticsearch. Tuy nhiên, để có vể vận hành Elasticsearch hiệu quả, việc hiểu cách Elasticsearch tổ chức dữ liệu là cần thiết. Về mặt vật lý, Elasticsearch tổ chức dữ liệu theo 3 cấp độ, với các khái niệm: cluster, node, và shard.
Một node là một instance đang chạy của Elasticsearch; trong khi đó một cluster bao gồm 1 hoặc nhiều nodes phối hợp để chia sẻ dữ liệu và workload. Khi một node được thêm vào hoặc loại bỏ khỏi cluster thì cluster sẽ tự tổ chức lại để có thể điều phối dữ liệu đều giữa các nodes. Để đạt được điều đó thì một node trong cluster sẽ được chọn làm master node – đóng vai trò như người quản lý, nó chịu trách nhiệm cho việc quản lý các thay đổi trong cluster như thêm hoặc xóa một node khỏi cluster. Master node không tham gia vào các thay đổi hoặc tìm kiếm ở cấp độ document trừ phi cluster chỉ có 1 node duy nhất, việc này giúp cho nó không trở thành bottleneck của hệ thống khi lưu lượng truy cập tăng lên. Trong Elasticsearch thì bất kỳ node nào cũng có thể trở thành master node. Trong khi nói về tổ chức vật lý bên trong Elasticsearch thì không thể không nói về các shards; trong Elasticsearch thì shard là đơn vị vật lý cấp thấp được sử dụng để tổ chức dữ liệu – nơi mà các documents trong một index sẽ được phân bổ trong một vài shards.
Hình trên minh họa mối quan hệ giữa index và các shards trong Elasticsearch. Về bản chất, index chỉ là một không gian tên logic chứa các documents, các nhà phát triển phần mềm ứng dụng sẽ làm việc với index thay vì trực tiếp với các shards; trong khi đó ở phía dưới, Elasticsearch sẽ tổ chức các documents trong cùng một index vào các shards.
Như vậy, khi làm việc với Elasticsearch, chúng ta quan tâm chính là về index. Thuật ngữ index có thể được hiểu theo nhiều nghĩa khác nhau, phụ thuộc vào ngữ cảnh: (i) index là một danh từ chỉ nơi lưu trữ các documents, nó giống như một cơ sở dữ liệu trong các cơ sở dữ liệu truyền thống; (ii index cũng có thể được hiểu là một động từ chỉ việc lưu trữ 1 document vào một index (danh từ), thường được gọi là quá trình indexing (lập chỉ mục); và (iii) inverted index chỉ một cấu trúc dữ liệu mà Elasticsearch và Lucene sử dụng để hỗ trợ truy xuất dữ liệu toàn văn bản nhanh chóng.
Ngày nay, Elasticsearch đã được sử dụng trong rất nhiều ứng dụng nói chung và thương mại điện tử nói riêng bởi nó được thiết kế để hỗ trợ mạnh mẽ trong tìm kiếm và phân tích với dữ liệu. Elasticsearch có thể được triển khai trên các máy chủ tại chỗ (on-premise) nhưng phổ biến hơn hết là trên môi trường đám mây. Vào năm 2015, Amazon Web Services đã triển khai Elasticsearch trên đám mây AWS để cho phép các nhà phát triển phần mềm chạy và vận hành các cụm máy chủ Elasticsearch trên môi trường đám mây. Việc triển khai Elasticsearch trên môi trường đám mây AWS là cần thiết bởi nó cho phép các ứng dụng trên hệ sinh thái AWS có thể dễ dàng giao tiếp, và nhà phát triển phần mềm dễ dàng giám sát hiệu năng hệ thống nhờ sự hỗ trợ gián tiếp từ AWS Cloudwatch và nhiều dịch vụ hạ tầng khác.
Đến năm 2021, Amazon Web Services khởi chạy dự án nguồn mở OpenSearch, như một bản sao chép từ phiên bản 7.10.2 của Elasticsearch. Và OpenSearch được phát triển, bảo trì và quản lý bởi Amazon Web Services. Dịch vụ hạ tầng từ đó cũng được đổi tên thành AWS OpenSearch.
Một số suy nghĩ cá nhân về thiết kế hệ thống. Bài viết này chỉ muốn tạo ra một cái nhìn High-level về thiết kế hệ thống, để người đọc có thể cảm nhận được rằng: À, thực ra các hệ thống lớn cũng… chỉ có thế thôi, ez game.
Đầu tiên, mọi hệ thống phức tạp đều được build từ những hệ thống nhỏ hơn, còn được gọi là các hệ thống con (sub-systems). Như vậy để thiết kế được hệ thống phức tạp thì cần hiểu những hệ thống con. Để hiểu được những hệ thống con thì không cách nào khác ngoài việc tự build những hệ thống con.
Làm cách nào để ghép các hệ thống con lại với nhau thành một hệ thống phức tạp hơn !? Để làm được điều đó, trước hết cần hiểu các giao thức mạng (Protocols), sau đó là các mô hình truyền thông điệp/giao tiếp giữa các máy chủ ứng dụng.
Có một số mô hình giao tiếp phổ biến, hầu hết chúng ta đều tiếp cận với mô hình Synchronous trước, hay còn gọi là mô hình Request-Response. Phần lớn sinh viên đại học dừng ở mức này, nhưng như thế là đủ, bởi sinh viên cần quan tâm hơn đến các kiến thức nền tảng.
Tuy nhiên, các hệ thống hiện nay quá phức tạp, phần lớn workloads là Asynchronous thay vì Synchronous. Chính vì vậy, việc học các mô hình giao tiếp Asynchronous là cần thiết, một số mô hình phổ biến như: Push model, Polling model,… Messgages Broker và Event-bus là hai khái niệm phổ biến trong các hệ thống xử lý Asynchronous workloads. Chúng về bản chất chỉ là 1 layer trung gian giúp de-coupling các hệ thống con với nhau.
Sau khi nắm được sơ sơ về các building blocks rồi thì chúng ta có xu hướng thiết kế theo “khuôn mẫu” –> Architecture Patterns ra đời. Việc học Architecture Patterns có 2 ý nghĩa: (i) thứ nhất giúp chúng ta build các hệ thống phức tạp nhanh hơn bởi những mẫu sẵn có, và (ii) giúp chúng ta hiểu nhanh hơn về cách giao tiếp giữa các hệ thống con với nhau, bởi con người có xu hướng thích làm theo mẫu (có thằng khác chứng minh rồi)
Tóm lại, nếu nắm được các building blocks kể trên thì kể ra trên đời này không có hệ thống phức tạp nào là không làm được :)))
p/s: Bốc phét vậy thôi chứ mình vẫn đang tiếp cận ở High-level, cần nghiên cứu nhiều hơn về networking protocols Viết bài là một cách học của mình :)))
Lập luận của đối thủ khiến tôi nhớ đến một kẻ ngoại đạo, người, khi được hỏi về thế giới đang đứng trên cái gfi, đã trả lời: "Trên một con rùa." Nhưng con rùa đứng trên cái gì? "Trên một con rùa khác."
—Joseph Barker (1854)
Vì vậy, bạn biết microservices là gì và hy vọng hiểu được những lợi ích chính của chúng. Bây giờ có lẽ bạn đang háo hức muốn đi và bắt đầu tạo ra chúng, phải không? Nhưng bắt đầu từ đâu? Trong chương này, chúng ta sẽ xem xét cách suy nghĩ về ranh giới của các microservice của bạn, hy vọng sẽ tối đa hóa những ưu điểm và tránh một số nhược điểm tiềm ẩn. Nhưng trước tiên, chúng ta cần một cái gì đó để làm việc cùng nó.
Giới thiệu về MusicCorp
Sách về ý tưởng hoạt động tốt hơn với các ví dụ. Nếu có thể, tôi sẽ chia sẻ những câu chuyện từ các tình huống thực tế, nhưng tôi nhận thấy rằng việc có một lĩnh vực hư cấu để hoạt động cũng rất hữu ích. Xuyên suốt cuốn sách, chúng ta sẽ quay trở lại lĩnh vực này, xem khái niệm microservices hoạt động như thế nào trong thế giới này.
Vì vậy, chúng ta hãy chuyển sự chú ý của chúng ta đến một trong những nhà bán lẻ trực tuyến tiên tiến nhất hiện nay – MusicCorp. MusicCorp gần đây là một nhà bán lẻ truyền thống, nhưng sau khi công ty từ bỏ mảng kinh doanh máy hát, mảng kinh doanh nền tảng của MusicCorp, họ ngày càng tập trung nhiều hơn vào việc kinh doanh trực tuyến. Công ty có một trang web, nhưng cảm thấy rằng bây giờ là lúc để tăng gấp đôi doanh số trên các nền tảng trực tuyến. Xét cho cùng, những chiếc iPod đó chỉ là một mốt nhất thời (rõ ràng là Zunes tốt hơn nhiều) và những người hâm mộ âm nhạc đang khá vui khi chờ đĩa CD được chuyển tới nhà của họ. Chất lượng bao giờ cũng hơn sự tiện lợi, phải không? Và trong khi chúng ta đang ở đó, điều mà mọi người vẫn tiếp tục về Spotify này là gì — một nền tảng hướng đến đối tượng thanh thiếu niên?
Mặc dù đi sau một chút so với phần còn lai, nhưng MusicCorp lại có tham vọng lớn. May mắn thay, họ đã quyết định rằng cơ hội tốt nhất để chiếm lấy thị phần là đảm bảo rằng nó có thể thực hiện những thay đổi dễ dàng nhất có thể. Microservices giành chiến thắng!
Điều gì tạo nên một dịch vụ tốt?
Trước khi nhóm từ MusicCorp thay đổi chính mình, tạo ra dịch vụ này đến dịch vụ khác trong nỗ lực cung cấp loại băng 8 bản nhạc cho tất cả mọi người và một vài thứ khác, hãy bắt đầu và nói một chút về ý tưởng cơ bản quan trọng nhất mà chúng ta cần ghi nhớ. Điều gì tạo nên một dịch vụ tốt? Nếu bạn vẫn sống sót sau một lần deploy SOA không thành công, bạn có thể có một số ý tưởng về nơi tiếp theo mà tôi nói đến. Nhưng đề phòng trong trường hợp bạn không phải là kẻ may mắn hoặc giả sử bạn chính là kẻ may mắn, tôi muốn bạn tập trung vào hai khái niệm chính: liên kết lỏng lẻo và tính liên kết cao (loose coupling and high cohesion). Chúng tôi sẽ nói chi tiết trong suốt cuốn sách về những ý tưởng và thực tiễn khác, nhưng chúng đều vô ích nếu chúng tôi làm sai hai điều này.
Mặc dù thực tế là hai thuật ngữ này được sử dụng rất nhiều, đặc biệt là trong ngữ cảnh của các hệ thống hướng đối tượng, nhưng điều đáng bàn về ý nghĩa của chúng đối với microservices là gì.
Liên kết lỏng lẻo
Khi các dịch vụ được kết hợp một cách lỏng lẻo, một sự thay đổi đối với một dịch vụ không yêu cầu thay đổi một dịch vụ khác. Toàn bộ quan điểm của microservice là có thể thực hiện thay đổi đối với một dịch vụ và deploy nó mà không cần thay đổi bất kỳ phần nào khác của hệ thống. Điều này thực sự khá quan trọng.
Những thứ gì gây ra sự kết hợp chặt chẽ? Một sai lầm cổ điển là chọn một phong cách tích hợp liên kết chặt chẽ giữa dịch vụ này với dịch vụ khác, khiến những thay đổi bên trong dịch vụ đòi hỏi nhưng nơi sử dụng nó phải thay đổi. Chúng ta sẽ thảo luận sâu hơn về cách tránh điều này trong Chương 4.
Một dịch vụ được kết hợp một cách lỏng lẻo cần biết rất ít về những dịch vụ mà nó cộng tác cùng. Điều này cũng có nghĩa là chúng tôi có thể muốn giới hạn số lượng các loại yêu cầu/gọi chức năng khác nhau từ dịch vụ này sang dịch vụ khác, bởi vì ngoài vấn đề tiềm tàng về hiệu suất, giao tiếp kiểu này có thể dẫn đến kết nối chặt chẽ.
Độ gắn kết cao
Chúng tôi muốn hành vi liên quan thi đi cùng nhau và hành vi không liên quan nên ở nơi khác. Tại sao? Vâng, nếu chúng ta muốn thay đổi hành vi, chúng ta muốn có thể thay đổi nó ở một nơi và giải phóng thay đổi đó càng sớm càng tốt. Nếu chúng tôi phải thay đổi hành vi đó ở nhiều nơi khác nhau, chúng tôi sẽ phải release nhiều dịch vụ khác nhau (có thể cùng một lúc) để thực hiện thay đổi đó. Việc thực hiện các thay đổi ở nhiều nơi khác nhau sẽ chậm hơn và việc deploy nhiều dịch vụ cùng một lúc là rất rủi ro — cả hai điều này chúng tôi đều muốn tránh.
Vì vậy, chúng tôi muốn tìm các ranh giới của các vấn đề trong lĩnh vực của mình để giúp đảm bảo rằng hành vi liên quan sẽ ở cùng một nơi và giao tiếp với các ranh giới khác một cách lỏng lẻo nhất có thể.
Bối cảnh có giới hạn
Cuốn sách của Eric Evans về Thiết kế theo hướng lĩnh vực – Domain-Driven Design (Addison-Wesley) tập trung vào cách tạo hệ thống mô hình hóa các lĩnh vực trong thế giới thực. Cuốn sách chứa đầy những ý tưởng tuyệt vời như sử dụng ngôn ngữ phổ biến, sự trừu tượng của kho lưu trữ, và những thứ tương tự, nhưng có một khái niệm rất quan trọng mà Evans giới thiệu lúc đầu đã hoàn toàn lướt qua tôi: bối cảnh có giới hạn. Ý tưởng ở đây là bất kỳ lĩnh vực cụ thể nào đều bao gồm nhiều bối cảnh có giới hạn và nằm trong mỗi bối cảnh là những thứ (Eric sử dụng mô hình – model từ rất nhiều, có lẽ tốt hơn là những thứ – things) không cần giao tiếp với bên ngoài cũng như những thứ được chia sẻ ra bên ngoài với các bối cảnh có giới hạn khác. Mỗi bối cảnh có giới hạn có một giao diện rõ ràng, nơi nó quyết định những mô hình nào sẽ chia sẻ với các bối cảnh khác.
Một định nghĩa khác về bối cảnh có giới hạn mà tôi thích là "một trách nhiệm cụ thể được thực thi bởi các ranh giới rõ ràng." Nếu bạn muốn thông tin từ bối cảnh có giới hạn hoặc muốn đưa ra các yêu cầu về chức năng trong bối cảnh có giới hạn, bạn giao tiếp với các ranh giới rõ ràng của nó bằng cách sử dụng các mô hình. Trong cuốn sách của mình, Evans sử dụng một định nghĩa tương tự của các tế bào, nơi mà "[c]ells (tế bào) có thể tồn tại bởi vì màng của chúng xác định những gì ra/vào và xác định những gì có thể đi qua."
Hãy quay lại một chút với công việc kinh doanh của MusicCorp. Lĩnh vực của chúng tôi là toàn bộ hoạt động kinh doanh mà chúng tôi có. Nó bao gồm tất cả mọi thứ từ nhà kho đến bàn tiếp tân, từ tài chính đến đặt hàng. Chúng tôi có thể hoặc không thể mô hình hóa tất cả những điều đó trong phần mềm của mình, nhưng đó vẫn là thứ mà chúng tôi đang điều hành. Chúng ta hãy nghĩ về các phần của lĩnh vực đó trông giống như bối cảnh có giới hạn mà Evans đề cập đến. Tại MusicCorp, kho hàng của chúng tôi là một tổ hợp nhiều hoạt động — quản lý các đơn hàng được chuyển đi (và trả lại), nhận hàng mới, tổ chức các cuộc đua xe nâng, v.v. Ở những nơi khác, bộ phận tài chính có lẽ ít thú vị hơn, nhưng vẫn có một chức năng rất quan trọng trong tổ chức của chúng tôi. Những nhân viên này quản lý bảng lương, giữ các tài khoản của công ty và đưa ra các báo cáo quan trọng. Rất nhiều báo cáo. Chúng có lẽ cũng có những món đồ chơi trên bàn thú vị.
Các mô hình được chia sẻ và mô hình ẩn
Đối với MusicCorp, chúng ta có thể coi bộ phận tài chính và kho hàng là hai bối cảnh có giới hạn riêng biệt. Cả hai đều có giao diện rõ ràng với thế giới bên ngoài (về báo cáo hàng tồn kho, phiếu thanh toán, v.v.) và chúng có thông tin chi tiết mà chỉ họ cần biết (xe nâng, máy tính).
Giờ đây, bộ phận tài chính không cần biết về hoạt động chi tiết bên trong của nhà kho. Tuy nhiên, nó cần phải biết một số điều — ví dụ như nó cần biết về lượng hàng tồn kho để giữ cho các tài khoản được cập nhật. Hình 3-1 cho thấy một ví dụ về sơ đồ bối cảnh (context diagram). Chúng tôi thấy các khái niệm nội bộ của nhà kho, như Người chọn (người chọn đơn hàng), giá đang chứa hàng hóa, v.v. Tương tự như vậy, sổ cái tổng của công ty là không thể thiếu đối với tài chính nhưng không được chia sẻ ra bên ngoài ở đây.
Hình 3-1. Mô hình chia sẻ giữa bộ phận tài chính và kho hàng
Tuy nhiên, để có thể xác định giá trị của công ty, các nhân viên tài chính cần thông tin về cổ phiếu mà chúng tôi nắm giữ. Mặt hàng tồn kho sau đó trở thành một mô hình được chia sẻ giữa hai bối cảnh. Tuy nhiên, lưu ý rằng chúng ta không cần phải tiết lộ mọi thứ về mặt hàng trong kho từ bối cảnh kho hàng một cách mù quáng. Ví dụ: mặc dù nội bộ chúng tôi lưu giữ hồ sơ về một mặt hàng trong kho về nơi nó sẽ được đặt trong nhà kho, nhưng mặt hàng đó không cần phải được tiết lộ thông tin đó trong mô hình dùng chung. Vì vậy, sẽ có sự phân biệt giữa đại diện chỉ bên trong và đại diện bên ngoài mà chúng tôi đưa ra. Theo nhiều cách, điều này báo trước về cuộc thảo luận xung quanh REST trong Chương 4.
Đôi khi chúng ta có thể bắt gặp các mô hình có cùng tên nhưng có ý nghĩa rất khác nhau trong các bối cảnh khác nhau. Ví dụ: chúng ta có thể có khái niệm trả lại, đại diện cho việc khách hàng gửi lại thứ gì đó. Trong bối cảnh của khách hàng, trả lại là tất cả những gì liên quan đến việc in nhãn vận chuyển, gửi một gói hàng và chờ hoàn lại tiền. Đối với nhà kho, điều này có thể đại diện cho một gói hàng sắp được chuyển đến và một mặt hàng trong kho cần được bổ sung. Sau đó, trong kho chúng tôi lưu trữ thông tin bổ sung liên quan đến việc trả lại liên quan đến các nhiệm vụ sẽ được thực hiện; ví dụ: chúng tôi có thể tạo một yêu cầu khôi phục lại. Kết quả của mô hình chia sẻ là các quy trình khác nhau sẽ trở nên liên kết và hỗ trợ các thực thể trong mỗi bối cảnh có giới hạn, nhưng đó là mối quan tâm nội bộ trong chính bối cảnh đó.
Mô-đun và Dịch vụ
Bằng cách suy nghĩ rõ ràng về những mô hình nào nên được chia sẻ và không chia sẻ các đại diện nội bộ của chúng tôi, chúng tôi tránh được một trong những cạm bẫy tiềm ẩn có thể dẫn đến kết hợp chặt chẽ (ngược lại với những gì chúng tôi muốn). Chúng tôi cũng đã xác định một ranh giới trong lĩnh vực của chúng tôi, nơi tất cả các khả năng kinh doanh có cùng chí hướng sẽ tồn tại, mang lại cho chúng tôi sự gắn kết cao mà chúng tôi mong muốn. Những bối cảnh có giới hạn này, sau đó, tự cho mình là những ranh giới sáng tác cực kỳ tốt.
Như chúng ta đã thảo luận trong Chương 1, chúng ta có tùy chọn sử dụng các mô-đun trong một ranh giới quy trình để giữ các đoạn mã nguồn liên quan lại với nhau và cố gắng giảm sự ghép nối với các mô-đun khác trong hệ thống. Khi bạn bắt đầu trên một mã nguồn cơ sở mới, đây có là một khởi đầu tốt. Vì vậy, khi bạn đã tìm thấy các bối cảnh trong lĩnh vực của mình, hãy đảm bảo rằng chúng được mô hình hóa trong mã nguồn cơ sở của bạn dưới dạng mô-đun, với các mô hình được chia sẻ và ẩn.
Các ranh giới mô-đun này sau đó trở thành ứng viên xuất sắc cho các microservice. Nói chung, microservices cần phù hợp trong bối cảnh nhất định. Một khi bạn đã trở nên rất thành thạo, bạn có thể quyết định bỏ qua bước giữ cho bối cảnh có giới hạn được sử dụng như một mô-đun trong một hệ thống monolithic hơn và chuyển thẳng sang tạo ra một dịch vụ riêng biệt. Tuy nhiên, khi bắt đầu, hãy giữ một hệ thống mới ở khía cạnh gần giống monolithic hơn; việc sai lầm trong việc nhìn nhận ranh giới của các dịch vụ có thể gây hậu quả tốn kém, vì vậy hãy chờ đợi mọi thứ ổn định cho đến khi khi bạn nắm bắt được lĩnh vực mới, đó một là điều hợp lý. Chúng ta sẽ thảo luận thêm về vấn đề này trong Chương 5, cùng với các kỹ thuật giúp tách các hệ thống hiện có thành các microservice.
Vì vậy, nếu các ranh giới dịch vụ của chúng tôi phù hợp với các bối cảnh được giới hạn trong lĩnh vực của chúng tôi và microservices của chúng tôi đại diện cho các bối cảnh, chúng tôi đã có một khởi đầu tuyệt vời trong việc đảm bảo rằng các microservice của chúng tôi được liên kết lỏng lẻo và gắn kết chặt chẽ.
Phân rã sớm
Tại ThoughtWorks, bản thân chúng tôi đã trải qua những thách thức khi cố gắng phân chia các microservice quá nhanh. Bên cạnh việc tư vấn, chúng tôi cũng tạo ra một vài sản phẩm. Một trong số đó là SnapCI, một công cụ tích hợp liên tục (Continuous Integration) được cài đặt và phân phối liên tục (Continuous Delivery – Sau đây cũng viết là Continuous Delivery hoặc CD) (chúng ta sẽ thảo luận về các khái niệm đó sau trong Chương 6). Trước đây, nhóm đã làm việc trên một công cụ tương tự khác, Go-CD, một công cụ Continuous Delivery mã nguồn mở hiện có thể được deploy cục bộ thay vì được cài đạt trên nền tảng điện toán đám mây.
Mặc dù đã có một số mã nguồn được sử dụng lại từ rất sớm giữa các dự án SnapCI và Go-CD, nhưng cuối cùng SnapCI lại trở thành một mã nguồn cơ sở hoàn toàn mới. Tuy nhiên, kinh nghiệm trước đây của nhóm trong lĩnh vực công cụ CD khuyến khích họ tiến nhanh hơn trong việc xác định ranh giới và xây dựng hệ thống của họ như một tập hợp của các microservice.
Tuy nhiên, sau một vài tháng, rõ ràng là các trường hợp sử dụng của SnapCI đã khác nhau một cách tinh tế đến mức việc tiếp nhận các ranh giới dịch vụ ban đầu là không hoàn toàn đúng. Điều này dẫn đến nhiều thay đổi được thực hiện trên các dịch vụ và tất nhiên chi phí cho sự thay đổi cùng gia tăng. Cuối cùng, nhóm đã hợp nhất các dịch vụ trở lại thành một hệ thống monolithic, cho họ thời gian để hiểu rõ hơn về ranh giới nên tồn tại. Một năm sau, nhóm nghiên cứu đã có thể tách hệ thống monolithic thành các microservices, có ranh giới ổn định hơn nhiều. Đây không phải là ví dụ duy nhất về tình huống này mà tôi đã thấy. Việc phân ra sớm một hệ thống thành microservices có thể tốn kém, đặc biệt nếu bạn là người mới tham gia vào lĩnh vực. Theo nhiều cách, việc có một codebase tồn tại mà bạn muốn phân rã thành microservice dễ dàng hơn nhiều so với việc cố gắng xây dựng microservice ngay từ đầu.
Năng lực liên quan đến công việc kinh doanh
Khi bạn bắt đầu nghĩ về các bối cảnh và của chúng giới hạn tồn tại trong tổ chức của mình, bạn không nên nghĩ theo cách dữ liệu được chia sẻ, mà là về khả năng mà các bối cảnh đó cung cấp cho phần còn lại của lĩnh vực. Ví dụ, nhà kho có thể cung cấp khả năng có được danh sách hàng tồn kho hiện tại hoặc bối cảnh tài chính có thể hiển thị các tài khoản cuối tháng hoặc cho phép bạn thiết lập bảng lương cho một đợt tuyển dụng mới. Những khả năng này có thể yêu cầu trao đổi thông tin — các mô hình được chia sẻ — nhưng tôi đã thấy một cách quá thường xuyên việc suy nghĩ về dữ liệu dẫn đến các dịch vụ không có hoạt động gì liên quan đến việc hoạt động của công ty, và chỉ dựa trên các CRUD (tạo, đọc, cập nhật, xóa). Vì vậy, trước tiên hãy đặt hỏi "Bối cảnh này làm gì?", Sau đó là "Vậy nó cần dữ liệu gì để làm điều đó?"
Khi được mô hình hóa dưới dạng dịch vụ, các khả năng này trở thành các hoạt động chính sẽ được hiển thị qua một đường dây với các thành phần khác
Một vấn đề lặp lại vô tận
Nguyên văn: Turtles All the Way Down là một câu thể hiện một vấn đề lặp lại một cách vô tận. https://en.wikipedia.org/wiki/Turtles_all_the_way_down
Khi bắt đầu, bạn có thể sẽ xác định được các bối cảnh một cách chi tiết. Nhưng những bối cảnh này đến lượt nó có thể chứa những bối cảnh khác. Lấy ví dụ, bạn có thể phân chia bối cảnh của nhà kho thành các khả năng liên quan đến hoàn thành đơn đặt hàng, quản lý hàng tồn kho hoặc nhận hàng hóa. Khi xem xét ranh giới của các microservice, trước tiên hãy nghĩ đến các bối cảnh lớn hơn, ít chi tiết hơn, sau đó chia nhỏ theo các bối cảnh lồng nhau này khi bạn đang tìm kiếm lợi ích của việc tách các đường nối giữa chúng ra.
Tôi đã gặp các bối cảnh lồng nhau vẫn bị ẩn trong các bối cảnh khác, hãy kết hợp các microservice với nhau để tạo ra hiệu quả tuyệt vời. Đối với thế giới thực, họ vẫn đang sử dụng các năng lực mà nhà kho cung cấp, nhưng họ không biết rằng các yêu cầu của họ đang thực sự được ánh xạ một cách minh bạch tới hai hoặc nhiều dịch vụ riêng biệt, như bạn có thể thấy trong Hình 3-2. Đôi khi, bạn sẽ quyết định rằng sẽ hợp lý hơn khi bối cảnh giới hạn cấp cao hơn không được mô hình hóa một cách rõ ràng như một ranh giới dịch vụ, như trong Hình 3-3, vì vậy thay vì một ranh giới trong bối cảnh nhà kho duy nhất, thay vào đó bạn có thể chia nhỏ ra thành hàng tồn kho, thực hiện đơn đặt hàng, và nhận hàng hóa.
Hình 3-2. Microservices đại diện cho các bối cảnh được giới hạn lồng nhau ẩn bên trong nhà kho
Hình 3-3. Các bối cảnh giới hạn bên trong nhà kho được bật lên thành bối cảnh cấp cao nhất của riêng chúng
Nói chung, không có quy tắc nào quá nghiêm ngặt về cách tiếp cận nào phù hợp nhất. Tuy nhiên, việc bạn chọn cách tiếp cận lồng vào nhau hay tiếp cận theo cách tách biệt hoàn toàn phải dựa trên cơ cấu tổ chức của bạn. Nếu việc thực hiện đơn hàng, quản lý hàng tồn kho và nhận hàng được quản lý bởi các nhóm khác nhau, thì họ có thể nên được coi là microservice cấp cao nhất. Mặt khác, nếu tất cả chúng được quản lý bởi một nhóm, thì mô hình lồng nhau sẽ có ý nghĩa hơn. Điều này là do sự tác động lẫn nhau của cấu trúc tổ chức và kiến trúc phần mềm, mà chúng ta sẽ thảo luận ở phần cuối của cuốn sách trong Chương 10.
Một lý do khác để cách tiếp cận lồng nhau được ưa thích hơn có thể là chia nhỏ kiến trúc của bạn để đơn giản hóa việc kiểm thử. Ví dụ: khi kiểm thử các dịch vụ sử dụng dịch vụ kho hàng, tôi không phải phân tích từng dịch vụ bên trong bối cảnh của kho hàng, mà chỉ là nhưng API ít chi tiết hơn. Điều này cũng có thể cung cấp cho bạn một đơn vị cô lập khi xem xét các bài kiểm thử trên phạm vi lớn hơn. Ví dụ: tôi có thể quyết định thực hiện các kiểm thử từ đầu đến cuối trong đó tôi khởi chạy tất cả các dịch vụ bên trong bối cảnh kho hàng, nhưng đối với tất cả các service liên quan khác, tôi có thể bỏ qua chúng. Chúng ta sẽ khám phá thêm về kiểm thử và cách ly trong Chương 7.
Vấn đề giao tiếp trong các hoạt động của doanh nghiệp
Những thay đổi mà chúng tôi thực hiện đối với hệ thống của mình thường là những thay đổi mà doanh nghiệp muốn thực hiện về cách hệ thống hoạt động. Chúng tôi đang thay đổi chức năng — khả năng — được hiển thị cho khách hàng của chúng tôi. Nếu hệ thống của chúng tôi được phân tách theo các bối cảnh có giới hạn đại diện cho lĩnh vực của chúng tôi, thì những thay đổi chúng tôi muốn thực hiện có nhiều khả năng bị tách biệt trong một ranh giới microservice duy nhất. Điều này làm giảm số lượng điểm mà chúng tôi cần thực hiện thay đổi và cho phép chúng tôi deploy thay đổi đó một cách nhanh chóng.
Điều quan trọng nữa là phải nghĩ đến sự giao tiếp giữa các dịch vụ nhỏ này theo cùng một cách hiểu về hoạt động trong doanh nghiệp. Việc mô hình hóa phần mềm của bạn và sau đó là toàn bộ công việc kinh doanh không nên dừng lại ở ý tưởng về các bối cảnh có giới hạn. Các định nghĩa và ý tưởng giống nhau được chia sẻ giữa các bộ phận trong tổ chức của bạn phải được phản ánh trong các giao diện. Có thể hữu ích khi nghĩ về các biểu mẫu được gửi giữa các dịch vụ nhỏ này, giống như các biểu mẫu được trong cả tổ chức
Ranh giới về kỹ thuật
Việc xem xét những gì có thể xảy ra khi các dịch vụ được mô hình hóa không chính xác là việc làm hữu ích. Trước đây, tôi cùng một số đồng nghiệp đã làm việc với một khách hàng ở California, giúp công ty áp dụng một số phương pháp lập trình rõ ràng hơn và hướng tới tự động kiểm thử nhiều hơn. Chúng tôi đã bắt đầu với một số công việc đơn giản và có đã kết quả, chẳng hạn như phân tích dịch vụ, khi đó chúng tôi nhận thấy điều gì đó đáng lo ngại hơn nhiều. Tôi không thể đi quá chi tiết về những gì ứng dụng đã làm, nhưng đó là một ứng dụng công khai với lượng lớn khách hàng toàn cầu.
Đội ngũ và hệ thống đã ngày càng lớn mạnh. Ban đầu chỉ là tầm nhìn của một người, hệ thống đã sử dụng ngày càng nhiều tính năng và ngày càng có nhiều người dùng hơn. Cuối cùng, tổ chức quyết định nâng cao năng lực của nhóm bằng cách để một nhóm các developer mới có trụ sở tại Brazil đảm nhận một số công việc. Hệ thống được tách ra, với nửa phía trước của ứng dụng về cơ bản là stateless, deploy một trang web công khai, như trong Hình 3-4. Phần còn lại đơn giản là sử dụng kĩ thuật gọi thủ tục từ xa (RPC) trên một kho dữ liệu. Về cơ bản, hãy tưởng tượng bạn đã sử dụng một lớp kho lưu trữ (repository layer) trong mã nguồn cơ sở của mình và biến đây thành một dịch vụ riêng biệt.
Hình 3-4. Ranh giới dịch vụ được phân chia qua đường nối kỹ thuật
Các thay đổi thường xuyên phải được thực hiện cho cả hai dịch vụ. Cả hai dịch vụ đều nói chuyện với nhau qua các lệnh gọi phương thức kiểu RPC cấp thấp, mà kiểu kết nối này rất cứng nhắc nhưng dễ đứt gãy (chúng ta sẽ thảo luận về vấn đề này trong Chương 4). Giao diện dịch vụ cũng được sử dụng theo cách rất tùy tiện, dẫn đến vấn đề về hiệu suất. Điều này dẫn đến nhu cầu về các cơ chế xử lý RPC theo lô rất phức tạp. Tôi gọi đây là kiến trúc củ hành (onion architecture), vì nó có rất nhiều lớp và khiến tôi phải khóc khi phải cắt qua nó.
Bây giờ về mặt nổi của nó, ý tưởng tách hệ thống monolithic trước đây dưa trên các nhóm địa lý/tổ chức là hoàn toàn hợp lý, vì chúng tôi sẽ mở rộng trong Chương 10. Tuy nhiên, ở đây, thay vì thực hiện theo chiều dọc, tập trung vào các công việc của nghiệp vụ thông qua ngăn xếp, nhóm đã chọn những gì trước đây là đã được tạo ra thành API và tạo ra một lát cắt ngang.
Không phải lúc nào cũng đưa ra quyết định để lập mô hình ranh giới dịch vụ dọc theo các đường nối kỹ thuật cũng sai lầm. Tôi chắc chắn đã thấy điều này rất có ý nghĩa khi một tổ chức đang tìm cách đạt được các mục tiêu hiệu suất nhất định, chẳng hạn. Tuy nhiên, việc tìm kiếm các đường nối này chỉ là động lực phụ của bạn chứ không phải mục tiêu chính.
Tóm tắt
Trong chương này, bạn đã tìm hiểu một chút về điều gì tạo nên một dịch vụ tốt và cách tìm các đường nối trong không gian vấn đề của chúng tôi, mang lại cho chúng tôi lợi ích kép của cả khớp nối lỏng và tính liên kết cao. Các bối cảnh có giới hạn là một công cụ quan trọng giúp chúng ta tìm ra các đường nối này và bằng cách sắp xếp các microservice của chúng ta theo các ranh giới này, chúng ta cần đảm bảo rằng hệ thống cuối cùng có mọi cơ hội để giữ nguyên các tính năng đó. Chúng ta cũng đã có một gợi ý về cách chúng ta có thể chia nhỏ các microservice của mình hơn nữa, điều gì đó chúng ta sẽ khám phá sâu hơn sau. Và chúng ta cũng đã giới thiệu MusicCorp, lĩnh vực ví dụ mà chúng ta sẽ sử dụng trong suốt cuốn sách này.
Những ý tưởng được trình bày trong Thiết kế theo hướng lĩnh vực – DDD của Eric Evans rất hữu ích cho chúng ta trong việc tìm ra ranh giới hợp lý cho các dịch vụ và tôi mới chỉ sơ lược ở đây. Tôi giới thiệu cuốn sách Implementing Domain-Driven Design (Addison- Wesley) của Vaughn Vernon để giúp bạn hiểu tính thực tiễn của phương pháp này.
Mặc dù chương này chủ yếu là những thứ mang tính tổng quát, nhưng chúng ta cần đi sâu vào các kỹ thuật hơn trong các chương tiếp theo. Có rất nhiều cạm bẫy liên quan đến việc deploy giao diện giữa các dịch vụ, mà chúng có thể dẫn đến tất cả các loại rắc rối và chúng ta sẽ phải đi sâu vào chủ đề này nếu muốn giữ cho hệ thống của mình không trở thành một gã khổng lồ, rối rắm lộn xộn.
Như chúng ta đã thấy cho đến nay, microservice cung cấp cho chúng ta rất nhiều sự lựa chọn và theo đó là rất nhiều quyết định để đưa ra. Ví dụ, chúng ta nên sử dụng bao nhiêu công nghệ khác nhau, chúng ta nên để các nhóm khác nhau sử dụng các thành ngữ lập trình khác nhau, và chúng ta có nên tách hoặc hợp nhất một dịch vụ không? Làm thế nào để chúng ta đưa ra những quyết định này? Với tốc độ thay đổi nhanh hơn và môi trường linh hoạt hơn mà các kiến trúc này cho phép, vai trò của kiến trúc sư của hệ thống cũng phải thay đổi. Trong chương này, tôi sẽ có một cái nhìn khá chắc chắn về vai trò của một kiến trúc sư của hệ thống là gì và hy vọng sẽ khởi động một cuộc tấn công cuối cùng vào toà tháp ngà voi (ám chỉ việc xa rời thực tế)
Việc so sánh khập khiễng
Bạn tiếp tục sử dụng từ đó. Nhưng tôi không nghĩ rằng nó có nghĩa là những gì bạn nghĩ nó.
— Inigo Montoya, từ Cô dâu công chúa
Kiến trúc sư của hệ thống là một công việc quan trọng. Họ chịu trách nhiệm đảm bảo rằng chúng ta có một tầm nhìn kỹ thuật hợp nhất, một tầm nhìn sẽ giúp hệ thống của chúng tôi có thể đáp ứng được nhu cầu của khách hàng. Ở một số công ty, họ có thể chỉ phải làm việc với một nhóm, trong trường hợp đó, vai trò của họ và trưởng nhóm kỹ thuật thường giống nhau. Ở những người khác, họ có thể xác định tầm nhìn cho toàn bộ chương trình làm việc, phối hợp với nhiều nhóm trên toàn thế giới, hoặc thậm chí có thể là toàn bộ tổ chức. Ở bất kỳ cấp độ nào họ hoạt động, vai trò này rất khó để xác định rõ ràng, và mặc dù nó thường là một bước tiến nghề nghiệp rõ ràng cho developer trong các tổ chức doanh nghiệp, nó cũng là một vai trò nhận được nhiều chỉ trích hơn hầu hết vai trò bất kỳ nào khác. Hơn bất kỳ vai trò nào khác, họ có thể có tác động trực tiếp đến chất lượng của các hệ thống được xây dựng, đến điều kiện làm việc của đồng nghiệp và khả năng của tổ chức của họ để đáp ứng với sự thay đổi, nhưng dường như chúng ta thường làm sai vài trò của công việc này. Tại sao vậy?
Ngành công nghiệp (phần mềm) của chúng ta là một ngành trẻ. Đây là điều mà chúng ta dường như đã quên, nhưng chúng ta chỉ tạo ra các chương trình chạy trên thứ mà chúng ta gọi là máy tính trong khoảng 70 năm trở lại đây. Vì vậy, chúng ta không ngừng tìm kiếm một thứ tương tự ở các ngành nghề khác để cố gắng giải thích những gì chúng ta đang làm. Chúng ta không phải là bác sĩ hay kỹ sư y tế, nhưng cũng không phải là thợ sửa ống nước hay thợ điện. Thay vào đó, chúng ta thường nằm ở lưng chừng, điều này khiến xã hội khó hiểu chúng ta, hoặc chúng ta không hiểu chúng ta đang ở đâu.
Vì vậy, chúng ta vay mượn từ các ngành nghề khác. Chúng tôi tự gọi mình là “kỹ sư” phần mềm, hoặc"Kiến trúc sư." Nhưng chúng ta không phải kiến trúc sư, nhỉ? Kiến trúc sư và kỹ sư (ở ngành khác) có một sự nghiêm khắc và tinh tế mà chúng ta chỉ có thể mơ ước, và tầm quan trọng của họ trong xã hội được hiểu rõ. Tôi nhớ đã nói chuyện với một người bạn của tôi, một ngày trước khi anh ta trở thành một kiến trúc sư có trình độ. “Ngày mai,” anh ấy nói, "nếu tôi cho bạn lời khuyên ở quán rượu về cách xây dựng một thứ gì đó và điều đó là sai, tôi sẽ phải chịu trách nhiệm. Tôi có thể bị kiện, vì theo pháp luật, tôi bây giờ là một kiến trúc sư có năng lực và tôi phải chịu trách nhiệm nếu tôi làm sai. " Tầm quan trọng của những công việc này đối với xã hội có nghĩa là cần phải có những bằng cấp cần thiết để đáp ứng. Ví dụ, ở Anh, bạn phải học tối thiểu bảy năm trước khi được gọi là kiến trúc sư. Nhưng những công việc này cũng dựa trên một lượng kiến thức có từ hàng nghìn năm trước. Và chúng ta? Không hẳn. Đó cũng là lý do tại sao tôi xem hầu hết các hình thức chứng chỉ CNTT là vô giá trị, vì chúng hầu như không thể khẳng định chúng ta tốt như thế nào.
Một phần trong chúng ta có nhu cầu được công nhận, vì vậy chúng tôi mượn tên từ những ngành nghề khác đã có được sự công nhận mà chúng tôi là một ngành khao khát. Nhưng điều này có thể gây hại gấp đôi. Đầu tiên, nó ngụ ý rằng chúng ta biết mình đang làm gì, khi nào thì rõ ràng là không. Tôi sẽ không nói rằng các công trình xây dựng và cầu nối không bao giờ sụp đổ, nhưng tỉ lệ ít hơn nhiều so với số lần các chương trình của chúng ta sẽ gặp sự cố, khiến cho việc so sánh với các kỹ sư là khập khiễng. Thứ hai, các phép loại suy bị chia nhỏ rất nhanh khi chỉ nhìn lướt qua. Để xoay chuyển tình thế, nếu việc xây dựng cây cầu giống như một chương trình, chúng tôi sẽ phát hiện ra rằng bờ biển xa bây giờ đã xa hơn 50 mét, rằng nó thực sự là bùn chứ không phải đá granit, và thay vì xây dựng một cây cầu mà đáng lẽ chúng tôi sẽ xây dựng – thay vào đó là một cây cầu đường bộ. Phần mềm của chúng ta không bị ràng buộc bởi các quy tắc vật lý giống như các kiến trúc sư hoặc kỹ sư thực sự phải đáp ứng và những gì chúng tôi tạo ra được thiết kế để linh hoạt, thích ứng và phát triển theo yêu cầu của người dùng.
Có lẽ thuật ngữ kiến trúc sư(Solution Architect hay Software Architect) đã gây hại nhiều nhất. Ý tưởng về một người lập kế hoạch chi tiết cho người khác giải thích và mong muốn điều này được thực hiện. Một sự cân bằng giữa một nghệ sỹ và một kỹ sư, giám sát việc tạo của những thứ tông thường với một tầm nhìn duy nhất, với tất cả các quan điểm khác là không phù hợp, ngoại trừ sự phản đối không thường xuyên từ kỹ sưvề các quy luật vật lý. Trong ngành của chúng ta, quan điểm này của kiến trúc sư của hệ thống dẫn đến một số thực hành khủng khiếp. Sơ đồ này đến sơ đồ khác, trang này qua trang khác của tài liệu, được tạo ra với mục đích cung cấp thông tin về việc xây dựng một hệ thống hoàn hảo, mà không tính đến tương lai về cơ bản không thể biết trước được. Hoàn toàn không có bất kỳ hiểu biết nào về mức độ khó thực hiện, hoặc chỉ là nó có thực sự hoạt động hay không, hãy để một mình có khả năng thay đổi khi chúng ta có hiểu biết nhiều hơn.
Khi chúng ta so sánh mình với các kỹ sư hoặc kiến trúc sư, chúng ta có nguy cơ khiến mọi người trở thành kẻ phá hoại. Thật không may, chúng tôi đang mắc kẹt với từ kiến trúc sư cho đến bây giờ. Vì vậy, điều tốt nhất chúng ta có thể làm là xác định lại ý nghĩa của nó trong ngữ cảnh của chúng ta.
Một tầm nhìn tiến hóa cho kiến trúc sư
Các yêu cầu của chúng tôi thay đổi nhanh hơn so với những người thiết kế và xây dựng các tòa nhà — các công cụ và kỹ thuật theo ý của chúng tôi cũng vậy. Những thứ chúng ta tạo ra không phải là những điểm cố định trong thời gian. Sau khi được đưa vào môi trường production, phần mềm của chúng tôi sẽ tiếp tục phát triển khi cách thức sử dụng thay đổi. Đối với hầu hết những thứ chúng tôi tạo ra, chúng tôi phải chấp nhận rằng một khi phần mềm đến tay khách hàng, chúng tôi sẽ phải phản ứng và thích nghi, thay vì nó là một sản phẩm không bao giờ thay đổi. Do đó, các kiến trúc sư của chúng tôi cần phải thay đổi suy nghĩ của họ thay vì việc tạo ra sản phẩm cuối cùng hoàn hảo, mà tập trung vào việc giúp tạo ra một framework trong đó các hệ thống phù hợp có thể xuất hiện và tiếp tục phát triển khi chúng tôi có hiểu biết nhiều hơn.
Mặc dù cho đến nay, tôi đã dành phần lớn thời lượng của chương để cảnh báo bạn không nên so sánh bản thân của chúng ta quá nhiều với các ngành nghề khác, nhưng có một điểm tương đồng mà tôi thích khi nói đến vai trò của kiến trúc sư của hệ thống CNTT và tôi nghĩ tốt hơn nên gói gọn những gì chúng ta muốn. vai trò hiện hữu. Erik Doernenburg lần đầu tiên chia sẻ với tôi ý tưởng rằng chúng ta nên nghĩ về vai trò của mình với tư cách là nhà quy hoạch thành phố hơn là kiến trúc sư cho môi trường xây dựng. Vai trò của người lập kế hoạch thị trấn hẳn đã quen thuộc với bất kỳ bạn nào đã chơi SimCity trước đây. Vai trò của người lập quy hoạch thành phố là xem xét vô số nguồn thông tin và sau đó cố gắng tối ưu hóa bố cục của thành phố để phù hợp nhất với nhu cầu của người dân hiện nay, có tính đến việc sử dụng trong tương lai. Tuy nhiên, cách anh ấy ảnh hưởng đến cách thành phố phát triển là rất thú vị. Anh ta không nói, “xây dựng tòa nhà cụ thể này ở đó”; thay vào đó, anh ta tạo ra các vùng trong**thành phố. Vì vậy, như ở SimCity, bạn có thể chỉ định một phần thành phố của mình là khu công nghiệp và một phần khác là khu dân cư. Sau đó, những người khác sẽ quyết định những tòa nhà chính xác được tạo ra, nhưng có những hạn chế: nếu bạn muốn xây dựng một nhà máy, nó sẽ cần phải ở trong một khu công nghiệp. Thay vì lo lắng quá nhiều về những gì xảy ra trong một khu vực, thay vào đó, người lập kế hoạch thành phố sẽ dành nhiều thời gian hơn để tìm hiểu cách thức di chuyển của mọi người và các tiện ích từ khu vực này sang khu vực khác.
Nhiều người đã ví một thành phố như một sinh vật sống. Thành phố thay đổi theo thời gian. Nó thay đổi và phát triển khi những cư dân sử dụng nó theo những cách khác nhau, hoặc khi các lực lượng bên ngoài định hình nó. Người quy hoạch thành phố cố gắng hết sức để dự đoán những thay đổi này, nhưng chấp nhận rằng việc cố gắng kiểm soát trực tiếp tất cả các khía cạnh của những gì xảy ra là vô nghĩa.
Việc so sánh với phần mềm nên rõ ràng. Khi người dùng sử dụng phần mềm của chúng tôi, chúng tôi cần phản ứng và thay đổi. Chúng ta không thể lường trước mọi điều sẽ xảy ra, và vì vậy thay vì lập kế hoạch cho bất kỳ trường hợp nào, chúng ta nên lên kế hoạch cho phép thay đổi bằng cách tránh việc tạo ra một điều cuối cùng không bao giờ thay đổi. Thành phố của chúng ta — hệ thống — cần phải là một nơi tốt đẹp, hạnh phúc cho tất cả những ai sử dụng nó. Một điều mà mọi người thường quên là hệ thống của chúng tôi không chỉ đáp ứng người dùng; nó cũng tạo điều kiện cho các developer và nhà kinh doanh, những người cũng phải làm việc ở đó và những người có công việc đảm bảo rằng nó có thể thay đổi theo yêu cầu. Để mượn một thuật ngữ từ Frank Buschmann, các kiến trúc sư của hệ thống có nhiệm vụ đảm bảo rằng hệ thống cũng có thể sử dụng được cho các developer.
Một nhà quy hoạch, cũng giống như một kiến trúc sư, cũng cần biết khi nào kế hoạch của mình không được sử dụng hoặc tuân thủ. Vì anh ta có ít chỉ thị hơn, nên số lần anh ta cần tham gia để chỉ ra sự đúng sai là ít nhất, nhưng nếu ai đó quyết định xây dựng một nhà máy xử lý nước thải trong khu dân cư, anh ta cần phải có khả năng đóng cửa nó.
Vì vậy, các kiến trúc sư của hệ thống của chúng tôi với tư cách là những nhà quy hoạch thị trấn cần phải định hướng theo hướng tổng quan và chỉ tham gia vào việc cụ thể hóa chi tiết thực hiện trong một số trường hợp cụ thể. Họ cần đảm bảo rằng hệ thống phù hợp với mục đích ngay bây giờ, nhưng cũng là một nền tảng cho tương lai. Và họ cần đảm bảo rằng đó là một hệ thống khiến người dùng và developer hài lòng như nhau. Điều này nghe có vẻ như một yêu cầu khá cao. Vậy, chúng ta bắt đầu từ đâu?
Phân vùng
Vì vậy, tiếp tục ẩn dụ về kiến trúc sư của hệ thống là người quy hoạch thành phố thêm một chút nữa, chúng ta có những khu vực nào? Đây là các ranh giới dịch vụ của chúng tôi hoặc có thể là danh sách các dịch vụ được nhóm lại một cách chi tiết. Là kiến trúc sư, chúng ta cần ít để ý về những gì xảy ra bên trong từng khu vực hơn là những gì xảy ra giữa các khu vực với nhau. Điều đó có nghĩa là chúng tôi cần dành thời gian suy nghĩ về cách các dịch vụ của chúng tôi nói chuyện với nhau hoặc đảm bảo rằng chúng tôi có thể duy trì tình trạng hoạt động tổng thể của hệ thống một cách chính xác. Mức độ tham gia của chúng tôi vào bên trong khu vực sẽ khác nhau
một phần nào đó. Nhiều tổ chức đã áp dụng microservices để tối đa hóa quyền tự chủ của các nhóm, điều mà chúng tôi sẽ mở rộng trong Chương 10. Nếu bạn ở trong một tổ chức như vậy, bạn sẽ dựa nhiều hơn vào nhóm đó để đưa ra quyết định phù hợp tại từng chỗ.
Nhưng giữa các khu vực, hoặc các ô trên bản đồ kiến trúc truyền thống của chúng tôi, chúng tôi cần phải cẩn thận; làm sai ở đây dẫn đến tất cả các loại vấn đề và có thể rất khó sửa chữa.
Trong mỗi dịch vụ, bạn có thể đồng ý với nhóm sở hữu khu vực đó chọn một kho lưu trữ dữ liệu hoặc công nghệ khác nhau. Tất nhiên, những mối quan tâm khác có thể xuất hiện ở đây. Xu hướng để các nhóm chọn công cụ phù hợp cho công việc của bạn có thể bị hạn chế bởi thực tế là việc thuê người hoặc di chuyển họ giữa các nhóm trở nên khó khăn hơn nếu bạn có 10 ngăn xếp công nghệ khác nhau để hỗ trợ. Tương tự, nếu mỗi đội chọn một kho dữ liệu hoàn toàn khác nhau, bạn có thể thấy mình thiếu đủ kinh nghiệm để chạy bất kỳ kho dữ liệu nào trong số họ trên quy mô lớn. Netflix, ví dụ, hầu hết đã chuẩn hóa và sử dụng Cassandra như một kho lưu trữ dữ liệu. Mặc dù nó có thể không hẳn đã là công nghệ phù hợp nhất cho tất cả các trường hợp, nhưng Netflix cảm thấy rằng giá trị thu được bằng cách xây dựng công cụ và kiến thức chuyên môn xung quanh Cassandra quan trọng hơn việc phải hỗ trợ và vận hành trên quy mô nhiều nền tảng khác có thể phù hợp hơn cho từng các nhiệm vụ nhất định. Netflix là một ví dụ điển hình, trong đó quy mô có thể là yếu tố mang tính quyết định mạnh nhất, bạn có thể thấy ý tưởng đó khá rõ ràng.
Tuy nhiên, phần giữa các dịch vụ với nhau mới là nơi mọi thứ có thể trở nên lộn xộn. Nếu một dịch vụ quyết định cung cấp REST thông qua HTTP, một dịch vụ khác sử dụng giao thức bộ đệm và một dịch vụ thứ ba sử dụng Java RMI, thì việc tích hợp chúng có thể trở thành một cơn ác mộng vì các dịch vụ sử dụng chúng phải chịu và hỗ trợ nhiều kiểu giao thức. Đây là lý do tại sao tôi cố gắng bám sát tôn chỉ rằng chúng ta nên “lo lắng về những gì xảy ra giữa các hộp và tự do trong những gì xảy ra bên trong.”
Kiến trúc mã nguồn
Nếu chúng tôi muốn đảm bảo rằng các hệ thống chúng tôi tạo ra có thể sử dụng được cho các developer của chúng tôi, thì các kiến trúc sư của chúng tôi cần phải hiểu tác động của các quyết định của họ. Ít nhất, điều này có nghĩa là dành thời gian cho nhóm và lý tưởng là nó có nghĩa là những developer này cũng thực sự dành thời gian viết mã nguồn cho nhóm. Đối với những bạn thực hành phát triển theo cặp**(pair-programing),** việc một kiến trúc sư tham gia nhóm trong một thời gian ngắn với tư cách là một thành viên của cặp sẽ trở thành một vấn đề đơn giản. Tốt nhất, bạn nên làm những câu chuyện bình thường, để thực sự hiểu công việc bình thường là như thế nào. Tôi không thể nhấn mạnh tầm quan trọng của kiến trúc sư khi làm việc cùng nhóm! Điều này hiệu quả hơn đáng kể so với việc gọi điện hoặc chỉ nhìn vào mã nguồn của cô ấy.
Về mức độ thường xuyên mà bạn nên làm điều này, điều đó phụ thuộc rất nhiều vào quy mô của (các) nhóm mà bạn đang làm việc. Nhưng điều quan trọng là nó phải là một hoạt động thường xuyên. Ví dụ: nếu bạn đang làm việc với bốn nhóm, dành nửa ngày cho mỗi nhóm bốn tuần một lần đảm bảo bạn xây dựng nhận thức và cải thiện giao tiếp với các nhóm mà bạn đang làm việc.
Phương pháp tiếp cận có nguyên tắc
Các quy tắc dành cho sự vâng lời của những kẻ ngu ngốc và sự hướng dẫn của những nhà thông thái.
— Thường được cho là của Douglas Bader
Việc đưa ra quyết định trong thiết kế hệ thống, tất cả chỉ xoay quanh sự đánh đổi và microservice mang đến cho chúng ta rất nhiều đánh đổi! Khi chọn một kho dữ liệu, chúng ta có chọn một nền tảng mà chúng ta có ít kinh nghiệm, nhưng điều đó mang lại cho chúng ta khả năng mở rộng tốt hơn không? Chúng tôi có thể sử dụng hai stack công nghệ khác nhau trong hệ thống của mình không? Vậy nếu sử dụng ba thì sao? Một số quyết định có thể được thực hiện hoàn toàn ngay tại chỗ với thông tin có sẵn cho chúng tôi, và đây là những thứ dễ thực hiện nhất. Nhưng những quyết định có thể phải được thực hiện trên thông tin không đầy đủ thì sao?
Việc lập khung ở đây có thể hữu ích và một cách tuyệt vời để giúp lập khung cho việc ra quyết định của chúng ta là xác định một bộ các nguyên tắc và thực hành hướng dẫn nó, dựa trên các mục tiêu mà chúng ta đang cố gắng đạt được. Chúng ta hãy xem xét từng thứ một.
Mục tiêu chiến lược
Vai trò của kiến trúc sư đã đủ khó khăn, vì vậy, may mắn là chúng tôi thường không phải xác định các mục tiêu chiến lược! Các mục tiêu chiến lược phải nói lên được vị trí của công ty và cách công ty tự thấy là tốt nhất để làm cho khách hàng hài lòng. Đây sẽ là những mục tiêu cấp cao và có thể hoàn toàn không bao gồm công nghệ. Chúng có thể được xác định ở cấp độ công ty hoặc cấp độ phòng ban. Chúng có thể là những thứ như “Mở rộng sang Đông Nam Á để mở khóa các thị trường mới” hoặc “Hãy để khách hàng đạt được càng nhiều càng tốt bằng cách sử dụng các dịch vụ tự phục vụ.” Điều quan trọng là đây là nơi tổ chức của bạn đứng đầu, vì vậy bạn cần đảm bảo công nghệ phù hợp với nó.
Nếu bạn là người xác định tầm nhìn kỹ thuật của công ty, điều này có thể có nghĩa là bạn sẽ cần dành nhiều thời gian hơn cho các bộ phận phi kỹ thuật trong tổ chức của mình (hoặc bộ phận kinh doanh, như chúng thường được gọi). Tầm nhìn thúc đẩy cho doanh nghiệp là gì? Và nó thay đổi như thế nào?
Nguyên tắc
Nguyên tắc là những quy tắc bạn đã thực hiện để điều chỉnh những gì bạn đang làm với một số mục tiêu lớn hơn và đôi khi sẽ thay đổi. Ví dụ: nếu một trong những mục tiêu chiến lược của bạn với tư cách là một tổ chức là giảm thời gian triển khai các tính năng mới, bạn có thể xác định một nguyên tắc nói rằng nhóm delivery có toàn quyền kiểm soát vòng đời của phần mềm của họ để deliver bất cứ khi nào họ sẵn sàng, độc lập với bất kỳ đội nào khác. Nếu một mục tiêu khác là tổ chức của bạn là dịch chuyển mạnh mẽ sang việc triển khai dịch vụ của mình ở các quốc gia khác, bạn có thể quyết định thực hiện một nguyên tắc rằng toàn bộ hệ thống phải có tính di động để cho phép nó được deploy tại các quốc gia đó nhằm tôn trọng chủ quyền của dữ liệu.
Bạn hoàn toàn không muốn có vô số nguyên tắc. Ít hơn 10 là một con số tốt – đủ nhỏ để mọi người có thể nhớ chúng hoặc để phù hợp với các áp phích nhỏ. Bạn càng có nhiều nguyên tắc, thì khả năng chúng trùng lặp hoặc mâu thuẫn với nhau càng lớn.
12 Factors của Heroku là một tập hợp các nguyên tắc thiết kế có cấu trúc xoay quanh mục tiêu giúp bạn tạo ra các ứng dụng hoạt động tốt trên nền tảng Heroku. Chúng cũng có thể có ý nghĩa trong các ngữ cảnh khác. Một số nguyên tắc thực sự là những ràng buộc dựa trên các hành vi mà ứng dụng của bạn cần thể hiện để hoạt động trên Heroku. Ràng buộc thực sự là một thứ rất khó (hoặc hầu như không thể) thay đổi, trong khi các nguyên tắc là thứ chúng ta quyết định lựa chọn. Bạn có thể quyết định gọi rõ ràng những điều đó là nguyên tắc so với những điều là ràng buộc, để giúp chỉ ra những điều bạn thực sự không thể thay đổi. Cá nhân tôi nghĩ rằng có thể có một số giá trị trong việc giữ chúng trong cùng một danh sách để khuyến khích những ràng buộc đầy thử thách thỉnh thoảng và xem liệu chúng có thực sự bất di bất dịch hay không!
Thực hành
Thực hành của chúng tôi là cách chúng tôi đảm bảo các nguyên tắc của chúng tôi đang được thực hiện. Chúng là một tập hợp các hướng dẫn chi tiết, thiết thực để thực hiện các nhiệm vụ. Chúng thường sẽ là công nghệ cụ thể, và phải đủ đơn giản để bất kỳ developer nào cũng có thể hiểu được chúng. Các phương pháp thực hành có thể bao gồm hướng dẫn mã hóa, thực tế là tất cả dữ liệu log cần được quản lý tập trung hoặc HTTP / REST là kiểu tích hợp tiêu chuẩn. Do bản chất kỹ thuật của chúng, các thực hành thường sẽ thay đổi thường xuyên hơn các nguyên tắc.
Cũng như các nguyên tắc, đôi khi thực hành phản ánh những hạn chế trong tổ chức của bạn. Ví dụ: nếu bạn chỉ hỗ trợ CentOS, điều này sẽ cần được phản ánh trong thực hành của bạn.
Thực hành nên làm nền tảng cho các nguyên tắc của chúng tôi. Một nguyên tắc nêu rõ rằng các nhóm deliver vận hành toàn bộ vòng đời của hệ thống của họ có thể có nghĩa là bạn có một thông lệ cho rằng tất cả các dịch vụ được deploy vào các tài khoản AWS riêng biệt, cung cấp khả năng tự quản lý tài nguyên và cách ly khỏi các nhóm khác.
Kết hợp các nguyên tắc và thực hành
Nguyên tắc của một người là thực hành của người khác. Ví dụ, bạn có thể quyết định gọi việc sử dụng HTTP/REST là một nguyên tắc hơn là một thực hành. Và điều đó sẽ ổn thôi. Điểm mấu chốt là giá trị khi có những ý tưởng bao quát hướng dẫn cách hệ thống phát triển và đủ chi tiết để mọi người biết cách thực hiện những ý tưởng đó. Đối với một nhóm đủ nhỏ, có lẽ là một nhóm duy nhất, việc kết hợp các nguyên tắc và thực hành có thể ổn. Tuy nhiên, đối với các tổ chức lớn hơn, nơi công nghệ và phương thức làm việc có thể khác nhau, bạn có thể muốn có một bộ thực hành khác ở những nơi chỗ khác nhau, miễn là cả hai đều hướng tới một bộ nguyên tắc chung. Ví dụ, một nhóm .NET có thể có một bộthực hành và một nhóm Java khác, với một bộ thực hành chung cho cả hai. Tuy nhiên, các nguyên tắc có thể giống nhau cho cả hai.
Một ví dụ trong thế giới thực
Đồng nghiệp của tôi, Evan Bottcher, đã phát triển sơ đồ thể hiện trong Hình 2-1 trong quá trình làm việc với một trong những khách hàng của chúng tôi. Hình này cho thấy sự tác động lẫn nhau của các mục tiêu, nguyên tắc và thực hành theo một định dạng rất rõ ràng. Trong vòng một vài năm, các hoạt động ở ngoài cùng bên phải sẽ thay đổi khá thường xuyên, trong khi các nguyên tắc vẫn khá tĩnh. Một sơ đồ như thế này có thể được in một cách độc đáo trên một tờ giấy và được chia sẻ, và mỗi ý tưởng đủ đơn giản để các developer trung bình có thể ghi nhớ. Tất nhiên, có nhiều chi tiết hơn đằng sau mỗi điểm ở đây, nhưng có thể trình bày rõ điều này ở dạng tổng hợp là rất hữu ích.
Hình 2-1. Một ví dụ thực tế về các nguyên tắc và thực hành
Có lý do để có tài liệu hỗ trợ một số mục này. Tuy nhiên, về cơ bản, tôi thích ý tưởng có một bộ mã nguồn mẫu mà bạn có thể xem, kiểm tra và chạy, là hiện thân của những ý tưởng này. Thậm chí tốt hơn, chúng ta có thể tạo ra công cụ làm đúng việc. Chúng ta sẽ thảo luận sâu hơn về vấn đề đó trong giây lát.
Tiêu chuẩn bắt buộc
Khi bạn đang nghiên cứu các phương pháp của mình và suy nghĩ về những đánh đổi mà bạn cần thực hiện, một trong những điểm cân bằng cốt lõi cần tìm là mức độ biến thiên cho phép trong hệ thống của bạn. Một trong những cách quan trọng để xác định những gì nên không đổi từ dịch vụ này sang dịch vụ khác là xác định một dịch vụ tốt, hoạt động tốt trông như thế nào. Dịch vụ “tốt” trong hệ thống của bạn là gì? Nó cần có những khả năng nào để đảm bảo rằng hệ thống của bạn có thể quản lý được và một dịch vụ không tốt sẽ không làm hỏng toàn bộ hệ thống? Và, cũng như với mọi người, những gì một công dân tốt trong một bối cảnh không phản ánh những gì nó trông như thế nào ở một nơi khác. Tuy nhiên, có một số đặc điểm chung của các dịch vụ hoạt động tốt mà tôi nghĩ là khá quan trọng để quan sát. Đây là một số lĩnh vực chính mà việc mọi thứ có quá nhiều khác biệt có thể dẫn đến một thời kì cực kì khó khăn. Như Ben Christensen từ Netflix đã nói, khi chúng ta nghĩ về bức tranh lớn hơn, “nó cần phải là một hệ thống gắn kết được tạo thành từ nhiều bộ phận nhỏ có vòng đời tự trị nhưng tất cả lại kết hợp với nhau”. Vì vậy, chúng tôi cần tìm sự cân bằng giữa việc tối ưu hóa choquyền tự chủ của từng microservice mà không làm hỏngbức tranh toàn cảnh. Xác định các thuộc tính rõ ràng mà mỗi dịch vụ phải có là một cách để xác định rõ ràng sự cân bằng đó nằm ở đâu.
Giám sát
Điều cốt lõiở đây là chúng tôi có thể vẽ ra các quan điểm nhất quán, tầm nhìn xuyên suốt các service về tình trạng sức khoẻ của hệ thống. Đây phải là cái nhìn toàn hệ thống, không phải là từng dịch vụ đơn lẻ. Như chúng ta sẽ thảo luận trong Chương 8, việc biết tình trạng của từng dịch vụ là hữu ích, nhưng thường chỉ khi bạn đang cố gắng chẩn đoán một vấn đề rộng hơn hoặc hiểu một xu hướng lớn hơn. Để làm cho điều này dễ dàng nhất có thể, tôi khuyên bạn nên đảm bảo rằng tất cả các dịch vụ cần đưa ra các chỉ số liên quan đến sức khỏe và giám sát chung theo cùng một cách.
Bạn có thể chọn áp dụng cơ chế đẩy (push), trong đó mỗi dịch vụ cần đẩy dữ liệu này vào một trung tâm xử lý. Đối với các chỉ số, đây có thể là Graphite, và đối với sức khoẻ của hệ thống, nó có thể là Nagios. Hoặc bạn có thể quyết định sử dụng hệ thống polling để lấy dữ liệu từ chính các nút. Nhưng bất cứ điều gì bạn chọn, hãy cố gắng giữ cho nó được chuẩn hóa. Làm cho công nghệ bên trong hộp trở nên mờ đục và không yêu cầu hệ thống giám sát của bạn phải thay đổi để hỗ trợ nó. Có một yêu cầu ở đây: log cần đặt tập trung ở một chỗ
Giao diện – Giao thức
Chọn một số lượng nhỏ các công nghệ giao diện đã xác định sẽ giúp tích hợp những người dùng mới. Một là một con số tốt khi nói về số lượng tiêu chuẩn. Hai cũng không quá tệ. Có 20 kiểu tích hợp khác nhau thì không tốt. Đây không chỉ là việc chọn công nghệ và giao thức. Ví dụ: nếu bạn chọn HTTP/REST, bạn sẽ sử dụng động từ hay danh từ? Bạn sẽ xử lý việc phân trang tài nguyên như thế nào? Bạn sẽ xử lý việc lập phiên bản của các điểm cuối như thế nào?
An toàn về mặt kiến trúc
Chúng tôi không thể để một dịch vụ có hành vi xấu làm hỏng bữa tiệc của tất cả mọi người. Chúng tôi phải đảm bảo rằng các dịch vụ của chúng tôi bảo vệ bản thân chúng khỏi nhưng vấn đề như downtime hay không thể gọi đến dịch vụ. Chúng ta càng có nhiều dịch vụ mà không xử lý đúng về khả năng thất bại của các dịch vụ khác khi gọi đến, thì hệ thống của chúng ta sẽ càng trở nên mong manh hơn. Điều này có nghĩa là bạn có thể sẽ muốn bắt buộc tối thiểu mỗi dịch vụ mà gọi đến dịch vụ khác phải có connection pool riêng và bạn thậm chí có thể đi xa hơn khi nói rằng mỗi dịch vụ cũng sử dụng một bộ ngắt mạch (circuit breaker). Điều này sẽ được đề cập sâu hơn khi chúng ta thảo luận về microservices ở quy mô lớn trong Chương 11.
Chơi theo luật cũng quan trọng khi nói đến mã phản hồi (response code). Nếu bộ ngắt mạch của bạn dựa vào mã HTTP và một dịch vụ quyết định gửi lại mã 2XX do lỗi hoặc nhầm lẫn mã 4XX với mã 5XX, thì các biện pháp an toàn này có thể bị ảnh hưởng. Các mối quan tâm tương tự sẽ áp dụng ngay cả khi bạn không sử dụng HTTP; hiểu sự khác biệt giữa một yêu cầu OK và được xử lý chính xác, một yêu cầu không tốt và ngăn dịch vụ làm bất cứ điều gì với nó và một yêu cầu có thể OK nhưng chúng tôi không thể biết được vì máy chủ không hoạt động là chìa khóa để đảm bảo chúng tôi có thể thất bại nhanh chóng và theo dõi các vấn đề. Nếu các dịch vụ của chúng tôi hoạt động nhanh và lỏng lẻo với các quy tắc này, chúng tôi sẽ dẫn đến một hệ thống dễ bị tấn công hơn.
Quản trị thông qua quy tắc
Cùng nhau và thống nhất về cách mọi thứ có thể được thực hiện là một ý kiến hay. Nhưng dành thời gian để đảm bảo rằng mọi người đang tuân theo các nguyên tắc này sẽ kém thú vị hơn, vì đang đặt gánh nặng lên các developer trong việc deploy tất cả những điều tiêu chuẩn này mà bạn mong đợi mỗi dịch vụ thực hiện. Tôi rất tin tưởng vào việc giúp bạn dễ dàng làm điều đúng đắn. Hai kỹ thuật mà tôi thấy hoạt động tốt ở đây là sử dụng các mẫu và cung cấp các khuôn mẫu dịch vụ.
Người làm mẫu
Viết tài liệu là tốt và hữu ích. Tôi thấy rõ giá trị, nên sau tất cả tôi đã viết cuốn sách này. Nhưng các developer cũng thích viết mã nguồn, và mã nguồn là thứ mà họ có thể chạy và khám phá. Nếu bạn có một bộ tiêu chuẩn hoặc phương pháp hay nhất mà bạn muốn khuyến khích, thì việc có những ví dụ mẫu mà bạn có thể chỉ cho mọi người sẽ hữu ích. Ý tưởng là mọi người không thể sai lầm chỉ bằng cách bắt chước một số bộ phận tốt hơn trong hệ thống của bạn.
Lý tưởng nhất, đây phải là những dịch vụ trong thế giới thực mà bạn có để làm mọi thứ ổn thỏa, chứ không phải là những dịch vụ biệt lập chỉ được deploy để trở thành những ví dụ hoàn hảo. Bằng cách đảm bảo rằng những ví dụ mẫu của bạn thực sự đang được sử dụng, bạn đảm bảo rằng tất cả các nguyên tắc bạn thực hiện thực sự có ý nghĩa.
Một dịch vụ mẫu phù hợp
Sẽ thật tuyệt nếu bạn có thể giúp tất cả các developer thực sự dễ dàng tuân theo hầu hết các nguyên tắc mà bạn có với rất ít công việc phải không? Điều gì sẽ xảy ra nếu, ngay từ đầu, các developer đã có hầu hết các mã nguồn để deploy các thuộc tính cốt lõi mà mỗi dịch vụ cần?
Dropwizard và Karyon là hai micro container mã nguồn mở, dựa trên JVM. Chúng hoạt động theo những cách tương tự, tập hợp một bộ các thư viện lại với nhau để cung cấp các tính năng như kiểm tra tình trạng, phục vụ HTTP hoặc hiển thị số liệu. Vì vậy, ngay từ đầu, bạn đã có một dịch vụ hoàn chỉnh với một servlet container có thể được nhúng và khởi chạy từ dòng lệnh. Đây là một cách tuyệt vời để bắt đầu, nhưng tại sao lại dừng lại ở đó? Trong khi bạn đang sử dụng nó, tại sao không lấy một cái gì đó như Dropwizard hoặc Karyon và thêm nhiều tính năng hơn để nó trở nên phù hợp với ngữ cảnh của bạn?
Ví dụ, bạn có thể muốn bắt buộc sử dụng bộ ngắt mạch. Trong trường hợp đó, bạn có thể tích hợp một thư viện như Hystrix. Hoặc bạn có thể có một thực tế rằng tất cả các chỉ số của bạn cần phải được gửi đến một máy chủ Graphite trung tâm, vì vậy có thể kéo thư viện mã nguồn mở như Dropwizard’s Metrics và định cấu hình nó để thời gian phản hồi và tỷ lệ lỗi được đẩy tự động đến một vị trí đã biết.
Bằng cách điều chỉnh một khuôn mẫu dịch vụ như vậy cho tập hợp các phương pháp phát triển của riêng bạn, bạn đảm bảo rằng các nhóm có thể tiến hành nhanh hơn và các developer cũng phải cố gắng làm cho dịch vụ của họ hoạt động dù trong điều kiện không tốt.
Tất nhiên, nếu bạn chấp nhận nhiều stack công nghệ khác nhau, bạn sẽ cần một khuôn mẫu dịch vụ phù hợp cho từng loại. Tuy nhiên, đây có thể là một cách bạn hạn chế một cách tinh vi các lựa chọn ngôn ngữ trong nhóm của mình. Nếu mẫu dịch vụ nội bộ chỉ hỗ trợ Java, thì mọi người có thể không khuyến khích chọn các stack thay thế nếu họ phải tự làm nhiều việc hơn. Netflix, chẳng hạn, đặc biệt quan tâm đến các khía cạnh như khả năng chịu lỗi, để đảm bảo rằng sự cố ngừng hoạt động của một bộ phận trong hệ thống của họ không thể khiến mọi thứ không hoạt động theo. Để xử lý điều này, một lượng lớn công việc đã được thực hiện để đảm bảo rằng có các thư viện trên JVM để cung cấp cho các nhóm các công cụ họ cần để giữ cho các dịch vụ của họ hoạt động tốt. Bất kỳ ai giới thiệu một stack công nghệ mới có nghĩa là phải tái tạo tất cả nỗ lực này. Mối quan tâm chính đối với Netflix không phải là về nỗ lực trùng lặp mà thiên về thực tế là rất dễ mắc phải sai lầm này. Rủi ro mà dịch vụ không thể chịu lỗi khi có nhưng phần được làm mới khi deploy là cao nếu nó có thể ảnh hưởng nhiều hơn đến hệ thống. Netflix giảm thiểu điều này bằng cách sử dụng các dịch vụ sidecar, giao tiếp cục bộ với một JVM đang sử dụng các thư viện thích hợp.
Bạn phải cẩn thận rằng việc tạo khuôn mẫu dịch vụ không trở thành công việc của một nhóm công cụ hoặc một nhóm kiến trúc sư tập trung, những người chỉ định cách mọi thứ nên được thực hiện, mặc dù thông qua mã nguồn. Việc xác định các phương pháp bạn sử dụng phải là một hoạt động tập thể, vì vậy, lý tưởng nhất là (các) nhóm của bạn nên chịu trách nhiệm chung về việc cập nhật khuôn mẫu này (phương pháp tiếp cận nguồn mở nội bộ hoạt động tốt ở đây).
Tôi cũng đã thấy tinh thần và năng suất của nhiều nhóm bị suy giảm nghiêm trọng khi có một framework bị áp đặt sử dụng. Trong nỗ lực cải thiện khả năng tái sử dụng mã nguồn, ngày càng nhiều công việc được đặt vào một framework tập trung cho đến khi nó trở thành một con quái vật khổng lồ. Nếu bạn quyết định sử dụng một khuôn mẫu dịch vụ phù hợp, hãy suy nghĩ thật kỹ về công việc của nó. Lý tưởng nhất, việc sử dụng nó nên hoàn toàn là tùy chọn, nhưng nếu bạn muốn áp dụng nó một cách mạnh mẽ hơn, bạn cần hiểu rằng tính dễ sử dụng cho các developer sẽ là động lực chính.
Cũng cần lưu ý về những nguy cơ khi mã nguồn được chia sẻ lại. Với mong muốn tạo ra mã có thể sử dụng lại, chúng ta có thể đã tạo ra nguồn gốc của sự bó buộc (coupling) giữa các dịch vụ. Ít nhất một tổ chức mà tôi đã nói chuyện với họ, họ lo lắng về điều này đến nỗi nó thực sự đã sao chép mã nguồn khuôn mẫu dịch vụ của mình theo cách thủ công vào mỗi dịch vụ. Điều này có nghĩa là việc nâng cấp lên khuôn mẫu dịch vụ cốt lõi sẽ mất nhiều thời gian hơn để được áp dụng trên toàn hệ thống, nhưng điều này ít liên quan đến nó hơn là nguy cơ của sự bó buộc. Các nhóm khác mà tôi đã nói chuyện chỉ đơn giản coi mẫu dịch vụ là một dependency kiểu nhị phân (đã được compile) được chia sẻ, mặc dù họ phải rất chăm chỉ trong việc không để bị DRY (don’t repeat yourself – đừng lặp lại chính mình) dẫn đến một hệ thống kết hợp quá chặt chẽ với nhau! Đây là một chủ đề mang nhiều sắc thái, vì vậy chúng ta sẽ khám phá chi tiết hơn trong Chương 4.
[note1]: Đây là một nguyên tắc trong việc phát triển phần mềm, nói đến việc không để mã nguồn bị lặp lại. Trong cuốn The Pragmatic Programer thì Dry được định nghĩa như sau: “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”
Nợ kỹ thuật – Technical Debt
Chúng ta thường bị đặt vào những tình huống mà chúng ta không thể theo kịp về tầm nhìn kỹ thuật của chúng ta. Thông thường, chúng ta cần phải lựa chọn cắt một vài góc để có được một số tính năng cấp thiết. Đây chỉ là một sự đánh đổi nữa mà chúng ta sẽ thấy mình phải thực hiện. Tầm nhìn kỹ thuật của chúng ta tồn tại là có lý do. Nếu chúng ta đi chệch khỏi lý do này, nó có thể mang lại lợi ích ngắn hạn nhưng phải trả giá dài hạn. Một khái niệm giúp chúng ta hiểu sự đánh đổi này là nợ kỹ thuật (technical debt). Khi chúng ta tích lũy món nợ này, cũng giống như nợ trong thế giới thực, nó có chi phí liên tục và là thứ chúng ta muốn trả bớt.
Đôi khi nợ kỹ thuật không chỉ là thứ mà chúng ta gây ra bằng cách đi tắt. Điều gì xảy ra nếu tầm nhìn của chúng ta đối với hệ thống thay đổi, nhưng không phải tất cả hệ thống của chúng ta đều phù hợp? Trong tình huống này, chúng tôi đã tạo ra các nguồn nợ kỹ thuật mới.
Công việc của kiến trúc sư hệ thống là nhìn vào bức tranh toàn cảnh hơn và hiểu được sự cân bằng này. Có một số quan điểm về mức độ nợ và những chỗ ảnh hưởng, là điều quan trọng. Tùy thuộc vào tổ chức của bạn, bạn có thể cung cấp một hướng dẫn theo kiểu nhẹ nhàng, và để các nhóm tự quyết định cách theo dõi và thanh toán khoản nợ. Đối với các tổ chức khác, bạn có thể cần phải làm việc có nguyên tắc và cấu trúc hơn, có thể là duy trì việc log những khoản nợ và xem xét thường xuyên.
Xử lý ngoại lệ
Vì vậy, các nguyên tắc và thực tiễn của chúng ta hướng dẫn cách xây dựng hệ thống của chúng ta. Nhưng điều gì xảy ra khi hệ thống của chúng ta đi chệch hướng này? Đôi khi chúng ta đưa ra quyết định chỉ là một ngoại lệ của quy tắc. Trong những trường hợp này, bạn nên ghi lại quyết định như vậy vào log ở đâu đó để tham khảo trong tương lai. Nếu tìm thấy đủ các trường hợp ngoại lệ, chúng ta cũng có thể thay đổi nguyên tắc hoặc thông lệ để phản ánh một cách hiểu mới về thế giới. Ví dụ, chúng tôi có thể có một thông lệ nói rằng chúng ta sẽ luôn sử dụng MySQL để lưu trữ dữ liệu. Nhưng sau đó chúng tôi thấy những lý do thuyết phục để sử dụng Cassandra để lưu trữ có khả năng mở rộng cao, tại thời điểm đó, chúng tôi thay đổi cách nói của mình để nói, “Sử dụng MySQL cho hầu hết các yêu cầu lưu trữ, trừ khi bạn mong đợi sự tăng trưởng lớn về khối lượng dữ liệu, trong trường hợp đó hãy sử dụng Cassandra.”
Tuy nhiên, tôi cần phải nhắc lại rằng mọi tổ chức đều khác nhau. Tôi đã làm việc với một số công ty nơi các nhóm phát triển có mức độ tin cậy và quyền tự chủ cao và ở đó các nguyên tắc rất gọn nhẹ (và nhu cầu xử lý ngoại lệ công khai sẽ giảm đáng kể nếu không bị loại bỏ). Trong các khu tổ chức có cấu trúc hơn, trong đó các developer có ít tự do hơn, việc theo dõi các ngoại lệ có thể rất quan trọng để đảm bảo rằng các quy tắc được đưa ra phản ánh đúng những thách thức mà mọi người đang phải đối mặt. Với tất cả những gì đã nói, tôi là một fan hâm mộ của microservices như một cách tối ưu hóa quyền tự chủ của các nhóm, mang lại cho họ nhiều quyền tự do nhất có thể để giải quyết vấn đề trong tầm tay. Nếu bạn đang làm việc trong một tổ chức đặt ra nhiều hạn chế về cách các developer có thể thực hiện công việc của họ, thì microservices có thể không dành cho bạn.
Quản trị và Lãnh đạo từ Trung tâm
Một phần của những gì kiến trúc sư hệ thống cần xử lý là quản trị. Quản trị mà tôi muốn đề cập đến là gì? Hóa ra Kiểm soát mục tiêu đối với Công nghệ Thông tin và những thứ Liên quan (Control Objectives for Information and Related Technology – COBIT) có một định nghĩa khá hay:
Quản trị đảm bảo rằng các mục tiêu của doanh nghiệp đạt được bằng cách đánh giá các nhu cầu, điều kiện và lựa chọn của các bên liên quan; thiết lập phương hướng thông qua ưu tiên và ra quyết định; và giám sát việc thực hiện, tuân thủ và tiến độ so với chỉ đạo và mục tiêu đã thống nhất.
— COBIT 5
[note2]: đây một framework được đề xuất bởi ISACA (Hiệp hội Kiểm tra và Kiểm soát Hệ thống Thông tin), nhằm giúp các tổ chức đang tìm cách phát triển, triển khai, giám sát và cải thiện quản trị CNTT và quản lý thông tin.
Quản trị có thể áp dụng cho nhiều thứ trong diễn đàn CNTT. Chúng tôi muốn tập trung vào khía cạnh quản trị kỹ thuật, điều mà tôi cảm thấy là công việc của kiến trúc sư. Nếu một trong những công việc của kiến trúc sư là đảm bảo tầm nhìn kỹ thuật, thì quản trị là đảm bảo những gì chúng tôi đang xây dựng phù hợp với tầm nhìn này và phát triển tầm nhìn nếu cần.
Kiến trúc sư chịu trách nhiệm về rất nhiều thứ. Họ cần đảm bảo có một bộ nguyên tắc có thể hướng dẫn sự phát triển và những nguyên tắc này phù hợp với chiến lược của tổ chức. Họ cũng cần đảm bảo rằng những nguyên tắc này không yêu cầu các phương pháp làm việc khiến các developer phải khổ sở vì nó. Họ cần cập nhật công nghệ mới và biết khi nào cần đánh đổi đúng. Đây là một trách nhiệm lớn khủng khiếp. Tất cả những điều đó, và họ cũng cần kéo mọi người theo — nghĩa là, để đảm bảo rằng các đồng nghiệp mà họ đang làm việc hiểu được các quyết định đang được đưa ra và được đưa vào thực tế để thực hiện chúng. Ồ, và như chúng tôi đã đề cập: họ cần dành một chút thời gian với các nhóm để hiểu tác động của các quyết định của họ và thậm chí có thể viết mã nguồn nữa.
Đó là một yêu cầu cao? Chắc chắn rồi. Nhưng tôi chắc chắn với quan điểm rằng họ không nên làm điều này một mình. Một nhóm quản trị hoạt động tốt có thể làm việc cùng nhau để chia sẻ công việc và định hình tầm nhìn.
Thông thường, quản trị là hoạt động của nhóm. Đó có thể là một cuộc trò chuyện thân mật với một nhóm đủ nhỏ hoặc một cuộc họp thường xuyên có cấu trúc hơn với tư cách thành viên nhóm chính thức cho phạm vi lớn hơn. Đây là lúc tôi nghĩ các nguyên tắc mà chúng ta đã đề cập trước đó nên được thảo luận và thay đổi theo yêu cầu. Nhóm này cần được dẫn dắt bởi một tay công nghệ và chủ yếu bao gồm những người đang thực hiện công việc được quản lý. Nhóm này cũng phải chịu trách nhiệm theo dõi và quản lý các rủi ro kỹ thuật.
Một mô hình mà tôi rất thích là có kiến trúc sư chủ trì nhóm, nhưng có phần lớn nhóm được thu hút từ các tay công nghệ của mỗi nhóm deliver sản phẩm — tối thiểu là những nhóm trưởng. Kiến trúc sư chịu trách nhiệm đảm bảo nhóm hoạt động, nhưng toàn bộ nhóm chịu trách nhiệm quản trị. Điều này chia sẻ công việc của kiến trúc sư và đảm bảo rằng có mức độ tham gia cao hơn vào các quyết định từ các nhóm. Nó cũng đảm bảo rằng thông tin luân chuyển tự do từ các nhóm vào toàn bộ tập thể và kết quả là việc đưa ra quyết định trở nên hợp lý và đầy đủ thông tin hơn nhiều.
Đôi khi, nhóm có thể đưa ra quyết định mà kiến trúc sư không đồng ý. Lúc này, kiến trúc sư phải làm gì? Tôi đã gặp tình trạng này trước đây, và có thể nói với bạn đây là một trong những tình huống khó khăn nhất phải đối mặt. Thông thường, tôi thực hiện cách tiếp cận mà tôi nên đi với quyết định của nhóm. Tôi cho rằng tôi đã cố gắng hết sức để thuyết phục mọi người, nhưng cuối cùng thì tôi thấy không đủ thuyết phục. Nhóm thường khôn ngoan hơn nhiều so với từng cá nhân, và tôi đã nhiều lần bị chứng minh là sai! Và hãy tưởng tượng việc một nhóm được cho không gian để đưa ra quyết định, và cuối cùng bị bỏ qua. Nhưng đôi khi tôi đã bác bỏ ý kiến của cả nhóm. Nhưng tại sao, và khi nào? Làm thế nào để bạn chọn cách làm?
Hãy nghĩ đến việc dạy trẻ đi xe đạp. Bạn không thể đi hộ cho họ. Bạn thấy chúng chao đảo, nhưng nếu bạn tham gia vào mỗi lần như thể chúng có thể ngã ra, thì chúng sẽ không bao giờ học được và trong mọi trường hợp, chúng sẽ ngã ra ít hơn bạn nghĩ! Nhưng nếu bạn thấy họ chuẩn bị lao vào dòng xe cộ hoặc vào một cái ao gần đó, thì bạn phải can thiệp. Tương tự như vậy, là một kiến trúc sư, bạn cần phải nắm chắc khi nào, theo nghĩa bóng, nhóm của bạn đang lái vào một cái ao. Bạn cũng cần lưu ý rằng ngay cả khi bạn biết mình đúng và bác bỏ ý kiến của nhóm, điều này có thể làm suy yếu vị trí của bạn và cũng khiến nhóm cảm thấy rằng họ không hề có tiếng nói. Đôi khi điều đúng đắn là đi cùng với một quyết định mà bạn không đồng ý. Biết khi nào nên làm điều này và khi nào không nên làm điều này là khó khăn, nhưng đôi khi đó lại là một điều quan trọng.
Xây dựng đội ngũ
Trở thành người chính chịu trách nhiệm về tầm nhìn kỹ thuật của hệ thống của bạn và đảm bảo rằng bạn đang thực hiện tầm nhìn này không chỉ là việc đưa ra quyết định về công nghệ. Chính những người bạn làm việc cùng sẽ thực hiện công việc. Phần lớn vai trò của người lãnh đạo kỹ thuật là giúp phát triển họ — giúp họ tự hiểu tầm nhìn — và cũng đảm bảo rằng họ cũng có thể là những người tham gia tích cực trong việc định hình và thực hiện tầm nhìn.
Giúp những người xung quanh bạn phát triển sự nghiệp có thể có nhiều hình thức, hầu hết đều nằm ngoài phạm vi của cuốn sách này. Tuy nhiên, có một khía cạnh mà kiến trúc microservice đặc biệt có liên quan. Với các hệ thống lớn hơn, monolithic, có ít cơ hội hơn để mọi người nâng cấp và sở hữu thứ gì đó. Mặt khác, với các microservice, chúng tôi có nhiều mã nguồn cơ sở tự trị và sẽ có các vòng đời độc lập của riêng chúng. Giúp mọi người thăng tiến bằng cách để họ làm chủ các dịch vụ riêng lẻ trước khi nhận thêm trách nhiệm có thể là một cách tuyệt vời để giúp họ đạt được mục tiêu nghề nghiệp của riêng mình, đồng thời giảm bớt gánh nặng cho người phụ trách!
Tôi rất tin tưởng rằng phần mềm tuyệt vời đến từ những con người tuyệt vời. Nếu bạn chỉ lo lắng về khía cạnh công nghệ của phương trình, bạn đang thiếu một nửa bức tranh rồi đó.
Tóm tắt
Để tóm tắt chương này, đây là những gì tôi thấy là trách nhiệm cốt lõi của kiến trúc sư khi phát triển:
Tầm nhìn
Đảm bảo có một tầm nhìn kỹ thuật được truyền đạt rõ ràng cho hệ thống sẽ giúp hệ thống của bạn đáp ứng các yêu cầu của khách hàng và tổ chức của bạn
Đồng cảm
Hiểu tác động của các quyết định của bạn đối với khách hàng và đồng nghiệp của bạn
Sự hợp tác
Tương tác với càng nhiều đồng nghiệp và đồng nghiệp của bạn càng tốt để giúp xác định, tinh chỉnh và thực hiện tầm nhìn
Khả năng thích ứng
Đảm bảo rằng tầm nhìn kỹ thuật thay đổi khi khách hàng hoặc tổ chức của bạn yêu cầu
Quyền tự trị
Tìm sự cân bằng phù hợp giữa tiêu chuẩn hóa và tạo quyền tự chủ cho các nhóm của bạn
Quản trị
Đảm bảo rằng hệ thống đang được deploy phù hợp với tầm nhìn kỹ thuật
Kiến trúc sư của hệ thống khi tiến hóa là người hiểu rằng để đạt được kỳ tích này là một sự cân bằng liên tục. Các lực đẩy luôn thúc đẩy bạn theo cách này hay cách khác, và việc đứng ở đâu để đẩy lùi hoặc đi đâu với dòng chảy thường là điều thường đi liền với kinh nghiệm. Nhưng phản ứng tồi tệ nhất đối với tất cả những lực đẩy chúng ta đến sự thay đổi là trở nên cứng nhắc hoặc cố định hơn trong suy nghĩ của chúng ta.
Mặc dù phần lớn lời khuyên trong chương này có thể áp dụng cho bất kỳ kiến trúc sư của hệ thống nào, nhưng các microservice cho chúng ta nhiều quyết định hơn để đưa ra. Vì vậy, có thể cân bằng tốt hơn tất cả những sự đánh đổi này là điều cần thiết.
Trong chương tiếp theo, chúng ta sẽ đưa ra một số nhận thức mới về vai trò của kiến trúc sư đối với chúng ta khi chúng ta bắt đầu suy nghĩ về cách tìm ra ranh giới phù hợp cho microservices.
Microservice là một cách tiếp cận đối với các hệ thống phân tán nhằm thúc đẩy việc sử dụng các dịch vụ chi tiết có vòng đời riêng của chúng, các dịch vụ này cộng tác với nhau. Bởi vì các microservice chủ yếu được mô hình hóa xung quanh các lĩnh vực kinh doanh, chúng tránh được các vấn đề của kiến trúc phân cấp truyền thống. Microservices cũng tích hợp các công nghệ và kỹ thuật mới đã xuất hiện trong thập kỷ qua, giúp họ tránh được sự sụp đổ của nhiều deploy kiến trúc hướng dịch vụ.
Cuốn sách này có đầy đủ các ví dụ cụ thể về việc sử dụng microservice trên khắp thế giới, bao gồm các tổ chức như Netflix, Amazon, Gilt và nhóm REA, những người đều nhận thấy rằng sự tự chủ ngày càng tăng mà kiến trúc này mang lại cho các nhóm trong công ty đó thuận lợi rất lớn.
Ai nên đọc cuốn sách này
Phạm vi của cuốn sách này rất rộng, vì hàm ý của các kiến trúc microservice chi tiết cũng rất rộng. Do đó, nó sẽ thu hút những người quan tâm đến các khía cạnh của thiết kế, phát triển, deploy, kiểm thử và bảo trì hệ thống. Những người trong số các bạn đã bắt đầu hành trình hướng tới các kiến trúc chi tiết hơn, cho dù là ứng dụng greenfield (một hệ thống hoàn toàn mới) hay là một phần của việc phân rã một hệ thống hiện có, kiểu giống monolithic, sẽ tìm thấy rất nhiều lời khuyên thiết thực để giúp bạn. Nó cũng sẽ giúp những người trong số các bạn muốn biết mọi rắc rối là gì, để bạn có thể xác định xem microservices có phù hợp với mình hay không.
Tại sao tôi viết cuốn sách này
Tôi bắt đầu nghĩ về chủ đề kiến trúc ứng dụng từ nhiều năm trước, khi làm việc để giúp mọi người cung cấp phần mềm của họ nhanh hơn. Tôi nhận ra rằng mặc dù các kỹ thuật tự động hóa, kiểm thử và Continuous Delivery trên cơ sở hạ tầng có thể hữu ích, nhưng nếu thiết kế tinh thần cơ bản của hệ thống không giúp bạn dễ dàng thực hiện các thay đổi, thì có những giới hạn đối với những gì có thể hoàn thành.
Đồng thời, nhiều tổ chức đang kiểm thử các kiến trúc lưu trữ chi tiết hơn để đạt được các mục tiêu tương tự, nhưng cũng để đạt được những thứ như cải thiện quy mô, tăng quyền tự chủ của các nhóm hoặc dễ dàng nắm bắt các công nghệ mới hơn. Kinh nghiệm của riêng tôi, cũng như của các đồng nghiệp của tôi tại ThoughtWorks và các nơi khác, buộc phải thực tế rằng việc sử dụng số lượng lớn các dịch vụ hơn với vòng đời độc lập của chúng dẫn đến nhiều vấn đề đau đầu hơn phải giải quyết. Theo nhiều cách, cuốn sách này được hình dung như một cửa hàng tổng hợp có thể giúp bao gồm nhiều loại tài liệu hàng đầu cần thiết để hiểu về microservices — điều gì đó đã giúp tôi rất nhiều trong quá khứ!
Một từ để nói về Microservices hôm nay
Microservices là một chủ đề nóng, mọi thứ đều thay đổi nhanh chóng. Mặc dù ý tưởng không phải là mới (ngay cả khi chính thuật ngữ microservice cũng đã xuất hiện rất lâu), nhưng kinh nghiệm của mọi người trên khắp thế giới, cùng với sự xuất hiện của các công nghệ mới, đang có ảnh hưởng sâu sắc đến cách chúng được sử dụng. Do tốc độ thay đổi nhanh chóng, tôi đã cố gắng tập trung cuốn sách này để nói về các ý tưởng hơn là các công nghệ cụ thể, biết rằng các chi tiết deploy luôn thay đổi nhanh hơn những suy nghĩ đằng sau chúng. Tuy nhiên, tôi hoàn toàn mong đợi rằng trong một vài năm nữa kể từ bây giờ, chúng ta sẽ học được nhiều hơn nữa về vị thế phù hợp của microservices và cách sử dụng tốt microservice.
Vì vậy, trong khi tôi đã cố gắng hết sức để chắt lọc ra những tinh túy nhất của chủ đề trong cuốn sách này, nếu chủ đề này khiến bạn hứng thú, hãy chuẩn bị cho nhiều năm học hỏi liên tục để luôn dẫn đầu về lĩnh vực nghệ thuật!
Đọc quyển sách này như nào
Cuốn sách này chủ yếu được sắp xếp theo định dạng dựa trên chủ đề. Do đó, bạn có thể muốn chuyển sang các chủ đề cụ thể mà bạn quan tâm nhất. Mặc dù tôi đã cố gắng hết sức để tham khảo các điều khoản và ý tưởng trong các chương trước, nhưng tôi muốn nghĩ rằng ngay cả những người tự cho mình là có kinh nghiệm cũng sẽ tìm thấy điều gì đó quan tâm trong tất cả các chương ở đây. Tôi chắc chắn khuyên bạn nên xem qua Chương 2, phần này đề cập một cách bao quát nhất chủ đề microservicecũng như cung cấp một số khung cho cách tôi tiếp cận mọi thứ trong trường hợp nếu bạn muốn đi sâu hơn vào một số chủ đề sau này.
Đối với những người mới làm quen với chủ đề này, tôi đã cấu trúc các chương theo cách mà tôi hy vọng sẽ có ý nghĩa khi đọc từ đầu đến cuối.
Dưới đây là tổng quan về những gì chúng tôi đề cập:
Chương 1, Microservice
Chúng ta sẽ bắt đầu bằng phần giới thiệu về microservices, bao gồm những lợi ích chính cũng như một số nhược điểm.
Chương 2, Sự tiến hoá của Kiến trúc phần mềm
Chương này thảo luận về những khó khăn mà chúng ta sẽ gặp phải và đánh đổi, cân nhắc với tư cách là kiến trúc sư của phần mềm và trình bày cụ thể về bao nhiêu điều chúng ta cần suy nghĩ với microservices.
Chương 3, Làm thế nào để Mô hình hóa Dịch vụ
Ở đây, chúng ta sẽ bắt đầu xác định ranh giới của microservices, sử dụng các kỹ thuật từthiết kế theo hướng nghiệp vụ để giúp tập trung suy nghĩ của chúng tôi.
Chương 4, Tích hợp
Đây là nơi chúng ta bắt đầu tìm hiểu sâu hơn một chút về các công nghệ cụ thể, khi chúng ta thảo luận về những loại kỹ thuật cộng tác dịch vụ nào sẽ giúp ích nhiều nhất cho chúng ta. Chúng tôi cũng sẽ đi sâu vào chủ đề giao diện người dùng và tích hợp với các sản phẩm thương mại (COTS) cũ và thương mại.
Chương 5, Tách phầnmềm monolithic
Nhiều người quan tâm đến microservices như lời giải cho các hệ thống monolithic lớn, khó thay đổi và đây chính xác là những gì chúng tôi sẽ đề cập chi tiết trong phần này
Chương 6, Deploy
Mặc dù cuốn sách này chủ yếu là lý thuyết, nhưng một số chủ đề trong cuốn sách đã bị ảnh hưởng bởi những thay đổi gần đây trong công nghệ cũng như việc deploy, chúng ta sẽ khám phá những thứ liên quan đến vấn đề này ở đây
Chương 7, Thử nghiệm
Chương này đi sâu vào chủ đề kiểm thử, một lĩnh vực đặc biệt quan tâm khi xử lý việc deploy nhiều dịch vụ rời rạc. Đặc biệt lưu ý là vai trò của các hợp đồng do khách hàng định hướng có thể giúp chúng tôi đảm bảo chất lượng phần mềm của mình.
Chương 8, Giám sát
Kiểm tra phần mềm của chúng tôi trước khi đến môi trường production sẽ không hữu ích nếu sự cố xảy ra khi chúng tôi golive và chương này khám phá cách chúng tôi có thể giám sát các hệ thống chi tiết của mình và đáp ứng với một số sự phức tạp nổi bật của hệ thống phân tán.
Chương 9, Bảo mật
Ở đây, chúng tôi sẽ kiểm tra các khía cạnh bảo mật của microservices và xem xét cách xử lý xác thực và ủy quyền giữa người dùng với dịch vụ và giữa các dịch vụ. Bảo mật là một chủ đề rất quan trọng trong ngành khoa học máy tính, một chủ đề quá dễ bị xem nhẹ. Mặc dù tôi không phải là một chuyên gia bảo mật, nhưng tôi hy vọng rằng chương này ít nhất sẽ giúp bạn xem xét một số khía cạnh bạn cần lưu ý khi xây dựng hệ thống, và hệ thống microservice nói riêng.
Chương 10, ĐịnhluậtConway và Thiết kế Hệ thống
Chương này tập trung vào sự tác động lẫn nhau của cơ cấu tổ chức và kiến trúc. Nhiều tổ chức đã nhận ra rằng rắc rối sẽ xảy ra nếu bạn không giữ hai bên hòa hợp. Chúng tôi sẽ cố gắng giải quyết tận cùng tình thế khó xử này và xem xét một số cách khác nhau để điều chỉnh thiết kế hệ thống với cấu trúc của nhóm của bạn.
Chương 11, Microservice khi mở rộng
Đây là lúc chúng tôi bắt đầu xem xét việc thực hiện tất cả những điều này trên quy mô lớn, để chúng tôi có thể xử lý nguy cơ lỗi gia tăng có thể xảy ra với số lượng lớn dịch vụ cũng như lưu lượng truy cập lớn.
Chương 12, Kết hợp tất cả lại với nhau
Chương cuối cùng cố gắng chắt lọc bản chất cốt lõi của những gì làm cho các microservice trở nên khác biệt. Nó bao gồm một danh sách bảy nguyên tắc microservices, cũng như mộttóm tắt những điểm chính của cuốn sách.
Các quy ước được sử dụng trong cuốn sách này
Các quy ước về kiểu chữ sau đây được sử dụng trong cuốn sách này:
In nghiêng
Cho biết các thuật ngữ mới, URL, địa chỉ email, tên tệp và phần mở rộng tệp.
Độ rộng chữ không đổi
Được sử dụng cho danh sách chương trình, cũng như trong các đoạn văn để chỉ các phần của chương trình như tên biến hoặc hàm, cơ sở dữ liệu, kiểu dữ liệu, biến môi trường, câu lệnh và từ khóa.
Độ rộng chữ không đổi in đậm
Hiển thị các lệnh hoặc văn bản khác mà người dùng phải nhập theo nghĩa đen.
Độ rộng chữ không đổi in nghiêng
Hiển thị văn bản cần được thay thế bằng các giá trị do người dùng cung cấp hoặc bằng các giá trị được xác định theo ngữ cảnh.
Sự nhìn nhận
Cuốn sách này dành riêng cho Lindy Stephens, nếu không có cô ấy thì quyển sách này đã không hình thành. Cô ấy đã khuyến khích tôi bắt đầu cuộc hành trình này, hỗ trợ tôi trong suốt quá trình viết lách thường xuyên căng thẳng và là đối tác tốt nhất mà tôi có thể yêu cầu. Tôi cũng muốn dành điều này cho bố tôi, Howard Newman, người đã luôn ở bên tôi. Điều này là cho cả hai người.
Tôi muốn chọn ra Ben Christensen, Vivek Subramaniam và Martin Fowler vì đã đưa cho tôi những phản hồi chi tiết trong suốt quá trình viết, giúp định hình cuốn sách này. Tôi cũng muốn gửi lời cảm ơn đến James Lewis, người mà đã cùng tôi uống bia cùng thảo luận về những ý tưởng được trình bày trong cuốn sách này. Cuốn sách này sẽ chỉ là một cái bóng của chính nó nếu không có sự giúp đỡ và hướng dẫn của họ.
Ngoài ra, nhiều người khác đã giúp đỡ và phản hồi về các phiên bản đầu tiên của cuốn sách. Cụ thể, tôi muốn cảm ơn (không theo thứ tự cụ thể) Kane Venables, Anand Krishnaswamy, Kent McNeil, Charles Haynes, Chris Ford, Aidy Lewis, Will Thames, Jon Eaves, Rolf Russell, Badrinath Janakiraman, Daniel Bryant, Ian Robinson, Jim Webber, Stewart Gleadow, Evan Bottcher, Eric Sword, Olivia Leonard, và tất cả các đồng nghiệp khác của tôi tại ThoughtWorks và trong toàn ngành, những người đã giúp tôi đạt được điều này.
Cuối cùng, tôi muốn cảm ơn tất cả những người tại O’Reilly, bao gồm Mike Loukides vì đã đưa tôi vào hội đồng quản trị, biên tập viên Brian MacDonald, Rachel Monaghan, Kristen Brown, Betsy Waliszewski và tất cả những người khác đã giúp đỡ tôi theo cách có thể không bao giờ biết về.
CHƯƠNG 1 Microservices
Trong nhiều năm nay, chúng tôi đã và đang tìm ra những cách tốt hơn để xây dựng hệ thống. Chúng tôi đã học hỏi từ những gì đã có trước đó, áp dụng các công nghệ mới và quan sát cách một làn sóng công ty công nghệ mới hoạt động theo những cách khác nhau để tạo ra các hệ thống CNTT giúp làm cho cả khách hàng và nhà phát triển của chính họ hạnh phúc hơn.
Cuốn sách Domain-Driven Design (Addison-Wesley) của Eric Evans đã giúp chúng tôi hiểu tầm quan trọng của việc thể hiện thế giới thực trong mã nguồn của chúng tôi và chỉ cho chúng tôi những cách tốt hơn để lập mô hình hệ thống của chúng tôi. Khái niệm Continuous Delivery cho thấy cách chúng tôi có thể đưa phần mềm của mình lên môi trường production một cách ngày càng hiệu quả hơn, truyền cho chúng tôi ý tưởng rằng chúng tôi nên coi mọi thay đổi là một ứng cử viên release. Sự hiểu biết của chúng tôi về cách hoạt động của Web đã giúp chúng tôi phát triển những cách tốt hơn để máy giao tiếp với cácmáy. Khái niệm kiến trúc lục giác (hexagonal architecture) của Alistair Cockburn đã hướng dẫn chúng tôi thoát khỏi kiến trúc nhiều lớp nơi logic kinh doanh có thể ẩn giấu. Nền tảng ảo hóa cho phép chúng tôi cung cấp và thay đổi kích thước các server của mình theo ý muốn, với cơ sở hạ tầng tự động hóa cung cấp cho chúng tôi cách xử lý những máy này trên quy mô lớn. Một số tổ chức lớn, thành công như Amazon và Google tán thành quan điểm về các nhóm nhỏ sở hữu toàn bộ vòng đời dịch vụ của họ. Và, gần đây hơn, Netflix đã chia sẻ với chúng tôi các cách xây dựng hệ thống chống phân mảnh ở quy mô mà chỉ 10 năm trước khó có thể hiểu được.
Domain-driven. Continuous Delivery. Ảo hóa theo yêu cầu. Tự động việc deploy cơ sở hạ tầng. Các team nhỏtự trị. Hệ thống ở quy mô lớn. Microservices đã xuất hiện từ thế giới này. Chúng không được phát minh hoặc mô tả trước thực tế; chúng nổi lên như một xu hướng hoặc một kiểu mẫu, từ việc sử dụng trong thế giới thực. Nhưng chúng tồn tại chỉ vì tất cả những gì đã qua trước đó. Trong suốt cuốn sách này, tôi sẽ rút ra những điểm mấu chốt từ công việc trước đây để giúp vẽ nên bức tranh về cách xây dựng, quản lý và phát triển microservices.
Nhiều tổ chức đã phát hiện ra rằng bằng cách áp dụng một cách chi tiết kiến trúc microservice, họ có thể cung cấp phần mềm nhanh hơn và nắm bắt các công nghệ mới hơn. Microservicecho phép chúng ta tự do hơn đáng kể để phản ứng và đưa ra các quyết định khác nhau, cho phép chúng ta phản ứng nhanh hơn với sự thay đổi không thể tránh khỏi tác động đến tất cả chúng ta.
Microservices là gì?
Microservices là các dịch vụ nhỏ, tự trị hoạt động cùng nhau. Hãy làm rõ quan điểm đó xuống sâu một chút và xem xét các đặc điểm làm cho các microservice trở nên khác biệt.
Nhỏ và tập trung vào việc làm tốt một việc
Lượng mã nguồn tăng lên khi chúng tôi viết mã nguồn để thêm các tính năng mới. Theo thời gian, có thể khó biết nơi cần thực hiện thay đổi vì khối lượng mã nguồn quá lớn. Mặc dù có một định hướng cho các mã nguồn kiểumonolithic được chia mô-đun rõ ràng, nhưng tất cả các ranh giới trong quá trình tùy ý này thường bị chia nhỏ. Mã nguồnliên quan đến các chức năng tương tự nhau bắt đầu lan rộng khắp nơi, khiến việc sửa lỗi hoặc deploy khó khăn hơn.
Trong một hệ thống monolithic, chúng tôi chiến đấu chống lại những lực lượng này bằng cách cố gắng đảm bảo mã của chúng tôi gắn kết hơn, thường bằng cách tạo ra các mô-đun hoặc trừu tượng. Sự liên kết — động lực để có các mã liên quan được nhóm lại với nhau — là một khái niệm quan trọng khi chúng ta nghĩ về microservices. Điều này được củng cố bởi định nghĩa của Robert C. Martin về Nguyên tắc trách nhiệm duy nhất, trong đó nêu rõ "Tập hợp những thứ thay đổi vì cùng một lý do và tách những thứ thay đổi vì những lý do khác nhau."
Microservices áp dụng cách tiếp cận tương tự đối với các dịch vụ độc lập. Chúng tôi tập trung vào việc đặt ranh giới dịch vụ của mình vào ranh giới về nghiệp vụ, làm cho nó rõ ràng là nơi mã sống cho một phần chức năng nhất định. Và bằng cách giữ cho dịch vụ này tập trung vào một ranh giới rõ ràng, chúng tôi tránh xa những cám dỗ để nó tăng trưởng quá lớn, và tất cả những khó khăn liên quan mà điều này có thể gây ra.
Câu hỏi tôi thường được hỏi lànhỏ như thế nào là nhỏ_? Đưa ra một số dòng mã nguồn là cảmột vấn đề, vì một số ngôn ngữ dễbiểu đạt hơn những ngôn ngữ khác và do đó có thể làm được nhiều hơn với ít dòng mã hơn. Chúng ta cũng phải xem xét thực tế rằng chúng ta có nhiều dependenc, bản thân chúng chứa nhiều dòng mã. Ngoài ra, một số phần trong domain của bạn có thể phức tạp về mặt pháp lý, đòi hỏi nhiều mã nguồn hơn. Jon Eaves tại RealEstate.com.au ở Úc mô tả một microservice là thứ có thể được viết lại sau hai tuần, một quy tắc chung có ý nghĩa đối với bối cảnh cụ thể của anh ấy.
Một câu trả lời hơi sáo rỗng khác mà tôi có thể đưa ra làđủ nhỏ và không nhỏ hơn_. Khi phát biểu tại các hội nghị, tôi gần như luôn đặt câu hỏi rằngai có một hệ thống quá lớn và bạn muốn chia_nhỏ nó_? Gần như tất cả mọi người đều giơ tay. Chúng tôi dường như có cảm giác rất tốt về những gì là quá lớn và vì vậy có thể lập luận rằng một khi một đoạn mã không còncảm thấyquá lớn nữa, thì có lẽ nó đã đủ nhỏ.
Một yếu tố lớn trong việc giúp chúng tôi trả lờinhỏ như thế nào_? Làdịch vụ phù hợp với cấu trúc nhóm như thế nào. Nếu mã nguồn cơ sở quá lớn để được quản lý bởi một nhóm nhỏ, tìm cách chia nhỏ nó là rất hợp lý. Chúng ta sẽ nói thêm về việc sắp xếp tổ chức ở phần sau.
Khi nói đến việc nhỏ đến mức nào là đủ nhỏ, tôi muốn nghĩ theo các thuật ngữ sau: dịch vụ càng nhỏ, bạn càng tối đa hóa lợi ích và mặt trái của kiến trúc vi mô. Khi bạn có những service nhỏ hơn, những lợi ích xung quanh việc dependency lẫn nhau sẽ tăng lên. Nhưng một số phức tạp nổi lên từ việc ngày càng có nhiều bộ phận cùng phát triển, điều mà chúng ta sẽ khám phá trong suốt cuốn sách này. Khi bạn xử lý tốt hơn vớisự phức tạp này, bạn có thể cố gắng cho các dịch vụ nhỏ hơn và nhỏ hơn.
Tự chủ
Microservice của chúng tôi là một thực thể riêng biệt. Nó có thể được deploy như một dịch vụ biệt lập trên nền tảng như một dịch vụ (PAAS), hoặc nó có thể là quy trình hệ điều hành của riêng nó. Chúng tôi cố gắng tránh đóng gói nhiều dịch vụ vào cùng một máy, mặc dù định nghĩa vềmáytrong thế giới ngày nay khá mơ hồ! Như chúng ta sẽ thảo luận ở phần sau, mặc dù sự cô lập này có thể thêm một số chi phí, nhưng kết quả là sự đơn giản khiến hệ thống phân tán của chúng tôi dễ dàng lập luận hơn nhiều và các công nghệ mới hơn có thể giảm thiểu nhiều rủi ro liên quan đến hình thức deploy này.
Bản thân tất cả các giao tiếp giữa các dịch vụ đều thông qua các cuộc gọi mạng, để thực thi sự tách biệt giữa các dịch vụ và tránh các nguy cơ kết nối chặt chẽ.
Các dịch vụ này cần có khả năng thay đổi độc lập với nhau và được tự deploy mà không yêu cầu khách hàng thay đổi. Chúng tôi cần suy nghĩ về những gì dịch vụ của chúng tôi nên để lộ và những gì chúng nên cho phép được ẩn. Nếu có quá nhiều sự chia sẻ, các dịch vụ mà chúng tôi cung cấp sẽ lại trở nên liên kết chặt chẽ hơn với phần nội tại của dịch vụ. Điều này làm giảm quyền tự chủ của chúng tôi, vì nó đòi hỏi sự phối hợp bổ sung với người dùng khi thực hiện các thay đổi.
Dịch vụ của chúng tôi có giao diện lập trình ứng dụng (API) và các dịch vụ cộng tác giao tiếp với chúng tôi thông qua các API đó. Chúng tôi cũng cần phải suy nghĩ về thuật ngữ công nghệ nào phù hợp để đảm bảo rằng bản thân điều này không ảnh hưởng đến khách hàng. Điều này có thể có nghĩa là chọn các API không liên quan gì về công nghệ để đảm bảo rằng chúng tôi không bịhạn chế bởi các lựa chọn công nghệ. Chúng ta sẽ quay lại nhiều lần với tầm quan trọng của các API tốt, được tách biệt trong suốt cuốn sách này.
Nếu không tách rời, mọi thứ sẽ bị chia nhỏ đối với chúng tôi. Quy tắc vàng: bạn có thể thực hiện thay đổi đối với một dịch vụ và tự deploy dịch vụ đó mà không cần thay đổi bất kỳ điều gì khác không? Nếu câu trả lời là không, thì bạn sẽ khó đạt được nhiều lợi thế mà chúng ta thảo luận trong suốt cuốn sách này.
Để thực hiện tốt việc phân tách, bạn sẽ cần phải lập mô hình dịch vụ của mình phù hợp và nhận các API một cách đúng đắn. Tôi sẽ nói về điều đó rất nhiều.
Các lợi ích chính
Các lợi ích của microservices rất nhiều và đa dạng. Nhiều lợi ích trong số này có thể thấy ở bất kỳ hệ thống phân tán nào. Tuy nhiên, microservices có xu hướng đạt được những lợi ích này ở một mức độ lớn hơn chủ yếu là do chúng tiếp nhận các khái niệm đằng sau hệ thống phân tán và kiến trúc hướng dịch vụ đến đâu.
Tự do về công nghệ
Với một hệ thống bao gồm nhiều dịch vụ cộng tác với nhau, chúng tôi có thể quyết định sử dụng các công nghệ khác nhau bên trong mỗi dịch vụ. Điều này cho phép chúng tôi chọn công cụ phù hợp cho từng công việc, thay vì phải chọn cách tiếp cận tiêu chuẩn hóa hơn, một cách làm phù hợp với tất cả thường trở thành mẫu số chung thấp nhất.
Nếu một phần trong hệ thống của chúng tôi cần cải thiện hiệu suất của nó, chúng tôi có thể quyết định sử dụng một công nghệ khác có khả năng đạt được mức yêu cầu hiệu suất tốt hơn. Chúng tôi cũng có thể quyết định rằng cách chúng tôi lưu trữ dữ liệu của mình cần phải thay đổi đối với các phần khác nhau trong hệ thống của chúng tôi. Ví dụ: đối với mạng xã hội, chúng tôi có thể lưu trữ các tương tác của người dùng trong cơ sở dữ liệu hướng biểu đồ (GraphQL) để phản ánh bản chất có tính liên kết cao của biểu đồ xã hội, nhưng có lẽ các bài đăng mà người dùng thực hiện có thể được lưu trữ trong cơ sở dữ liệu kiểu tài liệu, tạo ra một kiến trúc không đồng nhất như thể hiện trong Hình 1-1.
Hình 1-1 Microservices có thể cho phép bạn dễ dàng nắm bắt các công nghệ khác nhau
Với microservices, chúng tôi cũng có thể áp dụng công nghệ nhanh hơn và hiểu rõ những tiến bộ mới có thể giúp chúng tôi như thế nào. Một trong những rào cản lớn nhất đối với việc thử và áp dụng công nghệ mới là những rủi ro đi kèm với nó. Với một ứng dụng monolithic, nếu tôi muốn thử một ngôn ngữ lập trình, cơ sở dữ liệu hoặc framework mới, thì bất kỳ thay đổi nào cũng sẽ ảnh hưởng đến một lượng lớn hệ thống của tôi. Với một hệ thống bao gồm nhiều dịch vụ, tôi có nhiều chỗ mới để thử một phần công nghệ mới. Tôi có thể chọn một dịch vụ có lẽ là rủi ro thấp nhất và sử dụng công nghệ ở đó, cầnbiết rằng tôi có thể hạn chế mọi tác động tiêu cực tiềm ẩn. Nhiều tổ chức nhận thấy khả năng tiếp thu nhanh hơn các công nghệ mới là một lợi thế thực sự của họ.
Tất nhiên, việc áp dụng nhiều công nghệ không phải là không có rủi ro hay chi phí. Một số tổ chức chọn đặt một số ràng buộc đối với lựa chọn ngôn ngữ. Netflix và Twitter, chẳng hạn, chủ yếu sử dụng Máy ảo Java (JVM) làm nền tảng, vì họ hiểu rất rõ về độ tin cậy và hiệu suất của hệ. Họ cũng phát triển các thư viện và công cụ cho JVM giúp hoạt động trên quy mô lớn dễ dàng hơn nhiều, nhưng lại gây khó khăn hơn cho các dịch vụ hoặc ứng dụng khách (client) không dựa trên Java. Nhưng cả Twitter và Netflix đều không chỉ sử dụng một stack công nghệ cho tất cả các công việc. Một điểm khác đối với những lo ngại về việc sử dụng các công nghệ khác nhau là khối lượng công việc, khối lượng mã nguồn. Nếu tôi thực sự có thể viết lại microservice của mình trong hai tuần, bạn có thể giảm thiểu rủi ro khi sử dụng công nghệ mới.
Như bạn sẽ thấy trong suốt cuốn sách này, cũng giống như nhiều điều liên quan đến microservices, đó là tất cả về việc tìm kiếm sự cân bằng phù hợp. Chúng ta sẽ thảo luận về cách lựa chọn công nghệ trong Chương 2, chương này tập trung vào việctiến hóakiến trúc; và trong Chương 4, liên quan đến tích hợp, bạn sẽ học cách đảm bảo rằng các dịch vụ của bạn có thể phát triển công nghệ của chúng một cách độc lập với nhau mà không có sự kết hợp quá mức.
Khả năng phục hồi
Một khái niệm quan trọng trong kỹ thuật phục hồi là vách ngăn (vách ngăn ở khoang tàu thuỷ). Nếu một thành phần của hệ thống bị lỗi, nhưng lỗi đó không rò rỉ và lan ra, bạn có thể cô lập sự cố và phần còn lại của hệ thống có thể tiếp tục hoạt động. Ranh giới dịch vụ trở thành vách ngăn rõ ràng của bạn. Trong một dịch vụ monolithic, nếu dịch vụ bị lỗi, mọi thứ sẽ ngừng hoạt động. Với một hệ thống monolithic, chúng ta có thể chạy trên nhiều máy để giảm nguy cơ hỏng hóc và đổ vỡ hệ thống, nhưng với microservices, chúng ta có thể xây dựng các hệ thống xử lý lỗi toàn bộ của các dịch vụ và làm suy giảm chức năng tương ứng.
Tuy nhiên, chúng tôi cần phải cẩn thận. Để đảm bảo các hệ thống microservice của chúng tôi có thể nắm bắt đúng cách khả năng phục hồi được cải thiện này, chúng tôi cần hiểu các nguồn lỗi mới mà các hệ thống phân tán phải đáp ứng. Hệ thống mạng có thể và sẽ thất bại, và máy móc cũng thế. Chúng tôi cần biết cách xử lý vấn đề này và tác động (nếu có) của nó đối với người dùng cuối đang sử dụng phần mềm của chúng tôi.
Chúng ta sẽ nói thêm về khả năng xử lý tốt hơn và cách xử lý các chế độ lỗi trong Chương 11.
Mở rộng quy mô
Với một dịch vụ lớn, monolithic, chúng tôi phải mở rộng mọi thứ cùng với nhau. Một phần nhỏ của hệ thống tổng thể của chúng tôi bị hạn chế về hiệu suất, nhưng nếu hành vi đó bị khóa trong một ứng dụng monolithic khổng lồ, chúng tôi phải xử lý việc mở rộng mọi thứ thay vì một phần nhỏ. Với các dịch vụ nhỏ hơn, chúng ta chỉ có thể mở rộng các dịch vụ cần mở rộng quy mô, cho phép chúng ta chạy các phần khác của hệ thống trên phần cứng nhỏ hơn, cầnít sức mạnh hơn, như trong Hình 1-2.
Hình 1-2. Bạn có thể nhắm mục tiêu mở rộng quy mô chỉ ở những dịch vụ nhỏ cần nó
Gilt, một nhà bán lẻ thời trang trực tuyến, đã sử dụng microservices vì lý do chính xác này. Bắt đầu từ năm 2007 với một ứng dụng Rails kiểu monolithic, đến năm 2009 hệ thống của Gilt đã không thể đáp ứng với số lượng người dùng sử dụng nó. Bằng cách tách nhỏra các phần cốt lõi của hệ thống, Gilt chắc chắn có thể đáp ứng với lượng truy cập tăng đột biến và ngày nay có hơn 450 microservices, mỗi microservices chạy trên nhiều máy riêng biệt.
Khi sử dụng các hệ thống cung cấp theo yêu cầu như hệ thống được cung cấp bởi Amazon Web Services, chúng tôi thậm chí có thể áp dụng quy mô này theo yêu cầu cho những phần cần nó. Điều này cho phép chúng tôi kiểm soát chi phí của mình hiệu quả hơn. Không phải thường xuyên mà cách tiếp cận kiến trúc có thể tương quan chặt chẽ đến mức tiết kiệm chi phí gần như ngay lập tức.
Dễ deploy
Thay đổi một dòng mã nguồn đối với ứng dụng monolithic dài hàng triệu dòng yêu cầu toàn bộ ứng dụng phải được deploy để release thay đổi. Đó có thể là một lầndeploy có tác động lớn, rủi ro cao. Trong thực tế, việc deploy có tác động lớn, rủi ro cao sẽ không thường xuyên xảy ra do nỗi sợ hãi mà ai cũng có thể hiểu được. Thật không may, điều này có nghĩa là các thay đổi của chúng tôi được xây dựng và tích lũy giữa các bản release, cho đến khi phiên bản ứng dụng mới của chúng tôi bắt đầu được đưa lên môi trường production có hàng loạt thay đổi. Và sự khác nhau giữa các lần release càng lớn, thì nguy cơ chúng tôi gặp sự cố càng cao!
Với microservices, chúng ta có thể thực hiện thay đổi đối với một dịch vụ duy nhất và deploy nó một cách độc lập với phần còn lại của hệ thống. Điều này cho phép chúng tôi deploy mã nguồncủa mình nhanh hơn. Nếu sự cố xảy ra, nó có thể được cô lập nhanh chóng cho một dịch vụ riêng lẻ, giúp dễ dàng đạt được quá trình khôi phục nhanh. Điều đó cũng có nghĩa là chúng tôi có thể đưa chức năng mới của mình đến với những khách hàng nhanh hơn. Đây là một trong những lý do chính tại sao các tổ chức như Amazon và Netflix sử dụng các kiến trúc này — để đảm bảo họ loại bỏ nhiều trở ngại nhất có thể để đưa phần mềm ra thế giới thật.
Công nghệ trong lĩnh vực này đã thay đổi rất nhiều trong vài năm qua và chúng ta sẽ xem xét sâu hơn chủ đề deploy trong thế giới microservice trong Chương 6.
Liên kết với tổ chức
Nhiều người trong chúng tôi đã gặp phải các vấn đề liên quan đến các nhóm lớn vàmã nguồn cơ sở. Những vấn đề này có thể trở nên trầm trọng hơn khi nhóm bị phân tán. Chúng tôi cũng biết rằng các nhóm nhỏ hơn làm việc trên các mã nguồn cơ sở nhỏ hơn có xu hướng hiệu quả hơn.
Microservices cho phép chúng tôi điều chỉnh kiến trúc phù hợp hơn với tổ chức của mình, giúp chúng tôi giảm thiểu số lượng người làm việc trên bất kỳ mã nguồn cơ sở nào để đạt được điểm tốt về quy mô nhóm và năng suất. Chúng tôi cũng có thể chuyển quyền sở hữu dịch vụ giữa các nhóm để cố gắng giữ cho mọi người làm việc trên một dịch vụ được tập trung vào vị trí. Chúng ta sẽ đi vào chi tiết hơn về chủ đề này khi chúng ta thảo luận về định luật Conway trong Chương 10.
Khả năng kết hợp
Một trong những tiềm năng quan trọng của hệ thống phân tán và kiến trúc hướng dịch vụ là chúng tôi mở ra cơ hội tái sử dụng chức năng. Với microservices, chúng tôi cho phép sử dụng chức năng của mình theo những cách khác nhau cho các mục đích khác nhau. Điều này có thể đặc biệt quan trọng khi chúng ta nghĩ về cách khách hàng sử dụngphần mềm. Đã qua rồi cái thời mà chúng ta có thể suy nghĩ hạn hẹp về trang web trên máy tính để bàn hoặc ứng dụng di động của mình. Bây giờ chúng ta cần nghĩ đến vô số cách mà chúng ta có thể muốn kết hợp các khả năng với nhau cho Web, ứng dụng native, web di động, ứng dụng máy tính bảng hoặc cho thiết bị đeo. Khi các tổ chức chuyển từ suy nghĩ về các kênh hẹp sang các khái niệm toàn diện hơn về sự tương tác của khách hàng, chúng tôi cần các kiến trúc có thể theo kịp.
Với microservices, hãy nghĩ đến việc chúng tôi mở các đường nối trong hệ thống của mình mà các bên bên ngoài có thể giải quyết được. Khi hoàn cảnh thay đổi, chúng ta có thể xây dựng mọi thứ theo những cách khác nhau. Với ứng dụng monolithic, tôi thường có một đường may thô có thể được sử dụng từ bên ngoài. Nếu tôi muốn chia nhỏ điều đó để kiếm thứ gì đó hữu ích hơn, tôi sẽ cần một cái búa! Trong Chương 5, tôi sẽ thảo luận về các cách để bạn chia nhỏ các hệ thống monolithic hiện có và hy vọng thay đổi chúng thành một số microser có thể tái sử dụng và có thể kết hợp với nhau
Tối ưu hóa khả năng thay thế
Nếu bạn làm việc tại một tổ chức quy mô trung bình hoặc lớn hơn, rất có thể bạn biết đến một hệ thống mang tính di sản lớn và khó chịu nào đó đang nằm trong góc. Cái mà không ai muốn chạm vào. Điều quan trọng đối với cách công ty của bạn điều hành, nhưng điều đó tình cờ được viết trong một số biến thể Fortran kỳ lạ và chỉ chạy trên phần cứng đã hết tuổi thọ 25 nămtrước. Tại sao nó vẫn chưa được thay thế? Bạn biết lý do tại sao: đó là một công việc quá lớn và đầy rủi ro.
Với các dịch vụ riêng lẻ của chúng tôi có quy mô nhỏ, chi phí để thay thế chúng bằng cách deploy tốt hơn, hoặc thậm chí xóa chúng hoàn toàn, sẽ dễ quản lý hơn nhiều. Bạn có thường xuyên xóa hơn một trăm dòng mã chỉ trong một ngày và không lo lắng quá nhiều về điều đó? Với các microservice thường có kích thước tương tự, các rào cản đối với việc viết lại hoặc xóa hoàn toàn dịch vụ là rất thấp.
Các nhóm sử dụng phương pháp tiếp cận microservice cảm thấy thoải mái với các dịch vụ viết lại hoàn toàn khi được yêu cầu và chỉ giết dịch vụ khi không còn cần thiết nữa. Khi một mã nguồn cơ sở chỉ dài vài trăm dòng, rất khó để mọi người nảy sinh tình cảm gắn bó với nó và hơn nữa chi phí thay thế nó là khá nhỏ.
Còn về Kiến trúc Hướng Dịch vụ (SOA)?
Kiến trúc hướng dịch vụ (SOA) là một cách tiếp cận thiết kế trong đó nhiều dịch vụ hợp tác để cung cấp một số khả năng cuối cùng. Dịch vụ ở đây thường có nghĩa là một quy trình hoàn toàn riêng biệt chạy trong hệ điều hành. Giao tiếp giữa các dịch vụ này thường thông qua các cuộc gọi trên mạng chứ không phải là bằng cách gọi các phương thức trong một ranh giới quy trình (method call hoặc function call).
SOA nổi lên như một cách tiếp cận để loại bỏ những thách thức của những nền tảng monolithic lớn. Đó là một cách tiếp cận nhằm mục đích thúc đẩy khả năng tái sử dụng của phần mềm; Ví dụ: hai hoặc nhiều ứng dụng người dùng cuối đều có thể sử dụng các dịch vụ giống nhau. Nó nhằm mục đích giúp việc bảo trì hoặc viết lại phần mềm dễ dàng hơn, vì về mặt lý thuyết, chúng tôi có thể thay thế một phần mềm này bằng một phần mềm khác mà không ai biết, miễn là ngữ nghĩa của dịch vụ không thay đổi quá nhiều.
SOA và bản thân của nó là một ý tưởng rất hợp lý. Tuy nhiên, dù có nhiều nỗ lực nhưng vẫn chưa có sự đồng thuận caovề một cách làm SOA_tốt_. Theo quan điểm của tôi, phần lớn ngành công nghiệp phần mềm đã không thể nhìn nhận vấn đề một cách tổng thể và đưa ra một giải pháp thay thế hấp dẫn cho câu chuyện của các nhà cung cấp khác nhau trong lĩnh vực này.
Nhiều vấn đề đặt ra trước cửa SOA thực sự là các vấn đề với những thứ như giao thức truyền thông (ví dụ: SOAP), phần mềm trung gian của nhà cung cấp, thiếu hướng dẫn về mức độ chi tiết của dịch vụ hoặc hướng dẫn sai về việc chọn vị trí để phân chia hệ thống của bạn. Chúng tôi sẽ lần lượt giải quyết từng vấn đề này trong suốt phần còn lại của cuốn sách. Người hoài nghi có thể ám chỉ rằng các nhà cung cấp đã đồng ý (và trong một số trường hợp thúc đẩy) phong trào SOA như một cách để bán được nhiều sản phẩm hơn và những sản phẩm tự đặt tên đó cuối cùng đã làm suy yếu mục tiêu của SOA.
Phần lớn sự hiểu biết thông thường về SOA không giúp bạn hiểu được cách chia điều gì đó lớn thành điều gì đó nhỏ. Nó không nói về việc lớn như thế nào là quá lớn. Nó không nói đủ về những cách thực tế, thực tế để đảm bảo rằng các dịch vụ không trở nên quá khớp với nhau. Số lượng những điều không được giải đáp là nơi bắt nguồn của nhiều cạm bẫy liên quan đến SOA.
Phương pháp tiếp cận microservice đã xuất hiện từ việc sử dụng trong thế giới thực, giúp chúng ta hiểu rõ hơn về hệ thống và kiến trúc để thực hiện tốt SOA. Vì vậy, thay vì bạn nên nghĩ microservices như một cách tiếp cận cụ thể cho SOA giống như XP hoặc Scrum là những cách tiếp cận (framework) cụ thể để phát triển phần mềm theo phương pháp Agile.
Các kỹ thuật phân rã khác
Khi bạn nắm bắt được nó, nhiều lợi thế của một kiến trúc dựa trên microservice đến từ bản chất chi tiết của nó và thực tế là nó cung cấp cho bạn nhiều lựa chọn hơn về cách giải quyết vấn đề. Nhưng liệu các kỹ thuật phân tách tương tự có thể đạt được những lợi ích tương tự không?
Thư viện được chia sẻ
Một kỹ thuật phân tách rất chuẩn được tích hợp vào hầu như bất kỳ ngôn ngữ nào là chia nhỏ một mã nguồn cơ sở thành nhiều thư viện. Các thư viện này có thể được cung cấp bởi các bên thứ ba hoặc được tạo ra trong tổ chức của riêng bạn.
Thư viện cung cấp cho bạn một cách để chia sẻ chức năng giữa các nhóm và dịch vụ. Ví dụ, tôi có thể tạo một tập hợp các tiện ích thu thập hữu ích, hoặc có thể là một thư viện thống kê có thể được sử dụng lại.
Các nhóm có thể tự tổ chức xung quanh các thư viện này và bản thân các thư viện có thể được sử dụng lại. Nhưng có một số hạn chế.
Đầu tiên, bạn mất đi tính không đồng nhất của công nghệ thực sự. Thư viện thường phải sử dụng cùng một ngôn ngữ, hoặc ít nhất là chạy trên cùng một nền tảng. Thứ hai, sự dễ dàng mà bạn có thể mở rộng các phần của hệ thống của mình một cách độc lập với nhau bị hạn chế. Tiếp theo, trừ khi bạn đang sử dụng các thư viện được liên kết động, bạn không thể deploy một thư viện mới mà không deploy lại toàn bộ quy trình, do đó, khả năng deploy các thay đổi riêng biệt của bạn bị giảm. Và có lẽ nguyên nhân chính là bạn thiếu các liên kết rõ ràng xung quanh để xây dựng các biện pháp an toàn kiến trúc nhằm đảm bảo khả năng phục hồi của hệ thống.
Thư viện được chia sẻ vẫn có giá trị của nó. Bạn sẽ thấy mình đang tạo mã cho các nhiệm vụ phổ biến không dành riêng cho bất kì lĩnh vực nào của doanh nghiệp mà bạn muốn sử dụng lại trong toàn tổ chức, đây là một ứng cử viên rõ ràng để trở thành một thư viện có thể tái sử dụng. Tuy nhiên, bạn cần phải cẩn thận. Mã dùng chung được sử dụng để giao tiếp giữa các dịch vụ có thể trở thành điểm kết hợp (khiến mọi thứ kém linh hoạt), điều mà chúng ta sẽ thảo luận trong Chương 4.
Các dịch vụ có thể và nên sử dụng nhiều thư viện của bên thứ ba để tái sử dụng chung mã nguồn. Nhưng đókhông hẳn đã là giải pháp cho tất cả.
Mô-đun
Một số ngôn ngữ cung cấp các kỹ thuật phân rã mô-đun của riêng chúng vượt ra ngoài các thư viện đơn giản. Chúng cho phép một số quản lý vòng đời của các mô-đun, để chúng có thể được deploy thành một quy trình đang chạy, cho phép bạn thực hiện các thay đổi mà không cần gỡ bỏ toàn bộ quy trình.
Sáng kiến Cổng thông tin Nguồn mở (OSGI) đáng được gọi là một phương pháp tiếp cận công nghệ cụ thể để phân rã mô-đun. Bản thân Java không có khái niệm thực sự về mô-đun và chúng ta sẽ phải đợi ít nhất cho đến khi Java 9 thấy điều này được thêm vào ngôn ngữ. OSGI, nổi lên như một framework để cho phép cài đặt các trình cắm thêm (plugin) trong Eclipse Java IDE, hiện được sử dụng như một cách để trang bị thêm một khái niệm mô-đun trong Java thông qua một thư viện.
Vấn đề với OSGI là nó đang cố gắng thực thi những thứ như quản lý vòng đời mô-đun mà không có đủ sự hỗ trợ bằng chính ngôn ngữ đó. Điều này dẫn đến việc các tác giả mô-đun phải thực hiện nhiều công việc hơn để phân lập mô-đun thích hợp. Trong một ranh giới quy trình, họ cũng dễ rơi vào bế tắc khi làm cho các mô-đun kết hợp quá chặt chẽ với nhau, gây ra đủ loại vấn đề. Kinh nghiệm của bản thân tôi với OSGI, được đúc kết bởi các đồng nghiệp trong ngành, là ngay cả với những đội giỏi, OSGI vẫn dễ dàng trở thành một thứ phức tạp hơn nhiều so với những lợi ích của nó mang lại
Erlang theo một cách tiếp cận khác, trong đó các mô-đun được đưa vào ngôn ngữ ở runtime. Vì vậy, Erlang là một cách tiếp cận rất thuần thục để phân rã mô-đun. Các mô-đun Erlang có thể dừng, khởi động lại và nâng cấp mà không gặp vấn đề gì. Erlang thậm chí còn hỗ trợ chạy nhiều hơn một phiên bản của mô-đun tại một thời điểm nhất định, cho phép nâng cấp mô-đun một cách uyển chuyển hơn.
Khả năng của các mô-đun của Erlang thực sự rất ấn tượng, nhưng ngay cả khi chúng ta đủ may mắn để sử dụng một nền tảng khác có các khả năng này, vẫn có một điều gì đó thiếu sót giống như cách chúng ta làm với các thư viện được chia sẻ thông thường. Chúng tôi bị hạn chế nghiêm ngặt về khả năng sử dụng các công nghệ mới, bị hạn chế về cách chúng tôi có thể mở rộng quy mô một cách độc lập, có thể hướng tới các kỹ thuật tích hợp quá chặt chẽ và thiếu các liên kết dành cho các biện pháp an toàn kiến trúc.
Có một suy nghĩ cuối cùng rất đáng để chia sẻ. Về mặt kỹ thuật, có thể tạo ra các mô-đun độc lập, được kiểm chứng tốt trong một quy trình monolithic duy nhất. Và chúng ta hiếm khi thấy điều này xảy ra. Bản thân các mô-đun này sớm trở nên kết hợp chặt chẽ với phần còn lại của mã nguồn, và từ bỏ một trong những lợi ích chính của chúng. Một lần thực hiện việc chia nhỏ theo ranh giới của tiến trình sẽ vệ sinh sạch sẽ về mặt này (hoặc ít nhất là làm cho người ta khó có thể làm sai hơn!). Tất nhiên, tôi không gợi ý rằng đây phải là động lực chính cho việc phân tách quy trình, nhưng điều thú vị là những hứa hẹn về việc phân tách theo mô-đun trong ranh giới tiến trình hiếm khi được đưa ra trong thế giới thực.
Vì vậy, mặc dù việc phân rã mô-đun trong một ranh giới tiến trình có thể là điều bạn muốn làm cũng như phân rã hệ thống của bạn thành các dịch vụ, nhưng bản thân nó sẽ không thể giải quyết mọi thứ. Nếu bạn chỉ sử dụng Erlang, lợi ích việc sử dụng mô-đun của Erlang có thể giúp bạn đạt được một chặng đường rất dài, nhưng tôi nghi ngờ rằng nhiều người trong số bạn không thể làm điều đó. Đối với phần còn lại của chúng ta, chúng ta sẽ thấy các mô-đun cung cấp các loại lợi ích tương tự như các thư viện được chia sẻ.
Không có viên đạn bạc nào cả
Trước khi chúng ta kết thúc, tôi nên nói rằng microservices không phải là bữa trưa miễn phí hay viên đạn bạc, hay là một lựa chọn tồi như một chiếc búa vàng. Chúng có tất cả sự phức tạp liên quan của các hệ thống phân tán, và mặc dù chúng tôi đã học được rất nhiều về cách quản lý tốt các hệ thống bị phân tán (mà chúng tôi sẽ thảo luận trong suốt cuốn sách) thì vẫn còn nhiều khó khăn. Nếu bạn bảo lưu quan điểm hệ thống theo monolithic, bạn sẽ phải cải thiện nhiều hơn trong việc xử lý deploy, kiểm thử và giám sát để đạt được những lợi ích mà chúng tôi đã đề cập cho đến nay. Bạn cũng sẽ cần suy nghĩ khác về cách bạn mở rộng quy mô hệ thống của mình và đảm bảo rằng chúng có khả năng phục hồi. Cũng đừng ngạc nhiên nếu những thứ như giao dịch phân tán hoặc định lý CAP bắt đầu khiến bạn đau đầu!
Mọi công ty, tổ chức và hệ thống đều khác nhau. Một số yếu tố sẽ ảnh hưởng đến việc liệu microservices có phù hợp với bạn hay không và mức độ tích cực của bạn trong việc áp dụng chúng. Xuyên suốt mỗi chương của cuốn sách này, tôi sẽ cố gắng cung cấp cho bạn hướng dẫn làm nổi bật những cạm bẫy tiềm ẩn, những cạm bẫy này sẽ giúp bạn vạch ra một con đường ổn định.
Tóm tắt
Hy vọng rằng bây giờ bạn đã biết microservice là gì, điều gì làm cho nó khác với các kỹ thuật tổng hợp khác và một số ưu điểm chính là gì. Trong mỗi chương sau, chúng ta sẽ đi vào chi tiết hơn về cách đạt được những lợi ích này và cách tránh một số cạm bẫy phổ biến.
Có một số chủ đề cần đề cập, nhưng chúng ta cần bắt đầu từ đâu đó. Một trong những thách thức chính mà microservices đưa ra là sự thay đổi vai trò của những người thường dẫn dắt sự phát triển của hệ thống của chúng ta: các kiến trúc sư. Tiếp theo, chúng ta sẽ xem xét một số cách tiếp cận khác nhau đối với vai trò này có thể đảm bảo chúng ta tận dụng tối đa kiến trúc mới này.
Khi chúng ta xây dựng các ứng dụng với AWS, chúng ta truy cập các dịch vụ AWS khác nhau cho nhiều mục đích: lưu trữ tệp trong S3, lưu một số dữ liệu trong DynamoDB, gửi tin nhắn tới SQS, viết trình xử lý sự kiện bằng các hàm lambda và nhiều mục đích khác.
Tuy nhiên, trong những ngày đầu phát triển, chúng ta thích tập trung vào việc viết mã ứng dụng thay vì dành thời gian cho việc thiết lập môi trường để truy cập các dịch vụ AWS. Việc thiết lập môi trường phát triển để sử dụng các dịch vụ này tốn nhiều thời gian và phát sinh chi phí không mong muốn với AWS.
Để tránh bị sa lầy bởi những tác vụ thông thường này, chúng ta có thể sử dụng LocalStack để phát triển và kiểm tra các ứng dụng của mình với các triển khai mô phỏng của các dịch vụ này.
Tại sao nên dùng LocalStack?
Chúng ta thường hay sử dụng phương thức dummy như mock, fake, proxy object để chạy Unit test cho ứng dụng với việc sử dụng các liên kết bên ngoài như database, aws, …
Với LocalStack, chúng ta sẽ giả lâp môi trường trường AWS phía local. LocalStack hỗ trợ:
Chạy các ứng dụng tương tác với các dịch vụ AWS ở phía local mà không cần kết nối môi trường thật.
Tránh sự phức tạp của cấu hình AWS và tập trung vào phát triển.
Tích hợp chạy thử nghiệm với CI / CD pipeline.
Cấu hình và thử nghiệm các kịch bản lỗi.
Cách sử dụng LocalStack?
Việc sử dụng LocalStack của chúng tôi tập trung vào hai nhiệm vụ:
Chạy LocalStack.
Ghi đè URL điểm cuối AWS bằng URL của LocalStack.
LocalStack thường chạy bên trong vùng chứa Docker, nhưng thay vào đó chúng ta cũng có thể chạy nó dưới dạng ứng dụng Python.
Chạy Localstack với Python
Trước tiên, chúng ta cài đặt LocalStack bằng pip:
pip install localstack
Sau đó, chúng ta bắt đầu localstack bằng lệnh start:
localstack start
Thao tác này sẽ khởi động LocalStack bên trong vùng chứa Docker.
Chạy Localstack với Docker
Bạn cũng có thể sử dụng tệp docker-compose.yml từ Github và sử dụng lệnh này (hiện yêu cầu docker-compile phiên bản 1.9.0+):
docker-compose up
Tùy chỉnh LocalStack
Mặc định của LocalStack là tạo ra tất cả các dịch vụ được hỗ trợ với mỗi dịch vụ trong số chúng đang lắng nghe trên cổng 4566. Chúng ta có thể ghi đè hành vi này của LocalStack bằng cách thiết lập một vài biến môi trường.
Port mặc định 4566 có thể được ghi đè bằng cách đặt biến môi trường EDGE_PORT. Chúng tôi cũng có thể định cấu hình LocalStack để tạo ra một nhóm dịch vụ hạn chế bằng cách đặt danh sách tên dịch vụ được phân tách bằng dấu phẩy làm giá trị cho biến môi trường SERVICES:
Trong file docker-compose.yml này, chúng ta đặt biến môi trường SERVICES thành tên của dịch vụ chúng ta muốn sử dụng trong ứng dụng của mình (S3 và DynamoDB).
Kết nối với LocalStack
Chúng ta truy cập các dịch vụ AWS thông qua AWS CLI hoặc từ các ứng dụng của chúng ta bằng AWS SDK.
AWS SDK và CLI là một phần không thể thiếu trong bộ công cụ của chúng ta để xây dựng ứng dụng với các dịch vụ AWS. SDK cung cấp các thư viện máy khách bằng tất cả các ngôn ngữ lập trình phổ biến như Java, Node js hoặc Python để truy cập các dịch vụ AWS khác nhau.
Cả AWS SDK và CLI đều cung cấp tùy chọn ghi đè URL của AWS API. Chúng ta thường sử dụng điều này để chỉ định URL của máy chủ proxy của chúng ta khi kết nối với các dịch vụ AWS từ phía sau máy chủ proxy của công ty. Chúng ta sẽ sử dụng tính năng tương tự này trong môi trường local của chúng tôi để kết nối với LocalStack.
Chúng ta thực hiện việc này trong AWS CLI bằng các lệnh như sau:
aws --endpointurl http://localhost:4566 s3 ls
Việc thực thi lệnh này sẽ gửi các yêu cầu đến URL của LocalStack được chỉ định làm giá trị của tham số dòng lệnh URL điểm cuối (localhost trên port 4566) thay vì đến AWS thật.
Chúng ta sử dụng cách tiếp cận tương tự khi sử dụng SDK:
URI endpointOverride = new URI("http://localhost:4566");
S3Client s3 = S3Client.builder()
.endpointOverride(endpointOverride ) // <-- Overriding the endpoint
.region(region)
.build();
Ở đây, chúng ta đã ghi đè điểm cuối AWS của S3 bằng cách cung cấp giá trị của URL của LocalStack làm tham số cho phương thức endpointOverride trong lớp S3ClientBuilder.
Toàn bộ bài viết là giới thiệu về framework LocalStack. Trong bài viết sau, mình sẽ hướng dẫn mọi người cách tích hợp nó vào source code như nào nhé.
Nếu như bình thường ta thực hiện single upload tới s3 thì sẽ có 2 cách sau:
Upload file lên s3 qua server của chúng ta: Cái này thì reject vì chúng ta phải tốn thời gian upload lên server của mình rồi từ đó mới upload lên S3. Với large file thì gần như là không nên.
Upload từ client bằng cách sử dụng presign-url. Thay vì phải qua server trung gian thì chúng ta upload thẳng lên S3. Tốc độ cải thiện rất nhiều vì cách 1 đa phần thời gian tốn ở khâu upload lên server của chúng ta.
Nhưng nói gì thì nói single upload to S3 thì sẽ có những hạn chế sau dưới Maximum size chỉ là 5GB . Điều này thực sự hạn chế. Hơn nữa AWS cũng suggest chúng ta là file >100MB thì nên sử dụng Multipart Upload . Vậy ưu điểm chính của nó là:
Maximum size là 5TB
Tốc độ upload tốt hơn
Điều đó mình lựa chọn multipart và presign-url cho bài toán upload file dung lượng lớn. Server ở đây mình sử dụng là python. Client thì dùng angular.
Server
Phần server này mình sử dụng kiến trúc serverless để triển khai với ngôn ngữ Python Flask trên môi trường AWS Cloud
Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.
Phần Server chủ yếu sẽ có 3 api chính:
Đầu tiên là api start-upload, với logic là request tới s3 để lấy uploadId
Split file upload thành các chunks, ở đây chia thành 10MB/chunk nhé. Ở đây mình chia là 10MB vì minimum size của Multipart Upload là 10MB.
Thực hiện call api get-upload-url để lấy preSignurl cho từng chunk ở trên.
Upload các chunks bằng signurl.
Khi tất cả các chunks upload xong sẽ thực hiện call api complete-upload để xác nhận với S3 mình đã upload đầy đủ các part. Done.
try {
const FILE_CHUNK_SIZE = 10000000; // 10MB
const fileSize = file.size;
const NUM_CHUNKS = Math.floor(fileSize / FILE_CHUNK_SIZE) + 1;
let start, end, blob;
let uploadPartsArray = [];
let countParts = 0;
let orderData = [];
for (let index = 1; index < NUM_CHUNKS + 1; index++) {
start = (index - 1) * FILE_CHUNK_SIZE;
end = (index) * FILE_CHUNK_SIZE;
blob = (index < NUM_CHUNKS) ? file.slice(start, end) : file.slice(start);
// (1) Generate presigned URL for each part
const uploadUrlPresigned = await this.getPresignUrl({
fileName: file.name,
fileType: file.type,
partNo: index.toString(),
uploadId: uploadStartResponse.upload_id
});
// (2) Puts each file part into the storage server
orderData.push({
presignedUrl: uploadUrlPresigned.upload_signed_url,
index: index
});
const req = new HttpRequest('PUT', uploadUrlPresigned.upload_signed_url, blob, {
reportProgress: true
});
this.httpClient
.request(req)
.subscribe((event: HttpEvent<any>) => {
switch (event.type) {
case HttpEventType.UploadProgress:
const percentDone = Math.round(100 * event.loaded / FILE_CHUNK_SIZE);
this.uploadProgress$.emit({
progress: file.size < FILE_CHUNK_SIZE ? 100 : percentDone,
token: tokenEmit
});
break;
case HttpEventType.Response:
console.log('Done!');
}
// (3) Calls the CompleteMultipartUpload endpoint in the backend server
if (event instanceof HttpResponse) {
const currentPresigned = orderData.find(item => item.presignedUrl === event.url);
countParts++;
uploadPartsArray.push({
ETag: event.headers.get('ETag').replace(/[|&;$%@"<>()+,]/g, ''),
PartNumber: currentPresigned.index
});
if (uploadPartsArray.length === NUM_CHUNKS) {
console.log(file.name)
console.log(uploadPartsArray)
console.log(uploadStartResponse.upload_id)
this.httpClient.post(`${this.url}/complete-upload`, {
file_name: encodeURIComponent(file.name),
parts: uploadPartsArray.sort((a, b) => {
return a.PartNumber - b.PartNumber;
}),
upload_id: uploadStartResponse.upload_id
}).toPromise()
.then(res => {
this.finishedProgress$.emit({
data: res
});
});
}
}
});
}
} catch (e) {
console.log('error: ', e);
}
Có những chú ý sau.
Minimum size của Multipart Upload là 10MB. Maximum là 5TB
Được upload tối đã 10000 chunks cho 1 file thôi. Ví dụ ở đây mình chia 1 chunk là 10MB thì mình chỉ upload tối đa 200GB. Tương tự 1 chunk là 5GB => upload tối đa là 5TB.
Demo
Cài đặt server với serverless. Vào project serverless và gõ lệnh
sls deploy
Có những chú ý sau:
Sau khi cài đặt NodeJS thì cài Serverless framework
npm i -g serverless
Cần có account AWS
Config ở file serverless đang sử dụng aws profile mà mình đã setup ở local(khanhcd92). Nếu bạn dùng default hoặc cấu hình profile riêng thì hãy thay đổi nó nhé.
Cách cấu hình aws profile.
Khi bạn thiết kế một hệ thống thì bạn nên có suy nghĩ rằng không hệ thống nào có thể đảm bảo vận hành trơn chu mà không có vấn đề gì xảy ra. Hệ thống có thể sập vì rất nhiều lý do. Chúng ta luôn phải có phương án có thể khôi phục lại ngay tức thì để độ tổn thất cho khách hàng và công ty ở mức thấp nhất. Đặc biệt là các hệ thống trên Cloud.
Werner Vogels, CTO & VP, Amazon says:
Everything fails all the time.
Bài viết hôm nay mình sẽ giới thiệu về cách thiết kế 1 trang web tĩnh trên S3 mà đảm bảo khả năng hồi phục(Resiliency).
Khả năng phục hồi (Resiliency) là gì?
Khả năng phục hồi là khả năng hệ thống phục hồi sau lỗi do hệ thống quá tải, bị tấn công, các lỗi liên quan phần cứng hoặc phần mềm.
Amazon S3 rất khả dụng và có khả năng phục hồi cao đối với sự cố theo vùng nhưng liệu trang web có khả năng chống chịu với thảm họa khu vực như nguồn điện, thời tiết.,? đám mây có những cách tốt hơn để giúp cung cấp tính liên tục cho hoạt động kinh doanh và đây là một trong những cách.
Thiết kế khả năng phục hồi cho một trang web tĩnh trên AWS Cloud
Các dịch vụ sử dụng
Route 53
CloudFront
S3
IAM
Triển khai
Do chưa có domain có sẵn nên bước triển khai này mình sẽ chỉ sử dụng các dịch vụ:
CloudFront
S3
IAM
Bước 1: Tạo S3 bucket name static-web-sample-s3 với region Singapore và enable tính năng Versioning. Chi tiết
Bước 2: Tạo S3 bucket name static-web-sample-dr-s3 với region Tokyo và enable tính năng VersioningChi tiết
Ghi chú: Ở hướng dẫn này, web chính của mình triển khai lưu trên S3 với region Singapore, và web khôi phục khi xảy ra thảm hoạ sẽ được lưu trên S3 với region Tokyo.
Bước 3: Cấu hình replication giữa 2 bucket name
Ở bucket name static-web-sample-s3 chọn Managemet tab. Tiếp theo, ở phần Replication rules chọn Create replication rule.
Nhập rule name
Phần rule scope thì chọn option thứ 2
Ấn Browse S3 xong chọn đến bucket name static-web-sample-dr-s3
Phần IAM role, chọn Create new role
Phần Additional replication options thì không chọn gì cả rồi ấn Save
Kết quả sau khi cấu hình xong
Bước4: Tạo 1 trang page index.html đơn giản rồi upload nó lên S3 bucket name
Bước 5: Tạo CloudFront liên kết với S3 bucket name static-web-sample-s3. Chi tiết
Ghi chú: Sử dụng OAI cho CloudFront access đến S3 và chọn Yes, update the bucket policy
Bước 6: Tạo thêm origin của CloudFront liên kết tới S3 bucket name static-web-sample-dr-s3
Ghi chú: Sử dụng OAI cho CloudFront access đến S3 và chọn Yes, update the bucket policy
Bước 7: Tạo Origin Group của CloudFront. Nhập thông như hình bên dưới.
Chọn Create origin group
Chọn các origin cần sử dụng rồi thêm vào. Chú ý origin web chính sẽ được đặt lên đầu tiên.
Bước 8: Cập nhật Behavior default của CloudFront
Vì behavior default của CloudFront đang liên kết với origin của S3 bucket name static-web-sample-s3, nên chúng ta sẽ cần cập nhật sang sử dụng origin group.
Chọn Behavior Default rồi chọn Edit
Ở màn hình Edit, cập nhật origin sang origin group (DR-Group)
Bước 9: Đợi CloudFront Deploy
Bước 10: Sau khi CloudFront đã deploy xong, thì chúng ta vào brownser và nhập DNS link của CloudFront.
Bước 11: Xoá bucket name static-web-sample-s3 hoặc chỉ xoá file index.html trong bucket.
Bước 12: Bạn vào lại trình duyệt reload lại trang web nhé. Bạn sẽ thấy trang web vẫn hoạt động bình thường dù đã xoá file hoặc S3 bucket chưa source chính.