파이썬 이것저것/파이썬 딥러닝 관련

[python] docker stack + nginx를 이용한 grpc inference 서버 구축

agingcurve 2023. 10. 28. 14:54
반응형

docker swarm 네트워크를 사용하면 각각의 분산된 PC를 하나로 묶을 수 있고,

docker swarm의 ingress 네트워크를 사용하여 클러스터를 구축 가능하고,

이를 사용하여 각각 분산된 PC에 docker시스템을 관리 할 수 있다.

 

특히 ingress네트워크 상에서 docker 시스템 간 로드밸런싱이

가능하여 여러대의 PC에 손쉽게 Inferecne Docker를 구축이 가능하다.

 

그런데 모든 상황에서 이 로드밸런싱 기능을 사용할 수 있는 건 아니였다.

프로젝트 진행 중, 기존 https 방식의 REST호출을 사용할 때는 분산 로드 밸런싱이 됬지만, 

GRPC 통신 방식을 사용하면, 로드 밸런싱 기능을 사용할 수 없었다.

 

GRPC 통신을 통해서 로드밸런싱을 하려면 NGINX 프록시 서버로 사용하여 로드밸런싱을 사용할 수 있음을 알았다.

이를 사용하기 위해서 docker swarm의 docker컴포즈 방식인 docker stack을 사용하여 구축할 수 있었다.

 

서버는 python yolov5 + grpc를 사용하여 서버구축을 하였다.

https://github.com/DuarteMRAlves/yolov5-grpc

해당 git을 참조하여 구축하였다.

 

GitHub - DuarteMRAlves/yolov5-grpc

Contribute to DuarteMRAlves/yolov5-grpc development by creating an account on GitHub.

github.com

import concurrent.futures as futures
import io
import grpc
import grpc_reflection.v1alpha.reflection as grpc_reflect
import logging
import torch
import PIL.Image
import logging
import yolov5_service_pb2 as yolov5_service
import yolov5_service_pb2_grpc as yolov5_service_grpc
import time

_SERVICE_NAME = 'YoloV5'
_MODEL_REPO = 'ultralytics/yolov5'
_MODEL_VERSION = 'custom'
_MODEL_PATH = './model/best_safety.pt'

_PORT = 8061


class YoloV5Service(yolov5_service_grpc.YoloV5Servicer):

    def __init__(self):
        # Model for file/URI/PIL/cv2/np inputs and NMS
        self.__model = torch.hub.load(_MODEL_REPO, _MODEL_VERSION, path= _MODEL_PATH)

    def detect(self, request, context):
        """
        Receives a request to detect objects and
        replies with all the detected objects in the image

        Args:
            request: Request with the bytes of the image to process
            context: Context for the gRPC call

        Returns:
            The DetectedObjects protobuf message with the objects
            detected in the image

        """
        start = time.perf_counter()
        img_bytes = request.data ## grpc input image data
        img = PIL.Image.open(io.BytesIO(img_bytes))
        # Fix for PIL Images need file name in model
        img.filename = "file"
        with torch.no_grad():
            results = self.__model(img, size=640)
        end = time.perf_counter()
        infer_time = end - start
        return self.__build_detected_objects(results, infer_time)

    def __build_detected_objects(self, results, infer_time):
        # Only one image in each prediction so we can access predictions with [0]
        # Get normalized values with xyxyn
        objects = (self.__build_detected_object(line, results.names, infer_time) for line in results.xyxyn[0])
        return yolov5_service.DetectedObjects(objects=objects)

    def __build_detected_object(self, obj, names, infer_time):
        p1 = self.__build_point_from_2x1tensor(obj[:2])
        p2 = self.__build_point_from_2x1tensor(obj[2:4])
        conf = obj[-2]
        class_idx = int(obj[-1])
        class_name = names[class_idx]
        return yolov5_service.DetectedObject(
            class_name=class_name,
            class_idx=class_idx,
            p1=p1,
            p2=p2,
            conf=conf,
            infer_time = infer_time
        )

    @staticmethod
    def __build_point_from_2x1tensor(tensor):
        return yolov5_service.Point(x=tensor[0], y=tensor[1])


def main():
    """
    Runs the server and waits for its termination
    """
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    yolov5_service_grpc.add_YoloV5Servicer_to_server(
        YoloV5Service(),
        server
    )
    service_names = (
        yolov5_service.DESCRIPTOR.services_by_name[_SERVICE_NAME].full_name,
        grpc_reflect.SERVICE_NAME
    )
    grpc_reflect.enable_server_reflection(service_names, server)
    target = f'[::]:{_PORT}'
    server.add_insecure_port(target)
    logging.info('Starting YoloV5 server at %s', target)
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    logging.basicConfig(
        format='[ %(levelname)s ] %(asctime)s (%(module)s) %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        level=logging.INFO)
    main()

 

서버를 실행시키는 docker를 만들었다. (** docker swarm은 실행되는 프로세스가 없으면 container가 생성되지 않는다.)

공식 yolov5 Doceker 이미지에 추가적으로 설치하여 dockerfile로 서버를 실행시켜 주었다.

grpcio==1.35.0
grpcio-tools==1.35.0
protobuf==3.14.0
FROM yolov5_grpc_server:0.1

EXPOSE 8061

## protobuf
# CMD ["python3", "-m", "grpc_tools.protoc", "-I.", "--python_out=/usr/src/app", "--grpc_python_out=/usr/src/app", "yolov5_service.proto"]

CMD ["python3", "yolov5_service.py"]

 

그다음, nginx conf 파일을 정의하자

user nginx;

worker_processes auto;
worker_rlimit_nofile 10240;
events {}

http {
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" $server_port';
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }
    upstream grpcservers {
        server yoloapp:8061; # 실제 gRPC 서버의 주소와 포트
    }
    server {
        listen 1443 http2;
        location /YoloV5/detect {
            grpc_pass grpc://grpcservers; # SSL 없이 gRPC를 사용
        }
    }
}

 

이를 사용해서 docker 클러스터를 묶어 실행해 보자

version: '3'

services:
  yoloapp:
   image: yolov5_grpc_server:0.2
   ports:
     - 8061:8061
   volumes:
     - /mnt/storage2t/grpc_yolov5/app:/usr/src/app
   deploy:
     replicas: 2
   environment :
      - NVIDIA_VISIBLE_DEVICES=ALL
   networks: 
    - balance 
  
  proxy:
    image: nginx:latest
    ports:
      - 1443:1443
    depends_on:
      - yoloapp
    deploy:
      placement:
        constraints: [node.role == manager]
    volumes:
        - /mnt/storage2t/grpc_yolov5/app/nginx/nginx.conf:/etc/nginx/nginx.conf
    networks: 
      - balance
    command: ["nginx", "-g", "daemon off;"]

networks:
  balance:
    driver: overlay