Kubernetes

[Kubernetes] 쿠버네티스 클러스터 모니터링해보기 [4/4]

_su_min 2024. 5. 25. 20:52

1. 이 글을 통해 알아볼 내용

이번 글에서는 쿠버네티스 클러스터의 파드 정보가 프로메테우스를 통해 정상적으로 수집되는지와 그라파나를 통해 모니터링 제대로 되는지 확인해보도록 하겠습니다.

 

2. kube-prometheus-stack 설치

쿠버네티스에 kube-prometheus-stack(프로메테우스 + 그라파나) 설치시 helm을 사용하기 때문에 helm을 먼저 설치해주겠습니다. helm 공식 사이트 를 참고하여 helm을 설치해줍니다.

(sudo yum install -y wget)
wget https://get.helm.sh/helm-v3.15.1-linux-amd64.tar.gz
tar xvfz helm-v3.15.1-linux-amd64.tar.gz
sudo mv linux-amd64/helm /usr/bin/


prometheus 설치를 위해 helm repo에 prometheus-community 주소를 추가한 후 repo update 명령문을 실행해줍니다.

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

 
helm pull 명령어로 kube-prometheus-stack 을 다운로드하고 압축을 풀어줍니다.

helm pull prometheus-community/kube-prometheus-stack
tar xfz kube-prometheus-stack-58.7.2.tgz

 
압축해제 후 kube-prometheus-stack 폴더로 이동하여 values.yaml 파일에서 그라파나 관리자 패스워드를 바꿔줍니다.

cd kube-prometheus-stack
vi values.yaml

### 패스워드 변경
adminPassword: minimal123!!@@

 

다음으로 kube-prometheus-stack 에서 사용할 네임스페이스를 생성해줍니다. 네임스페이스 이름은 monitoring 입니다.

kubectl create namespace monitoring

 
prometheus 를 설치해줍니다. -n 옵션에는 네임스페이스인 monitoring 을 -f 옵션에는 values.yaml 파일을 명시해줍니다.

helm install prometheus . -n monitoring -f values.yaml

helm list -n monitoring

kubectl get pod -n monitoring

 
monitoring 네임스페이스의 서비스를 확인해봅니다.

kubectl get svc -n monitoring

 
외부 클라이언트에서 프로메테우스와 그라파나에 접근해서 웹 브라우저로 사이트를 확인을 해야 하는데 prometheus-kube-prometheus-prometheus 와 prometheus-grafana 의 서비스 타입이 ClusterIP입니다. 외부 접근을 허용하기 위해 서비스 타입을 nodePort로 수정해주겠습니다.

kubectl edit service -n monitoring prometheus-grafana

수정 전
수정 후

 

kubectl edit service -n monitoring prometheus-kube-prometheus-prometheus

수정 전
수정 후

 
이제 브라우저에서 웹페이지로 접속해보겠습니다. 그런데 접속에 실패합니다. 만약 접속에 실패한다면 방화벽 포트 개방이 되어있지 않아서 그런 것입니다. VPC 네트워크 - 방화벽 화면으로 이동해서 프로메테우스와 그라파나의 포트를 허용하는 규칙을 만들어주겠습니다. 방화벽 규칙 만들기를 눌러줍니다.

 
k8s-master 인스턴스 수정 페이지로 이동해서 위에서 만든 방화벽 규칙을 적용해줍니다.

 

3. 프로메테우스와 그라파나 동작 확인

이제 프로메테우스와 그라파나 페이지에 각각 접속해보겠습니다.

프로메테우스 사이트 접속

 

그라파나 사이트 접속

 
grafana 사이트에 접속하기 위해서는 로그인이 필요합니다. username 의 기본값은 admin 이고, password 는 앞서 values.yaml 에서 설정해주었던 minimal123!!@@ 입니다. grafana 사이트 접속 후 grafana connections - datasource 로 가보면 prometheus 가 default 로 설정되어 있는 것을 확인할 수 있습니다. 

 
그라파나 대시보드에서 쿠버네티스 클러스터를 모니터링하기 위해 그라파나 템플릿 사이트로 가서 Kubernetes / Views / K8s Cluster 의 ID 를 복사해서 템플릿을 Import 해줍니다.

 

 

 
대시보드 템플릿 Import 후 추가된 대시보드를 확인해보면 쿠버네티스 클러스터 모니터링이 정상적으로 이루어지는 것을 확인할 수 있습니다.

 

4. 금 채굴 애플리케이션 디플로이먼트 배포

쿠버네티스 클러스터 금 채굴 애플리케이션 테스트를 위해서 gold-mining-api 이미지를 도커 허브에 배포해두었습니다. 금 채굴 API 소스는 여기에서 확인하실 수 있습니다. 도커 허브에 배포된 도커 이미지를 이용해서 gold-mining-api 디플로이먼트를 배포해보겠습니다.

kubectl create deployment gold-mining-api --image=minimalcode/gold-mining-api
kubectl logs gold-mining-api-85c5f5bbdb-9rcwb

파드의 상태를 확인해보니 Error 로 찍혀있습니다. 그래서 파드의 로그를 확인해보았습니다. 로그에는 아래처럼 'processorMetrics' Bean을 생성할 수 없다는 구문과 함께 컨테이너 실행에 실패한 내용이 찍혀있었습니다.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'processorMetrics' defined in class path resource [org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.class]: Failed to instantiate [io.micrometer.core.instrument.binder.system.ProcessorMetrics]: Factory method 'processorMetrics' threw exception with message: java.lang.reflect.InvocationTargetException
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:485) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1335) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1165) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:959) ~[spring-context-6.1.4.jar!/:6.1.4]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.4.jar!/:6.1.4]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.3.jar!/:3.2.3]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.3.jar!/:3.2.3]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.3.jar!/:3.2.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.3.jar!/:3.2.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.3.jar!/:3.2.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.3.jar!/:3.2.3]
        at org.example.Main.main(Main.java:9) ~[!/:1.0.0]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:91) ~[api.jar:1.0.0]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:53) ~[api.jar:1.0.0]
        at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:58) ~[api.jar:1.0.0]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.micrometer.core.instrument.binder.system.ProcessorMetrics]: Factory method 'processorMetrics' threw exception with message: java.lang.reflect.InvocationTargetException
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:177) ~[spring-beans-6.1.4.jar!/:6.1.4]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:647) ~[spring-beans-6.1.4.jar!/:6.1.4]
        ... 26 common frames omitted
Caused by: java.lang.InternalError: java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.platform.Metrics.systemMetrics(Metrics.java:65) ~[na:na]
        at java.base/jdk.internal.platform.Container.metrics(Container.java:43) ~[na:na]
        at jdk.management/com.sun.management.internal.OperatingSystemImpl.<init>(OperatingSystemImpl.java:48) ~[na:na]
        at jdk.management/com.sun.management.internal.PlatformMBeanProviderImpl.getOperatingSystemMXBean(PlatformMBeanProviderImpl.java:279) ~[na:na]
        at jdk.management/com.sun.management.internal.PlatformMBeanProviderImpl$3.nameToMBeanMap(PlatformMBeanProviderImpl.java:198) ~[na:na]
        at java.management/sun.management.spi.PlatformMBeanProvider$PlatformComponent.getMBeans(PlatformMBeanProvider.java:195) ~[na:na]
        at java.management/java.lang.management.ManagementFactory.getPlatformMXBean(ManagementFactory.java:686) ~[na:na]
        at java.management/java.lang.management.ManagementFactory.getOperatingSystemMXBean(ManagementFactory.java:388) ~[na:na]
        at io.micrometer.core.instrument.binder.system.ProcessorMetrics.<init>(ProcessorMetrics.java:81) ~[micrometer-core-1.12.3.jar!/:1.12.3]
        at io.micrometer.core.instrument.binder.system.ProcessorMetrics.<init>(ProcessorMetrics.java:76) ~[micrometer-core-1.12.3.jar!/:1.12.3]
        at org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration.processorMetrics(SystemMetricsAutoConfiguration.java:59) ~[spring-boot-actuator-autoconfigure-3.2.3.jar!/:3.2.3]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:140) ~[spring-beans-6.1.4.jar!/:6.1.4]
        ... 27 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at java.base/jdk.internal.platform.Metrics.systemMetrics(Metrics.java:61) ~[na:na]
        ... 42 common frames omitted
Caused by: java.lang.NullPointerException: null
        at java.base/java.util.Objects.requireNonNull(Objects.java:208) ~[na:na]
        at java.base/sun.nio.fs.UnixFileSystem.getPath(UnixFileSystem.java:260) ~[na:na]
        at java.base/java.nio.file.Path.of(Path.java:147) ~[na:na]
        at java.base/java.nio.file.Paths.get(Paths.java:69) ~[na:na]
        at java.base/jdk.internal.platform.CgroupUtil.lambda$readStringValue$1(CgroupUtil.java:66) ~[na:na]
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:554) ~[na:na]
        at java.base/jdk.internal.platform.CgroupUtil.readStringValue(CgroupUtil.java:68) ~[na:na]
        at java.base/jdk.internal.platform.CgroupSubsystemController.getStringValue(CgroupSubsystemController.java:65) ~[na:na]
        at java.base/jdk.internal.platform.CgroupSubsystemController.getLongValue(CgroupSubsystemController.java:124) ~[na:na]
        at java.base/jdk.internal.platform.cgroupv1.CgroupV1Subsystem.getLongValue(CgroupV1Subsystem.java:175) ~[na:na]
        at java.base/jdk.internal.platform.cgroupv1.CgroupV1Subsystem.getHierarchical(CgroupV1Subsystem.java:149) ~[na:na]
        at java.base/jdk.internal.platform.cgroupv1.CgroupV1Subsystem.initSubSystem(CgroupV1Subsystem.java:84) ~[na:na]
        at java.base/jdk.internal.platform.cgroupv1.CgroupV1Subsystem.getInstance(CgroupV1Subsystem.java:60) ~[na:na]
        at java.base/jdk.internal.platform.CgroupSubsystemFactory.create(CgroupSubsystemFactory.java:116) ~[na:na]
        at java.base/jdk.internal.platform.CgroupMetrics.getInstance(CgroupMetrics.java:167) ~[na:na]
        ... 47 common frames omitted

 

에러를 해결해보기 위해서 구글링도 해보고 ChatGPT에게도 물어봤지만 딱히 방법을 찾지 못해서 Gradle에서 spring-boot-starter-actuator 라이브러리를 제외했습니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'

    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

//    implementation 'org.springframework.boot:spring-boot-starter-actuator'
//    implementation 'io.micrometer:micrometer-registry-prometheus'
}

 

다시 gold-mining-api-remove-actuator 이미지를 도커 허브에 배포하고 디플로이먼트를 생성했습니다. 이번에는 정상적으로 컨테이너가 실행되었습니다.

kubectl create deployment gold-mining-api --image=minimalcode/gold-mining-api-remove-actuator

 

파드가 정상적으로 실행된 것을 확인했으니 파드 개수를 3개로 늘리도록 하겠습니다.

kubectl scale deployment gold-mining-api --replicas=3

 

5. gold-mining-api 디플로이먼트를 인그레스 서비스에 추가

외부 클라이언트의 요청을 받아서 부하 테스트를 진행하기 위해서는 gold-mining-api 디플로이먼트를 인그레스 서비스로 외부에 노출할 필요가 있습니다. 현재 설치된 인그레스 서비스에 gold-mining-api 디플로이먼트를 위한 path 를 추가해줍니다.

kubectl edit ingress

인그레스로부터 라우팅된 요청을 받기 위해서 gold-mining-api 를 위한 서비스도 만들어줍니다. 인그레스 오브젝트에 명시한 서비스명과 동일하게 gold-mining-svc 이름으로 서비스를 만듭니다.

kubectl expose deployment gold-mining-api --name=gold-mining-svc --port=80,443

정상적으로 인그레스 서비스가 수정되었는지 포스트맨을 통해서 확인해보겠습니다. 쿠버네티스 클러스터 워커 노드중 1번 워커 노드의 공인 IP 주소(34.47.76.237) 로 금 채굴 데이터 저장 API 요청을 보내보겠습니다.

API 호출 결과 200 OK 정상 응답을 확인할 수 있습니다. 

 

6. 부하 테스트 모니터링

부하 테스트를 진행하기 위해서 금 채굴 클라이언트의 API 호출 주소를 쿠버네티스 클러스터 워커 노드중 1번 워커 노드의 공인 IP 주소(34.47.76.237) 로 변경해주겠습니다.

부하 테스트는 총 두 번 진행할 계획입니다. 첫 번째 테스트와 두 번째 테스트에서 각각 실행되는 스레드 수와 API 요청 수는 아래와 같습니다.

  MineralMint NuggetVentures OreProsEnterprises TreasureVein Threads Requests
1st 50 Th, 1200 Rq 40 Th, 1600 Rq 70 Th, 1100 Rq 120 Th, 2300 Rq 280개 6200개
2nd 50 Th, 12000 Rq 40 Th, 16000 Rq 70 Th, 11000 Rq 120 Th, 23000 Rq 280개 2000개

 

로컬 PC에서 금 채굴 클라이언트를 실행하여 쿠버네티스 클러스터의 1번 워커노드로 금 채굴 데이터 저장 API 를 호출하는 부하 테스트를 시작해보겠습니다. 클라이언트 소스는 여기에서 확인하실 수 있습니다.

부하테스트에 따른 그라파나의 화면은 아래와 같습니다.

부하테스트 모니터링 상단 부분
부하테스트 모니터링 하단 부분

부하 테스트가 진행되는 동안 각 워커노드의 네트워크 I/O량과 CPU 사용량이 증가한 것을 확인할 수 있습니다. 반면, 메모리의 경우에는 약간의 증가는 있었지만 네트워크 I/O 와 CPU 사용량의 증가에 비해 증가량이 적은 것 또한 확인할 수 있습니다. 시스템 자원을 모니터링하고 분석하는 내용은 이론적인 지식과 경험적 바탕이 필요한 부분이라 생각되어 추후에 더 알아보도록 하겠습니다.

여기까지 쿠버네티스 클러스터 환경에서 웹 애플리케이션을 배포하고 프로메테우스와 그라파나를 이용하여 모니터링하는 방법에 대해 알아보았습니다. 긴 글 읽어주셔서 끝까지 감사합니다. 🙇‍♂️