Tập tành tạo Pooling Thrift client with ThreadLocal in Java

Trong một hệ thống, khi cần giao tiếp giữa các services ta thường sử dụng RESTful API vì dễ dàng sử dụng và cài đặt. Tuy nhiên REStful thường được implement trên HTTP khiến nó đôi khi không đạt được performance đủ tốt. Lúc đó ta cần đến một vài phương thức RPC khác như Thrift, gRPC…

Thrift client is not thread-safe

Nếu bạn đã từng làm việc với Thrift thì sẽ biết rằng mỗi Thrift client tương ứng với 1 connection. Trong kiến trúc của Thrift, transport layer được thiết kế đơn giản chỉ là một wrapper trên một socket hoặc file, và nó hoàn toàn NOT thread-safe. Điều đó có nghĩa là bạn cần thiết kế để Thrift client thread-safe ở application level.

Có một số cách mà ta có thể nghĩ đến là:

  • Sử dụng lock mỗi khi gọi tới Thrift client, bảo đảm chỉ một thread được access tại một thời điểm.
  • Tạo mới client/connection mỗi khi gọi.
  • Tạo ra một pool các connections.

Sử dụng lock

Lock là cách đơn giản nhất để chia sẻ các tài nguyên không thread-safe, với Thrift cũng thế. Tuy nhiên, rõ ràng đây là cách tạo ra bad-performance vì các thread phải đợi nhau mỗi khi cần gọi tới Thrift client. Nó sẽ dễ dàng tạo ra một nút thắt cổ chai cho ứng dụng của bạn.

Tạo mới client/connection

Đây rõ ràng cũng không phải là một ý tưởng tốt. Quá nhiều connection được tạo ra và hủy bỏ mỗi lần thực hiện một RPC call rõ ràng là quá tốn kém. Thời gian khởi tạo một connection trước mỗi lần gọi thậm chí khiến performance còn kém hơn cách thứ nhất.

Tạo ra một pool connections

Ý tưởng của Thrift client pool tương tự như thread pool và connection pool thường được cài đặt trong các database client. Về cơ bản, một số Thrift client sẽ được khởi tạo trước, mỗi khi có một request tới pool, một client được lấy ra từ pool để thực thi, sau khi kết thúc, client được trở lại pool thay vì bị hủy bỏ. Tùy thuộc vào implementation, pool có thể đóng các connection lâu không được sử dụng, tạo mới connection nếu cần thiết và giới hạn số connection tối đa đồng thời.

Các bạn có thể tham khảo một cài đặt khá chi tiết để tạo ra một Thrift pool client. Tuy nhiên trong thực tế đôi khi bài toán của bạn gặp phải đơn giản hơn nhiều và không cần phải có một client pool phức tạp như bài viết trên. Sau đây tôi sẽ hướng dẫn bạn xây dựng một Thrift client pool theo một hướng khác.

Thrift client pool with ThreadLocal in Java

Trong Java có một class khá đặc biệt là ThreadLocal. Đây là một generic class cho phép bạn set một object vào nó. Điều làm nên sự đặc biệt của ThreadLocal là câu lệnh set object được gọi trên thread nào thì chỉ thread đó mới có thể get object đấy ra. Nghĩa là cùng hàm get nhưng gọi trên các thread khác nhau sẽ cho kết quả khác nhau.

Ta sẽ ứng dụng ThreadLocal để tạo ra thread-safe cho các Thrift client. Mỗi khi cần một client ở bất kì thread nào, ta sẽ check đã có một client tồn tại với thread này (có sẵn trong ThreadLocal) hay chưa ? Nếu có rồi đơn giản lấy ra và sử dụng, nếu chưa ta tạo mới và set client này vào ThreadLocal. Bởi vì các thread khác nhau không thể get ra cùng một client, ta luôn bảo đảm thread-safe.

Lí thuyết thế đủ rồi, giờ hãy bắt tay vào cài đặt cụ thể. Đầu tiên tạo một interface gọi là ClientFactory, đây là interface dùng để tạo ra một TServiceClient client cụ thể của bạn.

public interface ClientFactory {
T newClient(TProtocol protocol);
}

Cài đặt của pool cực kỳ ngắn gọn như sau:

public class ServiceClientPool implements AutoCloseable {
private final ServiceClientFactory clientFactory;
private final ThreadLocal clientThreadLocal = new ThreadLocal<>();
private final List clients = new ArrayList<>();
 
public ServiceClientPool(ServiceClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
 
public final T getClient() {
T client = clientThreadLocal.get();
if (client == null) {
TTransport transport = new TFastFramedTransport(TSocket("localhost", 7676));
TProtocol protocol = new TBinaryProtocol(transport);
transport.open();
 
client = clientFactory.newClient(protocol);
clientThreadLocal.set(client);
clients.add(client);
}
return client;
}
 
public void close() {
clients.forEach(client -> {
client.getInputProtocol().getTransport().close();
client.getOutputProtocol().getTransport().close();
});
}
}

Giải thích một chút:

  • Phần tạo mới Thrift client (bao gồm khởi tạo protocol và transport layer) chỉ là ví dụ, bạn có thể thay đổi theo bất kì cách nào mình muốn.
  • Vì không có cách nào để biết đã tạo ra tất cả bao nhiêu client, và làm cách nào để lấy toàn bộ client về từ ThreadLocal, ta cần lưu lại các client đã được tạo ra vào trong 1 list để quản lý và đóng các client này lại khi cần thiết.

Bây giờ mỗi khi sử dụng bạn chỉ cần đơn giản tạo ra một pool và gọi hàm getClient để lấy về object client cần gọi.

// tao ra mot lan duy nhat
ServiceClientPool clientPool = ...
 
// get client va su dung
UserService.Client client = clientPool.getClient();

Have fun !

Fivestar: 
No votes yet