[Python] Flask에 circuit breaker pattern 적용해보기

최근 웹 서비스는 내부적으로 외부 API를 호출하는 일이 빈번하다. 웹 페이지에 표현해야 할 데이터를 외부에서 불러와야 하는 경우가 많아지기도 했고, MSA (Microservices Architecture) 애플리케이션은 서비스들끼리 네트워크 통신을 하기 때문인데, 이러한 변화가 불러오는 문제가 있다. 외부 API에서 장애가 발생하면 API를 호출하는 서버에서도 같이 장애가 발생하는 것인데, 아주 가끔씩 발생하는 오류야 어떻게 예외처리를 하면 되지만 긴 시간동안 유지되는 장애가 발생하게 되면 호출하는 쪽에서도 해당 요청을 성공적으로 처리하기 위해서 재호출을 하거나 타임 아웃을 두고 호출이 성공되기를 기다리면서 리소스가 쌓이고, 호출되는 API 서버 또한 장애가 발생하는 API가 계속해서 호출되면 추가적인 장애가 발생하기도 한다.

이런 문제를 해결하기 위해서 웹 서비스에 circuit breaker pattern라는 것이 도입되었는데, 스위치가 달려있는 전기 회로를 보고 고안된 개념이다. 정상적으로 동작할 때는 close된 상태(전기 회로에서 스위치를 닫아 놓은 상태)로 API를 호출하고, 일정 시간 또는 횟수 이상 장애가 발생하면 open 상태(전기 회로도에서 스위치를 열어 놓으면 전기가 통하지 않는 개념과 같은 개념)로 변경하고 API 호출을 막는다. 전기 회로도

구현

이제 대충 개념을 알았으니 구현을 해보겠다. circuit breaker pattern은 꼭 web 서비스에서만 사용되는 개념은 아니지만, 나는 웹 개발자이기 때문에 간단한 웹 애플리케이션에 적용하여 구현해 보기로 했다.

먼저, flask를 사용하여 정말 간단한 웹 애플리케이션을 만들었다.

# web.py
from flask import Flask, Response


app = Flask(__name__)


@app.route("/", methods=["GET"])
def main():
    return Response("hello world")


app.run(host="localhost", port=3000, debug=True)

다음으로 API에 요청하여 hello world가 정상적으로 출력되는 지 확인한다.

# client.py
from requests import request


response = request(method="GET", url="http://localhost:3000")
print(response.text)
# hello world

정상적으로 동작하는 것을 확인했으니, 이번에는 API에 오류를 발생시킬 수 있는 파라미터를 추가하여 의도적으로 오류를 발생시켜 보겠다.

# web.py
@app.route("/", methods=["GET"])
def main():
    raise_error = request.args.get("raise_error", False)

    if raise_error:
        raise Exception()

    return Response("hello world")
# client.py
response = request(
    method="GET",
    url="http://localhost:3000",
    params={"raise_error": True}
)
print(response.text)
# 별다른 예외 처리를 하지 않았기 때문에, 오류 났다는 HTML이 출력된다.
"""
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
...
"""

이제 circuit breaker를 적용해보자, 이번에 사용할 circuit breaker 라이브러리는 circuitbreaker이다. pybreaker라는 것도 있는데 redis를 이용하여 구현되어 있다. (필자는 redis 깔기 귀찮아서 circuitbreaker를 사용하여 구현했다.)
사용 방법은 정말 간단한데, decorator를 추가해주면 된다.

# web.py
def fallback_func():
    return Response("circuit breaker opened")


@app.route("/", methods=["GET"])
# 감싸고 있는 함수가 1회(failure_threshold) 이상
# expected_exception에 지정된 오류가 날 경우
# fallback_function을 호출하여 response한다.
@circuit(
    failure_threshold=1,
    expected_exception=Exception,
    fallback_function=fallback_func,
)
def main():
    raise_error = request.args.get("raise_error", False)

    if raise_error:
        raise Exception()

    return Response("hello world")

client.py를 실행하여 결과 확인

python client.py
# 첫 오류 발생
"""
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
...
"""

python client.py
# 두 번째 오류 발생 시 아래 문구가 출력되는 것을 확인 할 수 있음
# circuit breaker opened

이후 일정 시간(recovery_timeout)이 지난 이후에 오류가 발생한 함수로 다시 응답하는데, 또 다시 오류가 발생하면 위와 같이 동작한다. (지정된 오류 횟수 초과 시 fallback_function으로 응답) 기본 recovery_timeout은 30초 인데, recovery_timeout 파라미터를 지정하여 변경할 수 있다.

...
@circuit(
    failure_threshold=1,
    expected_exception=Exception,
    fallback_function=fallback_func,
    recovery_timeout=5,
)
def main():
   ...

위와 같이 수정할 경우 오류가 발생하여 circuit breaker의 상태가 open(오류가 1회 이상 발생한 상태)으로 변경되면 fallback_funcion으로 응답하다가 5초 이후에 main 함수로 응답을 시도한다. 응답을 성공하였을 경우는 계속 main함수를 사용하여 응답하고 또 다시 오류가 발생하면 fallback_function을 사용하여 응답한다.

정리

circuit breaker pattern을 도입하지 않고 위와 같이 몇 회 이상 오류가 발생하면 지정된 함수로 응답하고, 얼마 이후 정상적으로 동작하는 지 시도해보고 예외 처리하는 로직을 작성하려면 여러 if문이 코드에 추가될테지만, 위와 같이 circuit breaker 라이브러리를 사용하면 간단하게 처리할 수 있다.

참고

nginx에서는 이러한 circuit breaker pattern을 구현하여 제공하고 있으니 사용해 보고자 하시는 분들은 사용해보면 좋을 것 같다. [링크]


Written by@EHX
Software Developer, Back-End Engineer

GitHubFacebook