병렬 워크 큐로 Job 실행하기

병렬 워크 큐로 Job 실행하기 (Coarse Parallel Processing Using a Work Queue)

이 예시에서는 병렬 워커 프로세스 여러 개로 쿠버네티스 Job을 실행할 거예요.

이 예시에서 각 파드는 생성될 때 작업 큐에서 작업 한 단위를 가져와 완료하고, 큐에서 삭제한 뒤 종료됩니다.

이 예시의 단계는 다음과 같습니다:

  1. 메시지 큐 서비스를 시작한다. 이 예시에서는 RabbitMQ를 사용하지만 다른 것을 써도 됩니다. 실제로는 메시지 큐 서비스를 한 번 설정하고 여러 Job에 재사용하게 됩니다.
  2. 큐를 만들고 메시지로 채운다. 각 메시지는 처리할 작업 하나를 나타냅니다. 이 예시에서 메시지는 긴 계산을 수행할 정수입니다.
  3. 큐의 작업을 처리하는 Job을 시작한다. Job은 파드 여러 개를 시작합니다. 각 파드는 메시지 큐에서 작업 하나를 가져와 처리하고 종료합니다.

출처: 문서

본문

시작하기 전에

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

클러스터에서 실행할 이미지를 업로드할 수 있는 컨테이너 이미지 레지스트리가 필요합니다.

이 작업 예시는 로컬에 Docker가 설치되어 있다고 가정합니다.

메시지 큐 서비스 시작하기

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

실제로는 클러스터에 메시지 큐 서비스를 한 번 설정하고, 여러 Job과 장기 실행 서비스에 재사용할 수 있습니다.

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

# make a Service for the StatefulSet to use
kubectl create -f https://kubernetes.io/examples/application/job/rabbitmq/rabbitmq-service.yaml
service "rabbitmq-service" created
kubectl create -f https://kubernetes.io/examples/application/job/rabbitmq/rabbitmq-statefulset.yaml
statefulset "rabbitmq" created

메시지 큐 서비스 테스트하기

이제 메시지 큐에 접근하는 것을 실험할 수 있어요. 임시 대화형 파드를 만들고, 그 위에 몇 가지 도구를 설치하고, 큐를 실험할 거예요.

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

# Create a temporary interactive container
kubectl run -i --tty temp --image ubuntu:22.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를 설치한다. 다음 명령은 그 파드 안의 대화형 셸에서 실행해야 하는 것들을 보여줍니다:

apt-get update && apt-get install -y curl ca-certificates amqp-tools python3 dnsutils

나중에는 이런 패키지를 포함하는 컨테이너 이미지를 만들게 됩니다.

다음으로 RabbitMQ의 Service를 발견할 수 있는지 확인한다:

# Run these commands inside the Pod
# Note the rabbitmq-service has a DNS name, provided by Kubernetes:
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

(IP 주소는 달라질 것입니다)

클러스터 DNS 애드온이 올바르게 설정되지 않았다면 이전 단계가 동작하지 않을 수 있어요. 그 Service의 IP 주소를 환경 변수에서도 찾을 수 있습니다:

# run this check inside the Pod
env | grep RABBITMQ_SERVICE | grep HOST
RABBITMQ_SERVICE_SERVICE_HOST=10.0.147.152

(IP 주소는 달라질 것입니다)

다음으로 큐를 만들고, 메시지를 게시·소비할 수 있는지 확인한다.

# Run these commands inside the Pod
# In the next line, rabbitmq-service is the hostname where the rabbitmq-service
# can be reached.  5672 is the standard port for rabbitmq.
export BROKER_URL=amqp://guest:***@rabbitmq-service:5672
# If you could not resolve "rabbitmq-service" in the previous step,
# then use this command instead:
BROKER_URL=amqp://guest:***@$RABBITMQ_SERVICE_SERVICE_HOST:5672

# Now create a queue:
/usr/bin/amqp-declare-queue --url=$BROKER_URL -q foo -d
foo

큐에 메시지 하나를 게시한다:

/usr/bin/amqp-publish --url=$BROKER_URL -r foo -p -b Hello

# And get it back.
/usr/bin/amqp-consume --url=$BROKER_URL -q foo -c 1 cat && echo 1>&2
Hello

마지막 명령에서 amqp-consume 도구는 큐에서 메시지 하나(-c 1)를 가져와 임의의 명령의 표준 입력으로 전달합니다. 이 경우 cat 프로그램이 표준 입력에서 읽은 문자를 출력하고, echo가 캐리지 리턴을 추가해 예시를 읽을 수 있게 합니다.

큐를 작업으로 채우기

이제 큐를 몇 가지 시뮬레이션된 작업으로 채운다. 이 예시에서 작업은 출력할 문자열입니다.

실제로 메시지의 내용은 다음과 같을 수 있습니다:

  • 처리해야 할 파일의 이름
  • 프로그램의 추가 플래그
  • 데이터베이스 테이블의 키 범위
  • 시뮬레이션의 구성 파라미터
  • 렌더링할 장면의 프레임 번호

Job의 모든 파드가 읽기 전용으로 필요로 하는 큰 데이터가 있다면, 보통 그런 데이터를 NFS 같은 공유 파일시스템에 두고 모든 파드에 읽기 전용으로 마운트하거나, 파드 안의 프로그램이 클러스터 파일시스템(예: HDFS)에서 기본적으로 데이터를 읽도록 작성합니다.

이 예시에서는 AMQP 명령줄 도구를 사용해 큐를 만들고 채울 거예요. 실제로는 AMQP 클라이언트 라이브러리를 사용해 큐를 채우는 프로그램을 작성할 수도 있습니다.

# Run this on your computer, not in the Pod
/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개의 메시지를 추가했습니다.

컨테이너 이미지 만들기

이제 Job으로 실행할 이미지를 만들 준비가 됐어요.

Job은 amqp-consume 유틸리티를 사용해 큐에서 메시지를 읽고 실제 작업을 실행할 거예요. 다음은 매우 간단한 예시 프로그램입니다:

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

chmod +x worker.py

이제 이미지를 빌드한다. 임시 디렉터리를 만들고 그곳으로 이동해 Dockerfileworker.py를 다운로드한다. 어떤 경우든 다음 명령으로 이미지를 빌드한다:

docker build -t job-wq-1 .

Docker Hub의 경우, 아래 명령으로 앱 이미지에 사용자 이름으로 태그를 붙이고 Hub에 푸시한다. <username>을 자신의 Hub 사용자 이름으로 바꾸세요.

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

대체 컨테이너 이미지 레지스트리를 사용 중이라면 이미지에 태그를 붙이고 그곳에 푸시하세요.

Job 정의하기

다음은 Job의 매니페스트입니다. Job 매니페스트의 사본을 만들고(./job.yaml), 사용한 이름과 일치하도록 컨테이너 이미지의 이름을 편집해야 합니다.

이 예시에서 각 파드는 큐의 항목 하나를 처리한 뒤 종료됩니다. 그래서 Job의 완료 수(completion count)는 완료된 작업 항목 수에 해당합니다. 그래서 예시 매니페스트는 .spec.completions8로 설정합니다.

Job 실행하기

이제 Job을 실행한다:

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

타임아웃을 두고 Job이 성공할 때까지 기다릴 수 있어요:

# The check for condition name is case insensitive
kubectl wait --for=condition=complete --timeout=300s job/job-wq-1

다음으로 Job을 확인한다:

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 2022 16:42:02 +0000
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:      container-registry.example/causal-jigsaw-637/job-wq-1
    Port:
    Environment:
      BROKER_URL:       amqp://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

그 Job의 모든 파드가 성공했습니다! 끝났어요.

대안

이 접근 방식의 장점은 "worker" 프로그램이 워크 큐가 있다는 것을 알도록 수정할 필요가 없다는 점이에요. worker 프로그램을 수정 없이 컨테이너 이미지에 포함할 수 있습니다.

이 접근 방식을 사용하려면 메시지 큐 서비스를 실행해야 합니다. 큐 서비스 실행이 불편하다면 다른 job 패턴 중 하나를 고려할 수 있어요.

이 접근 방식은 작업 항목마다 파드를 하나씩 만듭니다. 그러나 작업 항목이 몇 초밖에 걸리지 않는다면 작업 항목마다 파드를 만드는 것은 오버헤드가 많을 수 있어요. fine parallel work queue 예시처럼 파드당 여러 작업 항목을 실행하는 다른 설계를 고려하세요.

이 예시에서는 amqp-consume 유틸리티를 사용해 큐에서 메시지를 읽고 실제 프로그램을 실행했습니다. 이는 프로그램이 큐를 인지하도록 수정할 필요가 없다는 장점이 있어요. fine parallel work queue 예시는 클라이언트 라이브러리로 워크 큐와 통신하는 방법을 보여줍니다.

주의사항

완료 수가 큐의 항목 수보다 적게 설정되면 모든 항목이 처리되지 않습니다.

완료 수가 큐의 항목 수보다 많게 설정되면, 큐의 모든 항목이 처리됐더라도 Job이 완료된 것으로 보이지 않습니다. Job이 추가 파드를 시작하고 그들은 메시지를 기다리며 차단될 것입니다. 작업이 있을 때를 알아차리고 큐의 크기를 측정해 완료 수를 일치시키도록 하는 자신만의 메커니즘이 필요합니다.

이 패턴에는 드물게 경쟁 조건(race)이 있습니다. amqp-consume 명령이 메시지를 확인(acknowledge)한 시점과 컨테이너가 성공으로 종료되는 시점 사이에 컨테이너가 죽거나, 노드가 파드의 성공을 API 서버에 다시 게시하기 전에 노드가 크래시하면, 큐의 모든 항목이 처리됐더라도 Job이 완료된 것으로 보이지 않습니다.

더 알아보기 (Learn more)