docker compose로 애플리케이션(서비스)를 설치할 때는 해당 디렉토리로 이동후 설치합니다.
서비스가 여러개 있으므로 network 이름을 지정해 줍니다.
외부 포트로 노출하는 경우, port 테이블을 참고합니다.
재시작하는 경우가 있어 restart always 옵션을 항상 추가합니다.
healthcheck를 가능한 사용해 줍니다. 빠른 상태확인에 유용합니다.
기본적인 파일 구조는 다음과 같습니다.
네트워크 설정
docker-compose 파일에 정의된 서비스들은 기본적으로 같은 docker 네트워크가 생성되어서 공유됩니다.
예를 들어, service1/docker-compose.yaml을 실행한 네트워크와 service2/docker-compose.yaml을 실행한 네트워크는 각각 다르고, service1/docker-compose.yaml 파일에 정의된 contrainer들은 같은 네트워크를 공유합니다.
위의 docker-compose.yaml을 정의할 때
이렇게 네트워크를 external: true 옵션을 주게 되면, 다른 서비스의 네트워크를 연결할 수 있게 됩니다. 즉 위의 docker-compose.yaml에서는 app-network-1이라는 다른 docker network를 가져와서 연결하게 되는 의미입니다. 이렇게 container에 원하는 docker 외부 네트워크를 연결할 수 있습니다.
docker network를 확인하고 싶으면 다음 명령어를 입력하면 됩니다.
계정 확인
docker-compose.yaml 파일에서 environments을 보면 각 서비스에 로그인 할 수 있는 ID, 패스워드를 확인할 수 있습니다.
예를 들어 MinIO의 경우 다음과 같이 확인 할 수 있습니다.
각 계정 관련 정보에 대한 envrionments 이름은 서비스마다 다르니 필히 확인을 부탁드립니다.
포트 연결
서비스마다 docker images에 있는 내부 포트가 정해져있습니다. 각 서비스를 외부에서 사용하기 위해서는 외부포트에 먼저 연결해야 합니다. docker-compose.yaml 파일에서 ports 부분을 확인하면 알 수 있습니다.
docker에서 설치된 서비스를 외부 접근하기 위해서는 다음과 같은 단계를 따르면 됩니다.
(클라우드 사용시) 클라우드에서 서비스의 외부 port에 대해서 인바운드/아웃바운드 규칙을 설정합니다.
RabbitMQ는 메시지 큐를 관리하는 메시지 브로커로, 주로 비동기 메시지 전달과 작업 분산에 사용됩니다. AMQP 프로토콜을 기반으로 하며, 프로듀서(메시지를 보내는 측)와 컨슈머(메시지를 받는 측) 간의 통신을 중개해, 작업을 비동기적으로 처리하고 시스템의 부하를 줄입니다.
본 프로젝트에서는 celery에 사용하고 있습니다.
주요 특징은 다음과 같습니다.
큐잉: 메시지를 대기열에 저장하고 순차적으로 처리.
비동기 처리: 작업을 비동기적으로 처리하여 성능 향상.
확장성: 여러 소비자에게 작업을 분배하여 부하 분산 가능.
RabbitMQ는 분산 시스템, MLOps, 마이크로서비스 아키텍처 등에서 자주 사용됩니다.
포트관련 주의사항
rabbitmq는 broker 포트 (5672)와 웹 UI 포트(15672)가 있습니다. 내부에서 연결 사용에는 broker 포트를, UI로 큐 상태를 확인하고 싶을 때는 UI 포트를 사용합니다.
sudo vi /etc/docker/daemon.json
# 아래 내용을 입력합니다.
{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}
sudo systemctl daemon-reload # 업데이트된damon.json 적용
sudo service docker restart
sudo docker info | grep nvidia
# 실행 결과
Runtimes: io.containerd.runc.v2 nvidia runc
Default Runtime: nvidia
sudo docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu20.04 nvidia-smiMinIO
services: # 서비스들 정의
container-1: # 서비스 1
image: postgres:16.3 # 기존 이미지를 pull해서 사용
container_name: container-1-name # 컨테이너의 이름 정의
environment: # 컨테이너의 env
HELLO: WORLD
healthcheck: # healthcheck
test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] # test cmd
interval: 10s # healthcheck 실행 간격
timeout: 5s # timeout내로 결과가 없으면 unhealthy status
retries: 3 # timeout 있을 때 재시도 횟수
restart: always
networks: # 컨테이너에 연결되는 docker 네트워크
- app-network-1
container-2: # 서비스 2
build: # docker 이미지를 직접 빌드하는 경우
context: ./src
dockerfile: Dockerfile
command: ["python3" ,"main.py"] # docker image의 커맨드
ports:
- "8000:8000" # 외부port:내부port
restart: always
volumes:
- app-volume-1:/mnt # docker volume에 연결되는 컨테이너 경로로
depends_on:
- container-1 # 특정 서비스가 생성된 이후에 생성
volumes:
app-volume-1: # docker volume
external: true # 기존에 있던 docker volume인 경우
networks:
app-network-1: # docker network
external: true
#/home/dva-mlops/developments/dva-mlops/bentoml/config/requirements.txt
cd config
pip install -r requirements.txt
# BentoML 설치
bentoml==1.3.5
# MLflow 연동을 위한 추가 패키지 설치
minio
fastapi[standard]
python-dotenv
loguru
mlflow
boto
python-dotenv
#/home/dva-mlops/developments/dva-mlops/bentoml/scripts/setup_model_environments.py
# 모델 디렉토리와 Python 버전 매핑
MODEL_CONFIGS = {
'yolov8l': '3.10',
'botsort': '3.10',
'removallight': '3.10',
'removefp': '3.10',
}
dva-mlops/bentoml/models/yolov8lobb/
├── bentofile.yaml # BentoML 서비스 설정 및 빌드 구성
├── environment.yml # Conda 환경 설정 (의존성 관리)
├── requirements.txt # Python 패키지 의존성 (pip 설치용)
└── service.py # 실제 서비스 로직 구현
service: "service:YoloV8OBBService" # 서비스 진입점 지정
include:
- "*.py" # 서비스에 포함될 파일 패턴
python:
lock_packages: false # 개발 중에는 패키지 버전 유연성 확보
conda:
environment_yml: "./environment.yml"
docker:
system_packages: # CUDA, OpenCV 등 시스템 레벨 의존성
- ffmpeg
- libglib2.0-0
- libsm6
- libxext6
- libxrender1
channels:
- conda-forge
dependencies:
- python=3.10.0 # 특정 Python 버전 고정
- pip<=24.2
- pip:
# GPU 지원을 위한 PyTorch
- torch==1.13.1+cu117
- torchvision==0.14.1+cu117
# YOLO 관련
- ultralytics==8.2.78
# 서비스 프레임워크
- bentoml==1.3.5
# 로깅
- loguru==0.7.2
class DetectInput(BaseModel):
columns: List[str] # 입력 데이터 컬럼 정의
data: List[List[str]] # 실제 데이터 배열
class DetectOutput(BaseModel):
predictions: List[Dict[str, str]] # 예측 결과 포맷
request_counter = Counter(
name="yolo_obb_request_count",
documentation='Total number of YOLO OBB requests',
labelnames=['endpoint', 'status', 'http_status']
)
inference_time_histogram = Histogram(
name="yolo_obb_inference_time_seconds",
documentation='Time taken for YOLO inference',
labelnames=['endpoint', 'status']
)
@bentoml.service(
resources={
"gpu": 1,
"memory": "18Gi",
"gpu_type": "nvidia-a100-mig-1g-40gb"
}, # GPU 메모리 등 리소스 할당
workers=SERVICE_WORKERS, # 동시 처리 워커 수
traffic={
"timeout": 600,
"max_concurrency": 8,
"max_queue_size": 16
} # 트래픽 제어 설정
)
class YoloV8OBBService:
def __init__(self):
# 볼륨 마운트 설정
self.pipeline_volume = Path(PIPELINE_VOLUME)
self.logging_volume = Path(LOGGING_VOLUME)
# 로거 및 모델 초기화
self.setup_logger()
self.model = self.load_model()
# 시스템 모니터링 스레드 시작
self.monitoring_thread = threading.Thread(
target=self.monitor_system_resources,
daemon=True
)
self.monitoring_thread.start()
def load_model(self):
try:
# MLflow에서 모델 로드
model = bentoml.mlflow.load_model(f"{MODEL_NAME}:{MODEL_VERSION}")
self.logger.info(f"Completed model loading: model_name={model.__class__.__name__}")
return model
except Exception as e:
self.logger.error(f"Failed to load YOLO model: error={str(e)}")
raise ModelError(f"Failed to load YOLO model: error={str(e)}")
@bentoml.api
async def detect(self, dataframe_split: DetectInput, ctx: bentoml.Context) -> DetectOutput:
self.is_processing = True
start_time = time.time()
try:
# 입력 데이터 검증
if not dataframe_split.data:
raise InvalidArgument("Input data is empty")
# 시스템 리소스 체크
if psutil.virtual_memory().percent > 90:
raise ServiceUnavailable("System memory is critically low")
# 데이터프레임 변환 및 처리
df = pd.DataFrame(dataframe_split.data, columns=dataframe_split.columns)
predictions = []
# 배치 처리
for _, row in df.iterrows():
model_input = pd.DataFrame([{
"image_folder": row["image_folder"],
"result_path": row["result_path"]
}])
model_output = self.model.predict(model_input)
# 결과 포맷팅
predictions.append({
"results": str(result_path),
"status": model_output["status"][0],
"status_message": model_output["status_message"][0],
})
return DetectOutput(predictions=predictions)
except Exception as e:
self.logger.error(f"An unexpected error occurred: error={str(e)}")
return DetectOutput(predictions=[{
"results": "",
"status": "Failed",
"status_message": str(e)
}])
#dva-mlops/bentoml/scripts/automate_model_update.py
@handle_exception
def get_latest_model_version(model_name: str) -> Tuple[str, str]:
client = initialize_mlflow_client(model_name)
versions = client.search_model_versions(f"name='{model_name}'")
sorted_versions = sorted(versions, key=lambda x: int(x.version), reverse=True)
# Staging 단계의 모델 우선 선택
for version in sorted_versions:
if version.current_stage in ["Staging"]:
return version.version, version.run_id
#dva-mlops/bentoml/models/yolov8lobb/service.py
def load_model(self):
try:
# BentoML에서 최신 버전의 모델 로드
model = bentoml.mlflow.load_model(f"{MODEL_NAME}:{MODEL_VERSION}")
self.logger.info(f"Model loaded: {model.__class__.__name__}")
return model
except Exception as e:
raise ModelError(f"Failed to load model: {str(e)}")
# 실패한 이미지 제거
docker rmi {service_name}:{tag}
# Docker Compose 재시작
cd config/
docker compose up -d
#dva-mlops/bentoml/scripts/automate_model_update.py
try:
# 모델 업데이트 프로세스
except ModelUpdateError as e:
remove_failed_image(service_name, bento_tag)
sys.exit(1)