개발

분산환경은 로깅을 어떻게 할까

autocat 2023. 12. 5. 10:59

개요

slueth-zipkin을 통해서 하나의 Request가 어떻게 어떤서비스에서 얼마의 시간을 소요하고 흘러가는지 확인할 수 있었다.
전통적으로 서비스는 개별로 로그를 보관한다. 로그를 확인하기 위해서 개별 서비스의 로그를 찾아가면서 확인하는 비효율적인 작업을 타개하기 위해 ELK Stack이 도입되었다. 각각 Elasticsearch, Logstash, Kibana 의 약어이다. 분산된 로그데이터들을 중앙화하고 분석과 검색, 시각화에 용이하게 수집 및 정리하는데 사용된다.


ELK Stack

Elasticsearch

로그를 저장하는 저장소 역할을 한다.
실시간으로 대용량 데이터를 검색하고 분석하는데 사용되는 오픈소스 검색 엔진이다. 기본적인 full-text 검색 뿐 아니라, 구조화된 데이터와 비구조화된 데이터를 확장 가능하게 인덱싱하는데 사용된다. Elasticsearch는 분산 시스템으로 설계되어서 강력한 확장성을 보유하며, 복잡한 검색 작업을 매우 빠르게 처리할 수 있다.

Logstash

로그를 수집해오고 파싱하는 역할을 한다.
다양한 소스에서 데이터를 수집하여(input) 설정에 맞춰 변환(filter)한 뒤 ElasticSearch와 같은 저장소에 저장(output)을 하는 서버사이드 데이터 처리 파이프라인이다.
기본적으로 in-memory를 사용하여 파이프라인 단계(input->filter->output) 사이에 in-memory queues를 사용해 이벤트를 버퍼링한다.

Kibana

키바나는 단순하게 생각하면 수집된 데이터를 탐색 및 분석하는 수단이라고 할 수 있다. Elasticsearch로 저장된 데이터를 시각화하고, 데이터를 쉽게 분석할 수 있도록 도와준다.


ELK(+zipkin) 연동

zipkin을 올릴때 사용했던 docker-compose에 내용을 추가하면서 진행한다. 가장 간단한 세팅으로 ELK 구동을 목적으로 yml을 작성한다.
ELK는 동일한 버전을 사용하는것을 권장한다.

logstash 연결을 위한 logback 설정

먼저 logstash-logback-encoder 의존성을 추가해준다.
그리고 logback을 생성해주는데 가장 중요한건 logstash로 전달해주는 구문이다.

<!-- logback.xml -->
...
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">  
    <destination>${YOUR_LOGSTASH_HOST}:5000</destination>  
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">  
        <providers>  
            <pattern>  
                <pattern>  
                    {  
                    "date":"%date",  
                    "traceId":"%X{traceId:-}",  
                    "spanId":"%X{spanId:-}",  
                    "level": "%level",  
                    "project": "${projectName}",  
                    "pid": "${PID:-}",  
                    "thread": "%thread",  
                    "class": "%logger{36}",  
                    "message": "%message"  
                    }  
                </pattern>  
            </pattern>  
        </providers>  
    </encoder>  
</appender>
...

zipkin

이전글에서 생성한 zipkin은 in-memory에 데이터를 들고있다가 컨테이너가 멈춤과 동시에 사라지게된다. elasticsearch를 추가하면서 storage를 elasticsearch로 연결해준다.

zipkin:  
  image: openzipkin/zipkin  
  container_name: zipkin  
  environment:  
    - STORAGE_TYPE=elasticsearch  
    - "ES_HOSTS=http= http://elasticsearch:9200"
  ports:  
    - "9411:9411"
  networks:
    - elk
  depends_on:
    - elasticsearch

logstash

logstash:  
  image: docker.elastic.co/logstash/logstash:7.12.0  
  container_name: logstash  
  command: logstash -f /etc/logstash/conf.d/logstash.conf  
  volumes:  
    - ./config:/etc/logstash/conf.d
  ports:  
    - "5000:5000"  
  networks:  
    - elk  
  depends_on:  
    - elasticsearch  

실행순서를 조정을 위해 elasticsearch에 depends_on 두어 es가 켜진뒤에 실행되도록 했다.
logstash는 -f 옵션을 통해 설정파일을 불러오도록 한다.
volumes를 보면 상위 config폴더 를 컨테이너 내 /etc/logstash/conf.d 로 마운팅했다.
아래와 같은 트리로 보면 config 폴더 내부에 설정파일을 작성해둔걸 확인 할 수 있다.

├── config
│   └── logstash.conf
└── docker-compose.yml
# ./config/logstash.conf
input {  
  tcp {  
    port => 5000  
    codec => json_lines  
  }  
}  
output {  
  elasticsearch {  
    hosts => ["elasticsearch:9200"]  
  }  
}

config file의 속성에 대해서는 공식 Document의 Input/Filter/Output plugins 들을 확인하면 된다. 위 같은 경우에는 index패턴을 별도로 지정하지 않았기 때문에 logstash-{now/d}-00001와 같은 형식으로 전달된다.

elasticsearch

elasticsearch:  
  image: docker.elastic.co/elasticsearch/elasticsearch:7.12.0  
  container_name: elasticsearch  
  environment:  
    - node.name=elasticsearch  
    - discovery.type=single-node  
    - cluster.name=docker-cluster  
    - bootstrap.memory_lock=true  
    - "ES_JAVA_OPTS=-Xms512m -Xmx512m"  
  ulimits:  
    memlock:  
      soft: -1
      hard: -1  
  volumes:  
    - elasticsearch-data:/usr/share/elasticsearch/data  
  ports:  
    - "9200:9200" 
    - "9300:9300"  
  networks:  
    - elk  

elasticsearch에 2개의 포트포워딩이 들어간다.
기본적으로 9200포트는 HTTPS를 통한 클라이언트 트래픽에 사용되는 포트다.(eg.curl -XGET 'http://localhost:9200/_count?pretty')
9300포트는 elasticsearch 클러스터 내부의 노드들간의 통신에 사용된다.(지금 설정은 single-node이지만..)

kibana

kibana:  
  image: docker.elastic.co/kibana/kibana:7.12.0  
  container_name: kibana  
  environment:  
#      ELASTICSEARCH_URL: "http://elasticsearch:9200"  
    ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"  
  ports:  
    - "5601:5601"  
  depends_on:  
    - elasticsearch  
  networks:  
    - elk  

kibana 6.x.x버전 까지는 ELASTICSEARCH_URL로 ES 인스턴스의 URL을 명시하였으나 7.0.0 이후로는 ELASTICSEARCH_HOST로 변경되었다.

이렇게 모두 종합한 docker-compose.yml 은 아래와 같다.

# docker-compose.yml 
name: elkz-container  

services:  
  zipkin:  
    image: openzipkin/zipkin  
    container_name: zipkin  
    ports:  
      - "9411:9411"  
  logstash:  
    image: docker.elastic.co/logstash/logstash:7.12.0  
    container_name: logstash  
    command: logstash -f /etc/logstash/conf.d/logstash.conf  
    volumes:  
      - ./config/logstash.config:/etc/logstash/conf.d/logstash.conf 
    ports:  
      - "5000:5000"  
    networks:  
      - elk  
    depends_on:  
      - elasticsearch  
  elasticsearch:  
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.0  
    container_name: elasticsearch  
    environment:  
      - node.name=elasticsearch  
      - discovery.type=single-node  
      - cluster.name=docker-cluster  
      - bootstrap.memory_lock=true  
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"  
    ulimits:  
      memlock:  
        soft: -1  
        hard: -1  
    volumes:  
      - elasticsearch-data:/usr/share/elasticsearch/data  
    ports:  
      - "9200:9200"  
      - "9300:9300"  
    networks:  
      - elk  
  kibana:  
    image: docker.elastic.co/kibana/kibana:7.12.0  
    container_name: kibana  
    environment:  
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"  
    ports:  
      - "5601:5601"  
    depends_on:  
      - elasticsearch  
    networks:  
      - elk  
volumes:  
  elasticsearch-data:  
    driver: local  
networks:  
  elk:  
    driver: bridge

데이터 저장을 위한 volumes와 networks를 선언하고 저장한다.

depends_on의 실행순서에 관하여

depends_on을 실행순서를 조정하기 위해 위와 같이 사용하였는데 실제로는 내 생각대로 실행되지 않았다. 이 명령어는 단순히 실행에 대한 순서만 조정이 될뿐이었다.
의존을 갖고있는 서비스가 healthy한 상태에서 나머지 서비스가 실행되길 원했고 공식문서에서 답을 찾을수 있었다.

healthcheck

Docker document.healthcheck
healthcheck가 선언된 서비스에서 test속성에 정의된 명령어를 통해 자기 자신에게 HTTP요청을 보내면서 healthcheck를 시도한다. 이 명령이 성공상태 코드를 반환하면 서비스가 정상적으로 작동한다고 간주하고 healthy 라는 상태를 부여한다.

healthcheck:  
  test: [ "CMD", "curl", "-f", "http://localhost:9200" ]  
  interval: 30s  
  timeout: 20s  
  retries: 3 

healtcheck는 condition과 함께 쓰이며 예시는 아래와 같다.

zipkin:  
  ...
  depends_on:  
    elasticsearch:  
      condition: service_healthy

그 외 inverval,timeout,retries는 프로퍼티명부터 직관적으로 어떤 의미인지 설명하지 않아도 모두가 알것이라고 생각한다. druation에 대한 정의는 아래와 같이 docs에 써있다.

The supported units are 
`us` (microseconds),
`ms` (milliseconds),
`s` (seconds), 
`m` (minutes) and `h` (hours).
Values can combine multiple values without separator.
 10ms
 40s
 1m30s
 1h5m30s20ms

최종 docker-compose.yml

name: elkz-container  

services:  
  zipkin:  
    image: openzipkin/zipkin  
    container_name: zipkin  
    environment:  
      - STORAGE_TYPE=elasticsearch  
      - "ES_HOSTS=http://elasticsearch:9200"  
    ports:  
      - "9411:9411"  
    networks:  
      - elk  
    depends_on:  
      elasticsearch:  
        condition: service_healthy  

  logstash:  
    image: docker.elastic.co/logstash/logstash:7.12.0  
    container_name: logstash  
    command: logstash -f /etc/logstash/conf.d/logstash.conf  
    volumes:  
      - ./config:/etc/logstash/conf.d  
    ports:  
      - "5000:5000"  
    networks:  
      - elk  
    depends_on:  
      elasticsearch:  
        condition: service_healthy  

  elasticsearch:  
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.0  
    container_name: elasticsearch  
    environment:  
      - node.name=elasticsearch  
      - discovery.type=single-node  
      - cluster.name=docker-cluster  
      - bootstrap.memory_lock=true  
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"  
    ulimits:  
      memlock:  
        soft: -1  
        hard: -1  
    volumes:  
      - elasticsearch-data:/usr/share/elasticsearch/data  
    ports:  
      - "9200:9200"  
      - "9300:9300"  
    networks:  
      - elk  
    healthcheck:  
      test: [ "CMD", "curl", "-f", "http://localhost:9200" ]  
      interval: 30s  
      timeout: 20s  
      retries: 3  

  kibana:  
    image: docker.elastic.co/kibana/kibana:7.12.0  
    container_name: kibana  
    environment:  
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"  
    ports:  
      - "5601:5601"  
    networks:  
      - elk  
    depends_on:  
      elasticsearch:  
        condition: service_healthy  

volumes:  
  elasticsearch-data:  
    driver: local  
networks:  
  elk:  
    driver: bridge

구동

이전 이미지를 사용하지 않고 다시 이미지를 만들어 올리기 위해 --force-recreate 옵션과 --build 옵션을 같이 사용한다.

> docker compose up --build --force-recreate -d
 [+] Running 5/5
 Network elkz-container_elk Created
 Container elasticsearch    Started
 Container logstash         Created
 Container kibana           Created
 Container zipiin           Created

여기서 우린 healthcheck 옵션이 잘 들어갔다는걸 확인 할 수 있다.
healthcheck가 정상응답을 주기 전까지 elasticsearch를 제외한 나머지 컨테이너는 Created 상태로 기다리다가 elasticsearch 컨테이너가 Healthy 상태가 되면 나머지도 Started로 변경된다.

http://YOUR_HOST:5601 를 통해 Kibana의 UI로 들어갈 수 있는데 일단 탐색할 대상데이터를 위해 index pattern을 먼저 생성해줘야한다.

img1


처음 화면에서 'Kibana' 섹션을 클릭 한 뒤 'Add your data'를 통해 아래 화면으로 진입한다.

img2


index pattern을 별도로 지정하지 않았으니 logstash-* 를 입력하여 진행하면 생성이 된다.
Discovery 탭으로 이동해 위 인덱스의 로그들을 탐색 할 수 있다.

img3


수집된 로그데이터를 토대로 분석을 할수있는 대시보드도 아래같이 사용할수 있다.

대시보드