Lập trình

Circuit Breaker Design Pattern: Giải pháp xử lý lỗi hiệu quả

circruit-breaker-huongnq.id.vn

Trong các hệ thống phần mềm, đặc biệt là trong các hệ thống phân tán, việc handle các lỗi xảy ra trong quá trình các service giao tiếp với nhau luôn là một vấn đề nan giải. Các lỗi có thể xảy ra bất cứ lúc nào, dẫn đến sự cố gián đoạn, giảm hiệu suất và trải nghiệm người dùng tệ. Để giải quyết vấn đề này, có một design pattern mang tên Circuit Breaker được ra đời – một công cụ hữu hiệu giúp tăng cường khả năng phục hồi và độ tin cậy của hệ thống.

Tổng quan về Circuit Breaker Pattern

Circuit Breaker Pattern được lấy cảm hứng từ cầu dao điện, thiết bị đóng ngắt mạch điện để bảo vệ hệ thống khỏi hư hỏng khi xảy ra sự cố quá tải hoặc đoản mạch. Tương tự như vậy, trong hệ thống phân tán, Circuit Breaker đóng vai trò như một “bộ ngắt mạch” bảo vệ hệ thống khỏi các lỗi liên quan đến các dịch vụ từ xa.

Một trong những bức ảnh bạn sẽ search ra khi gõ “circuit breaker” trên google image.

Nguyên lý hoạt động của Circuit Breaker Pattern dựa trên ba trạng thái chính: Closed, Open và Half-Open.

Sơ đồ hoạt động của Circuit Breaker Pattern
  • Closed: Khi hệ thống hoạt động bình thường, Circuit Breaker ở trạng thái Closed, cho phép các request được thực hiện thoải mái, tẹt bô.
  • Open: Khi số lượng error vượt quá mức được cấu hình trước, Circuit Breaker chuyển sang trạng thái Open, ngắt kết nối với service bị lỗi và trả về lỗi ngay lập tức cho các request tiếp theo. Điều này giúp ngăn chặn việc hệ thống tiếp tục gửi yêu cầu đến service bị lỗi, gây thêm tải cho service và làm trầm trọng thêm sự cố. Thay vì tạm thời treo service thì có thể làm sập luôn server, điều này làm cho việc khôi phục, ứng cứu sự cố càng thêm khó khăn hơn.
  • Half-Open: Sau một khoảng thời gian nhất định, Circuit Breaker chuyển sang trạng thái Half-Open, cho phép một vài request đi qua để kiểm tra xem service đã khôi phục hoạt động bình thường hay chưa. Nếu service hoạt động ổn định, Circuit Breaker sẽ trở về trạng thái Closed. Ngược lại, nếu service vẫn tiếp tục xảy ra lỗi, Circuit Breaker sẽ trở lại trạng thái Open và tiếp tục theo dõi cho đến lần kiểm tra tiếp theo.

Circuit Breaker Pattern mang lại nhiều lợi ích cho hệ thống phân tán, bao gồm:

  • Tăng cường khả năng phục hồi: Circuit Breaker giúp hệ thống nhanh chóng phát hiện và xử lý các lỗi, ngăn chặn các sự cố lan rộng và giảm thiểu thời gian gián đoạn hệ thống.
  • Cải thiện độ tin cậy: Bằng cách ngắt kết nối với service bị lỗi và chuyển hướng các request đến các fallback service, Circuit Breaker giúp đảm bảo hệ thống vẫn có thể hoạt động ổn định ngay cả khi một số service bị tèo.
  • Giảm tải cho service: Khi phát hiện ra service đang quá tải, Circuit Breaker sẽ ngắt kết nối với service đó, giúp giảm load và cho service có thời gian phục hồi.

Use case thực tế

Tôi chạy một chiếc service nho nhỏ để chạy bot chat Telegram trên một con EC2 có cấu hình free trial với 1 core và 1GB RAM. Gần đây, tôi đã thử tích hợp LLM vào để con bot Telegram thông minh hơn. Nhưng để chạy LLM thì chắc chắn không thể chạy trên con EC2 cùi bắp đó được. Nên tôi chọn cách chạy LLM trên con PC I5 có chiếc VGA 1060 ở dưới bàn làm việc của tôi. Và web service sẽ gọi API đến máy PC để giao tiếp với LLM. Và bùm, con bot Telegram của tôi trở nên thông minh hơn, có khả năng generate văn bản từ LLM. Hào hứng với kết quả này, tôi quyết định chia sẻ bot với bạn bè, người thân và đồng nghiệp.

Chiếc bot rất thông minh

Mọi thứ tiếp tục diễn ra suôn sẻ cho đến khi một người bạn “thân” quyết định gửi hàng trăm message mỗi giây, khiến cho máy tính cá nhân của tôi – core i5 và một chiếc VGA cùi bắp, bị quá tải và treo máy. Tôi chỉ phát hiện ra khi chiếc quạt máy tính quay tít và kêu ầm ĩ, PC bị treo cứng và tôi phải dí nút nguồn để restart lại máy. Sau khi check log thì tôi biết được người spam không ai khác chính là một tài khoản Telegram quen thuộc “l**seo”

Trước tình huống này, tôi đặt ra câu hỏi: Làm thế nào để tôi có thể tự động ngăn chặn những tình huống tương tự? Làm thế nào để tôi có thể tạm ngắt kết nối đến PC chạy LLM khi nó bị timeout hoặc trả về error, để tránh quá tải, treo máy? Câu trả lời cho những vấn đề trên là áp dụng mô hình Circuit Breaker.

Đầu tiên tạo một file circuit_breaker.py như sau:

import time


class CircuitBreaker:
    FAILURE_THRESHOLD = 3  # Số lần lỗi tối đa cho phép
    RECOVERY_TIMEOUT = 5  # Thời gian chờ để chuyển từ trạng thái 'open' sang 'half-open'

    def __init__(self,
                 failure_threshold=FAILURE_THRESHOLD,
                 recovery_timeout=RECOVERY_TIMEOUT,
                 expected_exception=Exception):
        self.state = None
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.expected_exception = expected_exception
        self.set_closed()  # Khởi tạo Circuit Breaker ở trạng thái 'closed'
        self.failure_count = 0
        self.last_failure_time = None

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            # Kiểm tra và chuyển sang trạng thái 'half-open' nếu đã đến thời gian retry
            if self.is_open() and self.is_recovery_time():
                self.set_half_open()

            # Xử lý logic chính của hàm được wrap bởi Circuit Breaker
            if self.is_closed():
                try:
                    result = func(*args, **kwargs)
                    self.reset()
                    return result
                except self.expected_exception as e:
                    self.handle_failure(e)
                    raise Exception()
                    
            elif self.is_half_open():
                try:
                    result = func(*args, **kwargs)
                    self.set_closed()
                    self.reset()
                    return result
                except self.expected_exception as _:
                    self.set_open()
                    raise Exception()

        return wrapper

    def handle_failure(self, exception):
        self.failure_count += 1
        # Nếu vượt quá số lần lỗi cho phép, chuyển về trạng thái 'open' và ghi lại thời điểm lỗi
        if self.is_over_threshold():
            self.set_open()
            self.set_failure_now()
            print(f"Circuit breaker OPEN. Failure count: {self.failure_count}")
        else:
            print(f"Failure count: {self.failure_count}")

    def set_failure_now(self):
        self.last_failure_time = time.time()

    def is_over_threshold(self):
        return self.failure_count >= self.failure_threshold

    def is_recovery_time(self):
        return time.time() - self.last_failure_time >= self.recovery_timeout

    def is_open(self):
        return self.state == 'open'

    def is_closed(self):
        return self.state == 'closed'

    def is_half_open(self):
        return self.state == 'half-open'

    def set_open(self):
        self.state = 'open'

    def set_half_open(self):
        self.state = 'half-open'

    def set_closed(self):
        self.state = 'closed'

    def __setstate__(self, state):
        self.state = state

    def __getstate__(self):
        return self.state

    def reset(self):
        self.failure_count = 0
        self.last_failure_time = None

Ở function gọi đến LLM, thực hiện thêm decorator như sau:

from circuit_breaker import CircuitBreaker

@CircuitBreaker()
def get_llm_response(image_url):
   pass

Bùm, Circuit Breaker Pattern đã giúp tôi ngăn chặn được tình trạng quá tải cho chiếc PC của tôi. Người bạn “thân” của tôi đã không spam được con bot của tôi nữa, vì tôi đã set con bot về trạng thái private rồi. LOL

Đó là trong trường hợp service của tôi là pet project, dù LLM có bị ngắt kết nối, bot có không response thì cũng không ai phàn nàn gì cả. Nhưng trong thực tế, khi bạn là một đơn vị cung cấp dịch vụ, việc gián đoạn như vậy gây ra hậu quả cực kỳ lớn. Để hình dung rõ hơn, bây giờ tưởng tượng con bot của tôi nhờ áp dụng LLM và trở nên thông minh, nói chuyện siêu cute hạt me. Tôi đã được ký hợp đồng để tích hợp với bot chat chăm sóc khách hàng của một cửa hàng bán hoa tươi, và quan trọng hơn, tôi đã có tiền để thuê sever có VGA để chạy LLM.

Nhưng tôi lại nảy ra một ý tưởng để tiết kiệm chi phí mà vẫn đảm bảo bot không bị sập, khách hàng không cắt hợp đồng. Đó là tôi sẽ sử dụng song song 2 máy PC và server trên cloud để chạy LLM (giả sử AWS tính tiền kiểu pay as you go). Tôi sẽ cải tiến Circuit Breaker một chút, tích hợp thêm cơ chế fallback:

Sửa lại circuit_breaker.py:

import time

class CircuitBreaker:
    FAILURE_THRESHOLD = 3  # Số lần lỗi tối đa cho phép
    RECOVERY_TIMEOUT = 5  # Thời gian chờ để chuyển từ trạng thái 'open' sang 'half-open'
    FALLBACK_FUNCTION = None  # Hàm thay thế khi Circuit Breaker mở

    def __init__(self,
                 failure_threshold=FAILURE_THRESHOLD,
                 recovery_timeout=RECOVERY_TIMEOUT,
                 expected_exception=Exception,
                 fallback_function=FALLBACK_FUNCTION):
        self.state = None
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.expected_exception = expected_exception
        self.fallback_function = fallback_function
        self.set_closed()  # Khởi tạo Circuit Breaker ở trạng thái 'closed'
        self.failure_count = 0
        self.last_failure_time = None

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            # Kiểm tra và chuyển sang trạng thái 'half-open' nếu đã đến thời gian retry
            if self.is_open() and self.is_recovery_time():
                self.set_half_open()
            # Xử lý logic chính của hàm được wrap bởi Circuit Breaker
            if self.is_closed():
                try:
                    result = func(*args, **kwargs)
                    self.reset()
                    return result
                except self.expected_exception as e:
                    self.handle_failure(e)
            elif self.is_half_open():
                try:
                    result = func(*args, **kwargs)
                    self.set_closed()
                    self.reset()
                    return result
                except self.expected_exception as _:
                    self.set_open()

            return self.fallback_function(*args, **kwargs)

        return wrapper

     # ....
     # ....
     # code bên dưới vẫn vậy nha

Không quên sửa lại cả phần call api:

from circuit_breaker import CircuitBreaker

def get_cloud_llm_response():
   pass

@CircuitBreaker(fallback_function=get_cloud_llm_response)
def get_llm_response():
   pass

Như vậy, con bot của tôi sẽ vẫn sử dụng LLM chạy trên PC để tiết kiệm chi phí, tôi sẽ có lãi hơn. Nó sẽ chỉ dùng đến server cloud trong một vài dịp như 20/10, 20/11 hoặc 8/3 – khi lượng khách hàng chat vào box chat của cửa hàng hoa tăng lên và chiếc PC bị quá tải.

Đó là một trong những tình huống thực tế mà Circuit Breaker Pattern có thể được sử dụng, ngoài ra nó còn được sử dụng trong nhiều use case khác nữa – và chắc chắn một ngày đẹp trời sẽ cần tới nó.

Bonus:

Nếu muốn dùng Circuit Breaker mà không muốn code thì sao, chúng ta có thể sử dụng các lib có sẵn và rất ngon trên pypi như pybreaker, circruitbreaker.

Hoặc nếu không muốn sửa code, chúng ta có thể sử dụng các proxy open source như envoy, HAProxy… để cấu hình chạy circuit breaking.
Với 2 cách trên, chắc chắn nó ngon hơn nhiều so với những dòng tự code bên trên đó. LOL

2 Comments on “Circuit Breaker Design Pattern: Giải pháp xử lý lỗi hiệu quả

Leave a Reply

Your email address will not be published. Required fields are marked *