1 - 크론잡(CronJob)으로 자동화된 작업 실행

쿠버네티스 버전 1.21에서 크론잡이 GA (General Availability)로 승격되었다. 이전 버전의 쿠버네티스를 사용하고 있다면, 해당 쿠버네티스 버전의 문서를 참고하여 정확한 정보를 확인할 수 있다. 이전 버전의 쿠버네티스는 batch/v1 크론잡 API를 지원하지 않는다.

시간 기반의 스케줄에 따라 크론잡을 이용해서 잡(Job)을 실행할 수 있다. 이러한 자동화된 잡은 리눅스 또는 유닉스 시스템에서 크론 작업처럼 실행된다.

크론 잡은 백업을 수행하거나 이메일을 보내는 것과 같이 주기적이고 반복적인 작업들을 생성하는 데 유용하다. 크론 잡은 시스템 사용이 적은 시간에 잡을 스케줄하려는 경우처럼 특정 시간에 개별 작업을 스케줄할 수도 있다.

크론 잡에는 제한 사항과 특이점이 있다. 예를 들어, 특정 상황에서는 하나의 크론 잡이 여러 잡을 생성할 수 있다. 따라서, 잡은 멱등성을 가져야 한다.

제한 사항에 대한 자세한 내용은 크론잡을 참고한다.

시작하기 전에

  • 쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음의 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

크론 잡 생성

크론 잡은 구성 파일이 필요하다. 아래의 크론 잡 구성 .spec 파일의 예제는 매 분마다 현재 시간과 hello 메시지를 출력한다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox:1.28
            imagePullPolicy: IfNotPresent
            command:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

다음 명령을 사용하여 크론잡 예제를 실행한다.

kubectl create -f https://k8s.io/examples/application/job/cronjob.yaml

출력 결과는 다음과 비슷하다.

cronjob.batch/hello created

크론 잡을 생성한 후, 다음 명령을 사용하여 상태를 가져온다.

kubectl get cronjob hello

출력 결과는 다음과 비슷하다.

NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        <none>          10s

명령의 결과에서 알 수 있듯이, 크론 잡은 아직 잡을 스케줄하거나 실행하지 않았다. 약 1분 내로 잡이 생성되는지 확인한다.

kubectl get jobs --watch

출력 결과는 다음과 비슷하다.

NAME               COMPLETIONS   DURATION   AGE
hello-4111706356   0/1                      0s
hello-4111706356   0/1           0s         0s
hello-4111706356   1/1           5s         5s

이제 "hello" 크론 잡에 의해 스케줄된 실행 중인 작업을 확인했다. 잡 감시를 중지한 뒤에 크론 잡이 다시 스케줄되었는지를 확인할 수 있다.

kubectl get cronjob hello

출력 결과는 다음과 비슷하다.

NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        50s             75s

크론 잡 helloLAST SCHEDULE 에 지정된 시간에 성공적으로 잡을 스케줄했는지 확인해야 한다. 현재는 0개의 활성 잡이 있고, 이는 작업이 완료되었거나 실패했음을 의미한다.

이제, 마지막으로 스케줄된 잡이 생성한 파드를 찾고 생성된 파드 중 하나의 표준 출력을 확인한다.

# "hello-4111706356"을 사용자의 시스템에 있는 잡 이름으로 바꾼다
pods=$(kubectl get pods --selector=job-name=hello-4111706356 --output=jsonpath={.items[*].metadata.name})

파드의 로그를 출력한다.

kubectl logs $pods

출력 결과는 다음과 비슷하다.

Fri Feb 22 11:02:09 UTC 2019
Hello from the Kubernetes cluster

크론 잡 삭제

더 이상 크론 잡이 필요하지 않으면, kubectl delete cronjob <cronjob name> 명령을 사용해서 삭제한다.

kubectl delete cronjob hello

크론 잡을 삭제하면 생성된 모든 잡과 파드가 제거되고 추가 잡 생성이 중지된다. 가비지(garbage) 수집에서 잡 제거에 대해 상세한 내용을 읽을 수 있다.

크론 잡 명세 작성

다른 모든 쿠버네티스 구성과 마찬가지로, 크론 잡은 apiVersion, kind 그리고 metadata 필드가 필요하다. 구성 파일 작업에 대한 일반적인 정보는 애플리케이션 배포kubectl을 사용하여 리소스 관리하기 문서를 참고한다.

크론 잡 구성에는 .spec 섹션도 필요하다.

스케줄

.spec.schedule.spec 의 필수 필드이다. 이는 해당 잡이 생성되고 실행되는 스케줄 시간으로 0 * * * * 또는 @hourly 와 같이 크론 형식의 문자열을 받아들인다.

이 형식은 확장된 "Vixie cron" 스텝(step) 값도 포함한다. 이 내용은 FreeBSD 매뉴얼에 설명되어 있다.

스텝 값은 범위(range)와 함께 사용할 수 있다. 범위 뒤에 /<number> 를 지정하여 범위 내에서 숫자만큼의 값을 건너뛴다. 예를 들어, 시간 필드에 0-23/2 를 사용하여 매 2시간마다 명령 실행을 지정할 수 있다(V7 표준의 대안은 0,2,4,6,8,10,12,14,16,18,20,22 이다). 별표(asterisk) 뒤에 붙이는 스텝도 허용되며, "2시간마다"라고 지정하고 싶으면, 간단히 */2 를 사용하면 된다.

잡 템플릿

.spec.jobTemplate 은 잡에 대한 템플릿이며, 이것은 필수 필드다. 이것은 중첩되고 apiVersion 이나 kind 가 없는 것을 제외하고 과 정확히 같은 스키마를 가진다. 잡 .spec 을 작성하는 것에 대한 내용은 잡 명세 작성하기를 참고한다.

시작 기한

.spec.startingDeadlineSeconds 필드는 선택 사항이다. 어떤 이유로든 스케줄된 시간을 놓친 경우 잡의 시작 기한을 초 단위로 나타낸다. 기한이 지나면, 크론 잡이 잡을 시작하지 않는다. 이러한 방식으로 기한을 맞추지 못한 잡은 실패한 작업으로 간주된다. 이 필드를 지정하지 않으면, 잡에 기한이 없다.

.spec.startingDeadlineSeconds 필드가 (null이 아닌 값으로) 설정되어 있다면, 크론잡 컨트롤러는 잡 생성 완료 예상 시각과 현재 시각의 차이를 측정하고, 시각 차이가 설정한 값보다 커지면 잡 생성 동작을 스킵한다.

예를 들어, 200 으로 설정되었다면, 잡 생성 완료 예상 시각으로부터 200초까지는 잡이 생성될 수 있다.

동시성 정책

.spec.concurrencyPolicy 필드도 선택 사항이다. 이것은 이 크론 잡에 의해 생성된 잡의 동시 실행을 처리하는 방법을 지정한다. 명세는 다음의 동시성 정책 중 하나만 지정할 수 있다.

  • Allow(기본값): 크론 잡은 동시에 실행되는 잡을 허용한다.
  • Forbid: 크론 잡은 동시 실행을 허용하지 않는다. 새로운 잡을 실행할 시간이고 이전 잡 실행이 아직 완료되지 않은 경우, 크론 잡은 새로운 잡 실행을 건너뛴다.
  • Replace: 새로운 잡을 실행할 시간이고 이전 잡 실행이 아직 완료되지 않은 경우, 크론 잡은 현재 실행 중인 잡 실행을 새로운 잡 실행으로 대체한다.

참고로 동시성 정책은 동일한 크론 잡에 의해 생성된 잡에만 적용된다. 크론 잡이 여러 개인 경우, 각각의 잡은 항상 동시에 실행될 수 있다.

일시 정지

.spec.suspend 필드도 선택 사항이다. true 로 설정되면, 모든 후속 실행이 일시 정지된다. 이 설정은 이미 시작된 실행에는 적용되지 않는다. 기본값은 false이다.

잡 히스토리 한도

.spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit 필드는 선택 사항이다. 이들 필드는 기록을 보관해야 하는 완료 및 실패한 잡의 개수를 지정한다. 기본적으로, 각각 3과 1로 설정된다. 한도를 0 으로 설정하는 것은 잡 완료 후에 해당 잡 유형의 기록을 보관하지 않는다는 것이다.

2 - 작업 대기열을 사용한 거친 병렬 처리

이 예제에서는, 여러 병렬 워커 프로세스를 활용해 쿠버네티스 잡(Job)을 실행한다.

이 예제에서는, 각 파드가 생성될 때 작업 대기열에서 하나의 작업 단위를 선택하여, 완료하고, 대기열에서 삭제하고, 종료한다.

이 예제에서의 단계에 대한 개요는 다음과 같다.

  1. 메시지 대기열 서비스를 시작한다. 이 예에서는, RabbitMQ를 사용하지만, 다른 메시지 대기열을 이용해도 된다. 실제로 사용할 때는, 한 번 메시지 대기열 서비스를 구축하고서 이를 여러 잡을 위해 재사용하기도 한다.
  2. 대기열을 만들고, 메시지로 채운다. 각 메시지는 수행할 하나의 작업을 나타낸다. 이 예제에서, 메시지는 긴 계산을 수행할 정수다.
  3. 대기열에서 작업을 수행하는 잡을 시작한다. 잡은 여러 파드를 시작한다. 각 파드는 메시지 대기열에서 하나의 작업을 가져와서, 처리한 다음, 대기열이 비워질 때까지 반복한다.

시작하기 전에

기본적이고, 병렬 작업이 아닌, 의 사용법에 대해 잘 알고 있어야 한다.

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음의 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

메시지 대기열 서비스 시작

이 예시에서는 RabbitMQ를 사용하지만, 다른 AMQP 유형의 메시지 서비스를 사용하도록 예시를 조정할 수 있다.

실제로 사용할 때는, 클러스터에 메시지 대기열 서비스를 한 번 구축하고서, 여러 많은 잡이나 오래 동작하는 서비스에 재사용할 수 있다.

다음과 같이 RabbitMQ를 시작한다.

kubectl create -f https://raw.githubusercontent.com/kubernetes/kubernetes/release-1.3/examples/celery-rabbitmq/rabbitmq-service.yaml
service "rabbitmq-service" created
kubectl create -f https://raw.githubusercontent.com/kubernetes/kubernetes/release-1.3/examples/celery-rabbitmq/rabbitmq-controller.yaml
replicationcontroller "rabbitmq-controller" created

이 문서에서는 celery-rabbitmq 예제에 나오는 정도로만 rabbitmq를 사용한다.

메시지 대기열 서비스 테스트하기

이제, 메시지 대기열을 이용해 실험할 수 있다. 임시 대화형 파드를 만들어 그 위에 도구들을 설치하고, 대기열을 실험해본다.

먼저 임시 대화형 파드를 만든다.

# 임시 대화형 컨테이너를 만든다.
kubectl run -i --tty temp --image ubuntu:18.04
Waiting for pod default/temp-loe07 to be running, status is Pending, pod ready: false
... [ previous line repeats several times .. hit return when it stops ] ...

참고로 파드 이름과 명령 프롬프트는 위와 다를 수 있다.

다음으로 amqp-tools를 설치하여 메시지 대기열을 활용할 수 있게 한다.

# 도구들을 설치한다.
root@temp-loe07:/# apt-get update
.... [ lots of output ] ....
root@temp-loe07:/# apt-get install -y curl ca-certificates amqp-tools python dnsutils
.... [ lots of output ] ....

후에, 이 패키지들을 포함하는 도커 이미지를 만든다.

다음으로, rabbitmq 서비스를 발견할 수 있는지 확인한다.

# rabbitmq-service가 쿠버네티스로부터 주어진 DNS 이름을 갖는다.

root@temp-loe07:/# nslookup rabbitmq-service
Server:        10.0.0.10
Address:    10.0.0.10#53

Name:    rabbitmq-service.default.svc.cluster.local
Address: 10.0.147.152

# 주소는 다를 수 있다.

만약 Kube-DNS가 적절히 구축되지 않았다면, 전 단계 작업이 작동하지 않을 수 있다. 환경 변수를 통해서도 서비스 IP를 찾을 수 있다.

# env | grep RABBIT | grep HOST
RABBITMQ_SERVICE_SERVICE_HOST=10.0.147.152
# 주소는 다를 수 있다.

다음으로 대기열을 생성하고, 메시지를 발행하고 사용할 수 있는지 확인한다.

# 다음 줄에서, rabbitmq-service는 rabbitmq-service에 접근할 수 있는 
# 호스트네임이다. 5672는 rabbitmq의 표준 포트이다.

root@temp-loe07:/# export BROKER_URL=amqp://guest:guest@rabbitmq-service:5672
# 만약 전 단계에서 "rabbitmq-service"가 주소로 변환되지 않는다면,
# 이 커맨드를 대신 사용하면 된다.
# root@temp-loe07:/# BROKER_URL=amqp://guest:guest@$RABBITMQ_SERVICE_SERVICE_HOST:5672

# 이제 대기열을 생성한다.

root@temp-loe07:/# /usr/bin/amqp-declare-queue --url=$BROKER_URL -q foo -d
foo

# 대기열에 메시지를 하나 발행한다.

root@temp-loe07:/# /usr/bin/amqp-publish --url=$BROKER_URL -r foo -p -b Hello

# 다시 메시지를 돌려받는다.

root@temp-loe07:/# /usr/bin/amqp-consume --url=$BROKER_URL -q foo -c 1 cat && echo
Hello
root@temp-loe07:/#

마지막 커맨드에서, amqp-consume 도구는 대기열로부터 하나의 메시지를 받고(-c 1), 그 메시지를 임의의 명령 표준입력으로 전달한다. 이 경우에는, cat 프로그램이 표준입력으로부터 받은 값을 출력하고, echo가 캐리지 리턴을 더해주어 출력 결과가 보여진다.

작업으로 대기열 채우기

이제 몇 가지 "작업"으로 대기열을 채운다. 이 예제에서의 작업은 문자열을 출력하는 것이다.

실제로 사용할 때는, 메시지의 내용이 다음과 같을 수 있다.

  • 처리되어야 하는 파일들의 이름
  • 프로그램의 추가 플래그
  • 데이터베이스 테이블의 키(key) 범위
  • 시뮬레이션의 구성 파라미터
  • 렌더링해야 하는 씬(scene)의 프레임 번호

실제로는, 잡의 모든 파드에서 읽기-전용 모드로 필요한 큰 데이터가 있다면, 일반적으로 그 데이터를 NFS와 같은 공유 파일시스템에 넣고 모든 파드에 읽기 전용으로 마운트하거나, 파드 안에 있는 프로그램이 기본적으로 HDFS와 같은 클러스터 파일시스템으로부터 데이터를 불러들인다.

본 예제에서는, 대기열을 만들고 amqp 커맨드라인 도구를 이용해 대기열을 채울 것이다. 실제로는, amqp 라이브러리를 이용해 대기열을 채우는 프로그램을 작성하게 된다.

/usr/bin/amqp-declare-queue --url=$BROKER_URL -q job1  -d
job1
for f in apple banana cherry date fig grape lemon melon
do
  /usr/bin/amqp-publish --url=$BROKER_URL -r job1 -p -b $f
done

8개의 메시지로 대기열을 채웠다.

이미지 생성

이제 잡으로 실행할 이미지를 만들 준비가 되었다.

amqp-consume 유틸리티를 이용해 대기열로부터 메시지를 읽고, 실제 프로그램을 실행해 볼 것이다. 여기에 아주 간단한 예제 프로그램이 있다.

#!/usr/bin/env python

# 표준 출력만 출력하고 10초 동안 대기한다.
import sys
import time
print("Processing " + sys.stdin.readlines()[0])
time.sleep(10)

스크립트에 실행 권한을 준다.

chmod +x worker.py

이제 이미지를 빌드한다. 만약 소스 트리 안에서 작업하고 있다면, examples/job/work-queue-1로 디렉터리를 옮긴다. 아니면, 임시 디렉터리를 만들고, 그 디렉터리로 옮긴다. Dockerfileworker.py를 다운로드한다. 위 두 경우 모두, 다음의 명령을 이용해 이미지를 빌드한다.

docker build -t job-wq-1 .

도커 허브를 이용하기 위해, 앱 이미지를 사용자의 username으로 태깅하고 아래의 명령어를 이용해 허브에 푸시한다. <username>을 사용자의 허브 username으로 대체한다.

docker tag job-wq-1 <username>/job-wq-1
docker push <username>/job-wq-1

만약 구글 컨테이너 레지스트리를 이용하고 있다면, 앱 이미지를 사용자의 프로젝트 ID를 이용해 태깅하고, GCR에 푸시한다. <proejct> 부분을 사용자의 프로젝트 ID로 대체한다.

docker tag job-wq-1 gcr.io/<project>/job-wq-1
gcloud docker -- push gcr.io/<project>/job-wq-1

잡 정의

다음은 잡 정의이다. 잡의 사본을 만들고 위에서 정한 이름에 맞게 이미지를 수정하고, 파일 이름을 ./job.yaml이라 정한다.

apiVersion: batch/v1
kind: Job
metadata:
  name: job-wq-1
spec:
  completions: 8
  parallelism: 2
  template:
    metadata:
      name: job-wq-1
    spec:
      containers:
      - name: c
        image: gcr.io/<project>/job-wq-1
        env:
        - name: BROKER_URL
          value: amqp://guest:guest@rabbitmq-service:5672
        - name: QUEUE
          value: job1
      restartPolicy: OnFailure

이 예시에서는, 각 파드가 대기열로부터 얻은 하나의 아이템을 수행하고 종료한다. 그래서, 잡의 완료 횟수가 완료된 작업 아이템의 숫자에 대응한다. 예시에서 .spec.completions: 8이라 정한 것도, 대기열에 8개의 아이템을 넣었기 때문이다.

잡 실행

이제 잡을 실행한다.

kubectl apply -f ./job.yaml

이제 조금 기다린 다음, 잡을 확인한다.

kubectl describe jobs/job-wq-1
Name:             job-wq-1
Namespace:        default
Selector:         controller-uid=41d75705-92df-11e7-b85e-fa163ee3c11f
Labels:           controller-uid=41d75705-92df-11e7-b85e-fa163ee3c11f
                  job-name=job-wq-1
Annotations:      <none>
Parallelism:      2
Completions:      8
Start Time:       Wed, 06 Sep 2017 16:42:02 +0800
Pods Statuses:    0 Running / 8 Succeeded / 0 Failed
Pod Template:
  Labels:       controller-uid=41d75705-92df-11e7-b85e-fa163ee3c11f
                job-name=job-wq-1
  Containers:
   c:
    Image:      gcr.io/causal-jigsaw-637/job-wq-1
    Port:
    Environment:
      BROKER_URL:       amqp://guest:guest@rabbitmq-service:5672
      QUEUE:            job1
    Mounts:             <none>
  Volumes:              <none>
Events:
  FirstSeen  LastSeen   Count    From    SubobjectPath    Type      Reason              Message
  ─────────  ────────   ─────    ────    ─────────────    ──────    ──────              ───────
  27s        27s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-hcobb
  27s        27s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-weytj
  27s        27s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-qaam5
  27s        27s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-b67sr
  26s        26s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-xe5hj
  15s        15s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-w2zqe
  14s        14s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-d6ppa
  14s        14s        1        {job }                   Normal    SuccessfulCreate    Created pod: job-wq-1-p17e0

모든 파드가 성공했다. 야호.

대안

이러한 접근은 "워커" 프로그램을 작업 대기열에 맞게 수정하지 않아도 된다는 장점이 있다.

이 접근을 이용하려면, 메시지 대기열 서비스를 실행해야만 한다. 만약 메시지 대기열 서비스를 실행하는 게 불편하다면, 다른 잡 패턴을 고려해볼 수 있다.

이 접근은 모든 작업 아이템에 대해 파드를 생성한다. 만약 작업 아이템이 오직 몇 초밖에 걸리지 않는 작업이라면, 매 작업마다 파드를 생성하는 것은 아주 큰 오버헤드를 더할 수 있다. 하나의 파드가 여러 작업 아이템을 수행하는 이 예제를 고려해보자.

이 예제에서는, amqp-consume 유틸리티를 이용해 대기열로부터 메시지를 읽어 실제 프로그램을 실행했다. 이러면 메시지 대기열을 이용하기 위해 프로그램을 수정하지 않아도 된다는 장점이 있다. 다른 예제는 클라이언트 라이브러리를 이용해 작업 대기열과 소통하는 방법을 보여준다.

주의 사항

만약 작업 완료 수가 대기열에 있는 아이템의 숫자보다 적게 설정되면, 모든 아이템 처리되지 않는다.

만약 작업 완료 수가 큐에 있는 아이템의 숫자보다 많게 설정되면, 대기열에 있는 아이템이 모두 처리되어도, 잡이 완료됐다고 표시되지 않고, 메시지를 기다리는 과정에서 막히는 파드를 추가적으로 실행시킨다.

이 패턴에서는 경쟁 상태(race)가 잘 나타나지 않는다. 만약 amqp-consume 명령으로부터 메시지가 인정되는 시간과 컨테이너가 성공적으로 종료되는 시간 사이에 컨테이너가 종료되거나, kubelet이 api-server에게 파드가 성공했음을 알리기 전에 노드가 비정상적으로 종료되면, 대기열의 모든 아이템이 처리되었다 해도, 잡이 완료되었다고 표시되지 않는다.

3 - 작업 대기열을 사용한 정밀 병렬 처리

이 예에서는, 지정된 파드에서 여러 병렬 워커 프로세스가 있는 쿠버네티스 잡(Job)을 실행한다.

이 예에서는, 각 파드가 생성될 때, 작업 대기열에서 하나의 작업 단위를 선택하여, 처리하고, 대기열이 비워질 때까지 반복한다.

이 예에서의 단계에 대한 개요는 다음과 같다.

  1. 작업 대기열을 보관할 스토리지 서비스를 시작한다. 이 예에서는, Redis를 사용하여 작업 항목을 저장한다. 이전 예에서는, RabbitMQ를 사용했다. 이 예에서는, AMQP가 길이가 정해져 있는 작업 대기열이 비어있을 때 클라이언트가 이를 감지할 수 있는 좋은 방법을 제공하지 않기 때문에 Redis 및 사용자 지정의 작업 대기열 클라이언트 라이브러리를 사용한다. 실제로는 Redis와 같은 저장소를 한 번 설정하고 여러 작업과 다른 것들의 작업 대기열로 재사용한다.
  2. 대기열을 만들고, 메시지로 채운다. 각 메시지는 수행할 하나의 작업을 나타낸다. 이 예에서, 메시지는 긴 계산을 수행할 정수다.
  3. 대기열에서 작업을 수행하는 잡을 시작한다. 잡은 여러 파드를 시작한다. 각 파드는 메시지 대기열에서 하나의 작업을 가져와서, 처리한 다음, 대기열이 비워질 때까지 반복한다.

시작하기 전에

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음의 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

의 기본적이고, 병렬 작업이 아닌, 사용법에 대해 잘 알고 있어야 한다.

Redis 시작

이 문서의 예시에서는, 단순함을 위해, Redis의 단일 인스턴스를 시작한다. Redis를 확장 가능하고 중복적으로 배포하는 예에 대해서는 Redis 예시를 참고한다.

다음 파일을 직접 다운로드할 수도 있다.

작업으로 대기열 채우기

이제 몇 가지 "작업"으로 대기열을 채운다. 이 예제의 작업은 문자열을 출력하는 것이다.

Redis CLI를 실행하기 위한 임시 대화형 파드를 시작한다.

kubectl run -i --tty temp --image redis --command "/bin/sh"
Waiting for pod default/redis2-c7h78 to be running, status is Pending, pod ready: false
Hit enter for command prompt

이제 엔터 키를 누르고, redis CLI를 시작하고, 몇몇 작업 항목이 포함된 목록을 생성한다.

# redis-cli -h redis
redis:6379> rpush job2 "apple"
(integer) 1
redis:6379> rpush job2 "banana"
(integer) 2
redis:6379> rpush job2 "cherry"
(integer) 3
redis:6379> rpush job2 "date"
(integer) 4
redis:6379> rpush job2 "fig"
(integer) 5
redis:6379> rpush job2 "grape"
(integer) 6
redis:6379> rpush job2 "lemon"
(integer) 7
redis:6379> rpush job2 "melon"
(integer) 8
redis:6379> rpush job2 "orange"
(integer) 9
redis:6379> lrange job2 0 -1
1) "apple"
2) "banana"
3) "cherry"
4) "date"
5) "fig"
6) "grape"
7) "lemon"
8) "melon"
9) "orange"

자, 키 job2 가 있는 목록이 작업 대기열이 된다.

참고: Kube DNS를 올바르게 설정하지 않은 경우, 위 블록의 첫 번째 단계를 redis-cli -h $REDIS_SERVICE_HOST 로 변경해야 할 수 있다.

이미지 생성

이제 실행할 이미지를 만들 준비가 되었다.

redis 클라이언트와 함께 python 워커 프로그램을 사용하여 메시지 큐에서 메시지를 읽는다.

rediswq.py(다운로드)라는 간단한 Redis 작업 대기열 클라이언트 라이브러리가 제공된다.

잡의 각 파드에 있는 "워커" 프로그램은 작업 대기열 클라이언트 라이브러리를 사용하여 작업을 가져온다. 다음은 워커 프로그램이다.

#!/usr/bin/env python

import time
import rediswq

host="redis"
# Uncomment next two lines if you do not have Kube-DNS working.
# import os
# host = os.getenv("REDIS_SERVICE_HOST")

q = rediswq.RedisWQ(name="job2", host=host)
print("Worker with sessionID: " +  q.sessionID())
print("Initial queue state: empty=" + str(q.empty()))
while not q.empty():
  item = q.lease(lease_secs=10, block=True, timeout=2)
  if item is not None:
    itemstr = item.decode("utf-8")
    print("Working on " + itemstr)
    time.sleep(10) # Put your actual work here instead of sleep.
    q.complete(item)
  else:
    print("Waiting for work")
print("Queue empty, exiting")

worker.py, rediswq.pyDockerfile 파일을 다운로드할 수 있고, 그런 다음 이미지를 만들 수도 있다.

docker build -t job-wq-2 .

이미지 푸시

도커 허브(Docker Hub)를 위해, 아래 명령으로 사용자의 username과 앱 이미지에 태그하고 허브에 푸시한다. <username> 을 사용자의 허브 username으로 바꾼다.

docker tag job-wq-2 <username>/job-wq-2
docker push <username>/job-wq-2

공용 저장소로 푸시하거나 개인 저장소에 접근할 수 있도록 클러스터를 구성해야 한다.

Google Container Registry를 사용하는 경우, 사용자의 프로젝트 ID로 앱 이미지에 태그를 지정하고 GCR로 푸시한다. <project> 를 사용자의 프로젝트 ID로 바꾼다.

docker tag job-wq-2 gcr.io/<project>/job-wq-2
gcloud docker -- push gcr.io/<project>/job-wq-2

잡 정의

다음은 잡 정의이다.

apiVersion: batch/v1
kind: Job
metadata:
  name: job-wq-2
spec:
  parallelism: 2
  template:
    metadata:
      name: job-wq-2
    spec:
      containers:
      - name: c
        image: gcr.io/myproject/job-wq-2
      restartPolicy: OnFailure

사용자 자신의 경로로 gcr.io/myproject 를 변경하려면 잡 템플릿을 편집해야 한다.

이 예에서, 각 파드는 대기열의 여러 항목에 대해 작업한 다음 더 이상 항목이 없을 때 종료된다. 워커는 작업 대기열이 비어있을 때를 감지하고 잡 컨트롤러는 작업 대기열에 대해 알지 못하기 때문에, 작업이 완료되면 워커에게 신호를 보낸다. 워커는 성공적으로 종료하여 대기열이 비어 있음을 알린다. 따라서, 워커가 성공적으로 종료하자마자, 컨트롤러는 작업이 완료되었음을 인식하고, 파드가 곧 종료된다. 따라서, 잡 완료 횟수를 1로 설정했다. 잡 컨트롤러는 다른 파드도 완료될 때까지 기다린다.

잡 실행

이제 잡을 실행한다.

kubectl apply -f ./job.yaml

이제 조금 기다린 다음, 잡을 확인한다.

kubectl describe jobs/job-wq-2
Name:             job-wq-2
Namespace:        default
Selector:         controller-uid=b1c7e4e3-92e1-11e7-b85e-fa163ee3c11f
Labels:           controller-uid=b1c7e4e3-92e1-11e7-b85e-fa163ee3c11f
                  job-name=job-wq-2
Annotations:      <none>
Parallelism:      2
Completions:      <unset>
Start Time:       Mon, 11 Jan 2016 17:07:59 -0800
Pods Statuses:    1 Running / 0 Succeeded / 0 Failed
Pod Template:
  Labels:       controller-uid=b1c7e4e3-92e1-11e7-b85e-fa163ee3c11f
                job-name=job-wq-2
  Containers:
   c:
    Image:              gcr.io/exampleproject/job-wq-2
    Port:
    Environment:        <none>
    Mounts:             <none>
  Volumes:              <none>
Events:
  FirstSeen    LastSeen    Count    From            SubobjectPath    Type        Reason            Message
  ---------    --------    -----    ----            -------------    --------    ------            -------
  33s          33s         1        {job-controller }                Normal      SuccessfulCreate  Created pod: job-wq-2-lglf8


kubectl logs pods/job-wq-2-7r7b2
Worker with sessionID: bbd72d0a-9e5c-4dd6-abf6-416cc267991f
Initial queue state: empty=False
Working on banana
Working on date
Working on lemon

보시다시피, 사용자의 파드 중 하나가 여러 작업 단위에서 작업했다.

대안

대기열 서비스를 실행하거나 작업 대기열을 사용하도록 컨테이너를 수정하는 것이 불편한 경우, 다른 잡 패턴 중 하나를 고려할 수 있다.

만약 실행할 백그라운드 처리 작업의 연속 스트림이 있는 경우, ReplicaSet 이 있는 백그라운드 워커를 실행하는 것과, https://github.com/resque/resque와 같은 백그라운드 처리 라이브러리를 실행하는 것이 좋다.

4 - 확장을 사용한 병렬 처리

이 태스크는 공통 템플릿을 기반으로 하는 여러 개의 잡(Job)을 실행하는 것을 보여준다. 이 접근 방식을 사용하여 일괄 작업을 병렬로 처리할 수 있다.

이 예에는 apple, banana 그리고 cherry 세 항목만 있다. 샘플 잡들은 문자열을 출력한 다음 일시 정지하는 각 항목을 처리한다.

이 패턴이 보다 실질적인 유스케이스에 어떻게 부합하는지 알아 보려면 실제 워크로드에서 잡 사용하기를 참고한다.

시작하기 전에

사용자는 기본적인 내용과, 병렬 작업이 아닌 사용에 대해 익숙해야 한다.

쿠버네티스 클러스터가 필요하고, kubectl 커맨드-라인 툴이 클러스터와 통신할 수 있도록 설정되어 있어야 한다. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 적어도 2개 포함된 클러스터에서 실행하는 것을 추천한다. 만약, 아직 클러스터를 가지고 있지 않다면, minikube를 사용해서 생성하거나 다음의 쿠버네티스 플레이그라운드 중 하나를 사용할 수 있다.

기본 템플릿을 사용하려면 커맨드 라인 유틸리티 sed 가 필요하다.

고급 템플릿 예제를 따라하려면, 파이썬(Python)과 파이썬용 Jinja2 템플릿 라이브러리의 설치가 필요하다.

파이썬을 설정했으면, 다음을 실행하여 Jinja2를 설치할 수 있다.

pip install --user jinja2

템플릿 기반의 잡 생성하기

먼저, 다음의 잡 템플릿을 다운로드해서 job-tmpl.yaml 파일로 저장한다. 다운로드할 내용은 다음과 같다.

apiVersion: batch/v1
kind: Job
metadata:
  name: process-item-$ITEM
  labels:
    jobgroup: jobexample
spec:
  template:
    metadata:
      name: jobexample
      labels:
        jobgroup: jobexample
    spec:
      containers:
      - name: c
        image: busybox:1.28
        command: ["sh", "-c", "echo Processing item $ITEM && sleep 5"]
      restartPolicy: Never
# job-tmpl.yaml를 다운로드하기 위해 curl을 사용한다
curl -L -s -O https://k8s.io/examples/application/job/job-tmpl.yaml

다운로드한 파일은 아직 유효한 쿠버네티스 매니페스트가 아니다. 대신 해당 템플릿은 사용하기 전에 채워야하는 자리 표시자가 있는 잡 오브젝트의 YAML 표현이다. $ITEM 구문은 쿠버네티스에 의미가 있지 않다.

템플릿에서 매니페스트 생성하기

다음의 셸 스니펫은 sed 를 사용하여 루프 변수로 $ITEM 문자열을 바꾸고, jobs 라는 임시 디렉터리에 기록한다. 다음과 같이 실행한다.

# 처리할 각 항목에 대해 하나씩, 템플릿을 여러 파일로 확장한다.
mkdir ./jobs
for i in apple banana cherry
do
  cat job-tmpl.yaml | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
done

작동하는지 확인한다.

ls jobs/

출력 결과는 다음과 비슷하다.

job-apple.yaml
job-banana.yaml
job-cherry.yaml

모든 유형의 템플릿 언어(예를 들어, Jinja2, ERB)를 사용하거나, 프로그램을 작성하여 잡 매니페스트를 생성할 수 있다.

매니페스트에서 잡 생성하기

다음으로, 하나의 kubectl 명령으로 모든 잡을 생성한다.

kubectl create -f ./jobs

출력 결과는 다음과 비슷하다.

job.batch/process-item-apple created
job.batch/process-item-banana created
job.batch/process-item-cherry created

이제, 작업을 확인한다.

kubectl get jobs -l jobgroup=jobexample

출력 결과는 다음과 비슷하다.

NAME                  COMPLETIONS   DURATION   AGE
process-item-apple    1/1           14s        22s
process-item-banana   1/1           12s        21s
process-item-cherry   1/1           12s        20s

kubectl 명령에 -l 옵션을 사용하면 이 잡 그룹의 일부인 잡만 선택된다(시스템에서 관련이 없는 다른 잡이 있을 수 있음).

파드도 동일한 레이블 셀렉터를 사용하여 확인할 수 있다.

kubectl get pods -l jobgroup=jobexample

출력 결과는 다음과 비슷하다.

NAME                        READY     STATUS      RESTARTS   AGE
process-item-apple-kixwv    0/1       Completed   0          4m
process-item-banana-wrsf7   0/1       Completed   0          4m
process-item-cherry-dnfu9   0/1       Completed   0          4m

이 단일 명령을 사용하여 모든 잡의 출력을 한 번에 확인할 수 있다.

kubectl logs -f -l jobgroup=jobexample

출력 결과는 다음과 같아야 한다.

Processing item apple
Processing item banana
Processing item cherry

정리

# 생성한 잡 제거
# 클러스터가 자동으로 잡의 파드들을 정리
kubectl delete job -l jobgroup=jobexample

고급 템플릿 파라미터 사용하기

첫 번째 예제에서, 템플릿의 각 인스턴스는 하나의 파라미터를 가지고, 해당 파라미터는 잡의 이름에도 사용되었다. 그러나, 이름은 특정 문자들만 포함하도록 제한된다.

이런 약간 더 복잡한 예제는 Jinja 템플릿 언어를 사용하여 각 잡에 대한 여러 파라미터로 매니페스트를 생성한 다음 해당 매니페스트에서 오브젝트를 생성한다.

태스크의 이 부분에서는, 한줄 파이썬 스크립트를 사용하여 매니페스트 집합으로 템플릿을 변환한다.

먼저, 다음의 잡 오브젝트 템플릿을 복사하고 붙여넣기하여, job.yaml.jinja2 파일로 저장한다.

{% set params = [{ "name": "apple", "url": "http://dbpedia.org/resource/Apple", },
                  { "name": "banana", "url": "http://dbpedia.org/resource/Banana", },
                  { "name": "cherry", "url": "http://dbpedia.org/resource/Cherry" }]
%}
{% for p in params %}
{% set name = p["name"] %}
{% set url = p["url"] %}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: jobexample-{{ name }}
  labels:
    jobgroup: jobexample
spec:
  template:
    metadata:
      name: jobexample
      labels:
        jobgroup: jobexample
    spec:
      containers:
      - name: c
        image: busybox:1.28
        command: ["sh", "-c", "echo Processing URL {{ url }} && sleep 5"]
      restartPolicy: Never
{% endfor %}

위의 템플릿은 파이썬 딕셔너리(dicts)로 구성된 항목(1-4행)을 사용하여 각 잡 오브젝트에 대해 두 개의 파라미터를 정의한다. for 루프는 각 파라미터의 집합(나머지 행)에 대해 하나의 잡 매니페스트를 방출한다.

이 예제는 YAML의 기능에 의존한다. 하나의 YAML 파일은 여러 문서(이 경우, 쿠버네티스 매니페스트)를 포함할 수 있으며, 행에 있는 --- 로 구분된다. 출력 결과를 kubectl 에 직접 파이프를 사용해 잡을 생성할 수 있다.

다음으로, 이 한 줄 파이썬 프로그램을 사용하여 템플릿을 확장한다.

alias render_template='python -c "from jinja2 import Template; import sys; print(Template(sys.stdin.read()).render());"'

render_template 을 사용해서 파라미터와 템플릿을 쿠버네티스 매니페스트가 포함된 하나의 YAML 파일로 변환한다.

# 앞에서 정의한 앨리어스(alias)가 필요하다
cat job.yaml.jinja2 | render_template > jobs.yaml

render_template 스크립트가 제대로 동작하는지 확인하기 위해 jobs.yaml 을 볼 수 있다.

render_template 스크립트가 원하는대로 동작하는 것을 확인했다면, 스크립트의 출력 결과를 파이프를 사용하여 kubectl 에 보낼 수 있다.

cat job.yaml.jinja2 | render_template | kubectl apply -f -

쿠버네티스는 생성한 잡을 수락하고 실행한다.

정리

# 생성한 잡 제거
# 클러스터가 자동으로 잡이 있던 파드를 정리
kubectl delete job -l jobgroup=jobexample

실제 워크로드에서 잡 사용하기

실제 유스케이스에서, 각 잡은 동영상의 프레임을 렌더링하거나, 데이터베이스에서 행 범위를 처리하는 것과 같은 상당한 규모의 계산을 수행한다. 동영상을 렌더링하는 경우 프레임 번호에 $ITEM 을 설정한다. 데이터베이스에서 행을 처리하는 경우, 처리할 데이터베이스 행의 범위를 나타내도록 $ITEM 을 설정한다.

이번 태스크에서, 로그를 가져와 파드에서 출력 결과를 수집하는 명령어를 실행했다. 실제 유스케이스에서, 잡의 각 파드는 완료하기 전에 출력 결과를 내구성있는 스토리지에 기록한다. 각 잡에 대해 퍼시스턴트볼륨(PersistentVolume)을 사용하거나 외장 스토리지 서비스를 사용할 수 있다. 예를 들어, 동영상의 프레임을 렌더링하는 경우, HTTP를 사용하여 렌더링된 프레임 데이터를 각 프레임에 대한 다른 URL을 사용해서 URL에 PUT 한다.

잡과 파드의 레이블

잡을 생성한 후, 쿠버네티스는 한 잡의 파드를 다른 잡의 파드와 구별하기 위해서 추가 레이블을 자동으로 추가한다.

이 예시에서, 각 잡과 잡의 파드 템플릿은 jobgroup=jobexample 레이블을 갖는다.

쿠버네티스 자체는 jobgroup 이라는 레이블에 신경쓰지 않는다. 템플릿에서 생성한 모든 잡에 대해 레이블을 설정하면 한번에 모든 잡을 편리하게 조작할 수 있다. 첫 번째 예제에서 템플릿을 사용해서 여러 잡을 생성했다. 템플릿은 각 파드도 동일한 레이블을 가질 수 있도록 보장하므로, 단일 명령어로 이러한 템플릿 기반 잡들의 모든 파드에서 확인할 수 있다.

대안

많은 수의 잡 오브젝트의 생성을 계획 중이라면, 아마도 다음의 사항을 파악하게 될 것이다.

  • 레이블을 사용해도, 너무 많은 잡을 관리하는 것은 번거롭다.
  • 일괄적으로 많은 잡을 생성하는 경우, 쿠버네티스 컨트롤 플레인에 높음 부하를 가할 수 있다. 대안으로, 쿠버네티스 API 서버가 속도를 제한하여 429 상태의 사용자 요청을 일시적으로 거부할 수 있다.
  • 사용자는 잡의 리소스 쿼터로 제한될 수 있다. 한번에 많은 작업을 생성하면 API 서버가 사용자의 요청 중 일부를 영구적으로 거부한다.

아주 많은 잡 오브젝트를 생성하지 않고 많은 양의 작업을 처리하는데 사용할 수 있는 다른 잡 패턴도 있다.

잡 오브젝트를 자동으로 관리하기 위해 자체 컨트롤러를 작성하는 것도 고려할 수 있다.