ecsimsw

Picup의 모니터링, 내가 찾은 튜닝거리들 본문

Picup의 모니터링, 내가 찾은 튜닝거리들

JinHwan Kim 2023. 12. 6. 00:16

Picup의 모니터링 시스템

나는 모니터링을 잘 모른다. 경험도 부족하고 모니터링을 위한 공부를 해본 적도 없다. 정말 부끄럽지만 매번 코드만 짜고 배포했다고 자랑만 해왔지 내 애플리케이션이 메모리를 얼마나 차지하고 얼마나 많은 리소스를 잡고 있는지 고민해 본 적 없다. 그저 OOM가 안 나오게 대충 크게 메모리 설정하고, 또 대충 부하테스트 돌려서 가상 유저가 몇 명인지 정도만 확인해 본 거 같다. 

 

이 글은 아무것도 모르는 바보가 프로젝트에서 JVM, k8s, docker 를 모니터링하고 테스트하는 과정을 기록하기 위한 글이다. 그리고 그 과정에서 찾은 튜닝거리들과 옵션들, 내가 생각하는 주요 메트릭들을 정리해보았다. 거기엔 명확한 정답이 없다고 생각하거니와 정답이 있더라도 이 글, 내 방식은 아닐 수 있다. 

 

혹 우연히 이 글을 발견하신 분이 있고 같은 고민, 비슷한 상태에서 답답함에 검색을 하고 계신다면, 이 글의 모니터링, 테스트 방법보다는 '얘는 이런 식으로 생각하고 찾아갔구나' 라고 생각해 주셨으면 좋겠다. 그리고 거기에 더해 본인 상황에서 더 좋은 정답을 만드시는데 이 글이 조금이나마 도움이 되었으면 좋겠다.

 

1. Pic-up 에서 확인하는 모니터링 지표들

2. Api server 모니터하기 / 대시보드 소개

3. 부하테스트로 실험해보기

4. 내가 찾은 튜닝거리들 / Picup의 설정들과 근거 소개하기

 

Pic-up 에서 확인하는 모니터링 지표들

1. Api server (Spring boot actuator, micrometer-prometheus)

- 메모리 사용량
- Cpu 사용량
- 분당 request 수, 서버 에러 응답 수
- 응답 시간 (sending + waiting + receiving)
- 스레드 수와 상태
- GC 수행 시간과 횟수
- Eden space, Survivor space, Tenured space 사이즈

 

2. k8s cluster (kube-state-metic, node-exporter, Kubelet)

- 전체 노드 개수 / Unavailable 노드 수
- Replica 수 / Unavailable replica 수
- 실행 중인 Pod 수
- 실행 중인 Container 수
- Pod Cpu, Memory 사용률
- Pod 네트워크 송수신량
- Evicted 이벤트 수
- Pending, Unkown 상태의 pod 수

 

3. Prod server, docker containers (cAdvisor)

- 전체 컨테이너 개수
- 전체 컨테이너 메모리 사용률
- 전체 컨테이너 CPU 사용량
- 각 컨테이너별 메모리, CPU 사용률
- 각 컨테이너별 네트워크 송수신량

 

4. Prometheus targets

 

Api server 모니터하기 / 주요 대시보드 소개

Picup 프로젝트를 예제로 삼았다. 프로메테우스 / 그라파나를 설치하고 Picup 프로젝트의 메트릭을 수집한다. 아래는 튜닝 전 헬스 체크 외 별다른 요청이 없는 상황에서 그라파나 모습이다. 메모리 사용량과 최대 값, 1분간 요청 수를 확인할 수 있다.

 

그중 이 글에서 다룰 api-member 서버는 녹색이다. 약 40%의 메모리 사용률을 보이고 있다. 어떤 요청도 없는 상황에서 너무 많은 메모리를 잡고 있지는 않나 궁금하다. api-member 에 요청이 들어오면 메모리 변화가 어떻게 되는지 확인하고 메모리 최대 사이즈를 수정할 생각이다.

 

 

사실 위 전체 40% 메모리보다 집중했던 것은 GC pause count 수이다.

아래 표는 api member 서버의 1분당 GC pause 수, 아래 세 지표는 각각 eden, survivor, tenured(OG) 를 표시한다.

 

 

특히 위 GC pause count에서 노란색으로 표시되고 있는 minor GC, "Allocation Failure" 는 Eden space에 영역이 가득 차 Minor GC를 실행시킨다는 것인데, 이를 약 2분마다 하고 있는 것을 볼 수 있다.

 

Eden space 지표에서 녹색으로 표시된 사용 영역이 남색으로 표시된 최대치을 닿지 않고 있어 의아했는데 JVM에서 사용할 수 있는 메모리 양과 총 할당된 최대치가 다르다고 한다. JVM에서 사용 가능한 메모리량을 commited 라고 그래프에선 황색 라인으로 표시되고 있다.

 

부하테스트로 실험해보기

실제 요청이 가게되면 메모리 사용에 어떤 변화가 있을까 궁금했다. 가상 사용자들 만들고 부하를 만들어 요청을 얼마나 처리할 수 있고, 서버 상태는 어떻게 바뀌는지 확인해볼 생각이다.

 

그전에 Picup 은 k8s 에 배포되어 HPA 가 적용되어 있다. HPA로 Memory 사용률에 따라 pod 가 1개부터 3개까지 수평 확장된다. 특히 처리가 오래 걸리는 파일을 비동기 대량 삭제 처리하는 Storage 서버는 더더욱 그렇다. 오토 스케일링을 위한 기준과 개수 범위를 지정하고 그 기준에 따라 자동 확장/수축되도록 하였다.

 

우선 부하테스트로 메모리 사용률을 올려 Pod 가 정상 증가하는지 확인한다. k9s 로 이벤트를 확인하니 80%의 타겟 사용률을 넘어 현재 95%의 사용률이 되어 스케일 아웃이 시작됨을 확인할 수 있다.

 

 

HPA 가 적용된 분산 환경에서는 한 WAS의 상태 변화를 확인하기 쉽지 않다. 이번에는 HPA를 중단하고 WAS 를 하나만 남겨 단일 WAS의 상태 변화를 확인할 생각이다.

 

K6를 이용하여 100명의 가상 유저로 아래 시나리오를 1분 동안 반복 수행한다. (각 행위 후에는 1초의 지연시간으로 한 구간 반복에는 총 3초의 지연이 포함된다.)

 

1. 회원가입한다.
2. 방금 가입한 유저 정보로 로그인한다.
3. 마이페이지에서 유저 정보를 확인한다.

 

튜닝하고자 하는 프로젝트에 1분간 VU 100, 위 시나리오를 반복하는 것은 아직은 프로젝트 규모에 과분한 테스트라고 생각했다. 그 테스트에서 모든 5400개의 요청이 모두 정상 응답되었고 'sending + waiting + receiving'을 모두 더한 http_req_duration 값이 평균 142ms, 하위 90%까지 200ms 언저리, 최대 1s의 결과면 당장은 더 서버 성능적으로는 더 개선이 필요로 하지 않아도 될 것이라고 생각했다. 

 

이때 200ms 라는 기준을 잡는 것도 어려웠다. 서버 응답 시간의 바람직한 기준을 검색해 보았고 명확한 답은 당연히 없겠지 내가 찾은 레퍼런스에선 200~300ms 사이를 튜닝 포인트로 잡고 있는 경우가 많아 당장은 이를 따르기로 했다. 특히 구글의 server speed insight docs에서 200ms 이하로 서버 응답 속도를 개선하라는 말이 가장 힘 있는 참고 자료가 되었다.

 

 

이제 수집한 메트릭을 살펴보자. 기본 40%대의 메모리 사용률에서 55% 까지 메모리 사용이 올라갔다. 시작점이 40%이기에 메모리가 너무 적었나 생각했는데 예상했던 것보다는 적게 상승했다.

 

 

이번 테스트 내에서는 1분 동안 최대 50의 Minor GC가 실행되었다. OG도 그 기간 살짝 올라 committed 근접한 것을 볼 수 있었다. 

 

특이한 게 그 기간 Survivor space가 오르며 가득 찰 때 즈음 이들이 정리되면서, 그리고 OG로 넘어가면서 사용률이 감소하고 OG가 증가할 줄 알았는데 survivor space는 증가 없이 감소한 것을 볼 수 있었다. 최초의 몇 개가 Survivor에 남아있다가 Eden space가 작아 계속 Minor GC가 일어나고, 앞선 Survivor의 최초 몇 객체가 OG로 넘어간 게 아닌가 생각한다. 동시에 Member 서버에선 새로운 객체 생성이 잦고, 더 이상 survivor로 넘길 게 없는 임시 리소스와 장기 리소스가 명확한 서버라고 추리해 본다.

 

 

내가 찾은 튜닝거리들 /  Picup의 설정들과 근거 소개하기

모니터링 메트릭을 조사하고, 테스트하며 대시보드를 정리하면서 찾은 주요 튜닝거리들과 옵션을 정리한다. 

 

1. JVM 메모리 사이즈 

 

아래의 JVM 옵션을 사용해서 Heap 사이즈의 최대, 최소 값, Host ram으로부터 할당할 Max heap 사이즈 비율을 정의할 수 있다. MaxRAMPercentage의 default 값은 25로, 다른 옵션을 주지 않았다면 JVM 의 기본 max heap 은 host ram의 1/4이다. 

 

Xms - Heap 사이즈의 최솟값
Xmx - Heap 사이즈의 최댓값
XX:MaxRAMPercentage - Host ram에서 Max ram의 차지할 비율

 

Picup 프로젝트는 쿠버네티스에 Pod(컨테이너)으로 띄워져 있다. 앞선 테스트 결과 Max heap 사이즈 124 MiB는 충분하다고 판단했는데 kubernetes에서 해당 컨테이너에 할당하는 메모리가 512 MiB로 Max heap에 비해 너무 많다고 판단하여 컨테이너 메모리를 320 MiB로 낮추고 MaxRAMPercentage를 40%로 늘렸다.

 

2. YG, OG space /  Eden, Survivor space

 

YG 영역을 늘리면 Minor GC의 호출 빈도가 줄고 그 duration은 늘며 YG에서 OG로 이동하는데 걸러지는 데이터가 많아진다. 같은 총량이라면 반대로 OG 영역은 줄어들기에 Major GC의 호출 빈도가 늘어나나 duration은 줄어든다.

 

앞선 테스트처럼 따라서 장기 리소스보다 임시 리소스가 더 많다고 판단되는 경우 YG를 늘려 Minor GC의 빈도를 줄이고, OG의 영역으로 넘어가는 데이터를 줄여 Major GC의 호출 빈도는 크게 차이가 없도록 하는 것이 유리할 것이다.

 

XX:NewSize - YG 사이즈
XX:MaxNewSize - YG의 최대 사이즈
XX:NewRatio - OG : YG 비율

 

JVM 옵션에서 위 속성들을 이용하면 YG와 OG의 비율을 수정할 수 있다. NewRatio의 기본 값은 2로 특별한 설정이 없다면 OG를 YG 값의 2배로 설정된다.

 

앞선 테스트에서 테스트 서버는 임시 데이터가 많다고 판단했기에 YG의 비율을 늘려 Minor GC의 호출 수를 줄이고, OG로 넘어가기 전 더 많은 검증을 거치고 싶었다. NewRatio 값을 1로 수정하여 YG:OG를 1:2에서 -> 1:1로 수정하였는데 너무 큰 변화인지, 최소 비율이 1:2인지, 변경 후 예상하지 못한 GC duration과 호출 횟수를 만나고 1:2로 다시 수정했다. 

 

XX:SurvivorRatio - Eden : Survivor 비율

 

마찬가지로 'XX:SurvivorRatio' 옵션을 이용하면 Eden과 Survivor 영역의 비율을 수정할 수 있다. 기본 값은 8이다. 앞선 그라파나의 표에서 Eden 영역이 Survivor 영역에 8배가 되는 것으로 확인할 수 있다. 

 

3. JIB에서 JVM 옵션

 

Container image를 JIB로 빌드하는 경우 아래처럼 container.jvmFlags를 입력하는 것으로 JVM 옵션을 줄 수 있다. 아래 옵션은 각각 OG:YG 비율, Eden space : Survivor space 비율, Host ram에서 max heap 이 차지하는 비율을 설정할 수 있는 옵션을 기본값으로 대입해 둔 예시이다.

 

jib {
    from.image = "adoptopenjdk/openjdk11:jre-11.0.10_9-alpine"
    to.image = "ghcr.io/ecsimsw/picup/${project.name}"
    to.tags = ["latest"]
    container {
        jvmFlags = ['-XX:NewRatio=2', '-XX:SurvivorRatio=8', '-XX:MaxRAMPercentage=25']
    }
}

 

4. Kubernetes container resource 

 

Kubernetes의 Request는 Pod의 노드 스케줄링에, Limit은 Pod가 사용할 수 있는 자원의 범위를 제한한다. Request 양에 만족하는 Node가 없다면 Pod는 프로비저닝 되지 못한다.

 

Limit 양에 대한 고민이 더 어렵다. Pod가 Cpu Limit 사용량을 초과하게 되면 스로틀링이 일어나게 된다. 스로틀링이란 Cpu가 과열, 과사용되는 상황에서 자체적으로 쿨럭을 낮춰 손상을 방지하는 것을 말한다. 그리고 이 상태가 지속될 경우 우선순위에 따라 Pod를 퇴거시키게 된다. 당장은 애플리케이션 동작이 느려지게 된다 정도로 부작용을 이해한다.

 

반면 Memory는 과사용될 경우 스로틀링 없이 퇴거된다. 이때 Pod 퇴거 우선순위는 request, limit 지정량과 메모리 사용량에 따라 달라진다. 조금 더 자세하게는 QoS(Quality Of Service) 클래스를 키워드로 검색해 보길 바란다.

 

Picup의 경우에는 cpu의 request / limit 설정을 안 하고, memory 의 request / limit 을 동일하게 하였다. CPU request를 설정했다가 Pending에 걸리는 상황이 생기는 꼴을 피하려 한다. 또 괜한 Cpu limit으로 스로틀링에 걸리는 것보다 Cpu 지표 모니터링과 알람을 꼼꼼히 만들어 두면서 관리하면서, HPA를 설정하는 것으로 CPU 사용양에 따라 Pod를 오토스케일링하여 일시적이었던 Cpu 과사용에서 벗어나면 어떨지 생각한다. 그리고 조금 더 솔직하게는 지금 서버로 사용하고 있는 노드의 Cpu가 너무 넘친다. 원하는 부하테스트를 충분히 돌린 상황에서 전체 cpu 사용률이 15%를 안 넘는 것도 Cpu limit을 해제한 큰 이유가 되었다.

 

반면 Memory의 경우에는 컨테이너 당 사용하고자 하는 사용량을 명확히 할 생각으로 request와 limit 양을 지정하고 이를 동일하게 하였다. 그 원하는 사용량에 부합하지 않으면 아예 스케줄링을 하지 않고자 request를 지정하고 그 양에서 벗어나면 overcommited 로 표시할 생각이다. 

 

limit를 request와 동일하게 주는 이유는 이전에 request와 limit를 다르게 설정하여 운영한 적이 있었는데 request가 낮기에 Pod 프로비저닝은 문제가 없었지만 각 서비스들의 메모리 사용량이 조금만 높아지면 전체 메모리 사용량이 노드 limit 보다 커져 Pod 관리에 더 불편했던 경험이 있었다. 그래서 그냥 처음부터 각 컨테이너별 메모리 사용량을 정하고 그만큼을 확보량과 동시에 사용할 수 있는 양으로 정의하고 싶었다.

Comments