글을 작성하게 된 계기
프로젝트에서 버그가 발생해 이전 버전으로 롤백(Rollback)을 하게 되면서, 이 과정에서 배운 내용과 생각을 정리하기 위해 글을 작성하게 되었습니다.
1. 장애 대응
일정 관리를 도와주는 Dailyge 프로젝트에서 다른 사람의 일정이 조회되는 버그 가 발생했습니다. 장애를 완전히 해결하기까지 다음과 같은 프로세스를 거쳤는데, 어떻게 장애에 대응했는지, 그 과정에서 무엇을 배웠는지 하나씩 살펴보겠습니다.
- 장애 감지
- 장애 전파
- 장애 대응
- 장애 대책 마련
- 장애 회고 및 발표
1-1. 장애 감지
먼저 장애를 감지하는 단계입니다. 저는 같이 프로젝트를 하는 팀원으로부터 버그가 있다는 제보를 듣고 장애를 감지하게 되었습니다.
장애 감지는 모니터링, 알림, 다른 사람으로부터의 제보 등 다양한 방법이 존재합니다. 장애를 감지할 수 있는 최대한 다양한 경로 를 확보 후, 빠르게 장애를 인지할 수 있도록 합니다.
이전에는 고객 센터를 볼 때, ‘과연 이걸 사용하는 사람이 있을까?’라는 생각을 했습니다. 그런데 장애를 겪고 보니 다른 사람으로부터 제보 를 받는 것도 장애를 빠르게 감지하는 하나의 방법인 것을 깨닫게 되었습니다. 최대한 다양한 루트 열고 장애를 빠르게 감지할 수 있도록 외부와의 소통 에도 신경을 써봅시다.
위는 카카오워크 고객센터의 문의하기 사진 입니다.
1-2. 장애 전파
다음은 장애를 전파하는 단계입니다. 보통 개발은 팀 단위로 진행되기 때문에 현재 발생한 문제에 대해 팀원들이 인지할 수 있도록 장애 사항을 빠르게 전파합니다. 이때 가장 중요한 점은 정확한 장애 원인을 파악 해 빠르게 전파 하는 것입니다.
이때 조금 아쉬웠던 부분이 장애가 발생했을 때 빠르게 전파할 수 있는 장애 전파 템플릿 을 만들어두지 않았던 점인데요, 장애가 해결된 후, 팀원들과 상의해 템플릿을 만들었습니다. 육하원칙에 따라 작성하는 것이 가장 무난하다고 판단했는데, 이 부분은 팀에 맞게 작성하면 될 것 같아요. 😃
1
2
3
4
5
6
7
8
9
10
11
🚨 [장애 알림] 서비스 장애 발생 🚨
- 발생 시각: 2024-10-13 13:30 경
- 장애 유형: Critical
- 영향받는 도메인/서비스: Dailyge-Api
- 상세 내용: Redis 메모리 초과로 인한 로그인 불가
- 조치 상태: 현재 복구 작업 진행 중
- 담당자: 김복구(010-1234-5678)
- 예상 복구 시각: 2024-10-13 13:45 경
👉 **조치가 완료되면 알림 드리겠습니다.**
1-3. 장애 대응
다음은 장애 대응입니다. 롤백, 장애 격리, 서버 리소스 확보 등 실질적 조치 가 이루어지는 부분으로, 원인이 파악됐다면 가장 안정적인 최신 버전으로 롤백 합니다. 장애 없이 서비스가 잘 동작하던 버전으로 돌아가는 것이죠. Git을 사용하고 있다면 간단히 Revert로 이전 버전으로 롤백할 수 있습니다. 간단한 경우라면 Hotfix 브랜치를 딴 후, 버그가 난 부분만 수정할 수도 있고요.
이때, 안전한 최신 버전 을 파악하는 것도 중요한데, 이는 코드/기능의 변경 사항을 히스토리 로 남긴 후, 주기적으로 체크 해야 합니다.
글을 읽고 좋은 아이디어와 피드백 주신 진우님 감사합니다. 😃
그런데 빠르게 이전 버전으로 롤백해야 하는데, 빌드 시간이 오래 걸린다면 그동안 계속해서 장애를 겪게 됩니다. 현재 진행중인 프로젝트도 테스트 코드 때문에 빌드 시간이 꽤 오래 걸리는데요, 약 860개 정도의 테스트 코드가 실행된 후, 배포까지 약 8분 정도의 시간이 걸립니다. 테스트 코드가 더 많다면, 빌드 과정에 다른 프로세스가 있다면 더 많은 시간이 걸리겠죠?
이를 단축하기 위해서는 롤백을 위한 별도의 배포 파이프라인 을 만든 후, 이전 버전의 Artifact를 사용해야 합니다. 따라서 새로운 버전이 배포되더라도 이전 버전의 상태를 저장해두고 언제든 돌아갈 수 있도록 준비 해둡니다. AWS를 사용하면 ECR 또는 S3에 저장해 손쉽게 이를 사용할 수 있습니다.
만약 테라폼(Terraform)을 사용하고 있다면 이 과정을 비약적으로 단축할 수 있습니다. 명령어 한 줄로 이전 상태로 빠르게 돌아갈 수 있으며, 멱등성을 지니기 때문에, 이전 버전으로 롤백해도 사이드 이펙트가 없기 때문입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
resource "aws_ecs_task_definition" "dailyge_api_prod_deploy_task_def" {
family = "dailyge-api-prod"
network_mode = "bridge"
requires_compatibilities = ["EC2"]
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
container_definitions = jsonencode([
{
......
// 테라폼 코드 한 줄 수정으로 전체 인프라를 롤백 가능
image = "${var.ecr_repo}:${var.app_ver}}"
essential = true
stopTimeout = 10
cpu = 1024
memoryReservation = 768
......
1-4. 장애 대책 마련
같은 이유로 장애가 다시 발생하면 안 되기 때문에 문제가 발생한 근본적 원인을 분석 한 후, 대책을 마련합니다. 여기에는 시스템 알림 도입, QA 시스템 강화, 코드 리뷰 강화, 문서 작성 강화 등과 같은 다양한 방법이 존재합니다.
- 시스템 알림 도입
- QA 시스템 강화
- 코드 리뷰 강화
- 문서 작성 강화
피드백 때 나온 인상 깊었던 대책 중 하나는 코드 리뷰 강화 였습니다. 내가 아무리 꼼꼼하게 코드를 검토하고, 테스트 코드를 작성해도 내 사고 안에서 작성된 코드 는 그 틀을 깨기 힘들더라고요. 즉, 남이 내 코드를 볼 때, 그때야 보이는 실수가 꼭 존재했습니다. 따라서 코드 리뷰 의무화 는 필수라는 생각이 들었습니다.
추가로 코드 리뷰 내용 에 대한 의견도 나왔는데요, 프로젝트에서 이미 코드 리뷰를 도입 하고 있었지만 그럼에도 장애가 발생 했기 때문입니다. 이전 코드 리뷰에서 코드 스멜이나 포맷에 대한 체크가 주를 이루었는데, 이것보다 버그 예방 과 코드를 더 꼼꼼히 보는 것 이 조금 더 필요할 것 같다고 솔직하게 의견을 냈습니다. 물론 그 전에 제가 꼼꼼히 봐야겠지만요. 🏃
1-5. 장애 회고 및 발표
마지막으로 장애 회고 및 발표를 합니다. 이는 선택사항 이지만, 모든 장애 대응이 끝났다면 꼭 해볼 것을 권장해 드립니다. 장애를 돌아보는 과정에서 정말 많이 성장할 수 있거든요. 저는 팀원들 앞에서 발표했는데, 이 과정에서 어떤 경로로 문제를 인지했는지, 왜 문제가 발생했는지, 어떻게 대응했는지, 전체 시간은 얼마나 걸렸는지, 어떤 대책을 세울지 등을 설명하고 피드백을 주고받았습니다.
- 어떤 경로로 문제를 인지했는지
- 왜 문제가 발생했는지
- 어떻게 대응했는지
- 전체 시간은 얼마가 걸렸는지
- 어떤 대책을 세웠는지
이 과정에서 가장 크게 깨달은 점은 장애를 대처하는 것도 프로세스가 필요 하다는 점입니다. 당장은 잘 아는 사람이 빠르게 대처하는 것도 좋지만, 그분이 없을 때도 장애를 빠르게 해결하고, 시스템이 안정적으로 돌아가게 해야 하니까요. 따라서 장애가 발생했을 때, 과정을 돌아보고, 정리하며 발표하는 것을 꼭 해봅시다.
LINE Engineering에 LINE의 장애 보고와 후속 절차 문화라는 글이 있는데, 관심 있다면 한 번 읽어보세요.
처음엔 내 실수를 내 입으로 모두 앞에서 발표한다는 것이 부끄러웠습니다. 하지만 용기를 내서 발표하고 나니 조금 더 경각심을 가질 수 있었고, 미숙했던 대응에 대한 반성, 장애 프로세스가 필요하다는 점을 깨달을 수 있었습니다. 또 팀원들도 좋은 말을 해주셔서 더 미안하고, 감사한 마음도 가지게 됐네요.
2. 장애 예방
전반적인 장애 대응 프로세스에 대해 살펴보았는데, 추가로 장애 예방법 대해서도 조금 더 살펴보겠습니다.
- 모니터링
- 장애 격리
- Feature Toggle
2-1. 모니터링
모니터링을 통해 서버 리소스를 관리하고 장애를 예방 할 수 있습니다. 프로젝트에서 부하 테스트를 하던 중, 레디스 메모리 부족 으로 인스턴스가 종료되는 이슈가 발생했습니다. 레디스 메모리는 2G 였는데, 사용 중인 메모리가 1.5G를 넘으며 서버가 버티지 못했기 때문입니다. 만약 모니터링을 꼼꼼히 했다면, 이런 이슈가 발생하기 전 Scale Up을 통해 조처할 수 있었을 것입니다.
비슷한 상황이 스프링 애플리케이션과 RDB에서도 발생했는데요, 톰캣 쓰레드 개수를 너무 적게 잡다 보니 적은 쓰레드로 너무 많은 요청을 처리했고, 이 과정에서 CPU 사용률이 지속적으로 100% 가 되는 현상이 발생했습니다. 반대로 톰캣 쓰레드 개수를 너무 많이 늘린 후, RDB 커넥션 개수가 부족하기도 했고요. 따라서 서버 리소스를 지속적으로 체크하며 장애가 발생하기 전, 미리 대책을 세우도록 합시다.
이 외에도 서버 리소스 문제로 배포가 되지 않는 이슈도 있었는데, 관심 있다면 해당 포스팅을 참조해보세요.
2-2. 장애 격리
한 시스템 또는 외부 시스템에 장애가 발생했을 때, 이를 격리시켜 연쇄 장애 를 예방할 수 있습니다. 프로젝트에서 부하 테스트를 할 때, 레디스가 다운돼 로그인이 되지 않는 이슈가 발생한 적이 있습니다. 이 경우, 레디스를 1차/2차 캐시 형태로 만든 후, 1차 캐시에 장애가 발생하면 이를 격리하는 것을 통해 해결할 수 있습니다.
서킷브레이커(Circuit Breaker) 와 Fallback 패턴 을 통해 장애를 예방하는 것이죠. 1차 캐시가 다운됐을 때, 호출 실패가 일정 횟수/비율 을 초과하면 1차 캐시 레디스를 더 이상 호출하지 않도록 하는 것입니다. 이를 간단히 코드로 살펴보면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class UserCacheService {
private final Cache primaryCache;
private final Cache fallbackCache;
public UserCacheService(
final Cache primaryCache,
final Cache fallbackCache
) {
this.primaryCache = primaryCache;
this.fallbackCache = fallbackCache;
}
@CircuitBreaker(name = "primaryCache", fallbackMethod = "getFromFallbackCache")
public User getUser(final String userId) {
return primaryCache.get(userId);
}
// 1차 캐시가 실패했을 때 2차 캐시에서 데이터를 가져오는 메서드
private User getFromFallbackCache(final String userId, Throwable throwable) {
return fallbackCache.get(userId);
}
}
방법은 간단하지만 서비스 규모가 작아서 실제 구현하진 않았습니다.
시스템은 언제나 장애가 발생할 수 있기 때문에 장애가 발생했을 때, 한 시스템의 장애가 다른 시스템에 영향을 최소한으로 미치도록 하는 것이 중요합니다. 이를 위해 서킷 브레이커(Circuit Breaker) 와 같은 장치를 고려할 수 있습니다.
2-3. Feature Toggle
새로운 기능을 추가했을 때, 서비스 안정화 가 되기까지 API 호출로 해당 기능을 ON/OFF 시킬 수 있도록 하는 방법입니다. 다음과 같이 새로운 기능을 사용할 수 있는지에 대한 여부를 flag로 저장하는 것이죠.
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class NewFeatureConfig {
private boolean enable;
public boolean isEnable() {
return enable;
}
public void setNewFeatureEnabled(final boolean enable) {
this.enable = enable;
}
}
이후 해당 로직을 실행할 때, 새로운 기능을 사용할 수 있는지를 확인한 후, 사용할 수 있다면 이를 실행하고, 사용할 수 없다면 예외를 발생시킵니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@RequiredArgsConstructor
public class FeatureService {
private final FeatureConfig featureConfig;
public void executeFeature() {
if (featureConfig.isEnable()) {
// ON. 새로운 기능 실행
} else {
// OFF. 예외 발생
}
}
}
스위치 ON/OFF는 레디스의 Channel/Stream 또는 RabbitMq의 Fanout 을 통해 구현할 수 있습니다. 단, 이는 순서 보장 과 이벤트 유실 에 대한 문제를 고려해야 하므로 추가적인 장치가 필요합니다. 이를 통해 서비스를 운영하는 중에도 해당 기능의 동작 유무를 API로 컨트롤할 수 있어, 장애로 인한 피해를 최소화할 수 있게 됩니다.
3. 장애 공지
장애가 발생했다면 고객들에게 이를 알릴 수 있는 장치도 필요한데요, 이를 위해 어느 부분에서 장애가 발생 했고, 어떤 기능을 이용할 수 없는지 빠르게 전파할 수 있는 템플릿 도 함께 만들어 둡시다.
4. 정리
장애가 발생했을 때, 어떻게 대처했는지, 그리고 나아가 장애를 어떻게 예방할 수 있는지 살펴보았습니다. 하지만 장애는 언제든 발생할 수 있습니다. 따라서 장애가 발생할 수 있다는 것을 인정하고 장애가 발생하더라도 시스템이 자동으로 복구되고, 회복될 수 있도록 설계합니다. 또한 장애를 겪은 후, 회고를 통해 더 성장할 수 있도록 해봅시다.
- Feature toggle
- Feature Toggles (aka Feature Flags)
- LINE의 장애 보고와 후속 절차 문화
- 네이버 검색의 SRE 시스템
- 사이트 신뢰성 엔지니어링(SRE)이란 무엇인가요?