Python의 urllib3 Retry 객체는 POST에 대해서 retry하지 않는다.

1

Python

목록 보기
18/18

Python의 urllib3 Retry 객체는 POST에 대해서 retry하지 않는다.

python urllib3 Retry는 default로 GET, PUT, DELETE에서만 동작하고 POST에는 동작하지 않는다.

마이크로서비스 구조를 사용하다보면 각 pod간의 호출 sequence 불일치로 에러가 종종 발생한다. 간단히 말해서 A pod에서 B pod에게 http 요청을 보내고, B pod가 C pod에게 http 요청을 보내야한다고 하자. B pod가 짧은 순간에 A pod 요청을 받고, 처리 중 지연되어 A pod에서 timeout을 발생했는데 B pod가 C pod에 http 요청을 보내어, 다음 스텝으로 넘어가는 경우가 있다. 이런 경우 A pod에서는 작업이 안끝났다고 보고되는데 C pod에서는 작업이 끝났다고 보고된다.

이러한 경우를 대비해서 timeout과 retry를 꾸준히 설정해주어 cluster의 status를 최대한 맞춰주려고 한다. 즉, A pod에서 timeout이 발생해도 3번 정도 다시 시도하도록 해서 A pod 또한 작업을 완료하도록 하는 것이다. 문제는 이러한 retry가 작동하지 않는 문제가 생겼었다.

python3의 urllib3 모듈에 Retry를 사용해서 retry하도록 하였다. 문제는 이전에는 HTTP PUT에서는 retry가 동작했지만 HTTP POST에서는 retry가 동작하지 않는다는 것이다.

import urllib3
from urllib3.util import Retry

timeout = urllib3.Timeout(timeout)
retries = Retry(total=3)
urllib3.PoolManager().request('POST', url,fields=fields, headers=headers,retries=retries, timeout=timeout)

다음의 code는 retry가 동작하지 않는다. 즉 timeout이 발생한다해서 3번 retry하지 않는다는 것이다.

원래는 3번 retry하고 3번의 retry가 완료되면 MaxRetryError exception을 발생시킨다. 문제는 다음의 코드에서는 retry를 하지않고 ReadTimeoutError exception을 발생시킨다.

이러한 이유에 대해서 궁금해서 Retry객체를 분석해보니 이러한 부분이 있었다.

@six.add_metaclass(_RetryMeta)
class Retry(object):
    ...
    #: Default methods to be used for ``allowed_methods``
    DEFAULT_ALLOWED_METHODS = frozenset(
        ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
    )

DEFAULT_ALLOWED_METHODS라는 부분이 frozenset으로 구성되어있고 잘보면 POST가 없다. 즉, POST에는 동작하지 않는다는 것이다.

이를 토대로 위의 exception이 발생한 이유에 대해서 생각해보면 이해가 간다.
1. GET, PUT, DELETE => retry가 허용되므로 retry가 완료된 후에는 MaxRetryError exception 발생
2. POST => retry가 허용되지 않으므로 timeout 후, retry를 시도하지 않고 ReadTimeoutError exception 발생

그래서 POST역시도 retry해주고 싶다면 다음과 같이 allowed_methods를 설정해주면 된다.

import urllib3
from urllib3.util import Retry

timeout = urllib3.Timeout(timeout)
etries = Retry(total=retry, allowed_methods=frozenset(['GET', 'POST', 'PUT', 'DELETE']))
urllib3.PoolManager().request('POST', url,fields=fields, headers=headers,retries=retries, timeout=timeout)

이렇게하면 retry가 문제없이 POST에도 동작한다.

그런데, 왜 POST는 안되고 나머지는 될까? 이유는 간단하다. 'POST'는 idempotent하지 않다. 즉, 멱등성이 성립하지 않는다는 것이다. 멱등성이란 같은 호출을 했을 때, 동일한 응답이 오는 것을 보장하냐는 것이다. 일반적으로 GET, PUT, DELETE는 같은 요청에 대한 같은 응답이 온다. 즉, PUT으로 user를 만들면, 기존에 user가 있다해도 user를 덮어쓰고 동일한 응답을 뱉는 것이다. 이러한 경우 멱등성이 성립했다하고 idempotent하다고 한다.

POST는 멱등성이 성립하지 않는다. 즉, 같은 요청에 대한 다른 응답이 나올 수 있다는 것이다. 이는 backend server의 status를 변경했기 때문인데, POSTuser를 생성한다음, 같은 요청을 보내면 이미 user가 있으니 요청을 거절하겠다는 응답이 올 수 있다.

물론, 이러한 멱등성은 개발자가 의도하는 것이고 실제로 이렇게 설계되지 않을 수 있다. 그러나 국제적으로 http method를 정의한 내용은 POST는 멱등성이 성립되지 않는다는 것이다.

그렇기 때문에 retry시에 POST의 경우는 멱등성이 성립되지 않으므로 이전의 요청에 의해 backend server의 status가 바뀌어 다음 요청 때, 우리가 원하는 응답을 받지 못할 수 있다. 반면 GET, PUT, DELETE는 같은 요청을 몇번이고 보내도 같은 응답이므로 문제가 없다. 그래서 retry가 가능한 것이다.

결론적으로, POST를 retry하지 말도록 하자. 다만, 서비스 설계를 하다보면 언제나 HTTP method 정의나 RESTful 규약에 반드시 따를 수 없는 것이 현실이다. 따라서, 필요한 경우는 위와 같이 allowed_methods를 사용하여 POST를 허용하도록 하자.

0개의 댓글