ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 책 리뷰 - release의 모든 것 - 1부 안정성 구축
    카테고리 없음 2024. 7. 28. 18:51

    1장 운영 환경의 현실

     일단 개발자들의 잘못된 개발 태도를 꼬집고 시작한다. 내부 테스트와 qa를 통과하는 것에 초점을 맞춰 개발하면 안 된다는 것이다. 내부 테스트나 qa를 통과하는 것은 찐 목표의 과정일 뿐이다. 찐 목표는 바로 사용자가 서비스를 원활히 이용하게 하여 수익을 내는 것이다. 내부 테스트나 qa를 통과하는 것은 사용자가 서비스를 원활히 이용하는 것을 위한 과정일 뿐이어서, 이것들을 통과한다고 해도 사용자가 서비스를 원활히 이용하지 못하는 상황이 발생한다면 수익을 낼 수 없게 될 확률이 크다. 사실 이건 '목이 마르면 물을 마셔야 합니다.'처럼 당연한 소리이다. 당연한 소리가 이처럼 강조되는 것은 '모두가 알고는 있지만 실천하기는 어려운 이야기'라고 이해하면 되지 싶다. 즉, '아 그렇구나~' 하고 넘어가면 안 되고 '이렇게 하기 위해서는 피나는(?) 노력이 필요하겠구나!'하고 생각해야 하는 것이다.


    2장 항공사를 멈추게 한 예외

     한 항공사에서는 db 시스템 교체 작업을 진행했다가 아주 끔찍한 장애를 겪었다. 이유를 간단히 설명하자면, '구 시스템'을 '신 시스템'으로 교체했고 '구 시스템'을 종료했는데도 이 '구 시스템'으로 db 작업이 요청된 것이다. 이것으로 인해 발생된 에러는 db connection을 해제하지 않은 채 db 작업 요청을 종료해 버렸다. 원래는 에러가 발생된 후 connection을 해제해야 정상이다. 하지만 이 에러는 예측되지 못했어서 finally 구문에 있는 connection close가 실행되지 못했던 것이다. 그래서 pool에 있는 db connection이 모두 활성화된 채로 유지되었고, 이후의 모든 db 관련 요청은 비활성화된 db connection을 얻기 위해 무기한으로 대기하게 된 것이다.


    3장 시스템 안정화

     위 상황을 예측하긴 힘들었을 것 같다. 책에서도 그렇다고 쓰여있기도 하다. 그래서 개발자는 항상 어디서든 에러가 날 수 있음을 가정하고 방어적으로 코딩해야 할 것 같다. 외부 서버와 통신하는 코드에는 더 그렇다. 내가 작업하는 서버가 가장 맨 앞단에 위치하고 있다면 그나마 영향 범위가 적을 텐데, 만약 그렇지 않다면 내가 작업하는 서버의 오류가 여러 클라이언트 서버들로 전파될 수 있기 때문에 특히 조심해야 할 것 같다.

     

     책에서는 시스템에 문제가 있는 상황을 세 가지 단계로 구분한다.

    1. 장애 -> 시스템을 사용할 수 없는 상태
    2. 오류 -> 시스템을 사용할 순 있지만 의도대로 동작하지 않는 상태
    3. 결함 -> 의도대로 동작하지 않는 상태는 아니지만 그렇게 될 가능성이 존재하는 상태

     결함이 생기면 균열이 생기기 시작한다. 균열이 어느정도 심각해지면 오류가 된다. 오류가 지속되면 곧 장애가 된다. 아키텍처가 강하게 결합될수록 결함이 전파(균열)될 가능성이 높아진다. 결함이 없도록 유지하는 것은 불가능하다고 우리는 인정해야 한다. 그래서 결함이 오류가 되는 것을 막는 것에 특히 신경 써야 한다. 그리고 가장 먼저 집중해야 할 것은 '시스템 간의 결합을 약화시켜야 하는 것'이고, 이는 균열을 감속화할 것이다. 위 항공사의 개발자들이 이 점에 특히 집중했다면 최소한 항공사를 멈추게 하진 않았을 수도 있다.


    4장 안정성 안티 패턴

    통합 지점(외부 호출)

     외부 호출은 참 무섭다. 책에서는 "시스템 안정성은 서비스를 더 작게 많이 만들수록, saas와 더 많이 통합될수록, API 우선 전략으로 더 나아갈수록 더 약화된다"라고도 하는데, 시스템이 복잡해지고 연동이 많아질수록 위험하다. 외부 호출 클라이언트를 만들 때는 항상 무조건 checked exception을 throw 하도록 메서드들을 구현해야 할 것 같다.

    1. 너무 많은 요청

     tcp 연결 요청이 너무 많으면 수신 대기열에 요청들이 쌓이는데, 쌓인 요청들을 실행한 클라이언트들은 대기 및 블록 되게 된다. 블록 되고 있는 스레드는 다른 작업을 실행하지 못하기 때문에, 이런 스레드가 많아지면 그 서버의 전체 처리 능력은 떨어지게 된다. 외부 호출할 때 timeout을 잘 설정해야 하는 이유이다. 늦은 응답이 무응답보다 훨씬 나쁘다.

    2. 방화벽

     TCP는 연결을 무한정 유지할 수 있다. 그러나 방화벽은 일정 시간이 지나도 활동이 없는 연결을 비활성화할 수 있다. 예를 들어, 외부 호출에 사용되는 연결의 개수가 40개이고, 이 중 하나만 주기적으로 사용되는 상황을 가정해 보자. 나머지 39개 연결은 연결된 채로 대기 상태에 있다. 일정 시간이 지나면 방화벽은 이러한 비활성 연결들을 사용되지 않는다고 판단하여 끊을 수 있다. 하지만 TCP 스택은 이 연결이 끊어진 것을 인지하지 못하고, 여전히 연결이 유지되고 있다고 간주한다. 이때, 끊어진 39개 연결에서 목표 서버로 패킷을 전송하면, 방화벽은 이를 폐기할 것이다. 그러면 클라이언트의 소켓은 ACK를 받지 못하고, 해당 연결은 무한 대기 상태에 빠지게 된다.

    역시 timeout을 잘 설정해야 하는 이유이다.

    클러스터 장애 연쇄 반응

     클러스터 형태에서는 한 서버에 장애가 발생할 시, 그 서버의 요청이 나머지 서버로 분산된다. 이 장애의 원인은 클러스터 내 모든 서버에 존재할 가능성이 크기 때문에, 나머지 서버들도 곧 장애가 발생하게 되고 그 속도는 점점 빨라진다. 메모리 누수나 경쟁상태 등이 원인이 되는 경우가 많다. '오토 스케일링 이용 & 장애 전파 속도보다 확장 속도가 빠른' 상황에서는 오토 스케일링을 활용해봄직 하다. 

    사용자

     사용자가 몰리면 메모리가 많이 사용된다. 심지어 장애가 발생해서 작동이 중단되었을 때에도 메모리가 여전히 사용된다. 메모리가 너무 많이 사용되면 사용자에게 서비스를 제공하지 못하는 것은 물론, 오류 로그를 남기지 못할 수도 있다. 메모리를 적게 사용하는 습관을 가져야 한다. 예를 들면 세션을 적게 사용한다던가, '약한 참조'를 사용하는 방법 등이 있다. 또는 memcached나 redis 등의 외부의 메모리를 사용하는 방법도 있다.

     

     사용자가 몰리면 tcp 소켓도 허용량만큼 다 열려 버릴 수 있다. 한 ip당 약 64,511개의 포트에 소켓이 열릴 수 있는데, 사용자가 너무 많으면 이 허용치를 넘어설 수도 있는 것이다. 복잡한 방법일 수 있는데, 이는 가상 ip로 해결할 수 있다. 

    블록 된 스레드

     멀티스레딩 구조일 때, 메서드를 동기화하기 위해 스레드를 원활하게 블록 시키고 해제시키는 것은 어려운 작업이다. 완벽하게 처리할 자신이 없다면 CQRS 패턴 등을 적용하는 등 동기화해야 하는 상황을 최대한 줄이는 것이 좋다. 

     

     외부 라이브러리의 호출 클라이언트를 사용할 때 특히 주의해야 한다. 왜냐하면 이 클라이언트들이 호출에 대한 오류 처리를 제대로 안 해놨을 가능성이 있기 때문이다. 오픈 라이브러리는 그나마 괜찮은데 비공개 라이브러리는 디컴파일하지 않는 이상 그 여부를 알아차리기가 쉽지 않다. 불안하다면 외부 라이브러리의 호출 클라이언트를 한번 감싸서 사용하는 것이 좋을 것 같다.

    척도 효과

     척도 효과란, 복잡성이 증가할수록 문제가 발생할 가능성이 급격하게 커지는 현상을 말한다.

    1. 내부 서버 통신 증가

     내부 서버가 증가하면 내부 서버끼리의 통신하는 경우의 수는 서버 수의 제곱만큼 늘어난다. 매우 복잡해진다. 이런 상황이 오면 다른 통신 방법들을 고려해봄직 하다. udp 브로드캐스트, tcp 또는 udp 멀티캐스트, 발행/구독 메시징, 메시지 대기열 등의 방법들이 있다.

    2. 공유 자원

     비즈니스 로직을 수행하는 여러 서버들은 공유 자원을 제공하는 서버를 호출하는 경우가 흔하게 발생하곤 한다. 이 공유 서버를 '무공유 아키텍처'(자원을 공유하지 않음)로 구현하는 것이 좋다. 자원을 공유하는 서버로 구현되면 클라이언트 서버 간에 경합이 발생하는 등 여러 이유로 인해 데이터의 무결성이 해쳐지거나 처리 능력이 떨어질 수 있기 때문이다.

    처리 능력 불균형

     앞단 서버와 뒷단 서버의 처리 능력이 급작스럽게 불균형해질 수 있다. 그렇다고 모두 max치로 성능을 올려두는 것은 비효율적이다. 두 서버 모두 탄력적으로 성능을 조절할 수 있도록 해야 한다. 앞단 서버와 뒷단 서버의 스레드 수를 확인해서 조절하거나, 문제가 될 것 같은 트랜잭션이 포함된 로직을 대량으로 qa 해보면 좋다.

    도그파일

     dogpile이란, 다수의 요청이 한꺼번에 시도되는 현상을 말한다. dogpile로 인해 처리가 지연되거나 특정 자원을 요청하는 작업들이 몰릴 수 있다. 이 현상에 문제없이 대응하기 위해, 요청 시간들을 분산하거나 재호출 로직이 포함되어 있다면 재호출 주기를 느슨하게 설정하는 것이 좋다. @PostConstruct를 사용하여 미리 자원들을 로드시켜 놓는 것도 좋다.

    의도치 않은 대량 데이터 조회

     데이터를 많지 않은 테이블을 조회할 때 limit 없이 조회하는 경우가 있다. 테이블 내 데이터를 모두 조회하는 것이다. 평소에는 문제가 없겠지만, 만약 이 테이블에 데이터가 급격하게 증가한다면 애플리케이션 서버는 높은 확률로 순식간에 메모리가 가득 차버릴 것이다. 게다가 만약 조회 방식이 select for update 라면? 모든 행에 대해 배타락까지 걸리게 된다. 다른 트랜잭션이 이 테이블에 접근하지 못하게 되어 더 큰 문제로 번지게 되기도 한다.


    5장 안정성 패턴

    시간제한

     이 책에서는 timeout을 굉장히 많이 강조한다. 그만큼 중요하다는 말이다. timeout은 클라이언트가 자원과 시간을 불필요하게 소모하게 하기 때문이다. 한편, timeout은 재시도와 같이 쓰이곤 한다. timeout으로 인해 요청이 실패되면 다시 요청을 보내게 되는데, 즉시 재시도하는 것은 의미가 없을 가능성이 크다. 오히려 실패될 요청을 또 보내면 서버의 부하로 이어질 수도 있다. 시간을 좀 더 두고 재시도하는 편이 좋을 수 있다. spring에서는 RetryTemplate나 @Retryable을 사용하면 된다. 메시지 큐를 이용하는 방법도 있다.

    회로 차단기

     시스템 실패라는 '결함'의 균열이 확산되는 것을 막기 위해 회로 차단기(circuit breaker)를 적용할 수도 있다. 실패하는 요청들의 수가 임계점을 넘으면 회로 차단기가 열린다. 그러면 이후의 모든 요청들을 즉시 실패 처리한다. 그리고 조금 뒤에 몇 건의 요청들을 동작시켜 봐서 성공하는지 확인한다. 이를 반열림 상태라고 한다. 여전히 실패하면 열린 상태를 유지한다. 성공하면 반열림 상태를 점차 닫힘 상태로 전환한다. 화로 차단기 상태 히스토리 로그는 운영팀에 공유되면 좋다.

    격벽

     격벽이란, 구역을 나누는 칸막이이다. 서버에도 격벽 개념을 도입하면 균열의 확산을 어느 정도 막을 수 있다. 어떤 서버의 한 비즈니스 로직을 특정 성격별로 나눌 수 있다면 아예 별도 서버로 나누어서 운영하면 좋다. 인입이 많은 서버에는 좀 더 적극적인 확장 정책을 적용할 수도 있고, 인입이 적은 서버에는 최소한의 리소스만 적용하여 비용을 아낄 수도 있다. 서로가 영향을 덜 주기 때문에 균열의 확산을 막는 것에 도움이 되기도 한다. cpu 레벨에도 적용이 가능하다. 특정 프로세스에는 특정 코어만 지정하도록 하면, 이 프로세스에 장애가 발생할 때 나머지 코어는 정상 동작이 가능하다. 스레드 풀도 마찬가지.

    차라리 빠른 종료

     오류가 발생한 경우, 전제 시스템을 살리기 위해, 때로는 문제가 발생되고 있는 작은 시스템을 빨리 종료시키고 새로 띄우는 것이 나을 수 있다. 다만 이 작은 시스템이 어떤 형태이냐가 중요하다. 프로세스 내의 구성 요소 중 하나인 경우, 클라이언트는 작은 시스템이 종료되고 새로 띄워지는 것을 모를 정도로 빠르게 처리될 수 있다. 하지만 별도의 인스턴스인 경우, 모든 인스턴스가 재시작하는 데에 시간이 걸리기 때문에 썩 좋은 방법이 아닐 수도 있다. 한편 작은 시스템의 재시작하는 과정을 잘 모니터링하는 것이 중요하다. 새로 띄워지는 시스템에 여전히 오류가 있을 경우, 새로 띄워져도 계속 오류가 발생할 것이기 때문이다. 또한 이 시스템에 요청을 보내는 측에 회로 차단기가 잘 적용되어 있으면 좋다.

    배압

     tcp, thread pool, db pool 등 대기열을 사용하는 곳에선 대기열의 max size를 설정해야 한다. 그렇지 않으면 메모리가 가득 차버릴 수도 있기 때문이다. 그리고 대기열이 가득 차는 상황을 대비해야 한다. 대기열이 가득 차서 consumer가 이미 최대치로 동작하고 있을 때, producer가 대기열에 데이터를 더 넣지 못하도록 하는 등의 배압 개념을 적용하면 producer 역할을 하는 클라이언트 입장에서는 빠르게 응답을 받아서 적절한 처리를 할 수 있다.

    조속기

     자동화 시스템은 사람보다 매우 빠르게 동작한다. 결함이 오류로 바뀌면 오류는 걷잡을 수 없이 빠른 속도로 반복된다. 사람이 중간에 개입해서 오류를 수정할 시간을 확보하기가 어렵다. 이때 조속기 역할을 하는 장치가 있다면 오류를 해결하기 수월할 것이다. 기계는 단순 반복 작업만을 잘할 뿐, 긴급한 상황에 대처하는 역할은 사람이 훨씬 잘하기 때문에 조속기를 통해 사람이 개입할 시간을 벌어야 한다.

    댓글

Designed by Tistory.