본문으로 바로가기

[k3s] image가 사라지는 문제

category MLOps 2025. 7. 20. 20:01

base image

베이스 이미지는 레지스트리/이미지:태그로 구성된다. 예를 들어, " nvcr.io/nvidia/tensorflow:23.11-tf2-py3"와 같은 베이스 이미지가 있다면 아래와 같다.
레지스트리: nvcr.io/nvidia
이미지: tensorflow
태그: 23.11-tf2-py3
이미지는 1bit라도 바뀌면 달라지는 다이제스트라는 고유한 해시값(SHA256)으로 구분된다. 즉 같은 이미지명, 태그명이더라도 다이제스트는 달라질 수 있다.
 

이미지 pull 정책

컨테이너 생성 시 이미지 pull 정책은 3가지가 존재한다.
IfNotPresent
이미지가 로컬에 없는 경우만 pull한다. 즉 이미지의 이름, 태그명이 같으면 pull하지 않는다. 다이제스트를 질의하지 않으므로 이미지가 업데이트 된 경우 같은 이름, 태그명이더라도 다른 이미지이기 때문에 이미지의 업데이트 여부는 확인할 수 없다.
 
Always
컨테이너를 실행할 때마다 레지스트리에 이름, 다이제스트가 있는지 질의한다. 일치하는 다이제스트를 가진 컨테이너 이미지가 이미 로컬에 있다면 캐시된 이미지를 사용한다. 그렇지 않으면 검색된 다이제스트를 가진 이미지를 pull한다.
즉 항상 레지스트리에 이미지가 있는지 검색한 후, 로컬 캐시에 다이제스트가 있는지 확인한 후 이미지 pull을 결정한다.

태그명이 lastest라면 이미지의 다이제스트가 변경될 수 있지만 사용자는 이를 알 수 없다. 따라서 always를 사용하지 않는다면 lastest 태그 사용을 지양하는 것이 좋다.

 
Never
이미지를 pull하려고 하지 않는다. 만약 이미지가 로컬에 존재한다면 컨테이너를 실행하며, 존재하지 않으면 실행에 실패한다.
 

Garbage Collection

kubelet에는 클러스터 자원을 정리하기 위한 GC가 존재한다. 사용되지 않는 이미지에 대한 가비지 수집을 5분마다, 컨테이너에 대한 가비지 수집을 1분마다 수행한다.
가비지 수집을 결정하는 기준은 아래와 같다.
HighThresholdPercent
 이미지 디스크 사용량이 이 값을 초과하면 마지막으로 사용된 시간을 기준으로 오래된 이미지 순서대로 이미지를 삭제한다.
 
LowThresholdPercent
 GC는 이 값에 도달할 때까지 이미지를 삭제한다.

이러한 변수와 더불어 식별할 수 없고, 삭제된 컨테이너들을 오래된 순서대로 가비지 수집을 수행한다.

 
 

이미지가 사라지는 문제

로그를 살펴보면 현재 highThreshold가 85, lowThreshold가 80으로 설정되어 있다. 현재 사용량이 highThreshold를 초과하였고 GC는 실행중이 아닌 이미지를 삭제시키고 있었다. 문제를 해결하기 위한 방법은 3가지가 존재한다.
1. GC 정책 상향 조정
임계값을 85보다 높은 값으로 설정하여 GC가 트리거 되지 않게 한다. 그러나 이 값을 변경하면 디스크가 가득 찰 수 있고 이미지 pull 시도도 실패할 수 있다.
 
2. 디스크 용량 확보
안 쓰는 Docker 이미지를 제거하거나 캐시 저장 위치 디스크를 변경한다.
현재 사용하고 있는 이미지는 모두 사용중인 이미지이고 캐시 저장 위치 디스크를 변경할 수 있는 권한이 없다.
 
3. DaemonSet 설정: 
DaemonSet으로 Sleep 상태의 컨테이너를 띄워 이미지가 사용중인 상태로 변경한다.
GC는 사용되지 앟는 이미지에 대해 가비지 수집을 수행한다. 따라서 이미지를 사용중인 상태로 변경하면 GC가 해당 이미지를 제거하지 못한다.
 
현재 실행 가능한 방법이 3번밖에 존재하지 않기 때문에 DaemonSet 설정을 수행했다.
 

DaemonSet 설정

python의 kubernetes api를 사용했다.

class KubeUtils:
    def __init__(self):
        config.load_kube_config() # local에서 실행 시
        #config.load_incluster_config() # container에서 실행 시
        self.CoreApi = client.CoreV1Api()
        self.AppApi = client.AppsV1Api()
        self.batchApi = client.BatchV1Api()
        self.rbac_api = client.RbacAuthorizationV1Api()
    def applyDaemonSet(self):
        metadata = client.V1ObjectMeta(
            name="image-prepull",
            labels={"app": "image-prepull"},
            namespace="default"
        )

        container = client.V1Container(
            name="puller",
            image="nvcr.io/nvidia/tensorflow:23.11-tf2-py3",
            command=["sleep", "3600"]
        )

        # Pod 템플릿 정의
        pod_spec = client.V1PodSpec(
            containers=[container],
            tolerations=[client.V1Toleration(operator="Exists")],  
            restart_policy="Always",
            node_selector={"role": "workstation"} # workstation 노드만 적용
        )

        template = client.V1PodTemplateSpec(
            metadata=client.V1ObjectMeta(labels={"app": "image-prepull"}),
            spec=pod_spec
        )

        # DaemonSet 정의
        daemonset_spec = client.V1DaemonSetSpec(
            selector=client.V1LabelSelector(match_labels={"app": "image-prepull"}),
            template=template
        )

        daemonset = client.V1DaemonSet(
            api_version="apps/v1",
            kind="DaemonSet",
            metadata=metadata,
            spec=daemonset_spec
        )

        # DaemonSet 생성
        apps_v1 = client.AppsV1Api()
        apps_v1.create_namespaced_daemon_set(namespace="default", body=daemonset)

        print("DaemonSet 'image-prepull' 생성 완료")

role이 workstation인 node에만 DaemonSet을 적용하였다.
nvcr.io/nvidia/tensorflow:23.11-tf2-py3 이미지로 컨테이너를 실행하며 3600초 동안 sleep 후 컨테이너를 재실행한다.
 

DaemonSet 실행 후 이미지가 삭제되지 않는 것을 확인할 수 있다.
 
 
추후 workstation 재설정 시 docker의 캐시 디스크를 따로 확보할 예정이다.