토이 프로젝트로 사용할 로그 파이프라인을 구축해보고자 한다.
우선은 파이프라인은 무엇일까?
일반적으로 파이프라인(데이터 파이프라인)은 데이터의 입력에서부터 출력에 이르기까지의 프로세스들을 의미한다. 개인적으로는 소화와 비슷하다고 생각한다. 음식을 먹는 것 부터 배변활동까지의 여러 과정이 있기 때문이다.
(참고: Pipeline (computing) - Wikipedia)
일반적인 서비스에서는 RDB와 같은 데이터베이스를 통해 데이터를 저장하지만, 대규모 데이터를 처리와 같은 요구사항에 대응하기 위해 데이터 파이프라인이 필요하다.
로그 파이프라인은 데이터 파이프라인 중에서 로그 데이터를 대상으로 하는 파이프라인이며, (1) 웹 로그를 저장한 후 빠른 속도로 데이터 분석 (2) 대규모의 로그를 저장 (3) 중요한 로그 데이터들을 손실없이 저장과 같은 목적으로 사용된다.
구축할 파이프라인의 구조에 대해 설명하겠다.
Application, Log Aggreagotr, Storages로 구성되어있다. 1편에서는 Log Aggregator까지의 과정만을 소개한다.
이들은 맥북 로컬의 docker-compose로 구성할 예정이다.
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-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 mod download
을 진행한 뒤(Go의 라이브러리 의존성 처리 과정이다), 나머지 파일들을 옮겼다.Application에서 output.log 파일로 로그를 쌓고있다. (용어로써 적재라고 한다.)
Aggregator(로그 수집기)는 적재된 로그를 수집한 후 다양한 방식으로 처리(e.g. ES에 적재, 카프카에 프로듀싱 등)를 해주는 어플리케이션이다.
(Vector를 활용해 멀티 CDN 로그 및 트래픽 관리하기 을 참고하면 이 외 다양한 도구들이 소개되어있다.)
Aggregator는 별도 코드가 아니라, Fluentd라는 어플리케이션과 이에 대한 설정만을 활용했다. Fluentd는 데이터를 수집하여 처리하는 어플리케이션이다. 다양한 입력을 다양한 출력으로 연결할 수 있다고한다.
앞서 말했듯이, 우리가 사용할 것은
//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부분이며 표준 출력을 하는 역할이다.
Input에서 tail이라는 방식으로 로그를 수집했다. tail은 파일에 적재된 로그를 읽는 동작이다. 리눅스 명령어중 tail -f
를 생각하면 쉽다.
이의 동작 원리에 대해 궁금해 찾아보았다.
fluentd의 테일링 관련 코드(fluentd/lib/fluent/plugin/in_tail.rb at master · fluent/fluentd · GitHub) 를 확인해보면
.pos
파일에 기록한다..pos
파일을 바탕으로 어디까지 이미 읽었고, 어디부터 새로 읽는 것인지 (새로운 로그인지) 판단후 처리한다.추가로 일반적인 서비스에선 로그의 양이 많아질 경우 압축 후 보관하는 과정인 rotation이 일어나는데, 이에 잘 대응해주는 것도 수집기의 핵심 역할이다.
Output에서는 stdout이라는 방식으로 수집한 로그를 처리했다. stdout은 간단하게 말하면 print하는 역할이며, 정확히는 OS의 기본 출력 장치로 데이터를 출력하는 역할이다.대부분의 기본 출력 장치가 터미널/콘솔이다.
즉, 수집한 로그는 Fluentd가 속한 터미널에 출력되고 있을 것이다.
# 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
이어야 하기 때문이다.
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에서 처리할 수 있는 다양한 어플리케이션들에 대해 알아보고 이들에 대해 로그를 내보내볼 것이다.