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 사용량의 증가에 비해 증가량이 적은 것 또한 확인할 수 있습니다. 시스템 자원을 모니터링하고 분석하는 내용은 이론적인 지식과 경험적 바탕이 필요한 부분이라 생각되어 추후에 더 알아보도록 하겠습니다.
여기까지 쿠버네티스 클러스터 환경에서 웹 애플리케이션을 배포하고 프로메테우스와 그라파나를 이용하여 모니터링하는 방법에 대해 알아보았습니다. 긴 글 읽어주셔서 끝까지 감사합니다. 🙇♂️
'Kubernetes' 카테고리의 다른 글
[Kubernetes] Nginx Ingress Controller 에서 Path 라우팅 적용하기 (0) | 2024.06.22 |
---|---|
[Kubernetes] 쿠버네티스 서비스 알아보기 [3/4] (0) | 2024.05.11 |
[Kubernetes] 쿠버네티스 클러스터 구축해보기 [2/4] (0) | 2024.04.27 |