ESI-Apps 3.5 sẵn sàng lên cloud! by thachanh
updated on 2024-11-19 08:29:16
Kể về những thay đổi kể từ lần cuối cùng tôi sửa engine...
Tóm tắt
Đã hơn 2 năm kể từ khi tôi cập nhật ESI-Apps 3.4 và ESI-Press. Trong thời gian này, cách tôi nhìn nhận về Docker và DevOps đã thay đổi đáng kể. Docker giúp triển khai nhanh và hiệu quả hơn so với ảo hóa truyền thống, đặc biệt khi làm việc với cloud. Tôi đã thử nghiệm Docker với nhiều công nghệ và điều chỉnh ESI-Apps, vốn dựa trên Java EE, để phù hợp với xu hướng mới và đảm bảo hiệu quả cho cả khách hàng cũ lẫn mới.
TLDR: Chỉ cần đọc phần tóm tắt này là đủ.
Thay đổi trong suy nghĩ
Trong 2 năm qua, cách nhìn của tôi về Docker và DevOps đã thay đổi nhiều. Trước đây, tôi đã dùng Java EE truyền thống với tự động “build, test, deploy” và ảo hóa để giảm rủi ro bảo mật. Nhưng Docker vượt trội với khả năng đóng gói phần mềm cùng thư viện cần thiết, đảm bảo tính nhất quán từ dev đến production, tiết kiệm tài nguyên hơn ảo hóa. Dù tôi đã chuẩn hóa môi trường trên máy ảo, Docker vẫn hơn nhờ linh hoạt và khả năng gói gọn môi trường trong container.
Docker giúp triển khai dịch vụ trên cloud nhanh hơn. Tôi đã thử Docker với LAMP, Ruby và dùng cho nhân viên với Tomcat. Dù cấu hình Tomcat hay Java EE trên Docker phức tạp hơn, nhưng nếu ứng dụng được tối ưu hóa cho Docker, quá trình sẽ dễ dàng hơn nhờ script hay docker-compose. Các công cụ như Maven hoặc Ant cũng hỗ trợ tốt cho việc build và test.
Điều chỉnh cho ESI-Apps
ESI-Apps dựa trên Java EE, khác với cách triển khai trên Docker hoặc Cloud. Để chạy mượt trên cả 2 nền tảng và phục vụ khách hàng truyền thống lẫn mới, tôi đã điều chỉnh và thử nghiệm để xây dựng môi trường Dev mới cho team.
Đến quyết định về các thay đổi tính năng
Có thể đặt tham số kết nối DB từ biến môi trường
Java EE, cụ thể là Apache Tomcat (chỉ có phần Web Servlet) hay các platform khác như JBoss, WebLogic, WebSphere..., thì tham số hóa môi trường cho ứng dụng chạy là thông qua các resource và một số context parameter. Tài nguyên quan trọng nhất đối với Esi-Apps là data source, được lookup qua JNDI, chính là nơi để lấy kết nối database mà apps thao tác trên nó. Thông tin này là host, port, tên DB (hoặc gọi chung là JDBC URL) và username/password được định nghĩa ở mức server (tomcat thì ở trong context.xml/server.xml, JBoss thì ở standalone.xml), hay tomcat thì cũng có thể đặt trong META-INF/context.xml để cho phép tham số hóa thông tin kết nối DB (cơ mà cách này thì tùy theo môi trường mà apps sẽ deploy lên sẽ phải build file WAR riêng nhau). Đây là cách chuẩn mực của Java EE, do nó giúp ứng dụng linh hoạt và có thể deploy vào cả 1 cluster nhiều node dễ dàng. Tôi không tính dạng amateur khác là họ đặt tham số trong một properties file path cố định ở đâu đó.
Các dịch vụ cloud host cho Java, cụ thể là AWS Elastics Beanstalk cho Tomcat hoặc Java, thì dùng tham số qua System.getProperty cho các tham số bên ngoài thiết lập cho Environment, gồm cả RDS cũng như các thông số tùy chọn khác. Java EE không dùng phổ biến kiểu đó do mỗi Application Server sẽ share cho nhiều app nhiều env khác (khái niệm App. Server là của riêng Java EE có, hiểu thì nó thường là 1 JVM process để chạy nguyên bộ Java EE). Từ thay đổi này nên ESI-apps bổ sung thêm cách tự "new" ra lớp data source tương thích với tham số đặt trước như java -Dparam=value thay vì get từ JNDI. Từ bây giờ nếu tham số như JDBC_CONNECTION_STRING khi start JVM, thì đã có thể có Connection bình thường và ESI Apps hiểu giống như đặt DataSource, tương thích sử dụng ngay được.
Không chỉ system.getProperty, hàm system.getenv cũng được sử dụng để lấy thông số với các tên định trước và giá trị sẽ áp dụng vào ưu tiên số 1, nếu có. Lý do vì docker container thường sử dụng cách này để truyền tham số cho apps trong container, rất ít khi cần tạo/ánh xạ một file configuration như truyền thống tạo container chạy tomcat nữa. Deploy một gói WAR bây giờ là nguyên bản không đi kèm tham số CSDL. Kèm theo điều này, một lời gọi nạp JDBC driver qua Class.forName cũng tự được call nếu chưa đăng ký driver tương ứng do với Java EE nó lo cho bạn việc này, còn trong môi trường tomcat chạy thì JDBC Driver phải được load lên chủ động (và các gói jar cần thiết có thể đặt trong WEB-INF/lib cũng như tomcat/lib). ESI-Apps được sửa để nó hỗ trợ load class cho hầu hết các loại RDBMS phổ biến từ oracle, mysql, postgesql, SQL server hay cả derby thông qua một tham số môi trường có tên AWS_DATABASE_TYPE.
Từ nay thì gói WAR nền ESI-Apps 3.5 mới, nếu triển khai trên AWS EB hay docker có thể nhanh chóng launch một instance mới chạy tomcat 8 hoặc 9 (JDK 8 hay JDK11), tham số sẽ dùng chính các tham số EB truyền vào. Cho dù là môi trường test, dev (dùng DB là test), hay production có DB thật, không cần setup config file, không cần package lại War (nếu bạn đặt config trong META-INF/context.xml).
Hỗ trợ LOB.
Tới v3.5 thì BLOB/CLOB đã được hỗ trợ, được tối ưu tùy theo state và sẽ sử dụng temporally file nếu như khối binary object (file) cho vào/lấy ra từ database quá lớn hoặc ở thời điểm "off connection". Ở đây phải giải thích rằng, trong nội bộ value lưu trong ESI-Apps và SQLStmt là 1 instance độc lập với chính RDBMS Connection. Ví dụ giá trị string "ABC" khi gửi từ REST client nhập vào một biến, thì ESI-Apps sẽ lưu nó trong memory (Java heap) với 1 String object instance (immutable), string dài bao nhiêu thì memory tốn bấy nhiêu. Khi thao tác với DB, lúc đó mới mở connection vào DB rồi thực hiện lệnh SQL (ví dụ INSERT), thì giá trị string kia - có sẵn trong memory - được setString vào các PreparedStatement để execute. Việc này là để tối ưu số kết nối cũng như khoảng thời gian "lock" của CSDL, giảm tối đa wait time của DB transaction tránh bị giữ bởi việc chờ data đến hoặc ghi ra, sẽ tối ưu theo tính chất xử lý. Mọi giá trị input data (đã parse) của hàng loạt SQL statement đều phải ready khi thực thi, trừ khi nó là bulk-data load thì lại parse khi đang execute (để tối ưu load khối dữ liệu lớn). LOB cũng được ứng xử như thế, có 2 tình huống: nhận data đầy đủ trước (thành temp file) và update vào CSDL sau; hoặc on-fly trực tiếp write đẩy vào CSDL từng khối buffer (32 KB max) và DB tự quản lý khối temp data đó.
Điều này là vì LOB khác value bình thường ở chỗ, nó có thể rất to (hàng GB), và đọc ghi thường là qua InputStream/OutputSteam. Việc lưu cả GB trong memory dạng byte array tạm thời là dễ gây tràn heap. JDBC bình thương thì cung cấp API thao tác với LOB qua Blob/Clob object, object này tạo ra thì phải có kết nối với DB rồi (lệnh tạo là connection.createBlob()...), RDBMS sẽ cấp 1 locator (thường là temp tablespace) trên chính DB rồi đọc ghi LOB là thực chất sẽ gửi/nhận data lên thẳng DB, không tốn memory. ESI-Apps tùy theo size của LOB mà giữ trong memory hay đẩy ra file temp, rồi thực thi lệnh sau đó; hoặc nếu data load trực tiếp hoặc khi SELECT lấy LOB từ CSDL ra rồi có thể write thẳng ra OutputStream về client. Tất cả nhằm để tối ưu memory cho mỗi lần call để đảm bảo hàng ngàn thread chạy cùng lúc không gây đổ vỡ Java heap vì out of memory.
NULL, empty string & boolean values singleton
Đặc trưng của các thao tác CSDL, nhất là query dữ liệu có nhiều row trả về, là sự lặp lại giá trị NULL rất nhiều (ví dụ result set trả về 1 triệu row thì có thể đa số data là NULL, số 0, empty...). JDBC truy vấn giá trị object gặp NULL thì sẽ trả về "null" value, nhưng với data là primitive như số long, int, cả boolean thì lại là một value có giá trị 0 (nghĩa là với kiểu long - 8 bytes - là tốn 8 byte memory cho giá trị NULL). ESI-Apps sử dụng NULL Object (không phải null value) để tiện cho các phép kiểm tra. Để tiết kiệm memory, không phải tạo object trên heap object thì tất cả giá trị NULL, boolean TRUE, FALSE hay empty string, đều tham chiếu đến 1 static object trước - immutable singleton, các result set fetch dữ liệu ra nếu so sánh là data null, TRUE/FALSE và số 0 (zero), hoặc empty string chỉ tạo tham chiếu đến đây và không new object trên heap nữa (con trỏ tham chiếu thì vẫn có nhưng theo design - nó là 1 item trong 1 array, kích thước 4-8bytes/item tùy theo kiến trúc 32/64bit). Giảm số lượng cấp thêm object trên heap sẽ tăng hiệu suất của GC trên Java.
Empty data và NULL data.
Đi kèm với việc singleton trên, đó là so sánh, lưu trữ giá trị empty cũng là Singleton cho mỗi kiểu data và có quy tắc chung ổn định. Tùy cơ sở dữ liệu mà có hay không có giá trị empty nên đôi khi không phân biệt được. Ví dụ với Oracle DB thì empty string = NULL, nhưng với MySQL thì là khác, empty string là có Object tuy dữ liệu trống. Quy tắc so sánh trong ESI Apps có phân biệt 2 kiểm tra là value is null và value is empty và giờ đây nó ổn định với từng loại data type (lúc trước là giống hành xử của Oracle DB):
- Với String, Array, Bytes, LOB: empty gồm cả giá trị null.
- Với dữ liệu số, ngày tháng, boolean: không bao gồm, empty tương ứng là null.
- Rule chung là:
isEmpty(null) = TRUE, isEmpty("") = TRUE, isEmpty([]) = true
; cònisNull("") = FALSE, isNull([]) = FALSE là isNull(null) = TRUE
.
Jetty Embeded cho test
ESI-Apps nay kèm theo một module chạy với Jetty Embeded load tự động các file XML, để cho phép chạy trực tiếp dạng Java App, như kiểu Spring boot. Không cần Java EE container bên ngoài mới có thể chạy được, nhưng hiện tính năng mới là cho việc kiểm thử để chạy với unit testing, được mô tả kĩ hơn ở mục kế tiếp. Dự kiến sẽ chuẩn hóa để khởi động Jetty Embeded, sẽ giúp các developer triển khai API với ESI-Apps thực sự dễ hơn cho tạo hàng loạt Microservices để triển khai trên docker (một đặc tính mà Spring-boot được ca ngợi hết lời).
Unit testing
Kiểm thử luôn góp vai trò bảo vệ thành quả đầu tư công sức vào coding. Không có phần mềm nào là hoàn toàn không có lỗi, chỉ có ít hay nhiều, rủi ro bao nhiêu thôi. Test thông thường thì dùng sức người, dùng công cụ ít nhiều sẽ kiểm soát vấn đề chất lượng. Nhưng nếu phần mềm thay đổi nhiều quá, vòng tròn thay đổi ngắn quá thì testing sẽ nặng chi phí, chậm quá trình release. Unit test là kiểu test cơ bản nhất, có tính chất bắt buộc trong mô hình DevOps vì ở quá trình test tự động, các unit test sẽ được chạy..
Dù đã có một số lượng kha khá Unit test case ở v3.4, nhưng tôi đầu tư cho v3.5 một số lượng nhiều hơn và chuẩn hóa cẩn thận mức độ coverage của unit test, cộng với khả năng tự chạy trong Jetty embeded để cho phép kiểm thử full-stack (Test API từ client đến tận server với Jasmine và dùng Fetch API để call Rest API thử nhiều kịch bản), bổ sung hàng loạt các test case nữa cho junit (để test cơ bản data type và rule). Cả 2 đều có thể thực hiện tự động được dễ dàng, nhanh chóng, giúp cho development/release ESI-Apps sẽ nhanh hơn. Đây là bước chân quan trọng để gia tăng quy trình phát triển đi theo hướng DevOps, bổ sung nhiều tính năng hơn nữa mà không phá vỡ hay làm lỗi các tính năng đã proven. ESI-Apps là engine, khác với app, nên phần Ops của nó cần có ứng dụng, ESI-Press chính là thử nghiệm đầu tiên cho ESI-Apps 3.5 và xin xem tiếp...
Kiểm nghiệm thành quả với ESI-Press
App đã có dựa trên ESI-Apps 3.5 sẽ chỉ cần thay đổi để thống nhất hơn cấu trúc là chạy được, tương thích tốt. ESI-Press là xuất bản Web của ESI dựa trên nền ESI-Apps này (đọc bài ESI-Press), nay đã thêm khả năng kiểm tra mã reCaptcha realtime (post comment xong là gọi Google API kiểm tra ngay chứ không theo crontab nữa), thêm tối ưu EPFS và gia tăng chất lượng bằng hàng loạt Unit-test bổ sung thêm. ESI-Press cũng lên Docker theo ESI-Apps ở khả năng chạy trên Docker, trang Web bạn đang coi là nền ESI-Press trên Docker đấy (nó được chạy trong docker image chuẩn kéo về từ Docket Hub với Tomcat 9 và JDK 11). Chỉ mất có 1 ngày để tôi upgrade từ hệ thống live (do không phải đổi DB), hơn nữa, chạy song song mà không xung đột gì cả với Tomcat 7/CentOS 7 đang chạy cùng máy (gần zero downtime).
Và tất nhiên hiệu năng có tăng lên ít nhiều do 1 số tối ưu, cũng như LOB đã hỗ trợ. Nhưng quan trọng nhất, thành quả chứng minh ESI-Apps thêm rằng docker giúp tôi tiết kiệm thời gian release. ESI-Apps thậm chí đã chạy mượt trên AWS để sẵn sàng cho các app chuyển lên AWS - tôi đã phải làm demo gần đây với nó. V3.5 thì memory tiết kiệm hơn sẽ là một điểm lợi vì AWS EB chỉ cần 1 instance cấu hình t2.micro chỉ với 1G RAM, app vẫn có thể gánh cả vài chục đến trăm threads cùng lúc (dĩ nhiên, còn cần RDS nữa). Tiết kiệm tài nguyên hơn thì đồng nghĩa chi phí thuê cloud sẽ tiết kiệm hơn nhiều (do phí theo giờ, nếu khách đông quá thì được EB scale lên tự động).
Esi-Press tuy chạy trên Docker ok, nhưng chưa chạy được AWS Elastic Beanstalk do phần EPFS đang lưu là file/thư mục, AWS EB thì hạn chế lưu trữ ảnh/file trên web server (ví dụ nếu EB tự launch nhiều instance lên sẽ không hỗ trợ được EPFS chỉ có ở instance đầu tiên). Tôi sẽ dành thời gian viết bổ sung EPFS với tùy chọn AWS S3 thì rồi ESI-press hoàn toàn chạy được.