Building an EAI Middleware Layer
Don't Let One Slow Target Drag Down the Hub
한국어 원문으로 표시합니다.
목표
서킷 브레이커와 대상별 벌크헤드로 느리거나 죽은 대상 하나가 허브 전체를 끌어내리지 못하게 한다.
왜 중요한가
타임아웃은 한 건을 얼마나 기다릴지만 정한다. 죽은 대상을 계속 부르면 기다리는 스레드가 쌓여 허브가 먼저 숨이 막히고, 상관없는 거래까지 함께 멈춘다. 빨리 거절하는 장치가 대상마다 따로 있어야 장애가 번지지 않는다.
단계
/root/eaimw/breaker/breaker.py에CircuitBreaker(failure_threshold, reset_timeout, clock=time.monotonic)를 만든다.state(CLOSED·OPEN·HALF_OPEN),allow(),record_success(),record_failure(). 연속 실패가 기준에 닿으면 OPEN, OPEN 이면allow()는 False. 시각은 반드시clock()으로 읽는다(채점기가 가짜 시계를 넣는다).- OPEN 에서
reset_timeout이 지나면allow()가 한 번만 True 를 주고 HALF_OPEN 이 된다. 시험 호출이 성공하면 CLOSED, 실패하면 다시 OPEN(시간을 처음부터). 여러 스레드가 동시에 물어도 한 건만 허용한다(잠금). cp /opt/lab/fixtures/eaimw/breaker/relay_base.py /root/eaimw/breaker/relay.py로 시작해--fail-threshold(기본 5)·--reset-timeout(기본 10) 인자를 더하고, 대상 호출을 브레이커로 감싼다. 열려 있으면 부르지 않고 즉시 E904. 이 단계에서는 0000 이 아닌 결과를 실패로 센다.--max-inflight(기본 10) 동시 처리 한도를 둔다. 한도가 차 있으면 기다리지 않고 즉시 E905.- 브레이커와 동시 처리 한도를 대상(CORE·CLAIM)마다 따로 둔다. 계정계의 고장이나 포화가 청구 거래에 번지지 않아야 한다.
--breaker-log(기본/root/eaimw/breaker/breaker.log)에 상태가 바뀔 때마다<시각>|<대상>|<FROM>-><TO>한 줄을 남긴다.- 실패로 세는 것을 시스템 오류(E500·E901·E902)로 좁힌다. 업무 거절(B2xx)은 대상이 건강하게 답한 것이므로 성공으로 센다.
참고
- 동시 처리 한도:
threading.BoundedSemaphore(n)의acquire(blocking=False)가 False 면 한도가 찬 것. 끝나면 반드시release()(try/finally). - 상태 전이 기록:
allow()앞뒤와record_*()앞뒤의state를 비교하면 바뀐 순간을 알 수 있다. - 계정계 픽스처 스위치:
curl -XPOST localhost:9201/_ctl -d '{"mode":"fail"}'(500),"slow"(지연),"normal". 통계/_stats의calls·max_inflight. - 흔한 실수: 허브 전체에 브레이커 하나, 한도를 넘은 요청을 줄 세워 기다리게 하기, HALF_OPEN 에서 여러 건 허용,
time.time()을 직접 불러 가짜 시계를 무시하기.
연속 실패가 쌓이면 회로를 연다
/root/eaimw/breaker/breaker.py 의 CircuitBreaker 가 연속 실패 기준에서 OPEN 이 되고, OPEN 이면 allow() 가 False 다.
성공하면 실패 수를 0 으로 되돌려 '연속' 을 셉니다. 열린 시각은 time.time() 이 아니라 self.clock() 으로 기록하세요 — 채점기가 시계를 쥡니다.
한 건으로 떠보고 닫는다
reset_timeout 뒤 HALF_OPEN 에서 시험 호출 한 건만 허용하고, 성공이면 CLOSED, 실패면 다시 OPEN 으로.
allow() 안에서 OPEN 이고 시간이 지났으면 HALF_OPEN 으로 바꾸고, 시험 호출이 이미 나갔는지 표시를 둡니다. 두 스레드가 동시에 물어도 한쪽만 True 를 받게 잠금 안에서 판단하세요.
열려 있으면 부르지 않는다
relay_base.py 를 복사해 대상 호출을 브레이커로 감싸고, 열려 있으면 E904 로 즉시 답한다(--fail-threshold·--reset-timeout).
handle_tx 가 감쌀 자리입니다. allow() 가 False 면 call_target 을 부르지 않고 돌아오고, 부른 뒤에는 결과에 따라 record_success/record_failure 를 부릅니다.
한도를 넘으면 기다리지 않는다
--max-inflight 동시 처리 한도를 두고, 차 있으면 즉시 E905 로 답한다.
BoundedSemaphore 의 acquire(blocking=False) 가 False 면 한도가 찬 것입니다. 부른 뒤에는 예외가 나도 release 되게 try/finally 로.
대상마다 격벽을 나눈다
브레이커와 동시 처리 한도를 CORE·CLAIM 대상마다 따로 두어, 계정계의 고장·포화가 청구로 번지지 않게 한다.
하나씩 두던 것을 대상 이름을 열쇠로 한 딕셔너리로 바꿉니다. 격벽이 하나면 한 구획의 물이 배 전체로 찹니다.
상태 전이를 기록한다
--breaker-log 에 상태가 바뀔 때마다 '시각|대상|FROM->TO' 한 줄을 남긴다.
allow() 전후, record 전후의 state 를 비교해 다르면 한 줄을 씁니다. 여러 스레드가 쓰므로 잠금을 잡고 씁니다.
업무 거절은 실패가 아니다
실패로 세는 것을 E500·E901·E902 로 좁히고, B2xx 는 성공으로 센다.
잔액 부족은 계정계가 건강하게 답한 것입니다. 이것을 실패로 세면 월말의 업무 거절만으로 멀쩡한 계정계로 가는 길을 끊습니다.