Note: see the link below for the English version of this article.

https://duongnt.com/meterfilter-datadog

Dùng MeterFilter để thay đổi metric Resilience4j

Circuit Breaker là một pattern thường gặp. Nó cho phép ứng dụng của ta không tốn công thực hiện những thao tác mà tỷ lệ thất bại là cao. Khi phát triển ứng dụng bằng Kotlin, tôi thường sử dụng library Resilience4j để implement pattern này. Thư viện đó có sẵn module hỗ trợ Micrometer để giúp việc tích hợp với những hệ thống giám sát của bên thứ 3 trở nên đơn giản hơn.

Trong bài hôm nay, chúng ta sẽ tìm hiểu một số vấn đề khi tích hợp ứng dụng dùng Resilience4j chạy trên Amazon EC với Datadog; và xem cách interface MeterFilter giúp ta giải quyết những vấn đề này.

Chú ý: bài hôm nay cần tới một số kiến thức về Resilience4j, đặc biệt là về cách tạo đối tượng thuộc lớp CircuitBreakerRegistryCircuitBreaker.

Dùng Resilience4j với Micrometer và Datadog

Như đã nói ở trên, Micrometer là một facade để giúp việc tích hợp ứng dụng với cách hệ thống giám sát trở nên dễ dàng hơn. Nhưng trong bài này chúng ta sẽ chỉ xét trường hợp sử dụng Datadog. Code để gán một đối tượng thuộc lớp CircuitBreakerRegistry với một MeterRegistry là rất đơn giản.

val circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults()
TaggedCircuitBreakerMetrics
    .ofCircuitBreakerRegistry(circuitBreakerRegistry)
    .bindTo(meterRegistry)

Sau đó, ta có thể dùng CircuitBreakerRegistry để tạo đối tượng CircuitBreaker. Lúc này tất cả những request thực hiện thông qua circuit breaker sẽ tự động sinh metric cho Datadog. Ta có thể tham khảo danh sách chi tiết các metric đó tại đây.

Tuy nhiên, những metric mặc định đó không phải lúc nào cũng là phù hợp với nhu cầu của ta. Trong các phần tiếp theo, ta sẽ tìm cách xử lý 2 vấn đề dưới đây.

  • Tag name bị trùng lặp trong nhiều metric.
  • Giới hạn số metric mà Resilience4j sinh ra.

Tag "name" bị trùng lặp

Tình huống giả định trong bài là ứng dụng của ta dùng 2 circuit breaker (với tên gọi serviceAserviceB) để gửi request tới 2 service. Và ứng dụng này chạy trên một cluster Amazon EC2 được quản lý bởi Kubernetes.

Một dashboard đơn giản để theo dõi tổng số request

Như đã thấy trong link này, ta có thể theo dõi tổng số request chạy qua circuit breaker bằng metric resilience4j.circuitbreaker.calls. Metric này có hỗ trợ một tag gọi là name để phân biệt các service sử dụng circuit breaker riêng biệt. Nhưng khi ta thử dùng tag name đó để thống kê số request tới từng request thì đồ thị của ta lại trông như dưới đây.

Calls count grouped by name

Rõ ràng là không ai có thể đọc nổi đồ thị này. Nhưng tại sao nó lại có nhiều đường đến như vậy? Lẽ ra ta chỉ có 2 đường tương ứng với 2 service thôi chứ? Ta sẽ xem thử tag Overview trong dashboard.

Calls count grouped by name, details

Có thể thấy rằng đúng là tag name có 2 giá trị serviceAserviceB. Nhưng chúng bị trộn lẫn với rất nhiều giá trị hostname. Những hostname này từ đâu ra?

Hoá ra là đây chỉ là một trong rất nhiều tag mà Datadog tự động sinh ra khi ta tích hợp ứng dụng chạy trên Amazon EC2 với nó. Tag này ghi lại tên từng instance EC2 trong cluster của ta.

Đổi tên tag trong metric Resilience4j bằng MeterFilter

Giải pháp lý tưởng nhất để giải quyết vấn đề này là khiến Datadog ngừng sinh tag name. Nhưng thật tiếc là cho tới thời điểm 2/4/2023, tôi vẫn chưa tìm được cách nào để làm việc này. Vì thế ta sẽ dùng MeterFilter để đổi tên tag name trong metric của Resilience4j thành servicename. Bằng cách đó nó sẽ không bị trùng lặp với tag do Datadog sinh ra.

Ta sẽ thực hiện việc này bằng cách sử dụng hàm renameTag. Signature của nó là như sau.

  • meterNamePrefix: prefix của metric mà ta muốn đổi tên tag của nó. MeterFilter sẽ tìm và đổi tên tag trong tất cả các metric trùng với prefix này.
  • fromTagKey: tên gốc của tag mà ta muốn đổi tên
  • toTagKey: tên mới cho tag đó

Vì vậy code để đổi tên tag name trong tất cả các metric của Resilience4j thành servicename sẽ như dưới đây.

meterRegistry.config().meterFilter(
    MeterFilter.renameTag(
        "resilience4j.circuitbreaker", // Modify all Resilience4j metrics, not just resilience4j.circuitbreaker.calls
        "name",
        "servicename"
    )
)

Bây giờ ta có thể dùng CircuitBreakerRegistry với meterRegistry mới này, và dashboard của ta sẽ trông gọn hơn nhiều.

Calls count grouped by servicename

Calls count grouped by servicename, details

Giới hạn số metric mà Resilience4j sinh ra

Như đã nhắc tới trong link này, Resilience4j sinh ra tổng cộng 7 metric, mỗi cái đều có nhiều tag. Trong đó tag name (hoặc servicename nếu ta đổi tên nó như đã nói ở phần trước) là đáng chú ý nhất. Lý do vì Datadog tính tiền dựa trên tổ hợp giá trị của tất cả các tag. Nghĩa là mỗi khi ta thêm một circuit breaker, tag name sẽ có thêm một giá trị, và ta sẽ lại tốn thêm tiền.

Thật may là ta có thể giải quyết một phần vấn đề này bằng cách hạn chế số metric mà Resilience4j được phép sinh ra. Dưới đây là 2 giải pháp chính.

  • Tạo blacklist để chặn những metric ta không cần.
  • Tạo whitelist để chỉ cho phép sinh những metric mà ta cần.

Tạo blacklist với MeterFilter

Nếu ta chỉ cần bỏ đi số ít metric thì ta có thể dùng hàm deny. Hàm này nhận vào một predicate để kiểm tra xem mỗi metric có cần bị loại bỏ hay không.

// Không sinh metric "resilience4j.circuitbreaker.buffered.calls"
meterRegistry.config().meterFilter(
    MeterFilter.deny { it.name == "resilience4j.circuitbreaker.buffered.calls" }
)

Hoặc ta cũng có thể dùng hàm denyNameStartsWith. Hàm này chỉ kiểm tra xem tên từng metric có khớp với một prefix cho trước hay không.

// Loại bỏ 2 metric "resilience4j.circuitbreaker.state" và "resilience4j.circuitbreaker.slow.call.rate"
meterRegistry.config().meterFilter(
    MeterFilter.denyNameStartsWith("resilience4j.circuitbreaker.s")
)

Tạo whitelist với MeterFilter

Mặt khác, nếu ta chỉ muốn sinh một vài metric thì ta có thể dùng hàm denyUnless để tạo whitelist. Hàm này nhận vào một predicate để kiểm tra xem từng metric là cần thiết hay không.

// Chỉ sinh metric "resilience4j.circuitbreaker.state" và "resilience4j.circuitbreaker.slow.call.rate"
meterRegistry.config().meterFilter(
    MeterFilter.denyUnless { it.name.startsWith("resilience4j.circuitbreaker.s") }
)

Trong tất cả các tình huống ở trên, ta sẽ dùng CircuitBreakerRegistry với meterRegistry vừa được thiết lập.

Kết thúc

Ta thường tự mình viết code để sinh metric trong đa số các trường hợp. Vì thế ta có toàn quyền quyết định sinh ra metric nào với dạng nào. Nhưng nếu ta cần thay đổi metric sinh bởi library của bên thứ ba thì interface MeterFilter là một công cụ rất hữu ích.

A software developer from Vietnam and is currently living in Japan.

Leave a Reply