Các giai đoạn trong cấu hình phần mềm (Development time vs Build time vs Runtime)
Trong bài viết này, chúng ta sẽ cùng phân tích các thuật ngữ development time, build time và runtime để hiểu về cách cấu hình ứng dụng web hoặc phần mềm.
Vòng đời phát triển của một phần mềm thông thường sẽ trải qua một vài giai đoạn cấu hình, những điểm khác nhau giữa các giai đoạn này còn phụ thuộc vào ngôn ngữ lập trình, nền tảng công nghệ,... trong nội dung bài này, chúng ta hãy cùng xem xét ba giai đoạn sau:
Development time
Build time
Runtime
Sau đó, cùng nhau trả lời câu hỏi chúng ta có thể cấu hình những khía cạnh nào của phần mềm trong từng giai đoạn? Làm thế nào để thực hiện chúng?
Development time
Tại thời điểm này, chúng ta có thể xác định các hằng số, các magic number hoặc thậm chí là các cấu hình phức tạp hơn, như khởi tạo một triển khai cụ thể của một giao diện hoặc dịch vụ. Đây là những ví dụ về cấu hình tĩnh ( cố định, bất biến) cho phần mềm.
Hằng số vật lý là một ví dụ hoàn hảo vì chúng không bao giờ thay đổi. Một ưu điểm khác của kiểu cấu hình này là hằng số có sẵn khi khởi động, một class được load khi khai báo biến của class đó hoặc lúc khởi tạo các thực thể; ngoài ra, nó sẽ được lưu trong bộ nhớ để luôn sẵn sàng sử dụng.
public class GravitationalForceServiceImpl |
Build time
Đây là giai đoạn phức tạp nhất. Tùy thuộc vào công nghệ, chúng ta có thể thiết lập tập lệnh gradle hoặc npm với nhiều bước hoặc tác vụ. Các tác vụ điển hình có thể bao gồm: biên dịch, unit tests, integration tests, obscuration of code, bundling, packaging, ...
Chúng ta cũng có thể setup các tác vụ phức tạp hơn. Ví dụ: chúng ta có code base và các tệp cấu hình được phân phối và lưu trữ ở nhiều vị trí. Chúng ta có thể thiết lập một tác vụ sử dụng để tìm và nạp các dữ liệu này, biên dịch lại nó và tạo ra một số artifact (một jar, json, ...) được tải khi khởi động. Nếu quá trình xử lý các tài nguyên này mất nhiều thời gian, bằng cách thực hiện việc này tại build time, chúng ta sẽ tăng tốc được quá trình thực thi trong runtime, vì hệ thống của sẽ không cần tìm nạp các tài nguyên bởi artifact đã có sẵn trong bộ nhớ.
Sơ đồ sau đây mô tả một ví dụ đơn giản về quá trình này: công cụ của chúng tôi tìm nạp thông tin từ các máy chủ hoặc kho lưu trữ khác nhau và tạo ra một artifact để đưa vào môi trường của sản phẩm.
Runtime
Trong giai đoạn runtime, phần mềm của chúng ta sử dụng dữ liệu từ session của người dùng để thực hiện các business logic. Một số biến thường được sử dụng là: preferences, language, location, permissions,...
Ví dụ, hãy tưởng tượng người dùng đăng nhập vào phần mềm của chúng ta. Chúng ta có thể sử dụng preference hoặc thông tin session của họ để điều chỉnh giao diện người dùng (ví dụ: hiển thị nhãn bằng ngôn ngữ của người dùng, hiển thị theo ngôn ngữ từ trái sang phải hoặc từ phải sang trái, phân quyền theo vai trò của người dùng, ...).
Cấu hình của phần mềm trong runtime là động. Thông thường, cấu hình này sử dụng cùng với các design patterns như factory hay strategy patterns, dependency injection,... Ví dụ trong đoạn code này:
@RestController |
Chúng ta sử dụng thông tin từ request của người dùng, trong trường hợp này là language, để khởi tạo dịch vụ phù hợp và thực hiện các business logic.
Trong thế giới của Microservices, một ví dụ khác được cấu thành bởi dịch vụ khám phá, dịch vụ này được truy cập vào runtime để điều hướng yêu cầu của người dùng đến đúng dịch vụ ở back-end.
Giai đoạn nào là quan trọng nhất?
Chúng ta tìm hiểu ba thời điểm trong vòng đời phần mềm, nơi chúng ta thực hiện việc cấu hình. Nhưng nếu phải chọn ra pha nào là quan trọng nhất? Hãy chọn ra một vài tiêu chí tiêu biểu và xem từng giai đoạn hoạt động như thế nào từ đó chúng ta cân nhắc để đưa ra quyết định.
Cấu hình tĩnh hay động
Tại development time, chúng ta thực hiện thiết lập những thứ giống như hằng số không thể thay đổi. Mặt khác, khi chúng ta chuyển từ development time sang build time và sau đó là runtime, chúng ta sẽ linh hoạt hơn với các hoạt động phức tạp hơn.
Tất nhiên trong một dự án thực, chúng ta sẽ có sự kết hợp của ba loại cấu hình: sẽ có những thuật toán đơn giản có thể được cấu hình với các hằng số đơn giản và sẽ có những thứ phức tạp hơn đòi hỏi một logic động, như trong ví dụ về bộ điều khiển.
Vì vậy, chúng ta đưa ra quyết định dựa trên việc cấu hình logic của chúng ta có thay đổi theo thời gian hay không, độ phức tạp mà chúng ta cần.
Thời gian thực hiện
Khía cạnh này đánh dấu sự khác biệt giữa một bên là development, build time và bên kia là runtime. Giả sử rằng một số dữ liệu trong phần mềm của chúng ta được sử dụng cho business logic; ví dụ một bảng với tất cả các múi giờ. Chúng ta có các lựa chọn sau:
Lưu trữ dưới dạng hằng số (tdevelopment time);
Biên dịch lại thành json sau khi tìm nạp dữ liệu từ máy chủ thời gian và sau đó được tải vào bộ nhớ (build time);
Lưu trữ trong time server hoặc trong một DB truy cập trong thời gian chạy theo request.
Chúng ta có nhiều lựa chọn thay thế khác nhau. Nhưng chúng ta có đang yêu cầu nghiêm ngặt về thời gian phản hồi không? Nếu có, chúng ta nên tải sẵn dữ liệu vào bộ nhớ - tương ứng lựa chọn thứ nhất hoặc thứ hai. Nhưng nếu đó là một bộ đếm, dữ liệu không phải là bất biến và cần được cập nhật - trong trường hợp này chúng ta sẽ chọn tùy chọn thứ ba và tối ưu hóa nó, cuối cùng cấu trúc lại các logic để tải dữ liệu khi khởi động máy chủ.
Bộ nhớ
Ở mục trước, chúng ta đã sơ lược về cách tải trước dữ liệu sẵn trong bộ nhớ giúp làm giảm thời gian thực thi. Tất nhiên, điều này đi kèm với một chi phí. Tuy nó không phải là một vấn đề lớn với ứng dụng web hiện đại, nhưng nó có thể là một mối quan tâm trong một số môi trường đặc biệt như các phần mềm nhúng và các phần cứng với tài nguyên có hạn.Hãy quyết định cẩn thận xem chúng ta có thể lưu trữ dữ liệu trong bộ nhớ hay không và dữ liệu nào cần lưu trữ sẵn trong bộ nhớ. Nếu tồn tại các hạn chế, chúng ta có thể áp dụng một số kỹ thuật như:
Sử dụng các kiểu dữ liệu nguyên thủy và chỉ lưu trữ trong bộ nhớ các giá trị quan trọng nhất thay vì các đối tượng lớn;
Sử dụng phân trang hoặc các kỹ thuật tương tự khi truy xuất dữ liệu từ back-end.
Khả năng truy cập và bảo mật
Nhìn lại ví dụ của hình đầu tiên, trong giai đoạn xây dựng, dữ liệu được lấy từ các máy chủ khác nhau trong mạng, như máy chủ có codebase (gitlab hoặc github, subversion, ...), các repository với các thư viện khác…
Các vị trí này nằm bên trong mạng của chúng ta và không thể được truy cập từ bên ngoài. Điều này mang lại các lợi thế về bảo mật, vì chúng ta có thể có dữ liệu thô (code, configuration,...) trong các máy chủ nội bộ và chỉ đưa vào artifact cuối cùng một phiên bản được biên dịch và bảo mật của chúng.
Ở đây quy tắc là nếu có các thông tin quan trọng mà chúng ta không muốn bị lộ ra hoặc lưu trong các máy chủ với hạn chế truy cập từ bên ngoài, chúng ta có thể thiết lập một tác vụ tại build time để biên dịch lại dữ liệu này thành một artifact.
Phân chia nhiệm vụ
Việc phân phối dữ liệu ở một số nơi tuân theo nguyên tắc phân chia nhiệm vụ. Áp dụng nguyên tắc này giúp dễ dàng hơn khi thay đổi một thư viện hoặc tệp cấu hình mà không cần chạm vào các phần còn lại của codebase hoặc pipeline.
Nên áp dụng nguyên tắc này và thực hiện phân cấp các cấu hình phần mềm, đặc biệt là đối với logic phức tạp.
Kết luận
Có nhiều cách để cấu hình một phần mềm và trong bài này chúng ta đã tìm hiểu một số cách như cấu hình tại development time, tại build time hoặc runtime. Sau đó, chọn ra một số đặc điểm và đánh giá ưu và nhược điểm của chúng.
Trong thế giới thực, sẽ có nhiều khía cạnh hoặc yêu cầu khác mà chúng ta cần phải xem xét, một số yêu cầu đó có mức độ ưu tiên cao hơn thậm chí là bắt buộc. Ví dụ yêu cầu nghiêm ngặt về bộ nhớ, bị giới hạn về lượng dữ liệu có thể tải khi khởi động. Còn với các trường hợp còn lại, chúng ta sẽ linh hoạt hơn và tự do lựa chọn cách triển khai cấu hình phần mềm của mình.
Dù bằng cách nào, chúng ta phải tự hỏi bản thân xem chúng ta muốn hoàn thành điều gì và yêu cầu của chúng ta là gì (về bộ nhớ, độ phức tạp, thời gian thực thi, bảo mật,...). Từ đó, chúng ta đánh giá từng tùy chọn và chọn phương án phù hợp nhất với nhu cầu, hiểu được các đánh đổi đi kèm với lựa chọn đó và khắc phục.