작업 큐(Work Queue)를 사용한 세밀한 병렬 처리

작업 큐(Work Queue)를 사용한 세밀한 병렬 처리 (Fine Parallel Processing Using a Work Queue)

이 예시에서는 각각 별도의 파드로 실행되는 워커 프로세스로 여러 병렬 작업을 실행하는 Kubernetes Job을 실행해요.

이 예시에서 각 파드는 생성될 때 작업 큐(task queue)에서 작업 단위 하나를 집어 처리하고, 큐의 끝에 도달할 때까지 반복해요.

이 예시의 단계 개요:

  1. 작업 큐를 담을 스토리지 서비스를 시작해요. 이 예시에서는 작업 항목을 저장하는 데 Redis를 사용해요. 이전 예시에서는 RabbitMQ를 사용했어요. 이 예시에서는 Redis와 커스텀 작업-큐 클라이언트 라이브러리를 사용해요. AMQP는 클라이언트가 유한 길이 작업 큐가 비어 있는 때를 감지할 좋은 방법을 제공하지 않기 때문이에요. 실무에서는 Redis 같은 저장소를 한 번 설정하고, 많은 작업의 작업 큐와 다른 것들에 재사용할 거예요.
  2. 큐를 만들고 메시지로 채워요. 각 메시지는 해야 할 작업 하나를 나타내요. 이 예시에서 메시지는 길게 계산할 정수예요.
  3. 큐의 작업을 처리하는 Job을 시작해요. Job은 여러 파드를 시작해요. 각 파드는 메시지 큐에서 작업 하나를 가져와 처리하고, 큐의 끝에 도달할 때까지 반복해요.

출처: 문서

본문

시작하기 전에

Kubernetes 클러스터가 있어야 하고 kubectl 명령줄 도구가 클러스터와 통신하도록 구성되어 있어야 해요. 이 튜토리얼은 컨트롤 플레인 호스트가 아닌 노드가 최소 두 개 있는 클러스터에서 실행하는 것을 권장해요. 아직 클러스터가 없다면 minikube를 이용해 만들거나, 아래 Kubernetes 플레이그라운드 중 하나를 사용할 수 있어요.

클러스터에서 실행할 이미지를 올릴 수 있는 컨테이너 이미지 레지스트리가 필요해요. 예시는 Docker Hub를 사용하지만, 다른 컨테이너 이미지 레지스트리에 맞출 수 있어요. 또한 이 작업 예시는 Docker가 로컬에 설치되어 있다고 가정해요. Docker를 사용해 컨테이너 이미지를 빌드해요.

Job의 기본적이고 비병렬적인 사용법에 익숙해야 해요.

Redis 시작

이 예시에서는 단순함을 위해 Redis의 단일 인스턴스를 시작해요. Redis를 확장 가능하고 중복성 있게 배포하는 예시는 Redis 예시를 참고해요.

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

Redis의 단일 인스턴스를 시작하려면 redis pod와 redis service를 만들어야 해요.

kubectl apply -f https://k8s.io/examples/application/job/redis/redis-pod.yaml
kubectl apply -f https://k8s.io/examples/application/job/redis/redis-service.yaml

큐를 작업으로 채우기

이제 큐를 몇 가지 "작업"으로 채워볼게요. 이 예시에서 작업은 출력될 문자열이에요.

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

이제 enter를 치고, 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 작업 큐 클라이언트 라이브러리가 제공돼요. Job의 각 파드에 있는 "worker" 프로그램은 작업 큐 클라이언트 라이브러리를 사용해 작업을 가져와요. 다음과 같아요.

#!/usr/bin/env python

import time
import rediswq

host="redis"
# Uncomment next two lines if you do not have CoreDNS 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.py, Dockerfile 파일을 다운로드해 컨테이너 이미지를 빌드할 수도 있어요. 다음은 Docker로 이미지 빌드를 하는 예시예요.

docker build -t job-wq-2 .

이미지 밀어 넣기

Docker Hub의 경우, 애플리케이션 이미지에 사용자 이름으로 태그를 붙이고 아래 명령으로 Hub에 밀어 넣어요. <username>을 Hub 사용자 이름으로 바꿔요.

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

퍼블릭 저장소로 밀어 넣거나, 클러스터가 프라이빗 저장소에 접근할 수 있도록 구성해야 해요.

Job 정의

다음은 만들 Job의 매니페스트예요.

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

이 예시에서 각 파드는 큐의 여러 항목을 작업하고 더 이상 항목이 없을 때 종료돼요. 워커들이 스스로 작업 큐가 비었는지 감지하고, Job 컨트롤러는 작업 큐를 알지 못하므로, 워커가 작업을 마쳤을 때 신호를 보내는 것에 의존해요. 워커는 성공으로 종료함으로써 큐가 빈 것을 알려요. 그래서 어떤 워커가 성공으로 종료되는 즉시 컨트롤러는 작업이 끝났고 파드들이 곧 종료될 것임을 알아요. 따라서 Job의 완료 수(completion count)를 설정하지 않은 채 두어야 해요. Job 컨트롤러는 다른 파드들도 완료할 때까지 기다릴 거예요.

Job 실행

이제 Job을 실행해요.

# this assumes you downloaded and then edited the manifest already
kubectl apply -f ./job.yaml

조금 기다린 후 Job을 확인해요.

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 2022 17:07:59 +0000
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:              container-registry.example/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

타임아웃으로 Job이 성공할 때까지 기다릴 수 있어요.

# The check for condition name is case insensitive
kubectl wait --for=condition=complete --timeout=300s job/job-wq-2
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

보시다시피, 이 Job의 파드 하나가 여러 작업 단위를 작업했어요.

대안 (Alternatives)

큐 서비스를 실행하거나 컨테이너를 작업 큐를 사용하도록 수정하는 것이 불편하다면, 다른 job 패턴 중 하나를 고려할 수 있어요.

실행할 지속적인 백그라운드 처리 작업 스트림이 있다면, 대신 ReplicaSet으로 백그라운드 워커를 실행하는 것과, resque 같은 백그라운드 처리 라이브러리를 실행하는 것을 고려해봐요.

더 알아보기 (Learn more)