영웅은 죽지 않는다

[Linux/Shell Script] 크론탭(crontab)으로 딥러닝 플랫폼 유지보수하기 본문

Programming/Linux

[Linux/Shell Script] 크론탭(crontab)으로 딥러닝 플랫폼 유지보수하기

YoungWoong Park 2022. 9. 21. 11:14

 

머신러닝/딥러닝 모델을 활용한 플랫폼 개발 시

DB 형상관리가 잘 되어있다면 데이터가 계속해서 쌓여가는 구조일 것이다

 

그러면 그에 맞추어 모델 또한 반복적인 학습을 통해 성능을 높여 나가야 할 것이고,

그 간격은 매일, 혹은 일정 간격을 두고 지속적으로 성능개선과 유지보수를 해야 한다

 

 

여기선 CentOS 서버와 Oracle DB로 연결되어 있는 리눅스 환경 서버에서

리눅스 자체 라이브러리인 크론탭(crontab) 스케줄러를 통해

내가 일일이 매일매일 업데이트하거나 하드코딩 하지 않아도

스케줄러가 스스로 학습하고, 에러에 대응하고, 로그를 남겨놓는 쉘 스크립트 코드를 짜보았다

 

 


 

개발 시나리오 구성

 

스케줄러를 작성하기에 앞서 개발 시나리오를 구체화 했다

크론탭은 순차지향형 알고리즘이고 에러가 발생해도 멈추지 않고 아래로 내려가기 때문에

특히나 그 "순서"와 "목적"이 중요하다

 

일단 내 유지보수 시나리오는 이러하다

 

  • 기본적인 폴더들은 / home / crontab에 있다고 가정. 이를 $root 라고 한다
  • 딥러닝 모델은 $root / myapp.py에서 Fast-API uvicorn 8000포트로 사전에 띄워져 있다.
  • $root / train.py에 모델을 학습시키는 코드가 들어 있다
  • 학습 코드를 돌리면 $root / checkpoint 폴더 하위에 " {모델명}.pth "으로 저장된다
  • 모델명은 "{ID}_{YYYYMMDD}.pth" 이다. (id_해당 날짜.pth)
  • 모델이 생성되면 " $root / checkpoint / backup " 폴더 하위에 복사(백업)하고, 이전에 사용했던 모델은 삭제한다
  • $root / checkpoint / backup 폴더 내 학습 모델 중 생성된지 5일이 지난 모델을 삭제한다.
  • 모델이 변경된 후 fast-api를 종료(kill)했다가 다시 Run해야 한다

 

위와 같은 순서로 코드를 작성하고, 매 자정마다 이를 실행시키게끔 크론탭을 구성하면

 

학습  >  학습 모델 생성 및 백업  >  Fast-api가 바라보는 학습 모델 변경  >  Fast-api 종료 후 재실행

 

이 과정이 진행될 것이다.

 

백업 코드를 추가함으로써 모델링 이슈사항 발생 시 빠른 원상복구를 할 수 있고,

백업 모델을 주기적으로 관리함으로써 서버 자원을 절약할 수 있다

 

fast-api가 한 순간 재부팅되는 것 외에는 포트가 닫히지 않기에 끊김없는 서버 이용이 가능하고

무엇보다, 자동화 되어 있으므로 효율적으로 일괄 배치 작업을 진행할 수 있다

 

 

 


 

 

Shell Script 코드 작성

 

작성하기 전에 원하는 경로에 scripts.sh 파일을 생성하고,

터미널에서 chmod +x scripts.sh를 통해 권한을 부여한다

 

 

1. 학습

 

 

/home/crontab/scripts.sh

root="/home/crontab"
echo " "
echo "================================="
echo "< `date` >"

#================= TRAIN ==================#
echo "Train start"

python3 /home/crontab/train.py

echo "Train Done : `date`"

#==========================================#

 

 

컴파일 시 로그로 어느 부분에서 에러가 발생하는지 디버깅 하기 위해

출력(echo)를 많이 찍어두었다

 

그리고 `date`를 통해 크론탭 시작 시간과 학습 종료 시간을 나타냈다

작은따옴표 아님 주의 !

 

 

2. 학습 모델 검증

 

/home/crontab/scripts.sh

#================ VALIDATION =================#
# 파일 존재 여부
echo "pth file validation start!"

now=`date +%Y%m%d`
yesterday=`date -d "-1 days" +%Y%m%d`
echo now : $now
echo yesterday : $yesterday

FILE="koelectra*_$now.pth"
if [ -e $root/checkpoint/$FILE ]
then
   flag="True"
   echo "Koelectra File exists!"
else
   flag="False"
   echo "Koelectra File does Not exists!"
fi

FILE="kobert*_$now.pth"
if [ -e $root/checkpoint/$FILE ]
then
   flag="True"
   echo "Kobert File exists!"
else
   flag="False"
   echo "Kobert File does Not exists!"
fi

echo flag : $flag
#============================================#

 

학습 모델이 checkpoint 폴더에 잘 들어 있는지 확인하는 절차이다

나는 NLP 모델 두 개(KoBERT, KoELECTRA)를 사용했다.

학습 코드에 이 두 모델을 모두 학습시키므로, 학습이 끝나면 checkpoint 폴더에 두 모델에 대한 학습파일이 생성된다.

 

 

확인 후 모델 Load가 잘 되는지도 확인할 필요가 있기는 하다

그렇게 하려면 검증용 파이썬 파일을 하나 만들어 try except 구문으로 모델 Load를 시도하고,

제대로 불러오지 못할 경우 이전 모델을 그대로 사용하는 등 다른 조치를 취하게끔 하는 방법도 있다

(여기서는 따로 진행하진 않았다)

 

여기선 flag 처리를 통해 당일 기준 생성된 2개의 학습 모델 존재 여부를 저장하고, 아래에서 이를 활용한다

 

 

 

3. 백업

 

/home/crontab/scripts.sh

#================= FILE BACKUP ================#

echo "file backup / path.txt save start"

if [ "$flag" == "True" ]
then
   # 오늘 생성된 2개 모델을 백업
   cp $root/checkpoint/koelectra*_$now.pth $root/checkpoint/backup/
   cp $root/checkpoint/kobert*_$now.pth $root/checkpoint/backup/
   echo "backup file copy success!!"
  
   # 어제 존재했던 모델 제거
   rm $root/checkpoint/*_$yesterday.pth
   
   echo "Yesterday pth file remove success!!"
    
   # backup 파일중 생성한지 5일 이상 지난 학습파일 삭제
   find $root/checkpoint/backup/ -name '*.pth' -ctime +4 -exec rm {} \;
   
   # 파일 경로 txt 파일로 저장
   python3 /home/fastapi/crontab/path_to_text.py
   echo "file path save!"
   
fi

#===========================================#

 

 

먼저, 오늘 생성한 2개의 학습 모델 파일이 존재하면 (flag가 True이면) 백업(복사)을 시도한다

그리고 어제 활용했던 학습 모델은 이미 전날 백업을 했을 것이므로, 삭제를 한다.

그 후에는 백업 용량관리를 위해 백업된 파일 중 생성된지 5일이 경과한 파일을 삭제하는 과정을 거친다

 

마지막으로, 파일 경로를 txt 파일로 저장하는 파이썬 파일(path_to_text.py) 실행시키는데

이 과정은 모델 에러를 관리하기 위함에 필요한 과정이다

 

모델을 적용할 때 원래는 하드코딩을 통해 불러올 모델 경로를 직접 입력하여 API로 모델을 띄웠으나,

그렇게 되면 모델에 이슈사항이 있을 때 복구하기 위해서는 일일이 코드를 수정해야 하는 번거로움이 있다

 

그래서 모델 경로를 한 텍스트 파일에 저장해두고,

모델을 띄우는 API 부분에서 그 텍스트 파일을 읽어와 모델 경로 설정을 하게 된다면

 

모델에 에러가 발생했을 때 간단히 텍스트 파일 상의 모델만 이전 버전으로 수정하면 된다.

 

# path_to_text.py

from datetime import datetime
import glob

version = datetime.today().strftime("%Y%m%d %H:%M")

f = open("/home/fastapi/crontab/path.txt", 'w')

for filename in glob.glob('/home/fastapi/checkpoint/*.pth'):
    f.write(filename+'\n')
f.close()

 

4. 모델 로드 (API)

 

/home/crontab/scripts.sh

#================ FAST-API ==================#

# 기존 fastapi 종료

echo "FAST-API Close start"
port=$(ps -ef | grep 'uvicorn' | grep '8000')
echo process info : ${port}

get_pid=$(echo ${port} | cut -d " " -f2)

if [ -n "${get_pid}" ]
then
   kill -15 ${get_pid} &
   wait
   echo process is killed.
else
   echo running process not found.
fi

echo "FAST-API Close!!"

# fastapi 재실행

sleep 2
echo "New Fast-API Load"
nohup uvicorn --app-dir=/home/crontab myapp:app --reload --port=8000 &
wait
echo "fast-api success!!"

echo "< `date` >"
echo "==================================="
echo " "

 

/home/fastapi 폴더 하위 myapp.py에서 사전에 fast-api를 통해 모델을 띄워놓은 상태라고 가정한다

 

nohup으로 서버에서 백그라운드로 돌아가고 있었고, 바라보고 있는 학습 모델이 수정되었기에

종료(kill) 후 재시작(reload)해야 한다

 

그래서 ps -ef를 통해 현재 실행중인 fast-api의 pid값을 찾고 그 pid를 kill한 뒤, 다시 실행하는 것.

 

참고로, 일반적으로 리눅스에서 서버를 강제종료할 때 kill -9 {pid} 를 사용하지만

어찌된 일인지 그렇게 하면 해당 pid는 죽지만 다른 pid값으로 변경된 채로 서버가 살아있었다..

(죽였던 PID값에 2가 더해진 PID로 바뀜.. 목숨 두개인건가)

 

구글링을 해보니 -15로 해당 프로그램을 닫으라는 말이 있어서 해보았고

그랬더니 API 서버가 죽는걸 확인했다

 

 

 

5. 크론탭으로 스크립트 실행

 

 

1) Terminal에서 크론탭 실행하기

 

 

crontab -e

 

 

2) 크론탭에서 스크립트 백그라운드 실행

 

# m(0-59) h(0-23) day(1-31)  mon(1-12)  week(sun=0)   command


1 0 * * * /home/crontab/scripts.sh >> /home/crontab/logs/crontab_$(date +\%Y\%m\%d).log 2>&1

 

 

나는 매일 자정 0시 1분이 되면 스크립트를 실행하는 코드를 작성했다

 

 

그래야 학습 모델 버전을 의미하는 날짜도 업데이트 될 것이고,

매일매일 모델을 갱신해 유지보수 하는 것이 관리하기 용이하기 때문이다

 

그리고 스크립트에서 열심히 찍었던 출력 로그를 확인해야 함과 동시에

로그 파일들의 버전관리도 필요하므로

날짜별 네이밍된 로그파일로 이들을 빼와 어느 부분에서 오류가 발생했는지 확인한다

 

 


 

 

요렇게 작성을 완료한 후 다음날이 된다면 매일매일 아래 과정을 거치면 된다

 

 

1. 딥러닝 서버 잘 돌아가고 있나 테스트

2. scripts.sh에 오류는 없는지 테스트

3. 모델에 오류가 있다면 빠르게 이전 버전으로 전환 (path.txt 수정)

4. API에 문제가 있다면 nohup, 서버의 네트워크 환경 등 점검

 

 

실행 후 로그

 

 

========================================
< Wed Sep 21 16:40:01 KST 2022 >
Train start
Train Done : Wed Sep 21 16:40:11 KST 2022
pth file validation start!
now : 20220921
yesterday : 20220920
Koelectra File exists!
Kobert File exists!
flag : True
file backup / path.txt save start
backup file copy success!!
Yesterday pth file remove success!!
file path save!
FAST-API Close start
process info : root 1907 1633 12 16:36 ? 00:00:28 /usr/bin/python3.6 /usr/bin/uvicorn --app-dir=/home/fastapi myapp:app --reload --port=8000
process is killed.
FAST-API Close!!
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [1909]
==============================
현재시각 :  2022-09-21 16:36:30.554436
Fast API Start
fast-api start !!!
KoELCTRA model load start !!!
KoELECTRA model load complete!!!
KoBERT model load start !!!
KoBERT model load complete!!!
Fast API Ready...

==============================
New Fast-API Load
INFO:     Stopping reloader process [1907]
fast-api success!!
< Wed Sep 21 16:40:32 KST 2022 >
===================================
 
INFO:     Will watch for changes in these directories: ['/root']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [3399] using statreload
Some weights of the model checkpoint at monologg/koelectra-base-discriminator were not used when initializing koElectraForSequenceClassification: ['discriminator_predictions.dense.weight', 'discriminator_predictions.dense.bias', 'discriminator_predictions.dense_prediction.bias', 'discriminator_predictions.dense_prediction.weight']
- This IS expected if you are initializing koElectraForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
- This IS NOT expected if you are initializing koElectraForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
Some weights of koElectraForSequenceClassification were not initialized from the model checkpoint at monologg/koelectra-base-discriminator and are newly initialized: ['classifier.out_proj.bias', 'classifier.dense.bias', 'classifier.out_proj.weight', 'classifier.dense.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
The tokenizer class you load from this checkpoint is not the same type as the class this function is called from. It may result in unexpected tokenization. 
The tokenizer class you load from this checkpoint is 'BertTokenizer'. 
The class this function is called from is 'KoBertTokenizer'.
INFO:     Started server process [3418]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

 

 

아주 많은 시도 끝에 아주 깔끔한 로그가 나온 것을 확인했다 (오래걸리는 학습코드 제외)