Monitoring

Kubernetes 에서 Monitoring System(Prometheus) 운영경험기

토마스.dev 2019. 11. 27. 08:43

필자는 2017년 초부터 현재까지 약 2년 6개월에 가까운 기간동안 kubernetes 기반 모니터링 시스템을 운영하였다.(실제 Prometheus로 운영한 것은 2019년 2월부터)

기간으로만 따지면 매우 긴 기간이지만 그에 비해 경험이 충분하지 않다고 생각한다.(전체 시스템 Scalability에 한계가 있음)

 

하지만 k8s에서의 모니터링 시스템 운영할 예정이거나 운영하고 계시는 분들에게 미약하게나마 도움이 되고자 사용 경험담(실수, 팁 등)을 공유하고자 한다.

 

전체를 크게 3개 부분으로 나누었다.

1. Kubernetes 사용 관련

2. Prometheus 설치/운영 관련

3. Prometheus Query, Grafana 사용 관련

 

각각의 세부 내용은 중요하다고 생각하는 부분을 흐름이나 순서없이 나열하였으니 양해바란다.


Kubernetes 사용

Statefulset and Node Unknown(NodeLost)

Prometheus를 Statefulset으로 배포할때 주의해야할 부분이 있다. statefulset 은 deployment와 다르게 Node가 down상태가 됐을때 self-healing이 작동하지 않는다. 이는 statefulset 의 특성에서 찾아볼 수가 있는데, 말그대로 pod들이 stateful하다고 가정하기 때문에 만약 Node가 임시적으로 down이 된거라면 up되었을때 다시 pod은 정상적으로 돌아오게 된다. 헌데 deployment처럼 Node down을 감지하자마자 바로 pod을 다른 노드에 생성했다면 Split brain 이슈 등이 발생될 수 있다.

 그래서 prometheus를 statefulset으로 2개 이상 배포해 HA를 하고 싶어도 1개가 down 상태에 빠지면 metric이 down 기간동안 수집이 안되서 이상적인 HA 구성이 되지 않는다. 이를 보완하는 방법은 아래 Thanos 라는 opensource를 이용하면 어느정도 해결할 수 있다.

 

Stateless with Self-Healing

HA라는 것이 꼭 2개 이상을 띄우라는 얘기는 아니다. Self-healing도 하나의 HA대안일 수 있다. 클러스터의 리소스사용을 최소화 하고 싶고, 잠깐의 Downtime 정도는 괜찮다면 Stateless 타입의 Workload는 1개만 띄우고 k8s 의 self-healing에 맡기는 것도 하나의 HA방법이라 생각한다. 대표적으로 exporter 들이 해당될 수 있겠다.

 

Helm Chart Configuration 변경에 따른 Workload 재시작

Helm 을 사용하다보면, 어떤 특정 Configmap을 업데이트 했을때 이와 관련된 Workload(deployment, pod 등등)도 재시작을 하고 싶은 경우가 있다.

Helm에서는 어떤 workload와 Configmap이 연관이 되어 있는지 모르기 때문에 Configmap만 업데이트 될뿐 workload는 재시작시키지 않는다. 이를 해결하기 위해 다음과 같은 팁이 존재한다. 

annotations:
   checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

Configmap 기준으로 체크섬을 계산해서 workload annotation에 넣는 것이다. 

Configmap이 업데이트되면 이 체크섬이 변경될 것이며, 이것이 workload template에도 반영되기 때문에 관련 Workload가 재시작 한다.

 

CPU Request는 낮게, Limit은 높게

다른 앱에도 해당될수 있는 이야기겠지만 모니터링 특성상 쿼리 요청이 고르게 분포되지 않고, 스크랩도 Interval이 있기 때문에 CPU 사용률이 고르지 않다. 그렇기 때문에 Request와 Limit를 반드시 설정해야하는 정책이라면 Request는 낮게 혹은 평균 측정값 기준으로(배치를 빠르게)하고 Limit을 높게(측정구간 내 Max * 1.5배) 주는 것이 좋다(물론 전체 클러스터 CPU 사용률이 얼마 안된다는 것을 가정하는 것으로, 높으면 서로 영향을 끼치게 된다)

 

Pod은 그대로 두고 Container만 재실행

Pod은 재실행 시키면 이름과 IP가 변경되기 때문에 내부의 Container만 재실행시키고 싶은 경우 다음의 명령어들을 활용하면 된다.

kubectl exec -it [POD_NAME] -c [CONTAINER_NAME] -- /bin/sh -c "kill 1"
kubectl exec -it [POD_NAME] -c [CONTAINER_NAME] -- /sbin/killall5

(https://stackoverflow.com/questions/46123457/restart-container-within-pod)

 

Statefulset Update with Partition Strategy

Cluster 구성이 되어있는 Statefulset을 업데이트 할 때 데이터 무결성을 주의해야하는 경우가 있다.

업데이트를 하면 역순으로 하나씩 Pod이 내려갔다가 올라가는데, 데이터 동기화 등의 프로세스가 존재하고 이 시간이 길다면, Pod 재시작과 더불어 다음 pod을 바로 종료시키는 것은 문제가 될 수 있다.

이럴 때는 Partition 전략을 사용하여 수동적으로 Pod 재시작을 조종할 수 있다.

https://kubernetes.io/ko/docs/tutorials/stateful-application/basic-stateful-set/#staging-an-update

 

백업은 Cronjob

필자는 대부분의 Daily 백업에 Cronjob을 이용하였다. 백업을 하고나서 주의해야하는 부분은 백업데이터도 보관정책이 있기 때문에 백업전 expired된 데이터를 삭제해주는 것이 필요하다.

필자의 경우 Grafana mariadb를 mysqldump를 이용해서 s3에 백업을 했고, prometheus의 경우 thanos를 이용하여 s3에 자동으로 백업이 되는 구조라 따로 하지 않았다.

 

문제는 Network Network Network!

운영하면서 발생하는 대부분의 이슈는 90%가 네트웍이슈라 할수 있다. Prometheus 특성상(Pull) 방화벽에도 민감하고, scrape failed에 대한 fail over가 없기 때문에 k8s 네트웍 문제에 민감하다. 특히나 k8s이 openstack에 올라가 있다고 하면 더더욱 심각한 이슈들이 발생할 수 있는데, 여기서 문제는 openstack 단까지 분석하는게 어렵다는 것이다(특히나 팀이 나뉘어져 있고 배타적이라면 이슈 밝혀내는데도 상당한 삽질을 요한다). 그렇기 때문에 k8s을 사용할 때는 기본적으로 네트웍 레벨까지도 보는 실력이 있는 것이 좋다.

 

Affinity with Physical Machine(PM) Label

HA를 고려하여 보통 VM 혹은 Zone 기준으로 Affinity를 적용하지만, 만약 1개의 Zone으로 운영하는 경우 가능하면 PM기준으로도 Affinity를 적용할 수 있으면 좋다. PM이 다운되면 그 여파가 수개의 VM에 영향이 있으므로 VM기준으로만 HA를 해서는 안심할 수 없다.

 

External Name of Service

k8s service에는 ExternalName 타입이 존재한다. 잘 안쓰일 수도 있지만, 내부에서 외부에 주소가 바뀔수있는 서비스를 연결할 때 써먹으면 유용하다. 예를 들어, 특정 외부 IP주소를 내부 서비스(앱)들이 직접 하드코딩해서 가리키기 보다는 ExternalName을 통해 내부주소로 변경하면, 외부 주소 변경시에 각 서비스들의 설정을 변경하지 않고 간단하게 해당 ExternalName 매핑만 변경해주면 된다. 다만 DNS에서 해당 기능을 지원해야 하는데, 낮은 CoreDNS 버전에서는 해당 기능을 지원하지 않아 동작하지 않는 경우도 있다.

 


Prometheus 설치/운영

gzip federation

Federation의 Response는 순수한 텍스트이다. 그렇기 때문에 Federation시 데이터 트랙픽이 높을 수 있다. 거기에 Network bandwidth까지 낮다면 자칫 timeout이 발생할 수도 있다. 이럴 경우 gzip을 이용해서 federation을 하면 트래픽을 많이 낮출 수 있다. 수집을 당하는 prometheus에 gzip으로 Content-type을 주어 요청하면 gzip으로 리턴한다. 문제는 받는쪽에서는 이것을 decompress 하는 기능이 없기 때문에 사이에 nginx 같은 proxy를 두어야 한다.

 

nginx 주요 설정 예제

location ~ ^/target/(.*)$ {

  rewrite ^/(.+)/(.*) /$2 break;

  proxy_pass https://xxxx.prometheus.com

  proxy_http_version 1.1;

  proxy_set_header Accept-Encoding "gzip";

  gunzip on;

}

proxy_pass에 수집당하는 prometheus 주소를 명시하고, 수집하는 prometheus에서 nginx 주소로 federation을 걸면 된다.

물론 nginx의 위치는 수집하는 prometheus 쪽에 있어야 한다.

 

- 업데이트: 당황스런 부분인데.. 사실 prometheus는 기본적으로 gzip으로 동작하고 있다. 아래 파일의 scrape 함수를 참조하면 알수 있다.

https://github.com/prometheus/prometheus/blob/master/scrape/scrape.go

 

Thanos - long-term storage, HA and deduplication

https://github.com/thanos-io/thanos

Prometheus를 사용하다보면 설계적으로 보완이 필요한 부분이 있는데, 바로 HA와 long-term storage 가 약하다는 것이다.

Elasticsearch와 같이 Clustering이 안되기 때문에 2개 이상의 Prometheus를 각각 배포해야 하는데, 이마저도 1차원적인 방식으로 각각의 Prometheus가 별개의 metric을 모으기 때문에 실질적으로 다른 데이터를 가지고 있게 된다. (그래서 여러개를 하나의 서비스로 묶어 Grafana에서 보게되면 Refresh할때마다 다른 그래프를 보이게 된다). 

또한 이 때문에 TSDB를 저장하는 스토리지 구조가 확장성이 떨어진다(없다고 보는게 맞음. TSDB의 구조에 문제가 있다는 것이 아니고, Prometheus 자체가 single-node형태만 지원하기 때문). Remote Storage를 가이드 하고 있지만 경험상 또 하나의 DB를 두고 운영하기 때문에 운영 오버헤드가 발생할 수 있다.

 

위 두개의 문제와 더불어 배포구조상 멀티클러스터 구조를 커버하기가 어렵다. 알다시피 Prometheus는 손쉬운 discovery를 고려하여 클러스터 내부에 설치하게 된다. 멀티클러스터인 경우 클러스터 마다 설치되어 있기 때문에 vector matching도 어렵다.

 

이를 해결하는 방식으로 Thanos 라는 오픈소스가 존재한다. Thanos는 간단하게 말해서 여러 Prometheus를 묶어 하나의 싱글뷰 형태의 쿼리를 처리해주며(즉, 위에서 언급한 Refresh에 의한 그래프 변화가 없다. 내부적으로 그래프를 하나로 처리하기 때문) TSDB를 관리가 거의 필요없는 S3에 저장한다. 

 

Grafana 관점에서 보면 달라진 것이 없다. 기존 Prometheus 를 datasource로 하던 것을 주소만 Querier로만 바꿔주면 된다. PromQL을 그대로 사용하면 된다. Sidecar 와 Store GW를 통해 연결된 모든 Prometheus와 S3에 저장된 오래된 데이터까지 한번에 볼수 있다. 물론 Thanos 도 단점이 존재한다. 첫번째로는 Prometheus에 Sidecar를 붙여야하며(이를 해결하기 위해 Receiver 컴포넌트가 새로 개발되고 있긴하지만, 완전히 커버되지는 않는다), 두번째로 쿼리 속도가 최소 2배~최대 10배는 느릴수 있다는 것이다. 마지막으로 추가적인 리소스(CPU/Memory)가 필요하다는 것이다. 필자가 생각하기에는 단점보다 장점이 더 크다고 생각이 든다. 그리고 Thanos 오픈소스 개발속도가 빠른편이라 충분히 가능성이 있다고 본다.

 

CRD for Configuration and Prometheus-operator

 

모니터링할 대상이 많아질 수록 Prometheus의 Configuration 은 길어진다. file_sd_configs 을 이용하면 나름 정리를 할 수도 있겠지만, UI Console 화면에서 확인할 수가 없다. 또한 Relabel 등을 적용하다보면 길어질수 있고, 수정하다 다른 수집설정에 오류를 발생시킬 수 있다. 운영측면에서 Prometheus Configuration을 Configmap으로 그대로 사용하기에 오버헤드가 크다. 

 

이러한 운영부담을 어느정도 해결해주는 Operator Framework(설계패턴의 한 종류라고 볼 수 있다)이 존재한다. 자세한 내용은 기존에 적은 글이 있어 링크로 대신한다.

 

https://devthomas.tistory.com/6

https://devthomas.tistory.com/8

 

이 Framework 을 사용해서 모두 해결되면 좋겠지만, 몇가지 제약사항이 존재한다(나도 왜 이런 제약사항이 존재하는지 모르겠음. 천천히 컨트리뷰션을 해보려고 함)

먼저, static scrape을 사용하려면 별도 secret 으로 분리해야한다.(additional scrape config)

또한 Pod 대상으로만 모니터링 대상을 설정할 수 있다. Exporter를 배포할때 HA로 하는 경우도 있는데(사실 필자는 왜 굳이 stateless한걸 이렇게 까지 해야하는지 모르겠음) 이럴 경우에는 Pod을 바라보는게 아닌 service 만 바라보고 해야할 필요도 있다. 필자는 위 두가지 문제점을 다음과 같은 방식으로 해결하였다.

--> static scape파일들을 별도 Configmap 으로 만들고 additional scrape config 옵션을 file_sd_config으로 한다.

--> Annotation 기반 자동 수집방식을 추가하여 service 만 바라보게 한다.

 

여기에 한가지 더 문제점을 말하면 Operator가 Prometheus라는 CRD를 통해 Prometheus pod과 셋팅을 관리하기 때문에 최신Prometheus에서 신규 셋팅항목이 나오면 이를 바로 반영해 적용하기 어렵다.

 

Auto(Annotation) scrape - service, pod

수집대상을 운영자가 직접 관리하면 좋겠지만, 사용자들이 늘어나고 추가적인 수집요청이 늘어날수록 운영부담은 커질 수 밖에 없다. 이를 해결하려면 수집처리를 자동화 하는 것도 하나의 방법이다. 자동화하는 방법 중 많이들 알고 있는 Annotation기반 수집이 바로 그것이다. 

 

설정 예: 서비스 자체를 바라보는 방식과 Pod을 바라보는 방식

- job_name: 'kubernetes-pods'
  scrape_timeout: 30s
  kubernetes_sd_configs:
  - role: pod
  relabel_configs:
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
    action: keep
    regex: true
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scraped_by]
    action: keep
    regex: monitoring
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
    action: replace
    target_label: __metrics_path__
    regex: (.+)
  - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
    action: replace
    regex: ([^:]+)(?::\d+)?;(\d+)
    replacement: $1:$2
    target_label: __address__
  - action: labelmap
    regex: __meta_kubernetes_pod_label_(.+)
  - source_labels: [__meta_kubernetes_namespace]
    action: replace
    target_label: namespace
  - source_labels: [__meta_kubernetes_pod_name]
    action: replace
    target_label: pod_name


- job_name: 'kubernetes-services'
  scrape_timeout: 30s
  kubernetes_sd_configs:
  - role: service
  relabel_configs:
  - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
    action: keep
    regex: true
  - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scraped_by]
    action: keep
    regex: monitoring
  - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
    action: replace
    target_label: __metrics_path__
    regex: (.+)
  - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
    action: replace
    regex: ([^:]+)(?::\d+)?;(\d+)
    replacement: $1:$2
    target_label: __address__
  - action: labelmap
    regex: __meta_kubernetes_service_label_(.+)
  - source_labels: [__meta_kubernetes_namespace]
    action: replace
    target_label: namespace
  - source_labels: [__meta_kubernetes_name]
    action: replace
    target_label: service_name

 

물론 이렇게 하면 어떤 metric이 모이는지 운영자가 투명하게 확인하기가 어려우며, Metric 충돌이 생길수도 있다. 또한 수집 Sample한도를 정해놓지 않으면 Prometheus에 부하가 생기게 된다. 또한 Annotation이 key-value 구조다 보니 대상의 1개의 Port만 수집이 가능하다(Envoy처럼 Sidecar를 추가적으로 수집해야하는 경우에는 사용할 수 없다)

이와 더불어 interval, timeout등 개별 조정이 불가능하다.

 

Query Sample Limit

Scape을 하는데 소모되는 리소스는 얼마되지 않는다. 문제는 Query인데, Range Vector를 길게 쓰기라도 하면 Prometheus의 CPU와 Memory 사용량이 치솟으면서 liveness failed or OOM 으로 죽는 현상을 높은 확률로 만나게 된다.

 

Prometheus가 리턴하는 최대 Sample(<timestamp, value> 쌍) 수는 5천만개 이다. 1개의 Sample당 16바이트 크기이므로 총 800MB의 메모리가 필요로 한다. 여러사람이 쓰다보면 위험해질 가능성이 농후하다.

 

그래서 Prometheus에는 리턴하는 Sample갯수를 제한하는 옵션이 있다. 바로 query.max-samples 인데, 결과를 리턴하다가 Sample수가 제한수에 도달하면 결과대신 "Too many sample~" 이란 에러메시지를 리턴한다.

(이러한 옵션은 Thanos에도 존재한다)

 

이와 더불어 Grafana Datasource 설정시 Scrape Interval을 늘리는 것도 방법이다.

 

Helm chart config 분리

Prometheus-Operator의 Helm chart를 보면 Configuration과 서비스배포가 같이 포함되어 있다. 이 부분은 운영하는데 매우 번거로울 수 있다(Configuration만 변경하려고 했으나 Pod이 재시작된다든가).

그렇기 때문에 Configuration(ServiceMonitor,Rule)은 별도 Chart로 분리해 관리하는 것을 추천한다. CI/CD까지 고려한다면 각각의 Component들을 모두 분리하는 것이 바람직하다.

 

Shared and Federated Prometheus

Sharding을 지원하지 않는 Prometheus을 보완할수 있도록 수집방법을 다음과 같이 설계할 수 있다.

eBay에서 발표한 자료로 Federation 방법도 같이 설명하고 있는데, 필자가 생각하기에는 Best Practice라 본다.

(Thanos와 조합하기 좋아보인다)

 

Shared and Federated Prometheus Servers to Monitor Distributed Databases(eBay)

https://www.youtube.com/watch?v=YVj_aqUbpYs

 

Grafana 권한정책(prometheus filtering)

Grafana 는 Multi-Tenant를 지원한다. Organization 마다 사용자를 Assign하여 접근범위를 제한할 수 있고, User권한도 Admin/Editor/Viewer 로 나뉘어져 있다. Grafana에 ACL정책을 적용하려면 사용시나리오에 따라 몇가지 고민해볼 필요가 있다.

 

1. 기존 인증/권한이 있다면 어떻게 Integration 할 것인가?

2. Tenant가 어떤 리소스 기준으로 연동되어야 하나?

3. 사용자가 특정 metric만 봐야하나?

 

1번의 경우 Grafana에서 지원하는 것으로 충분하다면 고민할 필요 없겠지만, 그렇지 않을 경우에는 Grafana 앞단에 Proxy를 두는 것이 여러 방법 중 하나이다. 필자의 경우 Oauth2 proxy(https://github.com/bitly/oauth2_proxy)를 두고 이를 인증한 다음 내부 권한관리 시스템에서 권한을 확인하는 코드를 추가하여 권한에 따른 Org생성,User추가/삭제 등의 조치를 한 후 Grafana로 Proxying하도록 했다.(한번 인증/권한을 체크하고나면 이를 캐싱-Authorization Caching)

2번의 경우 Org 관리얘기이다. 자동으로 Org를 생성/삭제하고 User를 Assign해야 하는데, 이미 Grafana에서 지원하는 것으로 된다면 더이상 고려할 부분은 없다. 그러나 예를 들어 k8s의 namespace를 Org와 연동해야한다고 하면, 생성/삭제 이벤트를 Watching해야한다. 이벤트에 따라서 Grafana에 Org를 생성/삭제 할수 있어야 한다.(이러한 처리를 해주는 컴포넌트를 개발해야함. Kube-watch 오픈소스를 참고하기 바람. https://github.com/bitnami-labs/kubewatch)

3번의 경우 Prometheus에 Label filtering을 Injection해야한다. 필자의 경우 Datasource는 숨긴채(Editor권한까지만 줌) Datasource에 추가 입력란을 만들어서 거기에 Injection 할 Label조건(예를들어 namespace="monitoring" 이라든가)들을 입력하고 이를 Grafana Backend에서 쿼리처리시 쿼리를 파싱하여 Injection을 해주었다(이러한 방법은 모두 Grafana 오픈소스를 Customize 해야한다). 하지만 보통 Metric 권한까지 조정해야하는 경우는 namespace별로 Tenant를 관리할때 그렇고, 클러스터 단위로 Tenant를 관리한다면 굳이 이러한 방법까지 할 필요는 없다.

 

중요한 것은 가능하면 Grafana의 Multi-Tenant를 활용하면서 기존 인증/권한 시스템을 접목시키는 것이다.

 

TSDB Failed metric

Prometheus가 생산하는 metric은 대부분 중요하겠지만 필자를 가장 당황하게 만든 이슈와 관련된 metric을 고르자면 바로 이 metric들이 아닐까 싶다. 이 metric들은 TSDB 저장처리에 있어서 반드시 모니터링해야 한다. Prometheus는 WAL(Write-Ahead-Log)를 설정한 시간만큼 기록하고 이 시간이 지나면 chunk파일로 저장처리한다. 헌데 이 부분이 꼬일수가 있는데(Disk 깨짐, 오류 등등 특히나 Network Disk를 사용하면 빈도가 더 높아진다), 문제의 상황이 벌어지면 WAL이 chunk로 저장처리가 되지 않거나 Prometheus가 재기동시 복구가 되지 않기 때문에 데이터가 손실된다. 이를 방지하고자 다음의 3개 metric을 모니터링하는 것이 중요하다.

필자는 해당 metric 모니터링을 소홀히 하다가 6일이 지난 다음에야 metric이 저장되지 않고 있음을 인지했다;;
(WAL기록은 정상적으로 되고 있었기에 최근 2시간 까지는 정상적으로 표시되고 있었다)

prometheus_tsdb_compactions_failed_total
prometheus_tsdb_checkpoint_creations_failed_total
prometheus_tsdb_checkpoint_deletions_failed_total

재부팅되면 복구가 되는 경우도 있지만, checkpoint 에러는 WAL을 지워버려야 하기 때문에 데이터 손실과 직결된다.

 

Target Label Change

수집대상을 업그레이드할 때, k8s object spec에서 label 설정변경에 주의해야 된다.(특히나 오픈소스라면)

필자는 kube-state-metric 을 1.5에서 1.8로 올리면서 chart의 label이 변경됐다는 사실을 인지하지 못해 kube-state-metric의 metric 수집이 안되는 상황이 발생했었다. 

 

AlertMananger vs. Grafana Alert

꼭 둘중에 하나를 선택해야하는건 아니지만 두개의 장단점을 잘 파악해둘 필요가 있다.

간단하게 말하면 AlertManager는 Alert의 파급단계를 조정하거나 그룹으로 묶을수도 있고, 중요도 등 추가 Label처리가 가능하다. 

Grafana Alert은 기능이 단순하지만 차트 이미지도 같이 보낼 수도 있고, 사용자가 간편하게 설정할 수 있다(단 Template Variable은 사용못하는 한계가 있음).

필자는 후자를 선택했는데, 그 이유는 Alert을 설정하는 주체가 운영자가 아닌 각 서비스 담당자들이었기 때문이다. 물론 AlertManager에 설정이 편리하도록 대시보드 같은걸 개발해 제공해도 되겠지만, 초보수준에서 Alert을 설정할 때 차트에 대한 의존성이 강하기 때문에 최대한 편리한 UX를 제공하는 것이 낫다. 

하지만 No Data 이슈등이 빈번하게 발생할 가능성이 높아서(Alert 폭풍이라 한다) 제대로된 시스템을 운영하려면 AlertManager와 연동하여 별도의 Alert 시스템을 개발하는 것이 좋다고 생각한다.

 

Monitoring for Metering

미터링을 위해 모니터링을 이용하는 것에서 여러가지 의견이 있을 수 있다. 사실 모니터링 데이터는 샘플링 데이터이기 때문에 정확한 값을 측정해야하는 것이라면 미터링용도로 사용하기 어렵다 볼 수 있다. 하지만 가장 중요한 점은 "모니터링을 통해서만 수집이 가능한 미터링 데이터"가 있다는 것이다. CPU/GPU/Memory 등의 사용량은 어차피 샘플링 데이터이고 어떠한 다른방법을 써도 모니터링의 수집방법과 달라지는 것이 없기 때문에 이러한 데이터들은 모니터링을 통해서 측정/기록 하는 것이 맞다.

사용자수, 방문자수 등등 서비스의 데이터베이스 데이터에 기반한 것들은 굳이 모니터링을 통해서 모을 필요가 없다.(이렇게 모으기도 사실 기술적으로 힘들다)

 

설정 변경 후 Metric 생성여부 테스트는 필수

필자가 알면서도 계속 해서 실수하는 부분이 이것인데, 설정변경이 이루어지고나서 반드시 absent 함수를 이용해서 metric이 전부 나오는지 확인해야 한다. 특히나 다른 metric(rule등)에 영향이 있는 metric이 안나오기 시작하면 전부 생성이 안되기 때문에 문제는 심각해진다. 필자의 경우 python으로 간단한 스크립트를 만들어서 prometheus에 쿼리를 날려서 일괄 체크하는 테스트코드를 만들었다.

 


Prometheus Query, Grafana 사용

Running status

전체 사용량(점유량) 등을 파악할때, kube-state-metric의 kube_pod_container_resource_requests/limits metric을 사용하게 된다. 헌데 이 metric에는 실제 Running 상태가 아닌 Pod 정보도 있기 때문에 필터리 없이 계산하면 자칫 점유율이 100% 넘게 나올 수도 있다. 이럴때는 다음의 metric을 join하여 처리하는 것이 값이 정확하게 나온다.

max by(namespace, pod) (kube_pod_status_phase{phase="Running"} > 0)

 

Instance Label

Instance Label은 Job label과 더불어서 기본적으로 붙는 label로 Target의 주소와 Port로 구성되어 있다. 만약 Target 주소가 도메인이 아닌 IP로 되어있을 경우 Pod이 재실행됨에 따라 Instance label은 변하기 때문에 Series가 분리될 수 있어 SUM 등의 함수 사용시 주의해야 한다.

대표적인 exporter인 kube-state-metric 에서 생성하는 kube_pod_container_resource_requests를 예시로 들면,

 

kube_pod_container_resource_requests{resource="cpu"} 결과중,

kube_pod_container_resource_requests{container="dashboard-history",endpoint="http",instance="10.233.66.134:8080",job="kube-state-metrics",namespace="monitoring",node="sel-d1-node-4",pod="dashboard-history-1574618400-z85n8",resource="cpu",service="monitoring-kube-state-metrics",unit="core"}

만약에 kube-state-metric이 재시작된다면 instance 값이 변하게 되고, 여기에 SUM 함수를 적용하게 되면 이전 instance label과 바뀐 label이 모두 포함되어 두배로 뻥튀기 되는 현상이 발생한다. 이를 방지하려면 반드시 사전에 instance 를 제거하고 함수를 적용해야 하는데, 현실적으로 쿼리 마다 전부 한다는거 자체가 어려움이 있다. 해서 필자의 경우 다음과 같이 instance 값을 강제적으로 변경적용하였다.  

- source_labels: [__address__]
  regex: '[^:]+:(\d+)'
  target_label: instance
  replacement: kube-state-metrics:$1

이렇게 적용하면 kube-state-metric이 다시 시작하더라도 instance 값이 고정되어 계산식에 영향이 없다.

 

RecordRule

Range Vector나 Vector matching(Join)을 사용하면 쿼리가 상당히 느려진다. 보통 2개정도 Join하는데 3~4개 정도 필요하다고 하면 RecordRule로 사용하기를 바란다. Instant Vector로만 사용한다고 하면 괜찮은데 보통 Grafana사용시 전부 Range Query로 를 사용하기 때문에 엄청난 부하가 생긴다.

 

평균의 평균을 조심. %의 평균을 조심

한번 Aggregation처리를 한 결과를 다시 Aggregation을 하는 것은 자칫 잘못된 결과를 만든다.

rate(sum) 이 아닌 sum(rate)을 해야하는 것처럼 순서를 지켜야 하는 것도 있지만, %로 나오는 값은 왠만해서는 두번의 aggregation을 안하는 것이 좋다.(단 분모가 동일하다면 해도 됨) 사실 이런 부분들은 중등수학정도로도 충분히 이해할 수 있으나 필자와 같이 수학과는 많은 거리를 둔 분들은 처음에 헷갈릴수가 있다.

 

예를 들어 GPU의 경우 Usage가 나오는게 아닌 Utilization(%)으로 수집된다. 만약 노드별로 합하고 이를 다시 전체로 합해서 계산한다면 계산 오류가 발생한다. 1번 노드에는 4개의 GPU슬롯이 있는데 모두 100%를 사용했고, 2번 노드는 8개가 있는데 50%를 사용했다면 75%(=(100+50)/2)가 아닌 67%(=(4+4)/12)이다.

 

Grafana의 부하는 Frontend에서 생긴다.

과도한 쿼리에 의해 Grafana가 느려지거나 Stuck되는건 Grafana Frontend에 부하가 생겼기 때문이다.

Grafana Backend는 쿼리에 대해 Proxy역할만 하기 때문에 부하가 적다. Datasource 를 보더라도 Direct와 Proxy 모드로 나뉘어 지는데 이렇게 나뉘어져 있다는 것 자체가 Datasource 결과처리를 Frontend에서 하고 있다는 뜻이다.

 

rate이든 irate이든 의도한 목적을 정확히 표현해주지 못한다. 여러가지 metric을 조합하여 흐름(패턴)을 보는 것이다. 

어디서는 rate보다 irate을 보는게 좀더 peak 부분을 확인할수 있다고 한다. 하지만 사실 중요한건 둘다 정확한건 아니라는 것이다.

rate[5m] 으로 했는데, 만약 실제 peak가 1~2분에만 올랐다면 평균의 오류로 정확한 peak을 잡지 못한다. 맨끝 2개의 값을 가지고 계산하는 irate도 마찬가지이다. 그러므로 어떤 특성상황에서 잠깐 peak되는 부분을 확인하고 싶으면 rate외에 peak시 영향받는 다른 metric을 참고하는 것이 좋다. 예를 들어 cpu인 경우 부하여부를 보려면 container_cpu_usage_seconds_total 만 볼게 아니라 container_cpu_cfs_throttled_seconds_total를 같이 보는 것이 좋다는 것이다.