Thứ Hai, 14 tháng 8, 2023

Cấu hình để cài đặt cuốn chiếu zero downtime (ZDD) với Netflix Eureka, Zuul, Ribbon ở tình trạng sẵn dùng được (High Availability)

Về mặt lý thuyết


Ta cần chú ý đến 3 cache ở các thành phần sau : 1 ở gateway Zuul - đóng vai cổng vào để nhận các truy vấn đến; 1 ở load balancer Ribbon - đóng vai trò tác nhân cân bằng tải để điều hướng các truy vấn đến những dịch vụ web trong hệ thống; 1 của service discovery Eureka - dịch vụ chủ Eureka lưu dữ và cập nhật danh sách các dịch vụ web có trong hệ thống bao gồm các microservices. Mặc định mỗi cache của từng thành phần này sẽ tự cập nhật mỗi 30 giây. Vậy nên phải chờ khoảng 30+30+30 = 90 giây để tất cả các cache được cập nhật ở mọi lớp này.

Ta xét trường hợp một hệ thống HA (high availability) gồm hai replicas - mỗi một replica bao gồm một tập hợp hoàn chỉnh các dịch vụ web cho phép hệ thống hoạt động bình thường. Trong trường hợp này ta có thể tiến hành việc cài đặt như sau : 

- Hủy đăng ký của tất cả các dịch vụ web trong instance đầu tiên của Eureka server (dùng enpoint pause của actuator hoặc Eureka API PUT OUT_OF_SERVICE)
- Đợi 90 giây.
- Dừng tất cả các service, rồi cài đặt lại chúng cho instance thứ nhất này.
- Đợi 90 giây.
- Hủy đăng ký của tất cả các dịch vụ web trong instance thứ hai của Eureka server (dùng enpoint pause của actuator hoặc Eureka API PUT OUT_OF_SERVICE)
- Đợi 90 giây.
- Dừng tất cả các service, rồi cài đặt lại chúng cho instance thứ hai này.
- sau khoảng 90 giây, các dịch vụ web của instance thứ hai sẽ hoạt động trở lại.

Ngoài ra, cần phải cài đặt retry cho gateway để tránh trường hợp một dịch vụ web có vấn đề thì truy vấn vẫn sẽ được thực hiện trong một dịch web tương đương khác có sẵn. 


Cài đặt


1. Cấu hình để mở các endpoints actuator /pause và /shutdown

Trong tập tin application.yml

management:
endpoint:
pause:
enabled: true
restart:
enabled: true
resume:
enabled: true
shutdown:
enabled: true

Ví dụ dùng các endpoints này như sau :

$ curl -X POST http://localhost:1234/actuator/pause
$ curl -X POST http://localhost:1234/actuator/shutdown

2. Cài đặt retry

Trong tập tin pom.xml thêm Spring Retry vào dự án như sau :


        <!-- enable retry for non-reactive version (load-balanced RestTemplate) -->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

 Trong tập tin cấu hình application.xml của zuul, ta cần kích hoạt loadbalancer và retry như sau :

spring:
  cloud:
    # enable retry for reactive version (with WebClient)
    loadbalancer:
      retry:
        enabled: true
      ribbon:
        enabled: true

zuul:
  retryable: true

ribbon:
  OkToRetryOnAllOperations: true
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 3
  # 500 (Internal Server Error), 503 (Service Unavailable), 408 (Request Timeout)
  retryableStatusCodes: 500, 503, 408

 

 Trong các tập tin cấu hình application.xml của các services, ta cần cấu hình cho retry như sau :

ribbon:
  OkToRetryOnAllOperations: true
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 3
  # 500 (Internal Server Error), 503 (Service Unavailable), 408 (Request Timeout)
  retryableStatusCodes: 500, 503, 408

 

Xem thêm tại đây

https://openspacevn.blogspot.com/2023/07/cau-hinh-e-dung-auto-retry-cho-spring.html


3. Cài đặt cho các instance của HA

a. Cài đặt chung cho các instance trong tập tin application.xml

eureka:
instance:
prefer-ip-address: true
# Indicates the interval of heartbeats that the client sends to the server, in seconds (The default value is 30 seconds)
lease-renewal-interval-in-seconds: 1
# The time in seconds that the Eureka server waits since it received the last heartbeat from a client before it can remove that client from its registry (the default value is 90 seconds)
lease-expiration-duration-in-seconds: 1
client:
fetch-registry: true
register-with-eureka: true
healthcheck:
enabled: true
server:
# This property tells the Eureka server to run a job at this frequency to evict the expired clients. (the default value is 60 seconds)
eviction-interval-timer-in-ms: 1000
enableSelfPreservation: false



ribbon:
# in second
NFLoadBalancerPingInterval: 1
# in millisecond
ServerListRefreshInterval: 1000


server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 2m


b. Cài đặt cho instance 1 trong tập tin application_ha_1.xml

eureka:
client:
service-url:
defaultZone: http://${app1.address.ip}:8761/eureka/,http://${app2.address.ip}:8761/eureka/
preferSameZoneEureka: true
transport:
applicationsResolverUseIp: true
instance:
metadata-map:
zone: zone1



c. Cài đặt cho instance 2 trong tập tin application_ha_2.xml


eureka:
client:
service-url:
defaultZone: http://${app2.address.ip}:8761/eureka/,http://${app1.address.ip}:8761/eureka/
preferSameZoneEureka: true
transport:
applicationsResolverUseIp: true
instance:
metadata-map:
zone: zone2


d. Cấu hình cho từng server Eureka ở HA


eureka:
client:
fetch-registry: true
register-with-eureka: true


e. Dùng Eureka API để đặt dịch vụ web ở chế độ OUT_OF_SERVICE

Cú pháp :

PUT /eureka/apps/appId/instanceId/status?value=OUT_OF_SERVICE



Ví dụ :

curl -X PUT 
http://localhost:8761/eureka/apps/DEMO-SERVICE/demo-service:f42adf31bb76e181988fc292a1344be9/status?value=OUT_OF_SERVICE


Tham khảo


Ngoài ra có thể tham khảo thêm các properties trong class org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean

(chẳng hạn như : peerEurekaNodesUpdateIntervalMs, peerEurekaStatusRefreshTimeIntervalMs ...)


Các đường dẫn tham khảo :


https://www.credera.com/insights/zero-downtime-rolling-deployments-netflixs-eureka-zuul

https://cloud.spring.io/spring-cloud-netflix/2.0.x/multi/multi__service_discovery_eureka_clients.html

https://github.com/spring-cloud/spring-cloud-netflix/issues/1571

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#graceful-shutdown

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.profile-specific


Thứ Hai, 24 tháng 7, 2023

Xử lý lỗi khởi động mySqlRouter

 1. Nếu MySqlRouter không tìm thấy thư mục chứa tập tin  mysqlrouter.pid (chẳng hạn thư mục /var/run/mysqlrouter/ thì tạo thư mục ấy :

$ sudo mkdir /var/run/mysqlrouter/
$ sudo chown -R mysql:mysql /var/run/mysqlrouter/

 

2. Nếu service không khởi động được, ta có thể loại nó ra khỏi systemd rồi sau đó cập nhật tập biến RestartSec của tập tin service
$ sudo systemctl disable mysqlrouter.service


3. Cập nhật mysqlrouter.service để gia hạn thêm thời gian tái khởi động dịch vụ

$ sudo vi /etc/systemd/system/mysqlrouter.service

....

RestartSec=20

....


4. Sau đó cập nhật lại systemd
$ sudo systemctl daemon-reload


5.Rồi khởi động lại dịch vụ
$ sudo systemctl start mysqlrouter.service 



Một số ví dụ về Regex

1. Tìm một cụm chỉ có 5 chữ số :


Regex : ([0-9]{5}$)
Ex : 1234567891 12346 => Tìm thấy "12346"

 

2. Tìm một cụm đầu câu bắt đầu bằng một hoặc nhiều số và theo sau bằng một chữ cái hoặc không :


Regex : ^[0-9]+[aA-zZ]?
Ex : 10
12t
15A  


3. Tìm một cụm đầu câu bắt đầu bằng một hoặc nhiều số và theo sau bằng không hoặc nhiều chữ cái :


Regex : ^[0-9]+[aA-zZ]*.?
Ex : 10
12A
15th 


4. Tìm một từ cuối câu có hoặc không có dấu chấm :


Regex : [aA-zZ]*\.?$
Ex : etc
etc.


5. Tìm một cụm từ bắt đầu bằng 5 chữ số và một chuỗi các từ theo sau đó, có thể có dấu gạch ngang giữa 5 chữ số và chuỗi từ theo sau :


Regex : ([0-9]{5}\s[aA-zZ\-].+)
Ex : 15698 test
98567 - test


6. Tìm một cụm từ có dạng TYPE1.<bất kỳ từ nào>:TYPE2 :


Regex : TYPE1\.[aA-zZ]*.:TYPE2
Ex : TYPE1.TEST:TYPE2


Thứ Hai, 3 tháng 7, 2023

Một số lệnh với keytool

 Dưới đây là một số lệnh với keytool

 

1. Thêm certificat vào keystore

$ sudo keytool -importcert -keystore <đường dẫn đến keystore> -storepass <mật khẩu của keystore> -alias <tên dùng đại diện cho certificat> -file <đường dận tới tập tin certificat>  -noprompt

 

 Ví dụ :

$ sudo keytool -importcert -keystore /opt/java/lib/security/cacerts -storepass changeit -alias toan-test -file /tmp/toan_certif.crt  -noprompt

$ sudo keytool -importcert -storetype PKCS12 -keystore /opt/truststore.p12 -file /tmp/test_cert.pem -alias test-alias


2. Trích xuất certificat từ keystore

$ keytool -exportcert -keystore <đường dẫn đến keystore> -storeType PKCS12 -alias <tên dùng đại diện cho certificat> -file <đường dận tới tập tin certificat trích xuất>


 Ví dụ :

$ keytool -exportcert -keystore /opt/keystore.p12 -storeType PKCS12 -alias test -file ./certificat_cert.pem


3. Liệt kê các certificat trong keystore

$ keytool -list -keystore <đường dẫn đến keystore>


 Ví dụ :

$ keytool -list -keystore /opt/java/lib/security/cacerts -storepass changeit | grep toan

 

4. Xóa một certificat bằng alias trong keystore

 
$ keytool -delete -alias <tên dùng đại diện cho certificat> -keystore <đường dẫn đến keystore>

 

Ví dụ: 


$ keytool -delete -alias toan-test -keystore /opt/truststore.p12


Ngoài lề :

Sao chép tập tin với lệnh scp (hoặc scp.exe trong git bash) certificat


$ scp <đường dẫn đến tập tin gốc để copy> <login>@<địa chỉ ip>:<đường dẫn tới vị trí đích để copy tới đó>


Ví dụ: 

$ scp ./toan_certificat.crt toan@10.10.11.11:/tmp/




Cấu hình để dùng Auto Retry cho Spring Boot ở High Availability : Netflix Zuul, Eureka, Ribbon

 Khi Netflix Zuul được dùng như một gateway để điều hướng những truy vấn nhận được cho các services ở backend, có đôi lúc những truy vấn này có thể không được các service này trả lời.

Để tránh những trường hợp này, ta có thể cấu hình sao cho những truy vấn này được thực thi lại (retry) một cách tự động. Khi dùng Spring Cloud Netflix, ta cần thêm Spring Retry vào tập tin pom.xml của dự án để dùng cấu hình này. Khi Spring Retry được dùng, loadbalancer của Zuul (ở đây là Ribbon) sẽ tự động thực hiện retry những truy vấn thất bại.

Trong tập tin pom.xml thêm Spring Retry vào dự án như sau :


        <!-- enable retry for non-reactive version (load-balanced RestTemplate) -->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

 Trong tập tin cấu hình application.xml của zuul, ta cần kích hoạt loadbalancer và retry như sau :

spring:
  cloud:
    # enable retry for reactive version (with WebClient)
    loadbalancer:
      retry:
        enabled: true
      ribbon:
        enabled: true

zuul:
  retryable: true

ribbon:
  OkToRetryOnAllOperations: true
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 3
  # 500 (Internal Server Error), 503 (Service Unavailable), 408 (Request Timeout)
  retryableStatusCodes: 500, 503, 408

 

 Trong các tập tin cấu hình application.xml của các services, ta cần cấu hình cho retry như sau :

ribbon:
  OkToRetryOnAllOperations: true
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 3
  # 500 (Internal Server Error), 503 (Service Unavailable), 408 (Request Timeout)
  retryableStatusCodes: 500, 503, 408

Giải thích :

- Nếu trong các service, bạn dùng RestTemplate, thì mặc định retry không được kích hoạt. Để kích hoạt retry cho phiên bản non-reactive tức RestTemplate, bạn chỉ cần thêm Spring Retry vào dự án như trên.

- Cho phiên bản reactive tức WebClient, để kích hoạt retry bạn cần phải thêm như sau: spring.cloud.loadbalancer.retry.enabled=true và spring.cloud.loadbalancer.ribbon.enabled=true

- MaxAutoRetries –  Số lần truy vấn thất bại được tái thực thi trên cùng server  (default 0)

- MaxAutoRetriesNextServer – Số server được dùng để thực hiện retry, ngoại trừ server đầu tiên (default 0)

- retryableStatusCodes – danh sách status code của HTTP để thực hiện retry

- OkToRetryOnAllOperations – khi biến này là true, thì mọi loại truy vấn HTTP đều được thực hiện retry, chứ không bải chỉ có loại chi vấn GET được retry ở mặc định.



 

Thứ Sáu, 21 tháng 4, 2023

MySql : Ví dụ viết một thủ tục procedure

 Một thủ tục trong MySql có cú pháp như sau :

DELIMITER {custom delimiter}
CREATE PROCEDURE {procedureName}([optional parameters])
BEGIN
// procedure body...
// procedure body...
END
{custom delimiter}

Ví dụ
Ta có bảng customer như sau :


Table "customer"
Column | Type | Modifiers
------------+-------------+-----------
id | integer | not null
address | varchar(64) |
first_name | varchar(32) |
last_name | varchar(32) |
email | varchar(64) |
Indexes:
"customer_pkey" PRIMARY KEY, btree (id)

Ta muốn tạo một bảng temp_customer có cấu trúc giống hệt bảng customer; và bảng này có dữ liệu lấy từ bảng customer :

DROP TABLE IF EXISTS temp_customer;
CREATE TABLE temp_customer
(
id binary(16) not null primary key,
address varchar(64),
first_name varchar(32),
last_name varchar(32),
email varchar(64)
);

 

Ta định nghĩa một thủ tục như sau :

DROP PROCEDURE IF EXISTS ADD_TEMP_CUSTOMER;
DELIMITER $$
CREATE PROCEDURE ADD_TEMP_CUSTOMER()
BEGIN
DECLARE retrieved_address varchar(64);
DECLARE retrieved_first_name varchar(32);
DECLARE retrieved_last_name varchar(32);
DECLARE retrieved_email varchar(64);
DECLARE new_id binary(16);
DECLARE num_row int;
DECLARE finished INTEGER DEFAULT 0;

DECLARE customer_cursor CURSOR FOR
SELECT address, first_name, last_name, email FROM customer;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;

OPEN customer_cursor;
insert_loop:
LOOP
FETCH customer_cursor INTO retrieved_address, retrieved_first_name, retrieved_last_name, retrieved_email;
IF finished THEN
LEAVE insert_loop;
END IF;

SET @temp_count = (SELECT count(*) FROM temp_customer);
SELECT count(*) into num_row FROM temp_customer;

IF num_row = 0 THEN
SET new_id = UUID_TO_BIN(UUID());
INSERT IGNORE INTO temp_customer(id, address, first_name, last_name, email)
VALUES (new_id, retrieved_address, retrieved_first_name, retrieved_last_name, retrieved_email);
END IF;

END LOOP;
CLOSE customer_cursor;
END $$
DELIMITER ;

CALL ADD_TEMP_CUSTOMER();
DROP PROCEDURE IF EXISTS ADD_TEMP_CUSTOMER;

Giải thích : thủ tục này sẽ dùng một cursor để đọc hết bảng customer và lấy thông tin từ đây điền vào bảng temp_customer


Chú ý để có thể thực thi thủ tục, user phải có quyền thực thi nó, ta có thể gán quyền ấy như sau :

GRANT CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON *.* TO 'toan'@'127.0.0.1';
FLUSH PRIVILEGES;

Hoặc có thể gán mọi quyền như sau :

GRANT ALL PRIVILEGES ON *.* TO 'toan'@'127.0.0.1' WITH GRANT OPTION;
FLUSH PRIVILEGES;

 

Giới thiệu dùng thư viện JGit

 Thư viện JGit của Eclipse cho phép tương tác với một hệ thống Git quản lý mã nguồn. Ta thêm dependency của JGit vào tập tin pom.xml để xài thư viện này như sau :

		<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.5.0.202303070854-r</version>
</dependency>

Trong ví dụ dưới đây có dùng thư viện commons-io nên ta cũng thêm thư viện này vào tập tin pom.xml như sau :

		<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

Dưới đây là một ví dụ xài thư viện JGit :


import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;

public class GitBackUpFile {

public static void main(String[] args) {
File createdFile = new File("example.txt");
try {
Files.write(createdFile.toPath(), "File contents".getBytes());
System.out.println("file path : " + createdFile.getAbsolutePath());
backupInGit(createdFile.getAbsolutePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static void backupInGit(String sourceFileName) {
String branchName = "branch-local";
String gitDirectoryPath = "/path/to/repo" + File.separator + branchName;
File gitLocalDirectory = new File(gitDirectoryPath);
try {
FileUtils.deleteDirectory(gitLocalDirectory);
} catch (IOException e) {
System.out.println("Error deleting git local directory : " + e.getMessage());
}

if (!gitLocalDirectory.exists() && !gitLocalDirectory.mkdirs()) {
throw new RuntimeException("Error creating local git directory : " + gitDirectoryPath);
}

String branch = "refs/heads/" + branchName;
UsernamePasswordCredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider("username", "password");
try (Git git = Git.cloneRepository().setURI("https://github.com/example-eclipse/jgit.git")
.setDirectory(gitLocalDirectory)
.setBranchesToClone(Arrays.asList(branch))
.setBranch(branch)
.setCredentialsProvider(credentialsProvider)
.call()) {

String fileName = "fileToPushIntoGit.txt";
String fileAbsolutePath = gitDirectoryPath + File.separator + fileName;
Files.copy(Paths.get(sourceFileName), Paths.get(fileAbsolutePath), StandardCopyOption.REPLACE_EXISTING);

LocalDateTime timeStamp = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH");

git.add().addFilepattern(fileName).call();
git.commit().setMessage("Commit file " + fileName + " at " + timeStamp.format(dateTimeFormatter)).call();

PushCommand pushCommand = git.push();
pushCommand.setRemote("origin").setRefSpecs(new RefSpec(branch + ":" + branch));
pushCommand.setForce(true).setPushAll();
pushCommand.setCredentialsProvider(credentialsProvider);
pushCommand.call();

} catch (GitAPIException | IOException e) {
System.out.println("Error when backing up file to git" + e.getMessage());
}
}
}

Trong ví dụ trên ta tạo một tập tin example.txt và push nó vào một branch của depository git

Để có thể push tập tin này vào Git :
- Trước hết ta tạo một thư mục repository ở local để có thể clone repository từ depos Git về đây.
- Nếu đã có một repos git ở thư mục này từ trước rồi thì ta phải xóa nó đi, rồi tạo mới lại thư mục vì không khi clone sẽ báo lỗi.
- Tiếp theo ta clone về nhánh Git mà ta cần lưu tập tin vừa tạo.
- Sao chép tập tin cần lưu vào repos Git ở local này.
- Commit tập tin này vào repos local.
- Sau đó push nó vào repos ở xa.