Log Pipeline 구축하기 (1)

Hansu Park·어제
1

토이 프로젝트로 사용할 로그 파이프라인을 구축해보고자 한다.

도입

우선은 파이프라인은 무엇일까?

일반적으로 파이프라인(데이터 파이프라인)은 데이터의 입력에서부터 출력에 이르기까지의 프로세스들을 의미한다. 개인적으로는 소화와 비슷하다고 생각한다. 음식을 먹는 것 부터 배변활동까지의 여러 과정이 있기 때문이다.
(참고: Pipeline (computing) - Wikipedia)

일반적인 서비스에서는 RDB와 같은 데이터베이스를 통해 데이터를 저장하지만, 대규모 데이터를 처리와 같은 요구사항에 대응하기 위해 데이터 파이프라인이 필요하다.

로그 파이프라인은 데이터 파이프라인 중에서 로그 데이터를 대상으로 하는 파이프라인이며, (1) 웹 로그를 저장한 후 빠른 속도로 데이터 분석 (2) 대규모의 로그를 저장 (3) 중요한 로그 데이터들을 손실없이 저장과 같은 목적으로 사용된다.

구조

구축할 파이프라인의 구조에 대해 설명하겠다.

Application, Log Aggreagotr, Storages로 구성되어있다. 1편에서는 Log Aggregator까지의 과정만을 소개한다.

이들은 맥북 로컬의 docker-compose로 구성할 예정이다.

Application

역할

Application은 간단하게 로그를 발생시키는 어플리케이션이다. 정확히는 임의 로그를 생성하고, 이를 파일에 차곡차곡 쌓는다.

설명

func main() {  
	//파일 오픈
    output, err := makeOutput("output.log")  
    if err != nil {  
       panic(err)  
    }  
	//파일과 연결
    logger := generator.NewLogger(generator.ApacheCommonLine, output, 1)  
    fmt.Println("Logging Start")  
    //로그 쌓기(적재)
    err = logger.GenerateLogs()  
    if err != nil {  
       panic(err)  
    }  
}  

//파일 오픈
func makeOutput(path string) (*os.File, error) {  
    if path == "" {  
       return os.Stdout, nil  
    }  
    return os.OpenFile(path, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644)  
}

와 같이 간단하게 구성되어 있다.

generator는 랜덤한 로그를 생성하는 패키지이다. 이는 GitHub - adamliesko/fakelog: Go cmd-line / package for fake and random log data generation를 참고했다. (원하는 설정이 달라 코드를 가져왔다.)

랜덤한 로그는 Apache Web Server의 HTTP 형식의 로그를 발생하는데, 편향되지 않도록 가중치를 적용되어 있다.

109.116.244.146 leet_coder - [15/12/2024:06:28:51 +0000] "DELETE /login HTTP/1.1" 200 3651
121.201.16.177 leet_coder - [15/12/2024:06:28:52 +0000] "GET /article/1672 HTTP/1.1" 301 10966
26.192.249.222 sarah_cooper - [15/12/2024:06:28:53 +0000] "GET /signup HTTP/1.1" 200 3947

(로그 예시)

Docker 구성

docker-compose에서 구축하기 위해서 해당 어플리케이션을 도커이미지로 만들어야 했다.
이를 위해서 Dockerfile을 작성하였다.

# Use an official Golang image as the build environment  
FROM golang:1.21-alpine AS build  
  
# Set the working directory inside the container  
WORKDIR /log-app  
  
# Copy go.mod and go.sum files first for dependency caching  
COPY ./go.mod ./go.sum ./  
  
# Download dependencies  
RUN go mod download  
  
# Copy the entire project  
COPY . .  
  
# Build the Go app (replace `your-app` with your app name)  
RUN go build -o loggen  
  
# Use a minimal base image for the final build  
FROM scratch  
  
# Set working directory in final image  
WORKDIR /root/app  
  
# Copy the Go binary from the build stage  
COPY --from=build /log-app/loggen loggen  
  
# Command to run the  
CMD ["./loggen"]

참고할 점은 2가지이다.

  • 레이어 캐시 활용
    - go.sum, go.mod 파일 복사 및 go mod download을 진행한 뒤(Go의 라이브러리 의존성 처리 과정이다), 나머지 파일들을 옮겼다.
    - 이는 레이어 캐시를 활용하기 위함이며, 다른 파일들에서 변경사항이 발생하더라도, 앞 과정은 캐싱되어 불필요하게 반복하지 않아도 되어 성능을 개선할 수 있다.
  • 멀티 스테이지 빌드
    - 도커 이미지에서 가져오는 이미지가 2개이다. 첫 번째로 가져온 이미지는 빌드를 하기 위한 이미지, 두 번째로 가져온 이미지는 실행을 하기 위한 이미지이다.
    - 첫 번째 이미지는 빌드를 하기 위해 여러 파일들 (Go 컴파일러, 라이브러리들)이 들어있어 용량이 크다. (alphine이 작은 이미지 기존 3MB, golang-alphine은 70MB)
    - 두 번째 이미지는 실행이라는 목적에 최적화된 이미지이다. 엄청 경량화되어있다. (크기 800KB 정도 )
    - 참고: Go Docker 경량화
    - 이미지마다 환경이 다르기에 예상치 못한 결과(e.g. 기대했던 파일시스템의 옵션이 없거나 다른 경우 등)가 생길 수 있어 주의해야한다.

Log Aggregator

역할

Application에서 output.log 파일로 로그를 쌓고있다. (용어로써 적재라고 한다.)

Aggregator(로그 수집기)는 적재된 로그를 수집한 후 다양한 방식으로 처리(e.g. ES에 적재, 카프카에 프로듀싱 등)를 해주는 어플리케이션이다.

(Vector를 활용해 멀티 CDN 로그 및 트래픽 관리하기 을 참고하면 이 외 다양한 도구들이 소개되어있다.)

구조

Aggregator는 별도 코드가 아니라, Fluentd라는 어플리케이션과 이에 대한 설정만을 활용했다. Fluentd는 데이터를 수집하여 처리하는 어플리케이션이다. 다양한 입력을 다양한 출력으로 연결할 수 있다고한다.
image

앞서 말했듯이, 우리가 사용할 것은

  • 파일에 쌓은 로그를 수집 (Input)
  • 수집한 로그를 출력 (Output)
    이다.
//fluentd.conf
<source>  
  @type tail  
  path /app/output.log  
  pos_file /fluentd/log/output.pos  
  tag app.log  
  <parse>  
    @type none  
  </parse>  
</source>  
  
<match app.log>  
  @type copy  
  <store>  
    @type stdout  
  </store>  
</match>

Fluentd Application에서는 fluentd.conf파일을 통해 어떠한 입력을 어떻게 처리할 것인지 설정할 수 있다.

<source>는 Input부분이며 파일에 쌓은 로그를 수집하는 역할이다.
<match> 는 Output부분이며 표준 출력을 하는 역할이다.

tail

Input에서 tail이라는 방식으로 로그를 수집했다. tail은 파일에 적재된 로그를 읽는 동작이다. 리눅스 명령어중 tail -f를 생각하면 쉽다.

이의 동작 원리에 대해 궁금해 찾아보았다.

fluentd의 테일링 관련 코드(fluentd/lib/fluent/plugin/in_tail.rb at master · fluent/fluentd · GitHub) 를 확인해보면

  1. 로그가 생김(파일 이벤트)를 감지한다. (이는 보통 OS에서 코드로써 인터페이스를 제공해준다.)
  2. 로그를 읽고, 읽은 로그를 이후 처리 과정에 넘긴다.
  3. 읽은 위치를 .pos파일에 기록한다.
  4. 이후 로그 생김 이벤트를 감지하였을 때, .pos파일을 바탕으로 어디까지 이미 읽었고, 어디부터 새로 읽는 것인지 (새로운 로그인지) 판단후 처리한다.

추가로 일반적인 서비스에선 로그의 양이 많아질 경우 압축 후 보관하는 과정인 rotation이 일어나는데, 이에 잘 대응해주는 것도 수집기의 핵심 역할이다.

stdout

Output에서는 stdout이라는 방식으로 수집한 로그를 처리했다. stdout은 간단하게 말하면 print하는 역할이며, 정확히는 OS의 기본 출력 장치로 데이터를 출력하는 역할이다.대부분의 기본 출력 장치가 터미널/콘솔이다.

즉, 수집한 로그는 Fluentd가 속한 터미널에 출력되고 있을 것이다.

Docker 구성

# Use an official Golang image as the build environment  
FROM golang:1.21-alpine AS build  
  
WORKDIR /app  
  
COPY go.mod go.sum ./  
RUN go mod download  
  
COPY . .  
RUN go build -o consumer  
  
# Use a minimal base image for the final build  
FROM scratch  
  
WORKDIR /root/  
COPY --from=build /app/consumer .  
  
CMD ["./consumer"]

도커 파일은 간단하, USER을 구분한 이유는 fluentd를 실행하기 위해선 리눅스 유저가 fluentd이어야 하기 때문이다.

docker-compose

App과 Log Aggregator는 다른 도커 컨테이너에 속해있어, Aggreagotr가 App의 파일 변경을 직접 감지할 순 없다. 따라서 도커 볼륨을 활용하여 감지할 수 있도록 했다.

version: '3.8'  
  
services:  
  zookeeper:  
    image: confluentinc/cp-zookeeper:latest  
    environment:  
      ZOOKEEPER_CLIENT_PORT: 2181  
      ZOOKEEPER_TICK_TIME: 2000  
  
  kafka:  
    image: confluentinc/cp-kafka:latest  
    depends_on:  
      - zookeeper  
    environment:  
      KAFKA_BROKER_ID: 1  
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181  
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092  
    ports:  
      - "9092:9092"  
  
  go-consumer:  
    build:  
      context: ./consumer  
    depends_on:  
      - zookeeper  
      - kafka  
    environment:  
      - KAFKA_BROKER=kafka:9092  
    command: ["./consumer"]  
  
  app:  
    build:  
      context: ./app  
#    image: log-generator:v0.1         # Use the locally built image  
    container_name: loggen-my  
    volumes:  
      - shared-log:/root/app  # Mount the shared named volume  
    depends_on:  
      - aggregator  
  
  aggregator:  
    build:  
      context: ./aggregator  
    container_name: aggregator  
    environment:  
      - KAFKA_BROKER=kafka:9092  
    platform: linux/amd64  
    volumes:  
      - shared-log:/app                    # Mount the shared named volume  
      - ./aggregator/fluent.conf:/aggregator/etc/fluent.conf  # Fluentd configuration  
    command: ["fluentd", "-c", "/aggregator/etc/fluent.conf", "-vv"]  
    depends_on:  
      - kafka  
  
volumes:  
  shared-log:  # Define the shared named volume

도커 볼륨은

  • 네임드 볼륨
  • 익명 볼륨
  • 호스트 볼륨
    으로 나눌 수 있다.

익명 볼륨은 공유할 수 없다는 단점이 있고, 호스트 볼륨은 컨테이너간 공유 목적이라기 보다는 컨테이너와 호스트의 공유 목적이기에 파일 공유를 할 수 있고 이식성이 높은 네임드 볼륨을 활용했다.

마치며

여기까지의 과정을 정리하면 아래와 같다.
1. Application이 로그를 생성
2. Application이 생성한 로그를 파일에 적재
3. 파일에 적재한 로그를 Fluentd가 수집
4. 수집한 로그를 Fluentd가 출력

출력이라는 결과가 다소 아쉽지만, 이 외에 다양한 처리를 할 수 있다.
하둡이나 ES 등의 저장소로 적재하거나, Kafka로 프로듀싱하거나, 혹은 또 다른 Fluentd로 전송하여 계층화를 할 수도 있다.

다음에는 Output에서 처리할 수 있는 다양한 어플리케이션들에 대해 알아보고 이들에 대해 로그를 내보내볼 것이다.

0개의 댓글