Swap 메모리 체크 관련 문제 및 에러 로깅
서두
식사 중 슬랙 에러 알람이 엄청 울렸다.. 다행히 다른 컨슈머 서버에서 문제없이 처리 중이여서 에러 로깅이 관리 되고있기에
cloudwatch
로 바로 들어가 확인 후 코드 파악 후 문제를 정리하였는데
문제 상황
현재 서비스중인
ai 이미지 프로세싱
의 경우 스팟인스턴스에서 실행중인 컨슈머 서버에는 인스턴스의swap memory
를 체크하는 로직이 존재하는데,
해당 로직이 정상 동작하지 않았고 이로인해 연쇄적으로 예외처리 해놓은 로직들이 전부 실패하였고 메시지 큐를 처리하지 못하고 무한정 대기하는 문제가 발생하였다
스팟 인스턴스에는 크게 세개의 작업을하는 프로세스와 메시지큐가 존재하는데
- 사용자의 이미지 생성 요청을 받는 프로세스 - api 서버
- 해당 요청을 메시지 큐로 부터 받아 처리하기 위해 ai 프로세스에 요청을 보내는 프로세스 - 컨슈머 서버
- api서버와 컨슈머 서버 사이의 요청과 응답을 받는 - 메시지 큐
- ai 이미지를 생성하는 프로세스 - ai 프로세스
서비스 중인 컨슈머 어플리케이션은 메시지큐에서 요청 작업을 소모해 인스턴스에서 하나더 셋업중인ai 프로세스
(java
서비스와 통신하는python process
) 와 통신을 통해 작업 처리 중
상황 발생 환경 및 스택
spring boot 3.*
,java
,python
aws
,spot instance
문제 원인
결론만 보면, 꽤나 간단했던 문제같지만 실제로는 문제를 찾는데 시간이 걸렸다
트래킹을 해보니 여러 기존 레거시 + 내 실수등.. 쌓여있든 다양한 레거시들이 연쇄적으로 문제를 일으킨 상황이다…
순서대로 정리해보자 문제는 아래와 같다
개인 프로젝트에서 발생한 문제가 아니기때문에 자세히 적어선 안되고 사용한 기술에 대해서도 구체적으로 적지 않으려고합니다.
이러이러한 상황이 발생했고..이러이러한 방식으로 거슬러서 트래킹해서 해결해나갔구나 정도로만 적고자합니다.
문제는 각 순서대로 연쇄적으로 얽혀있었습니다.
1. 아직 세팅되지 않은 swap memory 체크
가장 첫번째 문제는, 서버가 인스턴스 셋업 → spring 셋업 후 최초 상태에서 큐의 메시지를 소모 전 swap memory 부족이라 잘못 판단한다는것이다
1) 인스턴스 셋업시 세팅한 swap memory
적용이 java application의
메모리 체크 단계에서 아직 되지않아 경우 발생
- 한달치 로그를 전부 다 찾아보니 정말 정말 간혹…
→ 체크가 완벽히 된 이후 부터 큐 메시지 소모하도록 바꾸자
2) 그리고 문제였던 java 로직은swap memory
부족하다 판단해버려서.. 실제로는 전혀 부족하지 않음에도.. ai 프로세스를 재시작 해버리게 됨
스왑 메모리 체크는 왜 하는건가?
- ai 프로세스의 경우에는 빠른 작업처리를 위해 사용하는 다양한 작업모델(
대략 1~4gb
)을 메모리에 로드하는 작업이 필요 -즉, 메모리 사전 로드없이 사용할때마다 매번 로드 후 모델 사용을 한다면 로드하고 사용하는것보다 약 5배이상 느림 - 다양한 모델 및
java 프로세스에서
필요한 메모리를 충족하기 위해swap memory
사용이 가성비가 괜찮기에, 메시지 큐 처리전swap memory
체크로직이 필요- 그렇다면.. swap 메모리도 용량은 무한이 아니니까 로드할 수있는 모델의 수와 전환 빈도가 고려되어야하는데..
-> 이 부분은 추측해서가 아닌 다양한 경험적인 요소를 통해 결정해야함
- 그렇다면.. swap 메모리도 용량은 무한이 아니니까 로드할 수있는 모델의 수와 전환 빈도가 고려되어야하는데..
2. 비정상적으로 체크해버린 swap memory로 서버가 실행 파일을 통해 재시작을 하고자했으나.. 실패
- 서버를 재시작하는 방법은 다양하기때문에 최대한 단순히 작성합니다
- 여기서 발생한 문제는 실행 파일 실행을 정상 종료하지 못해서 재시작을 실패했다
- 현재 서버는 재시작에 실패하면 0 성공하면 1을 리턴받아 실패시
retry를
한다. - 문제는 해당 실행파일의 종료코드가 두 값이 아닌 다른 값을 리턴하는 경우가 발생하였다.(종료코드 0,1이 아닌 코드)
→다양한 이유(시간초과, 메모리 부족, 해당 실행파일에서 리턴하는 custom 값 등)로 발생하는 종료 코드
에 대한 예외처리가 보완이 필요했기때문에 결국 서버는 재시작도
3. 서버를 종료시키는 shutdownConsumer메소드 실패
- 2의 예외 처리를 위해서 서비스는 서버 인스턴스를 종료시킨다. (재시작도 실패했으니 해당 서버의 초기화 과정부터 문제가 있다고 보고 종료후 새 스팟 인스턴스를 띄운다)
- 해당 메소드의 목적은 swap memory도 부족하고 ai 프로세스 재시작도 실패하여 계속해서 재시작 하기보다는 스팟인스턴스를 종료시키고 다시 인스턴스를 할당 받고자하는 목적으로 만들어진 메소드
- 문제는 정상 수행되지않았음에도 에러 catch가 되지않고 그냥 지나가버리는 잘못 만들어진 메소드 레거시
- 정상 수행이 되지않았다면 이유가 무엇인가?
- 해당 레거시는 서버를 종료시키는 실행 파일을 실행하는데 종료코드 처리에 관해서 누락된 부분이 있기때문..(exitcode 0,1처리는 했지만 다른 종료코드가 나타나면..? → 문제)
3번과정까지 오는 경우가 드물었고(2에서의 실패, 3에서 다른 종료코드) 그 보다 문제였던점은 테스트 코드의 부족
4. 서버가 종료되지않았기 때문에 java는 정상인줄 인식하고 다음 로직을 실행한다…
1) ai 서버는 당연히 재시작을 못했으니 죽은 상태여서 요청 실패하고 에러를 던짐
→ 또 해당 에러를 이미지 컨슈머(호출했던 상위 클래스) 에서 catch 2) 여기서 아래 5
번의 상황을 알고가야 하는데..
5. 서버의 최초 boot 과정에는 ai 프로세스의 빠른 시작을 위해 샘플데이터(실제 요청이 아닌)를 통한 처리가 들어간다
1) 기존의 컨슈머는 ai 프로세스가 생성을 할수없는 상황인 경우 사용자의 실제 요청이 공중에..? 버려지지 않기위해 메시지 큐를 캔슬시킨다 2) 문제는, 샘플데이터의 경우 메시지 큐에서 나온 실제 데이터가 아니기에 basicreject
를 하게 될경우 deliveryTag는 이미 ack되었기에(보내고 받은 메시지가 아닌 더미데이터니 메시지큐와 일절 상관없는 상황이였던것)
그냥 이때 정상적인 메시지 큐와의 연결마저 끊겨버렸다.. 3) 그리고 서버 상태 체크를 이어서 하는데 역시 ai 프로세스
는 죽어있으니 에러 알림 전송후 무한정 대기 상태에 들어감
6. 이미 끊겨버린 큐..
1) 4
의 상황을 인지한 개발자가 이후 부랴부랴 수동으로 ai 프로세스
를 재시작하고 다음 큐 메시지를 처리하나 싶었지만… 2) 큐는 이미 끊겼기에 메시지는 처리못하고 에러가 발생 channel is already closed due to channel error; protocol method…..
문제점 및 고민
- swap memory 세팅이 되지않았음에도 메모리 체크를 해버리니 반드시 ai 프로세스를 재실행 실행해버림
- 근본적인 문제, 1의 문제가 발생하지않았다면 모든 일이 정상동작했을 문제
- 메모리 체크시 부족한 경우 슬래알림 부재
- swap memory 세팅 여부 체크 로직 부재
- 재실행시 실패를 해버리면 shutdownConsumer를 하는데 정상 작동을 하지않고있음 + error 캐치가안되고있음
jshell
로 서버에서 직접 shutdown 메소드를 호출 해보면 → 정상동작함 = 권한문제, 기능자체의 동작 등은 문제 x- 현재 누락된 부분은 종료 코드에 따른 처리(실행 중 에러는 안 났지만 종료 코드가 0이 아닌 경우)
- 우선, 인스턴스 종료를 하는 기능자체가 필요하긴한가?
- 그러하다 인스턴스 종료 대신 다시 1, 2의 로직을 반복하는것은 얼마나 반복할지 모르고(인스턴스 자체의 문제일수도있기에) + 여러번 반복으로 리소스 소모보다 스팟 인스턴스 종료 후 재 할당이 더 합리적
- 샘플데이터처리 실패시(메시지 큐에서 나온 실제 메시지가 아닌) basicreject를 하고있음 - 큐메시지가 아닌데 reject을 하면안됨
- 메모리체크에서 최초 요청시에는 exception catch 후 알림 후 대기
해결 성공
- 메모리 세팅이 되었는지 체크(
ManagementFactory.*getPlatformMBeanServer
)를 통해 total, free 스왑이 0이라면(= 세팅이 안되었다) 스킵* - ec2 종료 스크립트의 종료코드 체크
- 아래와 같은 느낌으로 작성해보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int exitCode;
var processBuilder = new ProcessBuilder();
var process = processBuilder.command(
"aws", "ec2", "terminate-instances",
"--instance-ids", instanceId,
"--region", region
).start();
exitCode = process.waitFor();
if (exitCode == 0) {
return;
}
// 실행 중 에러는 안 났지만 종료 코드가 0이 아닌 경우 (비정상 동작)
String errorMsg;
try {
errorMsg = IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8);
} catch (Exception e) {
errorMsg = "에러 메시지 추출 중 오류 발생 " + e.getMessage();
}
throw new RuntimeException(errorMsg);
기존 basicAck 처리 수정
- ack가 늦게 되면 사용자가 이미지를 받는데 문제가 있는지, 다른 컨슈머 혹은 해당 컨슈머 본인에게 문제되는점이 있는지
- ack 처리전에 wait시키고 사용자가 그전에 이미지를 챙겨받는지 확인해봐야함
→ 챙겨받음 따라서 해당 사용자의 요청중 하나를 하나의 컨슈머가 이미지 생성완료 보고한후
메모리체크로직에서 시간이 많이 걸리거나 인스턴스가 종료돼도 다른 컨슈머들이 나머지 처리하는데 문제 없음(기존에는 ack를 먼저 해버리니까 그 다음 이미지가 unacked된 상태로 있어서 다른컨슈머가 건들지를 못했음) - ack 처리하기전에 (이미지 생성작업 및 보고는 끝난상황에서) 에러 발생시 큐를 잘 reject하는지 확인해봐야함
- ack 처리전에 wait시키고 사용자가 그전에 이미지를 챙겨받는지 확인해봐야함
- basicack를 밑으로 옮긴이후 작업처리에서 예외 발생시 문제 될부분이있는지(큐 캔슬이 안되는 부분이라던지)
- basicAck하다가 예외(i/o exception) 발생하면?
- 기존에는 큐 리젝 시켰음 - 이미지 완료 보고는 이미 된 상태라서 그냥 큐 리젝만 시키는거 사용자가 이미지를 받는데는 문제없음
- 그냥 예외가 throw되게하면 어떻게되는지 테스트
- 중요한건 다음 이미지 처리에 문제가 없어야한다
→ 그냥 throw하게 두면 안된다 예외만 출력되고 unacked되있던(4개의 메시지라면 최초 메시지에 대해서 처리후 완료 보고후 ack시 예외가 발생한 경우) ready로 돌아가버림 문제(첫번째 문제)는 두번째는
이후 메시지를 소모하지않고 그냥 멈춰버림
→ ai 프로세스 상태 문제로인지 먼저 체크후 에러 처리하자
- ack가 늦게 되면 사용자가 이미지를 받는데 문제가 있는지, 다른 컨슈머 혹은 해당 컨슈머 본인에게 문제되는점이 있는지