본문 바로가기
Observability/Prometheus & Grafana

Grafana Mimir란? 개념부터 설치까지

by wlsdn3004 2023. 11. 17.
728x90
반응형

 

Prometheus는 쿠버네티스 환경에서 많이 사용하는 인기 있는 오픈소스 모니터링 도구이다.

하지만 몇 가지 치명적인 단점이 있다. 

  • 확장 및 고가용성 문제
    프로메테우스는 단일 서버로 동작하게 구현되어 있다. 즉, 서버가 내려가면 그 시간 동안 메트릭을 수집할 수 없게 됨을 의미한다. 만약 프로메테우스 서버를 2개로 하여 모니터링을 하면 하나의 서버가 내려가더라도 다른 하나의 서버로 메트릭을 볼 수 있지만, 여전히 불완전한 아키텍처로 샤딩, Prometheus Federation 구성 등의 추가 작업이 필요하다.
  • 오래된 데이터 보관 문제
    프로메테우스는 메트릭을 로컬 디스크에 수집하여 보관하는데, 저장소의 용량이 한계에 도달하면 오래된 데이터가 자동으로 삭제되어 일정 시간이 지난 데이터는 조회할 수 없게 된다. 

(해당 내용은 [Prometheus Thanos 구성] 글에서 다루었던 내용이다)

 

이러한 단점들 때문에 많은 조직들은 Prometheus를 사용하면서도 Thanos나 Cortex 같은 추가적인 도구를 도입하여 위 문제를 해결하려 한다.

 

Grafana Mimir란?

Grafana Mimir는 2022년 Grafana Labs에서 개발한 오픈 소스 소프트웨어 프로젝트로 고성능, 장기, 분산형 메트릭 저장 시스템이다. Mimir는 Cortex의 기능과 GEM(Grafana Enterprise Metrics) 및 Grafana Cloud를 대규모로 실행하기 위해 개발한 기능을 모두 AGPLv3 라이선스에 따라 결합했다고 한다.

 

장점은 아래와 같다.

  • 100% Prometheus와 호환되어 기존 Prometheus 사용자가 쉽게 전환할 수 있다.
  • 분산 시스템으로 설계되어 고가용성을 지원하며, 수평 확장이 가능하다.
  • S3, GCS, Azure Blob와 같은 오브젝트 스토리지에 장기 데이터를 보관할 수 있다.
  • 샤딩 된 쿼리 엔진을 통해 쿼리를 병렬화하므로 빠른 쿼리가 가능하다.

 

주요 구성요소는 다음과 같다.

각 구성 요소들의 역할을 간단하게 설명하면 다음과 같다.

Distributor

  • 클라이언트로부터 수신한 metrics 데이터 유효성을 검증 후 시리즈를 분할 및 복제하여 해시를 통해 Ingester에게 전달하는 역할을 담당한다. 

Ingester

  • Distributor에게 전달받은 데이터를 장기 저장소에 저장하고, 쿼리에 대한 데이터(시리즈 샘플)를 반환하는 역할을 담당한다.

Store-gateway

  • 장기 저장소에 저장되어 있는 블록을 쿼리 하는 역할을 담당한다. 또한, 쿼리 속도를 가속화하기 위해 index, chunk, metadata 캐시를 지원한다.

Querier

  • 요청된 데이터 중 최근 데이터는 ingester에서, 장기 데이터는 store-gateway를 통해 장기 저장소에서 찾아 Query Frontend로 전달한다.

Query Frontend

  • 쿼리 요청이 오면 Result Cache에 캐시 된 데이터를 확인하고 없으면 Querier에 요청한다. 또한 병렬화를 위해 쿼리를 샤딩하여  메모리 내 대기열에 배치하고 요청자에게 데이터를 반환하는 역할을 담당한다.

Compactor

  • 장기 저장소에서 저장된 블록을 읽어 성능을 높이고 사용량을 줄이기 위해 블록을 압축하고 블록의 수명주기를 관리한다.

 

아래에 그림을 통해 Mimir의 동작을 쉽게 이해할 수 있다.

  1. Prometheus, Opentelemetry, Grafana Agent와 같은 도구로 데이터를 수집하여 remote write 기능을 사용하여 Mimir로 전달한다.
  2. Mimir는 수집기를 통해 전달받은 데이터를 샤딩 및 복제하여 보관한다.
  3. Grafana 시각화 도구로 Mimir를 data source로 등록하여 저장되어 있는 데이터를 쿼리 한다.

 

본 글에서는 Opentelemetry collector를 사용하여 remote write로 Grafana Mimir에 데이터를 보내고, Grafana Mimir의 데이터를 AWS S3에 보관하고, Grafana를 통해 Mimir의 보관되어 있는 Metrics 데이터를 쿼리 하는 실습을 다룬다.

 

구성은 아래와 같다.

 

 

 

전제 조건

구성 환경

  • AWS EKS : v1.24.17
  • Helm CLI :  v3.8.2
  • AWS CLI : 2.13.25

설치 버전

  • Opentelemetry : 0.88.0
  • Grafana Mimir : 2.10.3
  • Grafana : 10.1.5

 

1. Grafana Mimir 구성


Grafana Mimir가 AWS S3 버킷의 권한을 얻기 위해 IRSA를 설정한다.

iam policy 생성한다.

$ cat >grafana-tempo-s3-policy.json <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "MimirStorage",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::{S3 버킷 이름}",
                "arn:aws:s3:::{S3 버킷 이름}/*"
            ]
        }
    ]
}
EOF

 

$ aws iam create-policy --policy-name aws-mimir-s3 --policy-document file://grafana-mimir-s3-policy.json

 

IAM Role을 생성한다.

$ cat >trust-rs-mimir.json <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::{account}:oidc-provider/oidc.eks.{region}.amazonaws.com/id/{oidc}"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.{region}.amazonaws.com/id/{oidc}:sub": "system:serviceaccount:{namespace}:{serviceAccount}",
                    "oidc.eks.{region}.amazonaws.com/id/{oidc}:aud": "sts.amazonaws.com"
                }
            }
        }
    ]
}
EOF

 

$ aws iam create-role --role-name AWS-Mimir-Role --assume-role-policy-document file://trust-rs-mimir.json

 

위에서 만든 IAM Policy를 IAM Role에 추가한다. 여기서는 AWS Console에서 추가했다.

 

Grafana Mimir 설치를 위한 Helm Chart를 등록한다.

$ helm repo add grafana https://grafana.github.io/helm-charts
$ helm repo update

 

Helm chart values 파일을 아래와 같이 작성한다. (mimir-values.yaml)

## mimir-values.yaml
mimir:
  structuredConfig:
    limits:
      max_label_names_per_series: 60
      compactor_blocks_retention_period: 30d
    blocks_storage:
      backend: s3
      s3:
        bucket_name: {S3 버킷 이름}
        endpoint: s3.{region}.amazonaws.com
        region: {region}
      tsdb:
        retention_period: 13h
      bucket_store:
        ignore_blocks_within: 10h
    querier:
      query_store_after: 12h
    ingester:
      ring:
        replication_factor: 3
serviceAccount:
  create: true
  name: "mimir"
  annotations:
    "eks.amazonaws.com/role-arn": "{위에서 만든 role arn}"
minio:
  enabled: false

위에서 생성한 values 파일로 설치한다.

$ helm upgrade -i -n mimir-test mimir grafana/mimir-distributed --create-namespace -f mimir-values.yaml

 

Mimir ingester 로그를 통해 정상적으로 구동되었는지 확인한다.

$ kubectl logs -n mimir-test mimir-ingester-zone-a-0
...
ts=2023-11-20T00:57:00.375392598Z caller=memberlist_client.go:540 level=info msg="memberlist fast-join starting" nodes_found=4 to_join=4
ts=2023-11-20T00:57:00.401433727Z caller=memberlist_client.go:560 level=info msg="memberlist fast-join finished" joined_nodes=4 elapsed_time=40.29534ms
ts=2023-11-20T00:57:00.401609762Z caller=memberlist_client.go:573 level=info msg="joining memberlist cluster" join_members=dns+mimir-gossip-ring.mimir-test.svc.cluster.local:7946
ts=2023-11-20T00:57:00.429178427Z caller=memberlist_client.go:592 level=info msg="joining memberlist cluster succeeded" reached_nodes=4 elapsed_time=27.56901ms
...

 

2. Opentelemetry 구성


Opentelemetry Helm chart를 등록한다.

$ helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
$ helm repo update

 

Opentelemetry Helm chart values 파일을 작성한다. (otel.yaml)

## otel.yaml
mode: daemonset

clusterRole:
  create: true
  rules:
  - apiGroups:
    - ""
    resources:
    - pods
    - namespaces
    - nodes
    - nodes/proxy
    - services
    - endpoints
    verbs:
    - get
    - watch
    - list
  - apiGroups:
    - extensions
    resources:
    - ingresses
    verbs:
    - get
    - list
    - watch
  - nonResourceURLs:
    - /metrics
    verbs:
    - get
config:
  receivers:
    prometheus:
      config:
        global:
          scrape_interval: 60s
          scrape_timeout: 30s
        scrape_configs:
        - job_name: kubernetes-nodes-cadvisor
          bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
          kubernetes_sd_configs:
          - role: node
          relabel_configs:
          - action: labelmap
            regex: __meta_kubernetes_node_label_(.+)
          - replacement: kubernetes.default.svc:443
            target_label: __address__
          - regex: (.+)
            replacement: /api/v1/nodes/$$1/proxy/metrics/cadvisor
            source_labels:
            - __meta_kubernetes_node_name
            target_label: __metrics_path__
          - action: keep
            regex: $KUBE_NODE_NAME
            source_labels: [__meta_kubernetes_node_name]
          scheme: https
          tls_config:
            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
            insecure_skip_verify: true
        - job_name: kubernetes-nodes
          bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
          kubernetes_sd_configs:
          - role: node
          relabel_configs:
          - action: labelmap
            regex: __meta_kubernetes_node_label_(.+)
          - replacement: kubernetes.default.svc:443
            target_label: __address__
          - regex: (.+)
            replacement: /api/v1/nodes/$$1/proxy/metrics
            source_labels:
            - __meta_kubernetes_node_name
            target_label: __metrics_path__
          - action: keep
            regex: $KUBE_NODE_NAME
            source_labels: [__meta_kubernetes_node_name]
          scheme: https
          tls_config:
            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
            insecure_skip_verify: true
        - job_name: kubernetes-service-endpoints
          kubernetes_sd_configs:
          - role: endpoints
          relabel_configs:
          - action: keep
            regex: true
            source_labels:
            - __meta_kubernetes_service_annotation_prometheus_io_scrape
          - action: replace
            regex: (https?)
            source_labels:
            - __meta_kubernetes_service_annotation_prometheus_io_scheme
            target_label: __scheme__
          - action: replace
            regex: (.+)
            source_labels:
            - __meta_kubernetes_service_annotation_prometheus_io_path
            target_label: __metrics_path__
          - action: replace
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $$1:$$2
            source_labels:
            - __address__
            - __meta_kubernetes_service_annotation_prometheus_io_port
            target_label: __address__
          - action: labelmap
            regex: __meta_kubernetes_service_annotation_prometheus_io_param_(.+)
            replacement: __param_$$1
          - action: labelmap
            regex: __meta_kubernetes_service_label_(.+)
          - action: replace
            source_labels:
            - __meta_kubernetes_namespace
            target_label: kubernetes_namespace
          - action: replace
            source_labels:
            - __meta_kubernetes_service_name
            target_label: kubernetes_name
          - action: replace
            source_labels:
            - __meta_kubernetes_pod_node_name
            target_label: kubernetes_node
          - action: keep
            regex: $KUBE_NODE_NAME
            source_labels: [__meta_kubernetes_endpoint_node_name]
        - job_name: kubernetes-pods
          kubernetes_sd_configs:
          - role: pod
          relabel_configs:
          - action: keep
            regex: true
            source_labels:
            - __meta_kubernetes_pod_annotation_prometheus_io_scrape
          - action: replace
            regex: (https?)
            source_labels:
            - __meta_kubernetes_pod_annotation_prometheus_io_scheme
            target_label: __scheme__
          - action: replace
            regex: (.+)
            source_labels:
            - __meta_kubernetes_pod_annotation_prometheus_io_path
            target_label: __metrics_path__
          - action: replace
            regex: ([^:]+)(?::\d+)?;(\d+)
            replacement: $$1:$$2
            source_labels:
            - __address__
            - __meta_kubernetes_pod_annotation_prometheus_io_port
            target_label: __address__
          - action: labelmap
            regex: __meta_kubernetes_pod_annotation_prometheus_io_param_(.+)
            replacement: __param_$$1
          - action: labelmap
            regex: __meta_kubernetes_pod_label_(.+)
          - action: replace
            source_labels:
            - __meta_kubernetes_namespace
            target_label: kubernetes_namespace
          - action: replace
            source_labels:
            - __meta_kubernetes_pod_name
            target_label: kubernetes_pod_name
          - action: drop
            regex: Pending|Succeeded|Failed|Completed
            source_labels:
            - __meta_kubernetes_pod_phase
          - action: keep
            regex: $KUBE_NODE_NAME
            source_labels: [__meta_kubernetes_pod_node_name]
  extensions:
    health_check: {}
    memory_ballast: {}
  processors:
    memory_limiter:
      check_interval: 1s
      limit_percentage: 75
      spike_limit_percentage: 15
    batch:
      send_batch_size: 10000
      timeout: 10s
  exporters:
    prometheusremotewrite:
      endpoint: "http://mimir-nginx.mimir-test/api/v1/push"
      tls:
        insecure: true
      external_labels:
        label_name: $KUBE_NODE_NAME
  service:
    extensions:
    - health_check
    - memory_ballast
    pipelines:
      metrics:
        exporters:
        - prometheusremotewrite
        processors:
        - memory_limiter
        - batch
        receivers:
        - prometheus
ports:
  otlp:
    enabled: true
    containerPort: 4317
    servicePort: 4317
    hostPort: 4317
    protocol: TCP
    appProtocol: grpc
  otlp-http:
    enabled: true
    containerPort: 4318
    servicePort: 4318
    hostPort: 4318
    protocol: TCP
extraEnvs:
- name: KUBE_NODE_NAME
  valueFrom:
    fieldRef:
      apiVersion: v1
      fieldPath: spec.nodeName

 

위에서 생성한 otel.yaml파일로 설치한다

$ helm upgrade -i opentelemetry open-telemetry/opentelemetry-collector -n opentelemetry -f otel.yaml --create-namespace

 

데이터가 수집되면 Grafana Mimir의 ingest에서 약 2시간 간격으로 AWS S3 버킷으로 upload를 하는 걸 Mimir ingest pod의 로그를 통해 확인할 수 있다.

 

AWS S3에서 위 block 값으로 조회하면 정상적으로 upload 되어 데이터가 보관되는 걸 확인할 수 있다.

 

3. Grafana 대시보드 확인


Grafana service를 LoadBalancer type으로 노출하거나 kubectl port-forword 등을 사용하여 Grafana 대시보드에 접근하여 Grafana Mimir를 data source로 등록한다.

  • Home > Connections > Data sources > Prometheus

Prometheus server URL에 mimir-nginx의 prometheus path로 등록한다.

 

새로운 대시보드를 생성하여 메트릭을 확인한다.

  • Home > Dashboards > New dashboard > Edit panel

"kubelet_running_pods"는 pod의 개수를 알려주는 쿼리이다.

 

위에서 설정한 otel_values.yaml 파일의 config.exporters.external_labels 설정에서 "label_name: $KUBE_NODE_NAME" 으로 설정했기 때문에 해당 label을 통해 각 노드에 존재하는 opentelemetry-collector가 수집하는 pod의 개수를 확인할 수 있다.

 

실제로 위 값이 각 노드에 존재하는 pod의 개수와 같은지 확인한다.

$ kubectl get po -o wide -A --no-headers | grep ip-192-168-8-196.ap-northeast-2.compute.internal | wc -l
12
$ kubectl get po -o wide -A --no-headers | grep ip-192-168-7-238.ap-northeast-2.compute.internal | wc -l
16

 

 

마치며


요약하면, Grafana Mimir은 장기 데이터 저장, 확장성, 고가용성과 관련하여 Prometheus의 주요 한계를 극복할 수 있는 솔루션이다. 뿐만 아니라, 기존 Prometheus 환경의 TSDB 블록을 Mimir로 마이그레이션 하기 위한 "mimirtool"라는 도구를 제공하여 쉽게 전환할 수 있다. 이러한 특징들을 고려하면, Grafana Mimir은 Prometheus 사용자들에게 유용한 대안이 될 것 같다.

반응형

댓글