🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

DaemonSet과 Job/CronJob 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 29. · 10 Views

DaemonSet과 Job/CronJob 완벽 가이드

쿠버네티스에서 모든 노드에 파드를 배포하는 DaemonSet과 일회성/예약 작업을 처리하는 Job, CronJob을 초급 개발자도 쉽게 이해할 수 있도록 실무 예제와 함께 설명합니다.


목차

  1. DaemonSet_개념과_용도
  2. 로그_수집기_모니터링_에이전트_배포
  3. Job으로_일회성_작업_실행
  4. Job_완료_조건_설정
  5. CronJob으로_예약_작업
  6. 동시성_정책_설정

1. DaemonSet 개념과 용도

김개발 씨가 쿠버네티스 클러스터를 운영하던 중 고민에 빠졌습니다. "로그 수집 에이전트를 모든 노드에 하나씩 배포해야 하는데, Deployment로 하면 어떤 노드에 몇 개가 뜰지 모르겠네요." 선배 박시니어 씨가 다가와 말했습니다.

"그럴 땐 DaemonSet을 쓰면 돼요."

DaemonSet은 클러스터의 모든 노드(또는 특정 노드)에 정확히 하나의 파드를 실행하도록 보장하는 워크로드 컨트롤러입니다. 마치 모든 아파트 층마다 경비원을 한 명씩 배치하는 것과 같습니다.

노드가 추가되면 자동으로 해당 노드에도 파드가 생성되고, 노드가 제거되면 파드도 함께 정리됩니다.

다음 코드를 살펴봅시다.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-collector
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: log-collector
  template:
    metadata:
      labels:
        app: log-collector
    spec:
      containers:
      - name: fluentd
        image: fluentd:v1.14
        # 각 노드의 로그 디렉토리를 마운트합니다
        volumeMounts:
        - name: varlog
          mountPath: /var/log
      volumes:
      - name: varlog
        hostPath:
          path: /var/log

김개발 씨는 입사 6개월 차 주니어 DevOps 엔지니어입니다. 오늘 팀장님으로부터 새로운 미션을 받았습니다.

"우리 클러스터 노드가 10개인데, 각 노드의 로그를 중앙에서 수집해야 해요. 모든 노드에 로그 수집 에이전트를 배포해주세요." 김개발 씨는 처음에 Deployment를 생각했습니다.

replicas를 10으로 설정하면 되지 않을까요? 하지만 곧 문제를 깨달았습니다.

Deployment는 어떤 노드에 파드가 배치될지 보장하지 않습니다. 운이 나쁘면 한 노드에 파드가 여러 개 뜰 수도 있고, 어떤 노드는 파드가 없을 수도 있습니다.

선배 박시니어 씨가 해결책을 알려주었습니다. "그럴 때 쓰라고 DaemonSet이 있어요." DaemonSet을 이해하려면 아파트 경비 시스템을 떠올리면 됩니다.

아파트 관리사무소에서 "모든 층에 경비원을 한 명씩 배치하라"고 지시하면, 각 층마다 정확히 한 명의 경비원이 배치됩니다. 새 건물이 증축되면 그 층에도 경비원이 배치되고, 건물 일부가 철거되면 해당 경비원도 함께 철수합니다.

DaemonSet이 바로 이런 역할을 합니다. Deployment와 DaemonSet의 핵심 차이는 명확합니다.

Deployment는 "이 파드를 N개 실행해줘"라고 요청하는 것이고, DaemonSet은 "모든 노드에 이 파드를 하나씩 실행해줘"라고 요청하는 것입니다. 목적 자체가 다릅니다.

위 코드를 살펴보겠습니다. kind를 DaemonSet으로 지정한 것 외에는 Deployment와 구조가 비슷합니다.

하지만 replicas 필드가 없다는 점에 주목하세요. DaemonSet은 노드 수에 따라 자동으로 파드 수가 결정되기 때문입니다.

template 아래의 spec에서는 fluentd 컨테이너를 정의합니다. hostPath 볼륨을 사용해 각 노드의 /var/log 디렉토리를 마운트하는 것이 핵심입니다.

이렇게 하면 각 노드에서 발생하는 로그를 수집할 수 있습니다. 실제 현업에서 DaemonSet은 다양한 용도로 활용됩니다.

로그 수집기(Fluentd, Filebeat), 모니터링 에이전트(Prometheus Node Exporter), 네트워크 플러그인(Calico, Weave), 스토리지 데몬 등이 대표적입니다. 이들의 공통점은 모든 노드에서 실행되어야 한다는 것입니다.

한 가지 주의할 점이 있습니다. 기본적으로 DaemonSet은 마스터 노드에는 파드를 배포하지 않습니다.

마스터 노드에는 일반적으로 taint가 설정되어 있기 때문입니다. 마스터 노드에도 배포하려면 tolerations를 설정해야 합니다.

김개발 씨는 DaemonSet을 적용한 후 클러스터를 확인했습니다. kubectl get pods -o wide 명령어를 실행하니, 정확히 각 노드마다 하나씩 log-collector 파드가 실행되고 있었습니다.

새 노드를 추가해보니 자동으로 그 노드에도 파드가 생성되었습니다. "이거 정말 편하네요!" 김개발 씨가 감탄했습니다.

실전 팁

💡 - kubectl get daemonset으로 현재 상태를 확인하고, DESIRED와 CURRENT 숫자가 일치하는지 검증하세요

  • 특정 노드에만 배포하려면 nodeSelector나 nodeAffinity를 활용하세요

2. 로그 수집기 모니터링 에이전트 배포

김개발 씨가 DaemonSet 개념을 이해한 후, 본격적으로 프로덕션 환경에 로그 수집 시스템을 구축하려 합니다. "단순히 DaemonSet만 만들면 끝인가요?" 박시니어 씨가 고개를 저었습니다.

"실무에서는 리소스 제한, 권한 설정, 업데이트 전략까지 고려해야 해요."

실무에서 로그 수집기나 모니터링 에이전트를 배포할 때는 단순 배포를 넘어 리소스 제한, 보안 권한, 업데이트 전략을 함께 설정해야 합니다. 마치 경비원을 배치할 때 근무 시간, 권한 범위, 교대 방식을 정하는 것과 같습니다.

이를 통해 안정적이고 효율적인 운영이 가능해집니다.

다음 코드를 살펴봅시다.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      containers:
      - name: node-exporter
        image: prom/node-exporter:v1.5.0
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
          requests:
            memory: "64Mi"
            cpu: "50m"
        ports:
        - containerPort: 9100
          hostPort: 9100

김개발 씨는 이전에 배운 대로 DaemonSet을 만들어 배포했습니다. 그런데 얼마 지나지 않아 문제가 발생했습니다.

일부 노드에서 메모리 사용량이 급증하더니, 다른 중요한 애플리케이션까지 영향을 받기 시작한 것입니다. 박시니어 씨가 상황을 파악했습니다.

"로그 수집기가 리소스를 무제한으로 사용하고 있네요. 프로덕션에서는 반드시 리소스 제한을 걸어야 해요." 쿠버네티스에서 리소스 관리는 requestslimits 두 가지로 나뉩니다.

requests는 "최소한 이 정도는 확보해줘"라는 의미이고, limits는 "이 이상은 절대 쓰지 마"라는 의미입니다. 마치 월급에서 생활비는 보장하되 용돈에는 상한선을 두는 것과 비슷합니다.

위 코드에서 resources 부분을 보겠습니다. CPU 100m은 0.1 코어를 의미하고, 메모리 128Mi는 128 메가바이트를 뜻합니다.

이렇게 설정하면 node-exporter가 아무리 바빠도 지정된 리소스 이상은 사용하지 않습니다. 또 하나 중요한 것은 updateStrategy입니다.

DaemonSet을 업데이트할 때 모든 파드를 한꺼번에 내리면 잠시 동안 로그 수집이 중단됩니다. 이를 방지하기 위해 RollingUpdate 전략을 사용합니다.

maxUnavailable을 1로 설정하면 한 번에 하나의 파드만 업데이트됩니다. 나머지 파드들은 계속 동작하므로 서비스 중단이 최소화됩니다.

hostPort 설정도 눈여겨볼 만합니다. containerPort는 컨테이너 내부 포트이고, hostPort는 노드의 실제 포트입니다.

hostPort를 설정하면 각 노드의 해당 포트로 직접 접근할 수 있습니다. Prometheus가 각 노드의 9100 포트로 메트릭을 수집하는 것이 대표적인 예입니다.

실무에서는 보안 권한도 중요합니다. 로그 수집기가 노드의 파일 시스템에 접근해야 하므로, 적절한 securityContext 설정이 필요할 수 있습니다.

다만 과도한 권한 부여는 보안 위험을 초래하므로, 필요한 최소한의 권한만 부여해야 합니다. 배포 후에는 kubectl rollout status daemonset/node-exporter 명령어로 업데이트 진행 상황을 모니터링할 수 있습니다.

문제가 생기면 kubectl rollout undo daemonset/node-exporter로 이전 버전으로 롤백할 수도 있습니다. Deployment와 동일한 방식입니다.

김개발 씨는 리소스 제한과 업데이트 전략을 적용한 후 다시 배포했습니다. 이번에는 각 노드의 리소스 사용량이 안정적으로 유지되었고, 이미지 업데이트 시에도 서비스 중단 없이 순차적으로 진행되었습니다.

"역시 프로덕션은 다르네요." 김개발 씨가 고개를 끄덕였습니다.

실전 팁

💡 - 리소스 limits는 보수적으로 설정하되, 너무 낮으면 OOMKilled가 발생할 수 있으니 모니터링 후 조정하세요

  • hostPort를 사용하면 포트 충돌이 발생할 수 있으니 다른 DaemonSet과 중복되지 않도록 주의하세요

3. Job으로 일회성 작업 실행

어느 날 팀장님이 김개발 씨에게 새로운 요청을 했습니다. "데이터베이스 마이그레이션 스크립트를 쿠버네티스에서 실행해야 하는데, 한 번만 실행하고 끝나야 해요." 김개발 씨는 고민에 빠졌습니다.

Deployment로 만들면 계속 재시작될 텐데, 어떻게 해야 할까요?

Job은 한 번 실행되고 완료되면 종료되는 워크로드를 위한 컨트롤러입니다. 마치 청소 용역 업체가 와서 청소를 마치고 떠나는 것과 같습니다.

Deployment와 달리 파드가 성공적으로 완료되면 재시작하지 않습니다. 데이터베이스 마이그레이션, 배치 처리, 일회성 계산 작업 등에 적합합니다.

다음 코드를 살펴봅시다.

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  # 실패 시 최대 4번까지 재시도합니다
  backoffLimit: 4
  # 작업 완료 후 파드 유지 시간 (초)
  ttlSecondsAfterFinished: 100
  template:
    spec:
      containers:
      - name: migration
        image: myapp/migration:v1.2
        command: ["python", "migrate.py"]
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
      # 완료 후 재시작하지 않음
      restartPolicy: Never

김개발 씨가 처음 쿠버네티스를 배울 때 궁금했던 것이 있습니다. "모든 파드가 계속 실행되어야 하나요?

한 번만 실행하면 되는 작업은 어떻게 하죠?" 그 질문에 대한 답이 바로 Job입니다. 쿠버네티스의 Deployment나 DaemonSet은 "이 파드가 항상 실행되어야 한다"는 전제를 가집니다.

파드가 죽으면 다시 살리고, 오류가 나면 재시작합니다. 하지만 세상에는 한 번만 실행하면 되는 작업도 많습니다.

데이터베이스 스키마 마이그레이션, 대용량 데이터 처리, 보고서 생성 등이 그렇습니다. Job을 이해하려면 택배 기사를 생각하면 됩니다.

택배 기사는 물건을 배달하면 임무가 끝납니다. 배달이 완료되면 집에 돌아가지, 그 자리에서 계속 기다리지 않습니다.

만약 첫 배달에 실패하면 재배달을 시도하겠지만, 몇 번 시도해도 안 되면 결국 포기합니다. Job이 바로 이런 방식으로 동작합니다.

위 코드에서 핵심 설정들을 살펴보겠습니다. 먼저 backoffLimit은 실패 시 재시도 횟수입니다.

4로 설정하면 최대 4번까지 재시도합니다. 네트워크 일시 오류 같은 일시적 문제를 극복하기 위한 설정입니다.

ttlSecondsAfterFinished는 Job 완료 후 리소스 정리 시간입니다. 100으로 설정하면 완료 후 100초가 지나면 Job과 파드가 자동으로 삭제됩니다.

이 설정이 없으면 완료된 Job이 계속 쌓여서 클러스터가 지저분해집니다. restartPolicy가 Never로 설정된 점도 중요합니다.

일반적인 Deployment에서는 Always가 기본값이지만, Job에서는 Never 또는 OnFailure만 사용할 수 있습니다. Never는 실패해도 같은 파드를 재시작하지 않고 새 파드를 만들어 재시도합니다.

OnFailure는 같은 파드를 재시작합니다. 실무에서 Job을 사용할 때 자주 하는 실수가 있습니다.

마이그레이션 스크립트가 멱등성을 보장하지 않는 경우입니다. 멱등성이란 같은 작업을 여러 번 실행해도 결과가 같다는 의미입니다.

Job이 중간에 실패하고 재시도할 때, 이미 실행된 부분이 다시 실행될 수 있기 때문입니다. Job의 상태는 kubectl get jobs 명령어로 확인할 수 있습니다.

COMPLETIONS 열에서 1/1이 표시되면 성공적으로 완료된 것입니다. 로그는 kubectl logs job/db-migration으로 확인합니다.

박시니어 씨가 조언했습니다. "Job은 간단해 보이지만, 프로덕션에서는 여러 가지를 고려해야 해요.

타임아웃 설정, 리소스 제한, 그리고 실패 시 알림 같은 것들이죠." 김개발 씨는 고개를 끄덕이며 메모를 했습니다.

실전 팁

💡 - 마이그레이션 스크립트는 반드시 멱등성을 보장하도록 작성하세요

  • ttlSecondsAfterFinished를 설정하지 않으면 완료된 Job이 계속 남아있으니 주의하세요

4. Job 완료 조건 설정

김개발 씨가 Job을 사용해 대용량 데이터를 처리하는 배치 작업을 설계하고 있습니다. "데이터가 너무 많아서 하나의 파드로는 시간이 너무 오래 걸려요." 박시니어 씨가 말했습니다.

"그럴 땐 여러 개의 파드를 병렬로 실행하면 돼요. Job의 completions와 parallelism 설정을 활용하면 됩니다."

Job은 completionsparallelism 두 가지 설정을 통해 다양한 실행 패턴을 지원합니다. completions는 성공적으로 완료되어야 하는 파드 수이고, parallelism은 동시에 실행할 수 있는 파드 수입니다.

마치 이사할 때 짐을 나르는 인부의 총 인원수와 동시에 일하는 인원수를 정하는 것과 같습니다.

다음 코드를 살펴봅시다.

apiVersion: batch/v1
kind: Job
metadata:
  name: data-processor
spec:
  # 총 5개의 작업이 성공해야 완료
  completions: 5
  # 동시에 2개씩 실행
  parallelism: 2
  # 전체 작업 타임아웃 (1시간)
  activeDeadlineSeconds: 3600
  backoffLimit: 3
  template:
    spec:
      containers:
      - name: processor
        image: myapp/processor:v1.0
        command: ["python", "process.py"]
        env:
        - name: JOB_INDEX
          valueFrom:
            fieldRef:
              fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
      restartPolicy: Never

배치 처리 시스템을 구축할 때 흔히 마주치는 상황이 있습니다. 처리해야 할 데이터가 1000만 건인데, 하나의 프로세스로 처리하면 10시간이 걸립니다.

하지만 5개의 프로세스로 나누어 처리하면 2시간이면 끝납니다. 이런 병렬 처리를 쿠버네티스 Job으로 어떻게 구현할까요?

completionsparallelism이 그 해답입니다. 이 두 설정의 관계를 이해하려면 피자 배달을 생각해보세요.

completions가 5라는 것은 "5개의 피자를 배달해야 끝"이라는 의미입니다. parallelism이 2라는 것은 "배달 오토바이가 2대"라는 의미입니다.

오토바이 2대가 번갈아가며 5개의 피자를 모두 배달하면 Job이 완료됩니다. 위 코드를 자세히 살펴보겠습니다.

completions를 5로 설정했으므로, 파드가 5번 성공적으로 완료되어야 전체 Job이 완료됩니다. parallelism을 2로 설정했으므로, 한 번에 최대 2개의 파드가 동시에 실행됩니다.

처음에 2개가 시작하고, 하나가 완료되면 세 번째 파드가 시작하는 식입니다. activeDeadlineSeconds는 전체 Job의 타임아웃입니다.

3600초, 즉 1시간으로 설정했습니다. 1시간 안에 모든 작업이 완료되지 않으면 Job은 실패로 처리되고 모든 파드가 종료됩니다.

무한히 실행되는 것을 방지하는 안전장치입니다. 각 파드가 자신이 몇 번째 작업인지 알아야 할 때가 있습니다.

예를 들어 1000만 건의 데이터를 5개로 나누어 처리한다면, 각 파드가 자신의 담당 구간을 알아야 합니다. 이때 JOB_COMPLETION_INDEX 환경변수를 활용합니다.

쿠버네티스가 자동으로 0, 1, 2, 3, 4 값을 각 파드에 할당합니다. 실무에서 흔히 사용하는 패턴 몇 가지를 소개하겠습니다.

첫째, completions와 parallelism이 같으면 모든 작업이 동시에 시작합니다. 빠르게 끝내고 싶을 때 사용합니다.

둘째, parallelism을 1로 하면 순차적으로 하나씩 실행됩니다. 리소스가 제한적일 때 유용합니다.

셋째, completions 없이 parallelism만 설정하면 "작업 큐" 패턴을 구현할 수 있습니다. 이 경우 외부 큐에서 작업을 가져와 처리하고, 큐가 비면 종료됩니다.

주의할 점도 있습니다. parallelism을 너무 높게 설정하면 클러스터 리소스가 부족해질 수 있습니다.

또한 각 파드가 동일한 데이터를 중복 처리하지 않도록 작업 분배 로직을 잘 설계해야 합니다. 김개발 씨는 이 설정을 활용해 1000만 건의 데이터를 5개 파드로 병렬 처리하는 Job을 만들었습니다.

실행해보니 예상대로 2시간 만에 모든 처리가 완료되었습니다. "병렬 처리가 이렇게 간단할 줄 몰랐어요!" 김개발 씨가 감탄했습니다.

실전 팁

💡 - parallelism은 클러스터의 가용 리소스를 고려해서 설정하세요

  • JOB_COMPLETION_INDEX를 활용하면 각 파드가 다른 데이터 구간을 처리하도록 구현할 수 있습니다

5. CronJob으로 예약 작업

김개발 씨의 서비스가 점점 커지면서 새로운 요구사항이 생겼습니다. "매일 새벽 3시에 전날 로그를 정리하고, 매주 월요일에는 주간 리포트를 생성해야 해요." 박시니어 씨가 말했습니다.

"리눅스의 cron처럼 쿠버네티스에도 CronJob이 있어요."

CronJob은 지정된 스케줄에 따라 Job을 자동으로 생성하고 실행하는 컨트롤러입니다. 마치 알람 시계처럼 정해진 시간에 작업을 시작합니다.

Cron 표현식을 사용해 분, 시, 일, 월, 요일을 자유롭게 지정할 수 있어 다양한 스케줄링 요구사항을 충족할 수 있습니다.

다음 코드를 살펴봅시다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-cleanup
spec:
  # 매일 새벽 3시에 실행 (UTC 기준)
  schedule: "0 3 * * *"
  # 이전 작업이 아직 실행 중일 때 동작 정책
  concurrencyPolicy: Forbid
  # 실행 시작 기한 (시작 시간 이후 100초 내에 시작되어야 함)
  startingDeadlineSeconds: 100
  # 성공한 Job 이력 보관 개수
  successfulJobsHistoryLimit: 3
  # 실패한 Job 이력 보관 개수
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: cleanup
            image: myapp/cleanup:v1.0
            command: ["sh", "-c", "find /logs -mtime +7 -delete"]
          restartPolicy: OnFailure

운영 환경에서 정기적으로 실행해야 하는 작업은 생각보다 많습니다. 로그 정리, 데이터베이스 백업, 리포트 생성, 캐시 갱신 등이 대표적입니다.

전통적으로는 서버에 cron을 설정하거나 별도의 스케줄러 시스템을 구축했습니다. 하지만 쿠버네티스 환경에서는 CronJob으로 이 모든 것을 해결할 수 있습니다.

CronJob을 이해하려면 알람 시계를 떠올리면 됩니다. 알람 시계에 "매일 오전 7시에 울려"라고 설정하면, 매일 그 시간에 알람이 울립니다.

CronJob도 마찬가지입니다. "매일 새벽 3시에 이 Job을 실행해"라고 설정하면, 쿠버네티스가 알아서 그 시간에 Job을 만들고 실행합니다.

schedule 필드의 Cron 표현식을 이해해봅시다. "0 3 * * *"는 "매일 3시 0분"을 의미합니다.

형식은 "분 시 일 월 요일"입니다. 몇 가지 예를 들어보겠습니다.

"30 4 * * 1"은 매주 월요일 4시 30분, "0 0 1 * "는 매월 1일 자정, "/10 * * * *"는 10분마다 실행됩니다. 주의할 점이 있습니다.

쿠버네티스 CronJob의 시간은 기본적으로 UTC 기준입니다. 한국 시간은 UTC+9이므로, 한국 시간 새벽 3시에 실행하려면 UTC로 18시, 즉 "0 18 * * *"로 설정해야 합니다.

이 부분을 놓치면 예상과 다른 시간에 작업이 실행됩니다. concurrencyPolicy는 매우 중요한 설정입니다.

이전 작업이 아직 실행 중일 때 새 작업 시간이 도래하면 어떻게 할까요? 세 가지 옵션이 있습니다.

Allow는 상관없이 새 Job을 실행합니다. Forbid는 이전 Job이 끝날 때까지 새 Job 생성을 건너뜁니다.

Replace는 이전 Job을 종료하고 새 Job을 시작합니다. 대부분의 경우 Forbid가 안전합니다.

startingDeadlineSeconds는 예약 시간이 지나도 Job이 시작되지 못했을 때의 대기 시간입니다. 100초로 설정하면, 클러스터가 바빠서 정시에 시작하지 못해도 100초 이내에 시작하면 됩니다.

100초가 지나도 시작하지 못하면 해당 실행은 건너뜁니다. successfulJobsHistoryLimitfailedJobsHistoryLimit은 히스토리 관리입니다.

완료된 Job을 몇 개까지 보관할지 결정합니다. 디버깅을 위해 어느 정도는 남겨두되, 너무 많이 쌓이면 etcd 저장소에 부담이 됩니다.

kubectl get cronjobs 명령어로 CronJob 목록을 확인할 수 있습니다. SCHEDULE 열에서 현재 설정된 스케줄을, LAST SCHEDULE 열에서 마지막 실행 시간을 볼 수 있습니다.

수동으로 즉시 실행하고 싶다면 kubectl create job --from=cronjob/daily-cleanup manual-run 명령어를 사용합니다. 김개발 씨는 로그 정리 CronJob을 설정한 후 며칠간 모니터링했습니다.

매일 새벽 3시에 정확하게 Job이 생성되고 실행되는 것을 확인했습니다. "이제 새벽에 일어나서 직접 스크립트 돌릴 필요가 없겠네요." 김개발 씨가 안도의 한숨을 내쉬었습니다.

실전 팁

💡 - 시간대 혼동을 피하려면 주석으로 "한국 시간 기준 새벽 3시"처럼 명시해두세요

  • 테스트할 때는 schedule을 "*/2 * * * *"처럼 짧은 주기로 설정해 빠르게 확인하세요

6. 동시성 정책 설정

김개발 씨가 CronJob으로 데이터 동기화 작업을 설정했습니다. 그런데 어느 날 문제가 발생했습니다.

동기화 작업이 예상보다 오래 걸려서 다음 예약 시간이 되었는데, 이전 작업이 아직 끝나지 않은 것입니다. "두 작업이 동시에 돌면서 데이터가 꼬여버렸어요!" 박시니어 씨가 말했습니다.

"동시성 정책을 제대로 설정하지 않으면 그런 문제가 생겨요."

**동시성 정책(concurrencyPolicy)**은 CronJob에서 스케줄이 겹칠 때의 동작을 제어합니다. Allow는 동시 실행을 허용하고, Forbid는 이전 작업 완료까지 대기하며, Replace는 이전 작업을 중단하고 새 작업을 시작합니다.

작업의 특성에 따라 적절한 정책을 선택해야 데이터 정합성과 시스템 안정성을 보장할 수 있습니다.

다음 코드를 살펴봅시다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: data-sync
spec:
  schedule: "*/30 * * * *"
  # 동시 실행 금지 - 이전 Job이 완료될 때까지 대기
  concurrencyPolicy: Forbid
  # 시작 지연 허용 시간
  startingDeadlineSeconds: 200
  # 일시 중지 설정 (긴급 중단 시 사용)
  suspend: false
  jobTemplate:
    spec:
      # Job 레벨 타임아웃
      activeDeadlineSeconds: 1500
      backoffLimit: 2
      template:
        spec:
          containers:
          - name: sync
            image: myapp/sync:v2.0
            command: ["python", "sync.py"]
            resources:
              limits:
                memory: "512Mi"
                cpu: "500m"
          restartPolicy: OnFailure

동시성 문제는 스케줄링 시스템에서 가장 흔하면서도 치명적인 문제입니다. 예를 들어 30분마다 실행되는 데이터 동기화 작업이 있다고 가정해봅시다.

보통은 10분이면 끝나는 작업인데, 어느 날 데이터가 많아서 40분이 걸렸습니다. 그러면 다음 스케줄 시간에 이전 작업이 아직 돌고 있습니다.

이때 두 작업이 동시에 같은 데이터를 수정하면 어떻게 될까요? 최악의 경우 데이터가 손상되거나 일관성이 깨집니다.

이런 상황을 방지하기 위해 concurrencyPolicy가 존재합니다. 세 가지 정책을 자세히 살펴보겠습니다.

Allow는 "상관없다"는 의미입니다. 이전 Job이 실행 중이어도 새 Job을 만들어 실행합니다.

독립적인 작업, 예를 들어 단순 헬스체크나 각각 다른 데이터를 처리하는 경우에 적합합니다. Forbid는 "기다린다"는 의미입니다.

이전 Job이 실행 중이면 해당 스케줄은 건너뜁니다. 데이터 동기화처럼 순차 실행이 보장되어야 하는 작업에 적합합니다.

다만 작업이 계속 지연되면 스케줄을 계속 건너뛰게 되므로, startingDeadlineSeconds와 함께 사용하는 것이 좋습니다. Replace는 "대체한다"는 의미입니다.

이전 Job을 강제 종료하고 새 Job을 시작합니다. 최신 데이터만 중요한 경우, 예를 들어 캐시 워밍이나 현재 상태 스냅샷 같은 작업에 적합합니다.

하지만 이전 작업이 중간에 중단되므로 데이터 정합성에 주의해야 합니다. suspend 필드도 알아두면 유용합니다.

이 값을 true로 바꾸면 CronJob이 일시 중지됩니다. 스케줄 시간이 되어도 새 Job을 만들지 않습니다.

시스템 점검이나 긴급 상황에서 빠르게 작업을 멈출 수 있는 방법입니다. kubectl patch cronjob/data-sync -p '{"spec":{"suspend":true}}' 명령어로 즉시 중지할 수 있습니다.

activeDeadlineSeconds를 Job 레벨에 설정하면 개별 Job의 최대 실행 시간을 제한할 수 있습니다. 1500초, 즉 25분으로 설정했으므로, 25분 안에 끝나지 않으면 해당 Job은 실패 처리됩니다.

이렇게 하면 무한히 실행되는 좀비 Job을 방지할 수 있습니다. 실무에서 권장하는 패턴이 있습니다.

먼저 스케줄 간격의 80% 이내에 작업이 완료되도록 설계합니다. 30분 간격이면 24분 이내에 끝나야 합니다.

그리고 concurrencyPolicy는 기본적으로 Forbid를 사용하되, 작업 특성에 따라 다르게 설정합니다. activeDeadlineSeconds는 스케줄 간격보다 약간 짧게 설정합니다.

김개발 씨는 동시성 정책을 Forbid로 변경하고, activeDeadlineSeconds를 설정했습니다. 그 후로는 데이터 꼬임 문제가 발생하지 않았습니다.

"작은 설정 하나가 이렇게 중요하다니..." 김개발 씨가 새삼 느꼈습니다. 박시니어 씨가 덧붙였습니다.

"운영 환경에서는 이런 엣지 케이스를 항상 고려해야 해요. 평소에는 잘 되다가 어느 날 갑자기 터지거든요."

실전 팁

💡 - 대부분의 데이터 처리 작업에는 Forbid가 안전한 선택입니다

  • suspend 기능은 장애 상황에서 빠르게 작업을 멈출 수 있으니 기억해두세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Kubernetes#DaemonSet#Job#CronJob#WorkloadController#Kubernetes,DaemonSet,Job

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.