Trong các pattern để xây dựng kiến trúc Microservice, thì API Gateway là một pattern, một thành phần phổ biến. Trong bài viết này tôi sẽ trình bày Spring Cloud Gateway một cách thật đơn giản để các bạn nhập môn. Sau đó tôi sẽ giới thiệu thêm một filter RateLimiter để khống chế số lượng request trong một khoảng thời gian. Link mã nguồn trên github
Cấu trúc thư mục ví dụ demo
├── booklisting <-- REST microservice booklisting phục vụ ở đường dẫn http://localhost:8081/book/books hoặc có lúc là http://localhost:8081/books
├── filmlisting <-- REST microservice filmlisting phục vụ ở đường dẫn http://localhost:8081/book/books hoặc có lúc là http://localhost:8081/books
├── gateway <-- Spring Cloud Gateway cấu hình bằng application.yml
├── gwcode <-- Spring Cloud Gateway cấu hình bằng code
1. Một API Gateway căn bản sẽ có những gì?
Một API Gateway thường sẽ có những chứng năng chính sau đây:
- Routing: chuyển hướng các request đến dịch vụ (microservice) đứng sau gateway
- Load Balacing: phân tải. Một API Gateway có thể đếm được số request đến và phân phối cho nhiều dịch vụ cùng chức năng. Việc tạo ra các dịch vụ cùng chức năng sẽ là việc của Docker Swarm hay Kubernetes.
- Authentication - Authorization: API Gateway vẫn cần phối hợp với microservice chuyên quản lý người dùng, xác thực, phân quyền. Tuy nhiên API Gateway làm nhiệm vụ xác thực thì chúng ta sớm đạt mục tiêu Single Sign On (đăng nhập một lần).
- Prefilter: tiền xử lý các request đến, có thể loại bỏ, ngăn chặn hoặc thêm / bớt các trường trong header, giải mã token
- Postfilter: hậu xử lý các response từ microservice trả về.
- Rate Limiting: khống chế số lượng truy cập
- Circuit Breaker: cầu dao mở mạch khi xuất hiện lỗi từ các microservice phía sau. Khi mọi thứ trở lại bình thường thì đóng mạch.
- Logging: thu thập thông tin.
Vài điều bạn cần biết về API Gateway
- Một API Gateway mà làm quá nhiều việc (hay nó phức tạp, cồng kềnh) thì tốc độ chuyển hướng sẽ chậm lại
- Có thể nhân bản API Gateway được, điều này sẽ giúp xử lý được nhiều request hơn.
- Java chưa hẳn đã là ngôn ngữ phù hợp nhất để tạo ra API Gateway. Golang hiện nay được sử dụng nhiều tạo ra các phần mềm chuyên định tuyến, chuyển hướng mạng, tốc độ nhanh, tốn ít tài nguyên.
2. Tạo một Spring Cloud API Gateway đơn giản
Chúng ta tạo Spring Cloud API Gateway bằng cách tạo dự án Spring Boot thông thường, bổ xung dependency quan trọng nhất dưới đây.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Có 2 cách để cấu hình Spring Cloud API Gateway:
- Sử dụng file cấu hình application.properties hoặc application.yml.
- Lập trình code để cấu hình.
Lời khuyên của tôi là hãy ưu tiên sử dụng file cấu hình, bởi nó dễ hiểu, dễ sửa, và có thể kết hợp với Spring Cloud Config để tập trung hoá cấu hình. Chúng ta chỉ dùng code để viết bổ xung các filter, hoặc logic phức tạp. Hãy ưu tiên sự đơn giản. Vì càng đơn giản thì càng dễ nhân bản (scale out).
Tiếp theo tôi tạo ra 2 REST Microservice có tên là:
- BookListing lắng nghe ở http://localhost:8081/book/books
- FilmListing lắng nghe ở http://localhost:8082/film/films
2.1 Book Listing REST microservice
Định nghĩa model
@Data
@AllArgsConstructor
public class Book {
private String title;
private String author;
}
Tạo REST Controller trả về.
@RestController
@RequestMapping(value = "/book")
public class BookController {
@GetMapping("/books")
public ResponseEntity<List<Book>> getAllBooks() {
List<Book> books = new ArrayList<>();
books.add(new Book("Dế Mèn Phiêu Lưu Ký", "Tô Hoài"));
books.add(new Book("Sherlock Homes", "Arthur Conan Doyle"));
books.add(new Book("Lược sử loài người", "Yuval Noah Harari"));
return ResponseEntity.ok().body(books);
}
}
Chuyển vào thư mục booklisting gõ lệnh
./mvnw spring-boot:run
Bật trình duyệt và truy cập vào http://localhost:8081/book/books bạn sẽ thấy
[
{
"title": "Dế Mèn Phiêu Lưu Ký",
"author": "Tô Hoài"
},
{
"title": "Sherlock Homes",
"author": "Arthur Conan Doyle"
},
{
"title": "Lược sử loài người",
"author": "Yuval Noah Harari"
}
]
2.2 Film Listing REST microservice
Làm tương tự như với Book Listing, bật trình duyệt và truy cập vào http://localhost:8082/film/films bạn sẽ thấy
[
{
"title": "Tom & Jerry",
"actors": "Tom, Jerry, Snoop"
},
{
"title": "Money Heist",
"actors": "Professor, Berlin, Tokyo, Rio, Nairobi, Helsinki"
},
{
"title": "Làng Vũ Đại ngày ấy",
"actors": "Chí Phèo, Thị Nở, Bá Kiến, ông giáo"
}
]
2.3 Cấu hình Spring Cloud Gateway bằng application.yml
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: book
uri: http://localhost:8081/
predicates:
- Path=/book/**
- id: film
uri: http://localhost:8082/
predicates:
- Path=/film/**
Spring Boot Cloud Gateway sẽ khởi động Netty rồi lắng nghe ở cổng 8080. Khác với ứng dụng Spring Boot thông thường dùng TomCat. SB Cloud Gateway sử dụng cơ chế non-blocking do đó mặc định dùng Netty.
- Thẻ routes cài đặt các quy tắc phân luồng. Mỗi luồng có thể chuyển đến một microservice ở phía sau (tiếng Anh gọi là down stream - hạ lưu)
- Thẻ id đặt tên phân biệt từng luồng.
- Thẻ uri lưu địa chỉ của microservice
- Thẻ predicates lưu các quy tắc khớp các đường dẫn. Ví dụ /book/** sẽ gom tất cả các đường dẫn chúng thành phần đường dẫn đầu tiên là "book"
Khởi động SB Cloud Gateway, lắng nghe ở cổng 8080, rồi truy cập đường dẫn http://localhost:8080/book/books, bạn sẽ nhận được kết quả trả về như vào đường dẫn http://localhost:8081/book/books
3. Log hoạt động chuyển hướng của Spring Cloud Gateway
Để can thiệp, hay mở rộng tính năng của API Gateway, người ta có thêm vào các middleware hay còn gọi là filter. Với Spring Cloud, chúng ta có 2 loại: PreFitler và PostFilter.
Tôi tạo một LoggingFilter.java như sau. Mục tiêu là log địa chỉ URI của request tới và request đã được chuyển hướng.
package vn.techmaster.gateway;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
import java.net.URI;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LoggingFilter implements GlobalFilter {
Log log = LogFactory.getLog(getClass());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Set<URI> originalUris = exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
if (originalUris != null) {
URI originalUri = originalUris.iterator().next();
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
URI routeUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
log.info("Incoming request " + originalUri.toString() + " is routed to id: " + route.getId()
+ ", uri:" + routeUri);
}
return chain.filter(exchange);
}
}
Khi truy cập đến http://localhost:8080/book/books chúng ta sẽ thấy ở console in ra
Incoming request http://localhost:8080/book/books is routed to id: book, uri: http://localhost:8081/book/books
4. Lược bỏ một thành phần trong đường dẫn bằng filters: StripPrefix
Filter StripPrefix hoạt động như thế nào
StripPrefix=1
Vào http://localhost:8080/book/books thì đầu ra sẽ chuyển thành ```http://localhost:8081/books thành phần đường dẫn đầu tiên là book bị lược đi.
StripPrefix=2 sẽ có 2 thành phần đường dẫn bị lược đi.
Chỉnh lại cấu hình file application.yml
spring:
cloud:
gateway:
routes:
- id: book
uri: http://localhost:8081/
predicates:
- Path=/book/**
filters:
- StripPrefix=1
- id: film
uri: http://localhost:8082/
predicates:
- Path=/film/**
Trong file BookController.java, bạn bỏ đi dòng @RequestMapping(value = "/book")
@RestController
@RequestMapping(value = "/book")
public class BookController {
}
thành
@RestController
public class BookController {
}
để dịch vụ Book Listing phục vụ ở địa chỉ http://localhost:8081/books
Log trong màn hình console khi có request đến gatewate http://localhost:8080/book/books là
Incoming request http://localhost:8080/book/books is routed to id: book, uri: http://localhost:8081/books
5. Redis Rate Limiting khống chế số lượng truy cập
Có một số tình huống Rate Limiting rất hữu ích:
- Từ một địa chỉ IP không được phép gửi request login quá nhiều trong một khoảng thời gian
- Từ một login user không được gửi quá nhiều lệnh đặt mua, hay tạo comment, spam...
Rate Limiting ở một số điểm có thể dùng để chống lại DDOS attack vào một dịch vụ. Với Spring Cloud Gateway, thì tôi dùng Redis Rate Limiting filter.
trong pom.xml cần bổ xung dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
Spring Cloud Gateway cần nối đến Redis Server ở cổng 6379. Toàn bộ file cấu hình application.yml sau khi bổ xung Redis Rate Limiting và cấu hình địa chỉ Redis
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: book
uri: http://localhost:8081/
predicates:
- Path=/book/**
filters:
- StripPrefix=1
- id: film
uri: http://localhost:8082/
predicates:
- Path=/film/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter:
replenishRate: 1
burstCapacity: 2
requestedTokens: 1
redis:
database: 0
host: localhost
port: 6379
Tiếp đó bổ xung phương thực tạo @Bean kiểu KeyResolver. Nếu không có phương thức này, RateLimiter sẽ chặn luôn truy cập.
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just("1");
}
Tham khảo thêm bài viết này A look into Spring Cloud Gateway
5.1 Thí nghiệm vượt ngưỡng rate limiting
Để thí nghiệm quá tải Redis Rate Limiter tôi đặt giá trị 2 thuộc tính :
- replenishRate:1 số trung bình request có thể xử lý trong một giây là 1
- burstCapacity:2 số lượng tối đa request có thể tiếp nhận trong một giây là 2. Giây đầu tiên có 2 request thì giây thứ 2 không có request nào đến, thì trung bình 1 request / 1 giây vẫn trong giới hạn của Rate Limiter. Giây thứ 3, Gateway có thể nhận tiếp 1 request.
Hãy liên tục truy cập vào http://localhost:8080/film/films bạn sẽ thấy đôi khi Gateway trả về dữ liệu thành công, nhưng đối khi nó trả về mã lỗi HTTP ERROR 429 Too many requests. Tăng replenishRate và burstCapacity, bạn sẽ thấy Spring Cloud Gateway tiếp nhận, xử lý nhiều request / giây hơn.
5.2 Sử dụng Bombardier để kiểm thử tải
Bombarider viết bằng Golang, mở mã nguồn tại đây https://github.com/codesenberg/bombardier
Gõ lệnh sau đây vào terminal
bombardier -c 1 -n 200 http://localhost:8080/film/films
Trường hợp 1:
- replenishRate:1 và burstCapacity:2: phân bổ mã lỗi như sau 1xx - 0, 2xx - 2, 3xx - 0, 4xx - 198, 5xx - 0. Có 198 lỗi Too Many Requests trên tổng số 200 requests gửi đến.
- replenishRate:100 và burstCapacity:200: 1xx - 0, 2xx - 200, 3xx - 0, 4xx - 0, 5xx - 0. Cả 200 requests gửi đến đều được phục vụ thành công.
Rõ ràng là khi SP Gateway nâng mức khống chế API Gateway sử lý được nhiều reques hơn.
6. API Gateway suy giảm tốc độ phụ vụ như thế nào?
Gõ lệnh này termial, để kiểm tra tốc độ phản hồi của API Gateway
bombardier -c 200 -n 5000 http://localhost:8080/book/books
Kiểm tra tốc độ phản hồi của REST API đi qua API Gateway. Kết quả là Throughput: 1.51Mb/s
bombardier -c 200 -n 5000 http://localhost:8081/books
Kiểm tra tốc độ phản hồi của REST API không đi qua API Gateway là Throughput: 6.42MB/s. Như vậy là Spring Cloud làm suy giảm tốc độ phản hồi của REST API là 6.42MB/s xuống 1.51Mb/s.
Khi chúng ta vừa muốn một API Gateway có nhiều chức năng filter, lại tốc độ xử lý tốt chỉ còn cách đã scale up (thêm CPU, RAM) và sớm muộn sẽ phải scale out ra thành nhiều node.
7. Lập trình để cấu hình Spring Cloud API Gateway
Phần cấu hình trong file application.yml có thể bằng cách lập trình tạo ra một class GatewayConfig.
@Configuration
public class GatewayConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379)); //Cấu hình kết nối đến Redis Server
}
@Bean
// Cấu hình các quy luật chuyển hướng
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/book/**").uri("http://localhost:8081/"))
.route(r -> r
.path("/film/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()).setStatusCode(HttpStatus.TOO_MANY_REQUESTS)))
.uri("http://localhost:8082/"))
.build();
}
@Bean
// Tạo ra một bean component redisRateLimiter
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(100, 300);
}
}
Cách này hơi dài dòng, các bạn xem code ở đây https://github.com/TechMaster/SpringBoot28Days/tree/main/16-SpringCloudGateway/01-SpringCloudBasic/gwcode
8. Tổng kết
- Spring Cloud Gateway có thể cấu hình bằng Yaml hay Code. Hãy ưu tiên cách đơn giản trước.
- RateLimiting cần phải cài đặt Redis.
- Suy hao tốc độ phản hồi khi đặt một dịch vụ phía sau một Gateway là khá lớn.
Bình luận
mình làm theo gude nhưng mà test tải thì thấy không phần rate limit không có tác dụng.
Mình dùng jmester
kiểm tra redis cũng không thấy dữ liệu được lưu vào khi chạy ứng dụng