중단 없는 서비스 제공을 위한 삽질기

중단 없는 서비스 제공을 위한 삽질기

안녕하세요. 오랜만에 인사 드립니다.

회사 일로 바빠서 글을 많이 쓰진 못했는데 오늘 제 서버 중 하나에 장애가 발생 (정확히는 내부 사정으로 인해 외부망 연결이 되지 않는 상황) 하여 내부 공유 차 글을 써보려고 합니다.

내일 일찍 출근해야 되서 사진은 없고, 대부분 글로 채우는 점 양해 부탁 드립니다.

서막

회사에 있을 때 였습니다. 한참 일을 하고 있던 중에 제 휴대폰으로 문자가 한통 들어옵니다.

1
2
3
4
[SYNOPTICON-ALERT] <서버명> NODE DOWN, ARK PROTOCOL ACTIVATED
REGION: ap-northeast-1a, SERVICES: <서비스명>, <서비스명>, <서비스명>

[SYNOPTICON-ALERT] ALL SERVICES ARE RECOVERED

?? 전혀 죽을 일 없을 것 같던 서버가 죽어서 ARK(방주) 프로토콜이 활성화 되었고, 현재는 정상 운영중이라는 이야기였습니다.

왜 죽었는가?

그래서 이상해서 서버 상태를 휴대폰으로 확인해보니 진짜 접속이 안되는 현상이 발생했습니다??

이유는 알고 보니 서버가 있는 곳 옆에서 공사를 하는데, 공사 과정에서 인터넷 망을 건드렸는지 외부망 단절?이 발생했다는 점?

이로 인해 일부 서비스가 정상 작동하지 않았던 것으로 추정하고 있습니다. (아직 정확한 원인은 모릅니다)

이런 장애를 다른 서버에서 탐지해서 상호 합의 하에(뒤에 설명 나옴) 새 AWS 리전에 구축 하였고, 서비스는 정상 작동 하였습니다.

어떻게 복구하였는가?

제가 작업한 모든 서비스는 모두 AWS CloudFormation으로 인프라 코드를 작성, 타 서버에서 장애 발생 시 바로 AWS 인프라에 운용 가능하도록 준비 해 두었습니다.

그리고 synopticon 이라는 프로젝트를 통해 평소 운영중인 서버(서비스)에 대해 상호 감시할 수 있도록 체계를 구축 해 두었습니다.

이를 통해 특정 서버가 정상 운영되지 않고 있음을 확인 하였고, 상호 교환된 정보에 따라 감시 노드에서 탈락시키고 정상 연결될 때 까지 상태 체크 이외에 전달 값을 무효화 하는 조치를 하였습니다.

이후 제 AWS에 구축된 람다 함수를 호출하여 해당 서버에서 운영 중이던 서비스에 대해 CloudFormation 실행 명령을 수행 하였습니다.

수행 이후 기존 도메인에 등록된 A Class IP 주소를 신규 서버 주소로 변경하는 작업까지 완료하여 정상 복구하였습니다.

근데 이거 어디서 많이 들어 본 내용 아닌가요..?

Synopticon?

Go 기반의 혼자 쓰는 (비)오픈소스 프로젝트.

아이디어는 비트코인의 블록 검증에서 따와서 각 서버가 노드가 되어 해당 서버의 서비스 정보를 기반으로 블록을 생성, 검증하는 과정을 통해 “탈중앙화(?)” 된 서버 모니터링을 구축 하는 것이 목표인 프로젝트입니다.

현재는 Agent가 서버 내에서 돌아가면서 타 서버와 지속적인 통신을 통해 서버 정보를 교환하고 블록(서버 내 운영 중인 서비스 정보와 우선순위)을 생성하여 저장합니다.

각 서버가 에이전트를 통해 교환하는 블록의 예시 데이터는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
    'nodes': {
        'XXX': [
            'X-SERVICE-1',
            'X-SERVICE-2',
            'X-SERVICE-3',
        ],
        'YYY': [
            'Y-SERVICE-1',
            'Y-SERVICE-2',
            'Y-SERVICE-3',
        ]
    },
    'event': {
        'node-down': {
            'service': 'aws-lambda',
            'attributes': [
                '<람다 함수 주소>, <또 다른 람다 함수 주소>, ...
            ]
        }
    },
    'on-recovery': {
        'wait': 60,
    }
}

여기서 노드 내에 있는 리스트의 값을 해당 서버 내에 우선순위를 뜻합니다.

즉, X-SERVICE-1은 X-SERVICE-2보다 먼저 살려야 하는 서비스임을 말합니다.

이걸 만들고 나서 나름대로 서비스 별로 우선순위를 정해서 먼저 살려야 하는 서비스(대외 서비스) 부터 살렸습니다.

그리고 event의 경우 내가 특정 이벤트가 발생한 경우 호출할 서비스와 그 attribute를 뜻합니다.

여기서는 node-down 이벤트가 발생 시 aws-lambda 함수를 호출하는데 그 우선순위는 attributes 순서대로 따흡니다 (AWS도 죽을 수 있다는 점을 염두)

마지막으로, on-recovery의 경우에는 서버가 다시 복구 될 경우 어떻게 할 것인지에 대해 명시 되어 있습니다.

이 부분은 빡세게 구현할려고 한 것이 아니라서 (수동으로도 가능해서) ‘wait’ (대기하는 시간) 만 구현 했습니다. 이 코드에서는 60분 대기 후 연결 확인되면 복구 완료로 인식합니다.

TMI지만, 과거 독일의 상호감시 체제인 슈타지로 할까 했지만 잘못했다가 큰일 날 것 같아 그냥 고대 이름을 가져왔습니다. (그게 편하다)

여튼, 원래는 DR(재해복구) 에 관심이 많아서 만들기 시작했는데 이번에 잘 작동해서 기분이 매우 좋았습니다.

말만 들어보면 이것 만으론 부족하지 않나요?

YES. 그래서 저는 AWS를 적극적으로 이용햇습니다.

미리 일본(도쿄, 오사카), 미국(오리건, 버지니아), 독일(프랑크푸르트) 에 서비스 이미지와 CloudFormation 코드, lambda 함수를 배포해 두었습니다.

정확히는 서비스 코드 수정 뒤 마스터 머지 완료되면 github action을 이용하여 올리도록 작업 해 두었습니다.

그리고 만약에 사고 발생 시 synopticon 에서 제공하는 event listener 기능을 이용하여 람다 함수를 다른 함수에서 바로 호출하게 됩니다.

호출 뒤에는 람다에 정의된 대로 다 알아서 하는 것이라. 제가 신경 쓸 것은 람다에서 CloudFlare API를 이용하여 A Class 주소를 바꾸는 것 뿐?

정상 복구 뒤에는?

정상 복구 뒤에는 바로 AWS 인프라를 거두는 것이 아니라 일정 시간동안 다른 서버들이 정상 접근 여부를 확인 (이 정책에서는 60분) 한 뒤 기존 IP 주소로 변경합니다.

기존 IP 주소 변경 뒤에는 기존에 사용했던 모든 인프라를 삭제하고 사용했던 로그만 S3에 저장합니다.

끝..!

비용은?

EC2 c5.large 4시간 + EBS 128GB + EIP 등등 해서 300원 정도 들었습니다. 매우 싸죠?

근데 SMS 발송 비용이 80원… ㅋㅋ 400원 정도로 무중단 서비스를 구현할 수 있었습니다.

적은 비용으로 중단 없는 서비스를 구현할 수 있습니다.

참고로 저 때 다행히도 해당 시간대에 스팟 인스턴스가 있어서 스팟 가격으로 이용했습니다.

Lesson Learned

서버는 언제든 죽을 수 있다. 인프라는 언제든 죽을 수 있다. 항상 운영되는건 없다.

이번에는 데이터베이스 서버가 죽지 않아서 크게 문제될 것이 없었습니다만 하지만 디비 서버가 죽었다면..? (주기적인 백업과 replica 운영중)

둘 다 죽으면 어떻게 해야 하는가에 대해서도 고민해야 할 것 같습니다.

추가

AWS SMS 비용은 아무리 생각해도 너무 비싼 것 같습니다.

다른 서비스 다 죽었는데 내 서비스만 조금 있다가 바로 복구 되었다고..?