안녕하세요! 넓은 스펙트럼을 바탕으로 전체적인 그림을 보는 Ops Engineer 이재하입니다.
이번 글에서는 제가 KServe Contributor가 된 기념으로 썰(?)을 풀어볼까 합니다.
현업에서 Multi Cluster 환경에서 API 하나로 KServe InferenceService로 배포하는 솔루션을 개발하는 일을 하고 있었습니다. 1개의 GPU에 여러가지 모델을 Serving 해야하기 때문에 TrainedModel을 섞어서 사용하고 있었고, KServe의 TrainedModel StorageURI는 HTTP, HTTPS, S3, GS
만 지원하였습니다.
저희는 클라우드 환경에서는 GKE를 사용하고 있었기 때문에 GS를 Model Storage를 사용하였고,
GKE 및 로컬 클러스터에 Kubeflow와 KServe를 설치하며 모든 것이 순조(?)로웠습니다.
TrainedModel StorageURI에 gs://우리버킷/우리모델
을 기입하여 TrainedModel.yaml을 apply 한 순간
KServe Agent가 OOM 에러를 내뱉었습니다.
자 그럼 이 KServe Agent가 무엇을 하는지 알아봐야겠죠?
KServe Agent의 Log를 까보았더니 이 친구가 Google Cloud Storage에서 Model을 다운로드하는 것을 알았습니다. “Downloading 우리버킷의우리모델 to model dir /mnt/models”의 로그를 마지막으로 pod가 계속 재시작되는 문제가 발생했습니다. 하루는 계속 두면 어떻게 될까하는 생각으로 하루정도 관찰한 결과 4000번 이상의 재부팅과 agent pod가 Restart 되는 과정에서는 istio-proxy에 요청을 보낼 수 없었습니다.
저는 먼저 Golang을 한번도 사용해보지 않았고, 어떤 형식으로 되어있는지도 몰랐습니다. (이렇게 밑밥 한번)
모든 개발자분들이 공감해주셔야합니다. 가장 쉬운 디버깅 방법으로는 print문을 남발하는 것입니다.
네, 모든 함수에 print를 찍어보았고 마침내 저는 에러가 나는 함수를 찾을 수 있었습니다.
func (g *GCSObjectDownloader) DownloadFile(client stiface.Client, attrs *gstorage.ObjectAttrs, file *os.File) error {
reader, err := client.Bucket(attrs.Bucket).Object(attrs.Name).NewReader(g.Context)
if err != nil {
return fmt.Errorf("failed to create reader for object(%s) in bucket(%s): %w",
attrs.Name,
attrs.Bucket,
err,
)
}
defer func(reader stiface.Reader) {
closeErr := reader.Close()
if closeErr != nil {
log.Error(closeErr, "failed to close reader")
}
}(reader)
data, err := io.ReadAll(reader)
if err != nil {
return fmt.Errorf("failed to read object(%s) in bucket(%s): %w",
attrs.Name,
attrs.Bucket,
err,
)
}
return g.WriteToFile(data, attrs, file)
}
위 DownloadFile 함수에 ReadAll 함수로 인하여 스토리지에 있는 모델을 메모리에 적재했다가 한꺼번에 다운로드하는 것을 찾을 수 있었습니다. 이를 대체할 수 있는 함수가 있지 않을까 싶어 우리 전지전능하신 GPT 형님께 도움을 구해봤습니다.
네, 이렇게 저의 친한형(?)인 GPT형님과 깊은 대화를 나누어서 코드를 수정하였고, KServe Agent Docker image를 새로 만들어 다시 배포하여 테스트를 진행했습니다.
이렇게 22줄의 코드를 3줄로 줄이면서 코드 수정을 완료했습니다.
(사실 io.Copy로 바꾸면서 WriteToFile 함수가 필요가 없어졌습니다.)
자, 이제 PR을 올려봐야겠죠. 사실 Issues부터 등록할까 했지만, 나보다 똑똑한 형님들이 10분 컷 하실까봐,
“안돼. 어떻게 찾은 기회인데”라는 생각으로 코드 수정을 완료하고 Issues를 등록했습니다.
“님. KServe Agent가 GCS에서 모델 다운로드할 때 OOM 이슈나는거 확인해서 코드 고쳤는데 PR 확인해줘.”
“근데 이거 GCS에만 있는거 맞아?”
“https에도 똑같이 io.ReadAll 함수가 있는거 같은데 https로 사용하는 예제를 내가 본적이 없어서 내가 예제를 찾으면 추가로 PR 올릴게.”
이렇게 따봉 하나를 받으면서 PR을 올릴 준비를 완료했습니다.
아래 사진과 같이 PR 등록을 마무리하고, 리뷰가 되고 merge가 되길 기다렸습니다.
이미 삭제한 줄을 삭제해달란 리뷰가 들어왔고 이때 당시에 내가 영어가 짧아서 잘못 알아들었나? 싶어서 영어 잘하는 주변 지인분들께 “이게 무슨 뜻일까요”라고 묻고 다녔으며, 결국 그냥 다시 물어보라는 조언을 얻고 물어봤습니다. “이거 이미 삭제한건데 뭘 삭제하라는거임?” 얼마 지나지 않아 “아 지워진지 몰랐음, 근데 니 커밋에 signed commit이 빠졌는데 해줘.”라는 요청을 받았습니다.
저는 Signed commit을 해본적이 없었으며 Vertify라는 라벨이 붙었음에도 DCO를 통과하지 못했고, 몇번의 시도 끝에 DCO를 통과했습니다.
이제 Github Action workflow만 통과하면 됐는데, Golint를 통과하지 못했고, 다시 커밋을 진행했습니다.
다시 커밋을 진행했더니? approve를 다시 받으라는 github-bot의 명령이 있었고 저는 물었습니다.
”님 이거 approve 다시 받아야하는데 맞음?” maintainer는 “ㅇㅇ 이거 PR 처음이라 그럼, 그 다음부터는 안 그럴거임”이라는 답변을 받고 golint를 로컬에서 몇번을 돌려 테스트를 마치고 approve 요청을 했습니다.
“님들 나 workflows 다 통과했어”
“ㅇㅇ merge 해줌.”
이렇게 저의 KServe Contributor 되기 꿈을 이룰 수 있었고, 성공적으로 Merge가 되었습니다.
하지만 기쁨도 잠시 KServe Agent가 업데이트가 되나 싶었고 물어봤습니다.
”님들 근데 KServe Agent Docker가 push 되는지 모르겠음. 님들이 한번 확인좀”
maintainer들끼리 이야기하길 “ㅇㅇ KServe CI에서 될거임 고마워”, “근데 나는 github actions bot이 auto merge할 경우 자동으로 트리거되지 않는 문제가 있다.”라고 설명을 코멘트를 남겼는데 저는 이 부분을 해석하지 못해 일단 지켜보고 있습니다.
이렇게 KServe Contributor가 된 썰을 풀어보았습니다.
Signed Commit 때문에 속 썩였고, 내 코드가 비로소 merge 됐을 때의 희열은 아직도 잊히지 않습니다.
부족한 글력이지만 여기까지 읽어주신 분들께 감사 인사를 드리며, KServe PR을 확인하고 싶으신분들은 아래 링크 참고 부탁드리겠습니다.
https://github.com/kserve/kserve/pull/4018
추후에 모든 기술 블로그는 jaehai.com/post에 업로드 될 예정입니다.
제 글이 도움이 되셨다면 ops-jaeha Github 팔로우 부탁드리겠습니다!