[ Infra ]/Data Platform

[Data Platform] Apache Spark 구축 가이드(Spark Operator + Delta Lake)

환이s 2026. 6. 3. 02:16
728x90
반응형


Intro

 

안녕하세요. 환이s입니다 👋

 

지난 글에서는

데이터 레이크하우스 플랫폼 기반 스토리지인 Rook-Ceph를 Kubernetes 위에 구축하는 과정을 정리해 봤습니다.

 

이번 글에서는

그 위에 올라가는 두 번째 스택,

Apache Spark를 Kubernetes 환경에 구축하는 과정을 정리해보려고 합니다.

Spark를 Kubernetes 위에서 운영하다 보면

단순히 Job을 실행하는 것에서 끝나는 게 아니라,

  • Operator가 SparkApplication 리소스를 감시하고 Driver/Executor Pod를 자동으로 관리하고
  • 실행된 Job의 이벤트 로그가 Ceph S3에 저장되고
  • History Server가 그 로그를 읽어서 실행 이력을 보여주고
  • Delta Lake 포맷으로 Ceph S3에 데이터를 읽고 쓰는

전체 흐름이 유기적으로 연결되어야 합니다.

 

이번 글에서는

  • spark-operator 아키텍처가 어떻게 동작하는지
  • 실제로 어떤 순서로 설치하면 되는지
  • Delta Lake + Ceph S3 연동을 어떻게 구성하는지
  • Spark History Server와 File Browser까지 어떻게 구성하는지

까지 순서대로 확인해 보겠습니다.


1. spark-operator 아키텍처 개요

 

본격적인 설치에 앞서,

spark-operator가 어떤 구조로 동작하는지 먼저 이해하는 것이 중요합니다.

 

spark-operator는

Kubernetes의 Custom Resource를 활용해서

Spark Job을 선언형으로 관리할 수 있게 해주는 오퍼레이터입니다.

 

동작 흐름은 아래와 같습니다.

사용자가 SparkApplication yaml을 apply
        ↓
spark-operator가 SparkApplication 리소스 감지
        ↓
Driver Pod 생성
        ↓
Driver가 Executor Pod 생성 요청
        ↓
Executor Pod 생성 → Job 실행
        ↓
이벤트 로그 → Ceph S3 저장
        ↓
History Server → S3 로그 읽어서 UI 제공

 

이번 구성에서 사용하는 주요 CRD는 아래 3가지입니다.

 

CRD 역할
SparkApplication 단발성 Spark Job 정의
ScheduledSparkApplication 주기적으로 실행되는 Spark Job 정의
SparkConnect Spark Connect 서버 정의

 

즉, 여기서 확인할 수 있는 핵심은 spark-operator를 사용하면

복잡한 Spark Job 관리를 Kubernetes 리소스 선언 방식으로 단순화할 수 있다는 점입니다.


2. 설치 순서 

 

이번 구축에서 사용하는 매니페스트 디렉토리 구조는 아래와 같습니다.

manifests/
├── crds/        # SparkApplication CRD 등록 (공식 파일 그대로 사용)
├── spark/       # Namespace 설정
├── operator/    # Spark Operator 설치
├── rbac/        # Job 실행용 RBAC + S3 Secret
├── history/     # Spark History Server
└── streaming/   # Delta Lake Streaming Job + File Browser + PVC

 

설치 순서는 아래와 같습니다.

crds → spark(namespace) → operator → ⏳ → rbac → streaming(PVC/FileBrowser) → history → streaming(Job)

 

주의해야 할 포인트는 두 가지입니다.

 

첫 번째로 Operator Pod가 Running 상태가 된 이후에

SparkApplication을 apply해야 정상적으로 Job이 실행됩니다.

 

두 번째로 History Server를 apply하기 전에

Ceph S3에 spark-event-logs 버킷이 미리 생성되어 있어야 합니다.

버킷이 없으면 History Server가 기동 시 로그 디렉토리를 찾지 못해 오류가 발생합니다.

# spark-event-logs 버킷 사전 생성
aws s3 mb s3://spark-event-logs \
  --endpoint-url http://<노드IP>:30511 \
  --no-verify-ssl

3. Namespace 설정

 

이번 구성에서는 Namespace를 두 개로 분리해서 운영합니다.

# spark_namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: spark
# spark_operator_namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: spark-operator
kubectl apply -f spark/

 

 

Namespace  용도
spark SparkApplication Job 실행, History Server, File Browser
spark-operator Operator Deployment, RBAC

 

Namespace를 분리하는 이유는

단순히 리소스를 나누는 것에서 끝나지 않습니다.

 

Operator 설정에서 --namespaces=spark,airflow처럼

감시할 Namespace를 명시적으로 지정할 수 있기 때문에

Operator가 올라가는 공간과 Job이 실행되는 공간을 분리해두면

권한 범위와 감시 범위를 명확하게 관리할 수 있습니다.

 

즉, 여기서 확인할 수 있는 핵심은

Namespace 분리가 이후 Airflow 등 다른 스택과 연동할 때도

Operator 감시 범위를 유연하게 확장할 수 있는 기반이 된다는 점입니다.


4. CRD 등록

 

CRD는 Spark Operator 공식 GitHub에서 제공하는 파일을 그대로 사용합니다.

crds/
├── 10_scheduledsparkapplications.yaml
├── 20_sparkapplications.yaml
└── 30_sparkconnects.yaml

 

공식 파일은 아래 GitHub에서 확인할 수 있습니다.

https://github.com/kubeflow/spark-operator/tree/master/charts/spark-operator-chart/crds

 

spark-operator/charts/spark-operator-chart/crds at master · kubeflow/spark-operator

Kubernetes operator for managing the lifecycle of Apache Spark applications on Kubernetes. - kubeflow/spark-operator

github.com

 

kubectl apply -f crds/

 

CRD가 정상적으로 등록되었는지 확인합니다.

kubectl get crd | grep spark

 

아래처럼 3개가 등록되면 정상입니다.

scheduledsparkapplications.sparkoperator.k8s.io   ...
sparkapplications.sparkoperator.k8s.io            ...
sparkconnects.sparkoperator.k8s.io                ...

 

즉, 여기서 확인할 수 있는 핵심은

CRD가 먼저 등록되어야 이후 SparkApplication yaml을 apply 할 때

Kubernetes가 해당 리소스 타입을 인식할 수 있다는 점입니다.


5. Operator 설치

 

Operator 관련 파일은 아래 순서로 구성되어 있습니다.

operator/
├── 10_spark-operator-account.yaml       # ServiceAccount
├── 20_spark-operator-clusterRole.yaml   # ClusterRole
├── 30_spark-operator-clusterRoleBinding.yaml
├── 40_spark-operator-deployment.yaml    # Operator Deployment
├── 50_spark-operator-pdb.yaml           # PodDisruptionBudget
└── 60_spark-operator-service.yaml       # metrics / health Service

 

ServiceAccount / ClusterRole / ClusterRoleBinding

apiVersion: v1
kind: ServiceAccount
metadata:
  name: spark-operator
  namespace: spark-operator
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: spark-operator
rules:
  - apiGroups: ["sparkoperator.k8s.io"]
    resources:
      - sparkapplications
      - sparkapplications/status
      - scheduledsparkapplications
      - scheduledsparkapplications/status
      - sparkconnects
      - sparkconnects/status
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources:
      - pods
      - pods/log
      - services
      - configmaps
      - secrets
      - events
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["coordination.k8s.io"]
    resources: ["leases"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: spark-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: spark-operator
subjects:
  - kind: ServiceAccount
    name: spark-operator
    namespace: spark-operator

 

ClusterRole을 사용하는 이유는

Operator가 spark, airflow 등 여러 Namespace의 SparkApplication 리소스를 동시에 감시해야 하기 때문입니다.


Operator Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spark-operator
  namespace: spark-operator
  labels:
    app: spark-operator
spec:
  replicas: 2
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: spark-operator
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
  template:
    metadata:
      labels:
        app: spark-operator
    spec:
      serviceAccountName: spark-operator
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchLabels:
                    app: spark-operator
      containers:
        - name: controller
          image: harbor.local/library/spark-operator-controller:2.4.0
          imagePullPolicy: IfNotPresent
          args:
            - controller
            - start
            - --namespaces=spark,airflow
            - --leader-election=true
            - --leader-election-lock-namespace=spark-operator
            - --leader-election-lock-name=spark-operator-lock
            - --enable-metrics=true
            - --metrics-bind-address=:8080
            - --health-probe-bind-address=:8081
          ports:
            - name: metrics
              containerPort: 8080
            - name: health
              containerPort: 8081
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: 2000m
              memory: 2Gi
          startupProbe:
            httpGet:
              path: /healthz
              port: 8081
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8081
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8081
            initialDelaySeconds: 15
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6

 

주요 설정 포인트는 아래와 같습니다.

 

HA 구성

replicas: 2
--leader-election=true
--leader-election-lock-namespace=spark-operator

 

Operator를 2개 띄우고

Leader Election을 활성화해서 한쪽이 장애가 나도 나머지가 즉시 Active로 전환됩니다.

 

다중 Namespace 감시

--namespaces=spark,airflow

 

spark와 airflow 두 Namespace의 SparkApplication을 동시에 감시합니다.

이후 Airflow 편에서 SparkApplication을 트리거할 때 이 설정이 필요합니다.

 

podAntiAffinity

podAntiAffinity:
  preferredDuringSchedulingIgnoredDuringExecution:
    - topologyKey: kubernetes.io/hostname

 

Operator Pod 2개가 서로 다른 노드에 배치되도록 유도합니다.

 

Probe 설정

startupProbe:
  failureThreshold: 30  # 최대 150초(5s * 30)까지 스타트업 허용
livenessProbe:
  initialDelaySeconds: 30
  failureThreshold: 6

 

startupProbe의 failureThreshold: 30

컨테이너가 완전히 뜨기 전에 livenessProbe가 먼저 실패해서 Pod가 재시작되는 것을 방지합니다.


PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: spark-operator-pdb
  namespace: spark-operator
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: spark-operator

 

노드 드레인이나 업그레이드 시에도

Operator Pod가 최소 1개는 항상 Running 상태를 유지하도록 보장합니다.


Service (metrics / health)

apiVersion: v1
kind: Service
metadata:
  name: spark-operator
  namespace: spark-operator
spec:
  selector:
    app: spark-operator
  ports:
    - name: health
      port: 8081
      targetPort: 8081
    - name: metrics
      port: 8080
      targetPort: 8080

 

이후 Prometheus 구축 편에서

이 Service의 metrics 포트를 통해 Spark Operator 메트릭을 수집할 예정입니다.

kubectl apply -f operator/

 

Operator Pod가 정상적으로 올라왔는지 확인합니다.

kubectl get pods -n spark-operator -w

 

아래처럼 2개가 Running 상태가 되면 다음 단계로 진행합니다.

NAME                             READY   STATUS    RESTARTS   AGE
spark-operator-xxxxxxxxxx-xxxxx  1/1     Running   0          ...
spark-operator-xxxxxxxxxx-xxxxx  1/1     Running   0          ...

 

즉, 여기서 확인할 수 있는 핵심은

Operator Pod 2개가 모두 Running 상태가 된 이후에만

SparkApplication 리소스를 정상적으로 처리할 수 있다는 점입니다.


6. RBAC 설정 : spark-sa와 S3 Secret

 

spark-sa ClusterRole 설정

# spark_job_rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: spark-sa
  namespace: spark
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: spark-driver-role
rules:
  - apiGroups: [""]
    resources:
      - pods
      - pods/log
      - services
      - configmaps
      - secrets
      - events
      - persistentvolumeclaims
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"]
  - apiGroups: ["sparkoperator.k8s.io"]
    resources:
      - sparkapplications
      - sparkapplications/status
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: spark-driver-rb
subjects:
  - kind: ServiceAccount
    name: spark-sa
    namespace: spark
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: spark-driver-role

 

⚠️ spark-sa를 ClusterRole로 설정한 이유

처음에는 일반 Role로 구성했으나, Driver Pod가 실행 중에 SparkApplication 리소스를 조회하려고 할 때 아래와 같은 403 에러가 발생했습니다.

User "system:serviceaccount:spark:spark-sa" cannot get resource
"sparkapplications" in API group "sparkoperator.k8s.io"

SparkApplication은 클러스터 전체 범위의 리소스이기 때문에 Namespace 범위인 Role로는 접근이 불가능합니다. 이 문제를 해결하기 위해 ClusterRole로 격상했습니다.


S3 Secret 설정

# spark_secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: spark-sa-token
  namespace: spark
  annotations:
    kubernetes.io/service-account.name: spark-sa
type: kubernetes.io/service-account-token

💡 운영 환경 팁: 현재 SparkApplication의 sparkConf에 S3 AccessKey / SecretKey가 평문으로 작성되어 있습니다. 운영 환경에서는 아래처럼 Kubernetes Secret으로 분리해서 관리하는 것을 권장합니다.

apiVersion: v1
kind: Secret
metadata:
  name: ceph-s3-credentials
  namespace: spark
stringData:
  access-key: "YOUR_ACCESS_KEY"
  secret-key: "YOUR_SECRET_KEY"
kubectl apply -f rbac/

 

즉, 여기서 확인할 수 있는 핵심은

Driver Pod가 SparkApplication 리소스를 조회하려면 ClusterRole 수준의 권한이 필요하다는 점입니다.


7. Spark History Server 구성

 

Spark History Server는 완료된 Job의 실행 이력을

Ceph S3에 저장된 이벤트 로그를 읽어서 UI로 제공합니다.

 

History Server를 apply하기 전에

반드시 Ceph S3에 spark-event-logs 버킷이 생성되어 있어야 합니다.

# 버킷 사전 생성 (Ceph RGW NodePort 사용)
aws s3 mb s3://spark-event-logs \
  --endpoint-url http://<노드IP>:30511 \
  --no-verify-ssl

# 버킷 생성 확인
aws s3 ls \
  --endpoint-url http://<노드IP>:30511 \
  --no-verify-ssl

 

버킷이 확인되면 History Server를 배포합니다.

 

# spark-history-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spark-history-server
  namespace: spark
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spark-history-server
  template:
    metadata:
      labels:
        app: spark-history-server
    spec:
      containers:
        - name: spark-history-server
          image: harbor.local/library/spark-streaming-custom:3.5.1-v5
          command:
            - /opt/spark/bin/spark-class
            - org.apache.spark.deploy.history.HistoryServer
          env:
            - name: SPARK_HISTORY_OPTS
              value: >-
                -Dspark.history.fs.logDirectory=s3a://spark-event-logs/
                -Dspark.history.ui.port=18080
                -Dspark.history.fs.update.interval=10s
                -Dspark.hadoop.fs.s3a.impl=org.apache.hadoop.fs.s3a.S3AFileSystem
                -Dspark.hadoop.fs.s3a.endpoint=http://rook-ceph-rgw-ceph-store.rook-ceph.svc:80
                -Dspark.hadoop.fs.s3a.access.key=YOUR_ACCESS_KEY
                -Dspark.hadoop.fs.s3a.secret.key=YOUR_SECRET_KEY
                -Dspark.hadoop.fs.s3a.path.style.access=true
                -Dspark.hadoop.fs.s3a.connection.ssl.enabled=false
                -Dspark.hadoop.fs.s3a.aws.credentials.provider=org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider
                -Dspark.hadoop.fs.s3a.metadatastore.impl=org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore
                -Dspark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension
                -Dspark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog
          ports:
            - containerPort: 18080
          resources:
            requests:
              cpu: 200m
              memory: 512Mi
            limits:
              cpu: 500m
              memory: 2Gi
---
# spark-history-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: spark-history-server
  namespace: spark
spec:
  selector:
    app: spark-history-server
  ports:
    - port: 18080
      targetPort: 18080
      nodePort: 30112
  type: NodePort

 

주요 설정 포인트는 아래와 같습니다.

-Dspark.history.fs.logDirectory=s3a://spark-event-logs/

 

SparkApplication에서 spark.eventLog.dir

지정한 경로와 동일해야 합니다. History Server가 이 경로에서 이벤트 로그를 읽어옵니다.

 

-Dspark.history.fs.update.interval=10s

 

10초마다 S3에서 새로운 로그를 폴링합니다.

Job이 완료된 후 최대 10초 내에 History Server UI에 반영됩니다.

 

kubectl apply -f history/

 

History Server UI는 아래 주소로 접근할 수 있습니다.

http://<노드IP>:30112

 

즉, 여기서 확인할 수 있는 핵심은

SparkApplicationspark.eventLog.dir

History ServerlogDirectory동일한 S3 경로를 가리켜야 하고,

해당 버킷이 미리 생성되어 있어야 완료된 Job의 실행 이력이 정상적으로 보인다는 점입니다.


8. File Browser + PVC 구성

 

File Browser는 CephFS PVC에 저장된

Python 스크립트 파일을 웹 UI로 관리할 수 있는 도구입니다.

 

# spark-file-browser.yaml

# Spark 스크립트 저장용 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: spark-data-pvc
  namespace: spark
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: rook-cephfs
  resources:
    requests:
      storage: 1Gi
---
# File Browser 설정 저장용 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: spark-filebrowser-config-pvc
  namespace: spark
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: rook-cephfs
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ceph-explorer
  namespace: spark
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ceph-explorer
  template:
    metadata:
      labels:
        app: ceph-explorer
    spec:
      securityContext:
        runAsUser: 185
        fsGroup: 185
      containers:
        - name: explorer
          image: harbor.local/library/filebrowser:latest
          ports:
            - containerPort: 80
          args: ["--database", "/config/database.db", "--noauth"]
          volumeMounts:
            - name: spark-data
              mountPath: /srv/spark
            - name: fb-config
              mountPath: /config
      volumes:
        - name: spark-data
          persistentVolumeClaim:
            claimName: spark-data-pvc
        - name: fb-config
          persistentVolumeClaim:
            claimName: spark-filebrowser-config-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: ceph-explorer-svc
  namespace: spark
spec:
  type: NodePort
  ports:
    - port: 80
      nodePort: 30071
  selector:
    app: ceph-explorer

 

주요 포인트는 아래와 같습니다.

 

PVC storageClassName

storageClassName: rook-cephfs
accessModes:
  - ReadWriteMany

 

CephFS StorageClass를 사용해서 RWX 모드로 PVC를 생성합니다.

Driver Pod와 Executor Pod 모두 동일한 PVC를 동시에 마운트 할 수 있습니다.


SparkApplication과 PVC 연결

 

SparkApplication에서 이 PVC를 아래처럼 마운트 해서

Driver와 Executor 모두 동일한 Python 스크립트에 접근합니다.

sparkConf:
  "spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
  "spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"
  "spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
  "spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"

 

File Browser UI는 아래 주소로 접근할 수 있습니다.

http://<노드IP>:30071

 

즉, 여기서 확인할 수 있는 핵심은

CephFSRWX 특성 덕분에 Driver / Executor / File Browser 세 곳이 동일한 PVC를 동시에 마운트해서 스크립트를 공유할 수 있다는 점입니다.


9. Delta Lake Streaming Job 실행

 

이제 실제 SparkApplication을 실행합니다.

 

이번 구성에서는 Kafka에서 syslog를 읽어서

Delta Lake 포맷으로 Ceph S3에 저장하는 Streaming Job입니다.

# spark-streaming-job.yaml
apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
  name: streaming-job-test
  namespace: spark
spec:
  type: Python
  sparkVersion: "3.5.1"
  pythonVersion: "3"
  mode: cluster
  image: "harbor.local/library/spark-streaming-custom:3.5.1-v5"
  mainApplicationFile: "local:///opt/scripts/syslog_consumer.py"
  restartPolicy:
    type: Never
  timeToLiveSeconds: 600
  sparkConf:
    # Delta Lake 설정
    "spark.sql.extensions": "io.delta.sql.DeltaSparkSessionExtension"
    "spark.sql.catalog.spark_catalog": "org.apache.spark.sql.delta.catalog.DeltaCatalog"

    # Ceph S3 접속 설정
    "spark.hadoop.fs.s3a.impl": "org.apache.hadoop.fs.s3a.S3AFileSystem"
    "spark.hadoop.fs.s3a.endpoint": "http://rook-ceph-rgw-ceph-store.rook-ceph.svc:80"
    "spark.hadoop.fs.s3a.access.key": "YOUR_ACCESS_KEY"
    "spark.hadoop.fs.s3a.secret.key": "YOUR_SECRET_KEY"
    "spark.hadoop.fs.s3a.path.style.access": "true"
    "spark.hadoop.fs.s3a.connection.ssl.enabled": "false"
    "spark.hadoop.fs.s3a.aws.credentials.provider": "org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider"

    # S3Guard 비활성화
    "spark.hadoop.fs.s3a.metadatastore.impl": "org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore"

    # 이벤트 로그 설정
    "spark.eventLog.enabled": "true"
    "spark.eventLog.dir": "s3a://spark-event-logs/"
    "spark.eventLog.compress": "false"

    # S3A 업로드 최적화
    "spark.hadoop.fs.s3a.multipart.size": "104857600"
    "spark.hadoop.fs.s3a.fast.upload": "true"
    "spark.hadoop.fs.s3a.fast.upload.buffer": "bytebuffer"

    # 처리량 대비 설정
    "spark.hadoop.fs.s3a.threads.max": "50"
    "spark.hadoop.fs.s3a.connection.maximum": "100"

    # PVC 볼륨 마운트
    "spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
    "spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"
    "spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
    "spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"

  driver:
    cores: 1
    memory: "2g"
    memoryOverhead: "1g"
    serviceAccount: spark-sa

  executor:
    cores: 1
    instances: 3
    memory: "2g"
    memoryOverhead: "2g"

 

주요 설정 포인트를 섹션별로 살펴보겠습니다.

 

mainApplicationFile

mainApplicationFile: "local:///opt/scripts/syslog_consumer.py"

 

local://은 Driver Pod 내부 파일 시스템 경로를 가리킵니다.

 

앞서 PVC를 /opt/scripts에 마운트했기 때문에

File Browser로 업로드한 Python 스크립트를 이 경로에서 바로 실행할 수 있습니다.


Delta Lake 카탈로그 설정

"spark.sql.extensions": "io.delta.sql.DeltaSparkSessionExtension"
"spark.sql.catalog.spark_catalog": "org.apache.spark.sql.delta.catalog.DeltaCatalog"

 

Delta Lake 포맷을 사용하려면 이 두 설정이 반드시 필요합니다.


Ceph S3 엔드포인트

"spark.hadoop.fs.s3a.endpoint": "http://rook-ceph-rgw-ceph-store.rook-ceph.svc:80"
"spark.hadoop.fs.s3a.path.style.access": "true"

 

Ceph RGW의 클러스터 내부 서비스 주소를 사용합니다.

path.style.access: true는 Ceph RGW가 가상 호스팅 방식 대신 경로 방식의 S3 URL을 사용하기 때문에 반드시 설정해야 합니다.


S3A 업로드 최적화

"spark.hadoop.fs.s3a.multipart.size": "104857600"  # 100MB 단위 멀티파트
"spark.hadoop.fs.s3a.fast.upload": "true"
"spark.hadoop.fs.s3a.threads.max": "50"
"spark.hadoop.fs.s3a.connection.maximum": "100"

 

Streaming Job은 지속적으로 데이터를 S3에 쓰기 때문에 멀티파트 업로드와 커넥션 풀 설정이 성능에 영향을 줍니다.


Executor 구성

executor:
  cores: 1
  instances: 3
  memory: "2g"
  memoryOverhead: "2g"

 

memoryOverhead는 JVM 외부 메모리(Python 프로세스, 버퍼 등)를 위한 설정입니다.

Python 기반 Streaming Job은 overhead를 넉넉하게 잡는 것이 안정적입니다.

 

kubectl apply -f streaming/

 

Job이 정상적으로 실행되는지 확인합니다.

kubectl get sparkapplication -n spark

 

아래처럼 RUNNING 상태가 되면 정상입니다.

NAME                  STATUS    ATTEMPTS   START                  FINISH   AGE
streaming-job-test    RUNNING   1          2024-xx-xxTxx:xx:xxZ   <nil>    ...

 

Driver와 Executor Pod 상태도 확인합니다.

kubectl get pods -n spark
NAME                                    READY   STATUS    RESTARTS   AGE
streaming-job-test-driver               1/1     Running   0          ...
streaming-job-test-exec-1               1/1     Running   0          ...
streaming-job-test-exec-2               1/1     Running   0          ...
streaming-job-test-exec-3               1/1     Running   0          ...

 

Spark UI는 아래 NodePort로 실시간 모니터링할 수 있습니다.

# spark-streaming-job-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: streaming-job-test-ui-nodeport
  namespace: spark
spec:
  type: NodePort
  selector:
    spark-role: driver
    spark-app-name: streaming-job-test
  ports:
    - port: 4040
      targetPort: 4040
      nodePort: 30111
# Spark UI 접근
http://<노드IP>:30111

 

즉, 여기서 확인할 수 있는 핵심은

SparkApplicationRUNNING 상태가 되면

Driver 1개 + Executor 3개가 자동으로 생성되고,

Spark UI에서 Stage / Task / Storage 상태를 실시간으로 확인할 수 있다는 점입니다.


10. 전체 구성 검증

 

모든 설치가 완료된 후 최종 상태를 확인합니다.

# Operator 상태
kubectl get pods -n spark-operator

# Spark Job / History Server / File Browser 상태
kubectl get pods -n spark

# SparkApplication 상태
kubectl get sparkapplication -n spark

# PVC 상태 확인
kubectl get pvc -n spark

 

정상적인 경우 아래처럼 보입니다.

# spark-operator namespace
NAME                             READY   STATUS    RESTARTS   AGE
spark-operator-xxxxxxxxxx-xxxxx  1/1     Running   0          ...
spark-operator-xxxxxxxxxx-xxxxx  1/1     Running   0          ...

# spark namespace
NAME                             READY   STATUS    RESTARTS   AGE
spark-history-server-xxxxxxxxx   1/1     Running   0          ...
ceph-explorer-xxxxxxxxxx         1/1     Running   0          ...
streaming-job-test-driver        1/1     Running   0          ...
streaming-job-test-exec-1        1/1     Running   0          ...
streaming-job-test-exec-2        1/1     Running   0          ...
streaming-job-test-exec-3        1/1     Running   0          ...

 

각 UI 접근 주소 정리입니다.

 

서비스 주소 용도

서비스 주소 용도
Spark UI http://<노드IP>:30111 실행 중인 Job 실시간 모니터링
History Server http://<노드IP>:30112 완료된 Job 실행 이력 조회
File Browser http://<노드IP>:30071 Python 스크립트 파일 관리

 

즉, 여기서 확인할 수 있는 핵심은

Operator가 정상 동작하고,

SparkApplicationRUNNING 상태이며,

History Server UI에서 이벤트 로그가 보이면 Spark + Ceph S3 + Delta Lake 전체 연동이 완료된 것이라는 점입니다.


📝 마무리

 

이번 글에서는

Kubernetes 위에서 spark-operator를 활용해 Delta Lake 기반 Streaming Job을 구축하는 과정을 정리해 봤습니다.

 

정리하면,

  • spark-operator → SparkApplication 리소스를 감시해서 Driver/Executor Pod 자동 관리
  • Delta Lake + Ceph S3 → S3A 프로토콜로 Ceph RGW에 Delta 포맷으로 읽기/쓰기
  • History Server → S3 이벤트 로그 기반으로 완료된 Job 이력 조회
  • File Browser → CephFS PVC를 통한 Python 스크립트 웹 관리

라고 볼 수 있습니다.

 

특히 이번 구축에서 주의해야 할 포인트는

  • CRD 등록 후 → Operator 설치
  • Operator Running 확인 후 → SparkApplication apply
  • History Server apply 전 → spark-event-logs 버킷 사전 생성 필수
  • spark-sa는 ClusterRole이 필요 (SparkApplication 조회 권한 때문)
  • S3 path.style.access: true 설정 필수 (Ceph RGW 연동 시)

이 5가지였습니다.

 

다음 편에서는 이번에 구축한 Spark 위에서 Kafka 스트리밍 파이프라인을 연결하는 방법을 다뤄보겠습니다. 🙌

 

궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있다면 언제든지 편하게 말씀해 주세요! 😊

728x90
반응형