오늘은 NFS를 설정해서 Kubeflow Pipeline에서 저장되는 DL model을 호스트에 저장시키는 방법을 알아봅시다!
Minikube-Kubeflow로 MLOps를 진행하다보니 몇가지 한계점이 느껴지더군요.
PV/PVC를 설정해도 pod 내부(훈련 컨테이너)에서 모델을 저장해도 PV에 저장될 뿐, 이 PV가 호스트에 연결되어 있지 않다.(아래서 진행하려는 설정 문제죠, 클라우드는 이런 부분을 신경쓰지 않아도 자동화 시켜준다는게 편합니다)
모델 레지스트리(Model Registry)를 구성하여 모델의 버전을 관리하고, 모델 간의 성능도 비교하고, 로그를 착실히 쌓아가야 하는데 이러한 부분이 자동으로 구축되지 않는다.(AWS sagemaker는 모델 그룹으로 관리할 수 있습니다.)
Minikube의 PV에서 바로 모델을 서빙해도, 외부에서 해당 External-uri로 접근할 수가 없다.
Kubeflow는 Artifact를 Minio에 저장한다. 하지만 Minio에 저장할 수 있는 파일은 용량이 적어야하고, 컴포넌트 간 데이터의 IO는 용량이 큰 파일을 사용할 수 없다.(Kubeflow의 한계점입니다.)
결국 A 컴포넌트의 결과(훈련된 모델)를 B 컴포넌트의 Input으로 활용하기 위해선 kfp SDK의 기능을 활용할 수 없기 때문에, A와 B가 동시에 접근 가능한 모델 스토리지가 있어야 저장/로드를 활용할 수 있다.
이런 다양한 문제로 인해
1. 서버의 Host Path를 NFS로 설정하기
2. 쿠버네티스(Minikube)의 Node에선 PV로 NFS 경로를 마운트하기
3. 파이프라인에선 Node의 PV에 PVC를 연결해서 모델을 저장하기.
위와 같이 문제를 해결해보고자 합니다. 결국 모델이 서버 자체에 저장되도록 하는거죠!(사실 AWS S3 등으로 스토리지를 설정하면 편합니다. 하지만 해결방법이 있는데도 복잡하다고 지속적으로 클라우드 과금을 할 순 없잖아요?)
Server: Ubuntu 18.04
Client: Minikube GPU node(None driver)
아래 내용은 우분투 18.04를 기반으로 진행합니다.
OS 및 버전에 따라 내용이 달라지니 각 운영체제의 NFS 구성 방법은 따로 확인해주시기 바랍니다!
sudo apt update
sudo apt install nfs-kernel-server
- 위와 같은 안내창이 뜨면 install the package maintainer's version
(패키지 관리자 버전 설치)를 선택합시다.
sudo mkdir -p /mnt/nfs_share
sudo chown -R nobody:nogroup /mnt/nfs_share/
sudo chmod 777 /mnt/nfs_share/
/mnt/nfs_share 192.168.0.14/24(rw,sync,no_subtree_check)
sudo vim /etc/exports
sudo exportfs -a
sudo systemctl restart nfs-kernel-server
sudo ufw allow from 192.168.0.14/24 to any port nfs
sudo ufw enable
sudo ufw status
보통의 클라이언트(OS나 VM에 존재하는 k8s Node 등)은 해당 링크 참고하여 진행하시면 됩니다.
저는 Minikube GPU 설정으로 인해 driver가 None이라 호스트 서버 자체가 Node입니다.
그래서 Node에 ssh접속(minikube ssh)에 접속해서 작업할 수 없기 때문에, 일반적인 방법이 아닌 바로 PV를 nfs-server 경로로 설정하는 방식으로 진행해보겠습니다.
(사실 None driver에서 로컬 마운트가 아닌 NFS를 사용하는게 맞는건가 싶긴한데, 로컬 마운트보다 NFS가 10배 이상 IO이 빠르다고 하여 시도해봅니다)
현재 코드에서(코드는 제공드릴 수 없습니다만, 아래서 설명을 위해 일부 첨부하니 참고부탁드립니다.)
PV가 어떻게 선언되어 있는지 살펴보자.
kubectl get pv -A
kubectl describe pv <NAME> -n <NAMESPACE>
https://kubernetes.io/ko/docs/concepts/storage/volumes/
볼륨을 사용하려면, .spec.volumes 에서 파드에 제공할 볼륨을 지정하고 .spec.containers[*].volumeMounts 의 컨테이너에 해당 볼륨을 마운트할 위치를 선언한다. 컨테이너의 프로세스는 컨테이너 이미지의 최초 내용물과 컨테이너 안에 마운트된 볼륨(정의된 경우에 한함)으로 구성된 파일시스템을 보게 된다. 프로세스는 컨테이너 이미지의 최초 내용물에 해당되는 루트 파일시스템을 보게 된다. 쓰기가 허용된 경우, 해당 파일시스템에 쓰기 작업을 하면 추후 파일시스템에 접근할 때 변경된 내용을 보게 될 것이다. 볼륨은 이미지의 특정 경로에 마운트된다. 파드에 정의된 각 컨테이너에 대해, 컨테이너가 사용할 각 볼륨을 어디에 마운트할지 명시해야 한다.
위와 같은 내용을 아래 코드를 보면서 이해해보자.
.spec.containers[*].volumeMounts
에는 컨테이너에 특정(nfs_shared)를 마운트 할 위치를 선언하는 것이다.즉 컨테이너의 /mnt/export
에 nfs를 마운트하여 동기화시키고자 한다.
.spec.volumes
에 제공할 볼륨을 지정한다. # Generate TFJob Chief and Worker specs with the best hyperparameters.
tfjob_chief_spec = {
"replicas": 1,
"restartPolicy": "OnFailure",
"template": {
"metadata": {"annotations": {"sidecar.istio.io/inject": "false"}},
"spec": {
"containers": [
{
"name": "tensorflow",
"image": "docker.io/moey920/train_forecasting_model:latest",
"command": ["sh", "-c"],
"args": [
"python /code/main.py --tf_export_dir=/mnt/export --df_me={} {}".format(
df_me, best_hps
)
],
"resources": {
"limits": {
"ggaman.com/vram": 2
}
},
"volumeMounts": [
{"mountPath": "/mnt/export", "name": "forecasting-model-volume"}
],
}
],
"volumes": [
{
"name": "forecasting-model-volume",
"persistentVolumeClaim": {
"claimName": str(model_volume_op.outputs["name"])
},
}
],
},
},
}
kfp.dsl.VolumeOp는 PVC를 생성하는 오퍼레이터이며, 이렇게 요청된 PVC에 따라 PV가 없으면 동적 프로비저닝을 통해 PV를 생성하고 PV-PVC간 마운트가 이루어진다.
model_volume_op = dsl.VolumeOp(
name="model-volume",
resource_name="model-volume",
size="20Gi",
modes=dsl.VOLUME_MODE_RWM,
)
======
해야할 것
아래 이미지처럼 /dev/nvme0n1p1
이 mnt/nfs_shared
에 마운트 되어야함? 하지만 /dev/nvme0n1p1은 루트인데..?
helm install nfs-client -n kube-system \
--set nfs.server=10.10.10.10 \
--set nfs.path=/n123456_cluster \
stable/nfs-client-provisioner
현재 storage-provisioner가 자동으로 host-path로 동적 프로비저닝을 지원하고 있다. 이걸 nfs 동적 프로비저닝으로 바꿔야하는데..
또한 Reclaimpolicy도 Retain으로 변경해야 한 컨테이너 PVC가 해제되고(모델은 저장되고) 다른 컨테이너에 PVC가 PV에 연결되었을 때 데이터가 남아있다.(버전을 하드코딩으로 /1
로 지정하고 있는 부분은 해결이 필요)
다음 포스팅에서 실제로 외부 프로비저닝 도구를 활용해서 PVC 생성 시 설정한 NFS 서버에 PV가 생성됩니다!