ESI Press

ESI-Apps 3.5 sẵn sàng lên cloud!

Kể về những thay đổi kể từ lần cuối cùng tôi sửa engine...
Đã hơn 2 năm kể từ lần cuối cùng tôi cập nhật cho ESI-Apps 3.4 cũng như ESI-Press tương ứng. Khi tôi chuyển trạng thái làm việc sang là bán thời gian vì ESI chuyển chiến lược giảm thiểu hoạt động để tìm thời cơ. Những lúc rảnh rỗi tôi vẫn suy nghĩ định hướng và cập nhật cho ESI-Apps cũng như trăn trở bài toán phát triển bền vững.

**TLDR:** bạn chỉ cần đọc mỗi mục đầu tiên sau đây là đủ.

# Bắt đầu từ thay đổi trong suy nghĩ

Trong 2 năm qua, suy nghĩ của tôi về docker, nghĩ về mô hình DevOps, đến thời điểm này thì nhận thức của tôi về chúng đã thay đổi cơ bản về chất. Tôi nhận thấy ý nghĩa cốt lõi của mô hình DevOps đó không chỉ là hưởng lợi từ "build, test, deploy tự động" - điều mà chúng tôi đã làm tốt với mô hình truyền thống của Java EE, cũng như docker thì có isolation giữa các container để giảm rủi ro bảo mật - điều mà tôi đã áp dụng ảo hóa trước đó nhiều năm khi triển khai dịch vụ. Docker có lợi thế về cách package gói phần mềm kèm với phiên bản thư viện cần thiết sao cho ít gặp vấn đề nhất, xuyên suốt thống nhất từ dev, sang test, sang production và đặc biệt, nó tiết kiệm tài nguyên hệ thống rất nhiều so với ảo hóa, hiệu quả hơn. Tôi đã xử lý vấn đề phiên bản thư viện từ ngày xưa với cách thức là chuẩn hóa phiên bản sử dụng khi khởi đầu dự án, cũng như có thể tạo nhiều máy ảo với phiên bản Linux khác nhau để ứng phó với các ứng dụng yêu cầu thư viện khác nhau. Và vì làm việc với Java thì thì quá trình hiện tại của tôi cũng ít tốn công, cũng như khá ổn định do ép buộc setup đồng nhất môi trường dev, build, test trên laptop nhân viên (nhiều OS khác nhau khó nên ghim lại quy định laptop tất cả nhân viên phải chạy Linux), tất cả đảm bảo chạy trên server Linux nó mượt mà nhất, quá trình release ít trục trặc nhất. Ấy vậy tôi vẫn thua cách làm của docker, container chỉ cần nhân Linux đủ cao, còn lại thì mọi thứ, thư viện, command cho đến JDK đều là được package kèm chính xác như laptop dev.

Điểm mấu chốt này khiến tôi suy nghĩ về triển khai docker so với cách mà Java EE làm cái nào hiệu suất nhất, xét khía cạnh tạo ra dịch vụ trên cloud cũng như tự vận hành cloud riêng. Chú thích thêm là tôi đã làm docker chạy thật với LAMP, Ruby (do khốn khổ với RedMine bản mới không hợp phiên bản ruby cũ, các gems cũ có trên CentOS 7, phải chạy Fedora 28), cũng như đã cho nhân viên dev với docker container chạy tomcat (mỗi người 1 container). Tôi khi chạy docker cho LAMP với PHP chỉ deploy cái cây thư mục htdocs + file htaccess ánh xạ thư mục tương ứng vào cái container image là xong (config đi theo hết trong đó). Nhưng Java EE, Tomcat, hay JBoss không đơn giản chỉ là gói WAR, hay EAR vào là chạy mà còn cả tá thiết lập XML trong file server.xml hay là standalone.xml (nếu app có thay đổi, thêm JMS Queue chẳng hạn, bạn phải cập nhật file đó). Setup một docker cho Tomcat không nhanh hơn "yum install tomcat" vì vẫn phải đi tạo hoặc copy cả tá file config và library đi kèm. Nhưng nếu ứng dụng viết ra tương thích cách mà docker thiết lập tham số, thì việc tạo file config không cần thiết nữa, 1 shell script chứa lệnh tạo hoặc có thể cả docker-compose sẽ giúp quá trình deploy nhanh chóng. Việc test/build thì maven hay ant giúp cho việc này đơn giản và một lần nữa, shell script là cách mà các dev quen thuộc.

![DevOps là sự liền mạch quá trình phát triển và triển khai](canstockphoto77489218.jpg)

ESI-Apps là dựa trên Java EE, hệ sinh thái có sự khác biệt với cách triển khai docker hoặc cho Cloud ít nhiều. Nên muốn triển khai trên cloud thì buộc phải có sự điều chỉnh thì mới có lợi ích sử dụng, để khi phát triển cho cả 2 bên đều chạy mượt, phục vụ cho cả các khách hàng truyền thống lẫn khách hàng mục tiêu mới, bắt nhịp xu thế ứng dụng công nghệ. Tôi đã quyết định thực hiện và thử nghiệm, kết hợp để hoàn thiện một cách thức làm việc, một môi trường Dev mới cho anh em.

# Đế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òn `isNull("") = 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](post/35)), 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.

Add new comment



Comments

1. Thạch

Không thể hay có thể? reply