Nameko를 사용한 Python 마이크로서비스 소개

게시 됨: 2022-03-11

소개

마이크로서비스 아키텍처 패턴 은 유연성과 탄력성을 고려할 때 인기가 높아지고 있는 아키텍처 스타일입니다. Kubernetes와 같은 기술과 함께 이전과는 달리 마이크로서비스 아키텍처를 사용하여 애플리케이션을 부트스트랩하는 것이 점점 더 쉬워지고 있습니다.

Martin Fowler 블로그의 고전 기사에 따르면 마이크로서비스 아키텍처 스타일은 다음과 같이 요약할 수 있습니다.

간단히 말해서, 마이크로서비스 아키텍처 스타일은 단일 애플리케이션을 작은 서비스 모음으로 개발하는 접근 방식이며, 각각은 자체 프로세스에서 실행되고 경량 메커니즘(종종 HTTP 리소스 API)과 통신합니다. 이러한 서비스는 비즈니스 기능을 중심으로 구축되며 완전히 자동화된 배포 기계를 통해 독립적으로 배포할 수 있습니다.

즉, 마이크로서비스 아키텍처를 따르는 애플리케이션은 통신 프로토콜을 사용하여 서로 통신하는 여러 개의 독립적이고 동적인 서비스로 구성됩니다. HTTP(및 REST)를 사용하는 것이 일반적이지만 AMQP(Advanced Message Queuing Protocol)를 통한 RPC(Remote Procedure Call)와 같은 다른 유형의 통신 프로토콜을 사용할 수 있습니다.

마이크로서비스 패턴은 SOA(서비스 지향 아키텍처)의 특정 사례로 생각할 수 있습니다. 그러나 SOA에서는 ESB(Enterprise Service Bus)를 사용하여 서비스 간의 통신을 관리하는 것이 일반적입니다. ESB는 일반적으로 매우 정교하며 복잡한 메시지 라우팅 및 비즈니스 규칙 적용을 위한 기능을 포함합니다. 마이크로서비스에서는 "스마트 엔드포인트 및 덤 파이프"라는 대체 접근 방식을 사용하는 것이 더 일반적입니다. 즉, 서비스 자체에 모든 비즈니스 로직과 복잡성(높은 응집력)이 포함되어야 하지만 서비스 간의 연결은 가능(높은 디커플링), 이는 서비스가 통신할 다른 서비스를 반드시 알 필요는 없음을 의미합니다. 이것은 아키텍처 수준에서 적용되는 관심사의 분리입니다.

마이크로서비스의 또 다른 측면은 각 서비스 내에서 어떤 기술을 사용해야 하는지에 대한 시행이 없다는 것입니다. 다른 서비스와 통신할 수 있는 모든 소프트웨어 스택으로 서비스를 작성할 수 있어야 합니다. 각 서비스에는 자체 수명 주기 관리도 있습니다. 이 모든 것은 회사에서 팀이 서로 다른 기술과 관리 방법론을 사용하여 별도의 서비스를 작업하도록 하는 것이 가능하다는 것을 의미합니다. 각 팀은 비즈니스 기능에 관심을 갖고 보다 민첩한 조직을 구축하는 데 도움이 됩니다.

파이썬 마이크로서비스

이러한 개념을 염두에 두고 이 기사에서는 Python을 사용하여 개념 증명 마이크로서비스 애플리케이션을 구축하는 데 중점을 둘 것입니다. 이를 위해 Python 마이크로서비스 프레임워크인 Nameko를 사용합니다. RPC over AMQP가 내장되어 있어 서비스 간에 쉽게 통신할 수 있습니다. 또한 이 자습서에서 사용할 HTTP 쿼리에 대한 간단한 인터페이스가 있습니다. 그러나 HTTP 끝점을 노출하는 마이크로 서비스를 작성하려면 Flask와 같은 다른 프레임워크를 사용하는 것이 좋습니다. Flask를 사용하여 RPC를 통해 Nameko 메서드를 호출하려면 Flask를 Nameko와 상호 운용하기 위해 구축된 래퍼인 flask_nameko를 사용할 수 있습니다.

기본 환경 설정

Nameko 웹사이트에서 추출한 가장 간단한 예제를 실행하여 시작하고 목적에 맞게 확장해 보겠습니다. 먼저 Docker가 설치되어 있어야 합니다. 예제에서는 Python 3을 사용할 것이므로 Python 3도 설치해야 합니다. 그런 다음 python virtualenv를 만들고 $ pip install nameko 를 실행합니다.

Nameko를 실행하려면 RabbitMQ 메시지 브로커가 필요합니다. Nameko 서비스 간의 통신을 담당합니다. 하지만 시스템에 종속성을 하나 더 설치할 필요가 없으므로 걱정하지 마십시오. Docker를 사용하면 사전 구성된 이미지를 간단히 다운로드하여 실행할 수 있으며 작업이 완료되면 컨테이너를 중지하기만 하면 됩니다. 데몬 없음, apt-get 또는 dnf install .

Nameko가 RabbitMQ 브로커와 대화하는 Python 마이크로서비스

$ docker run -p 5672:5672 --hostname nameko-rabbitmq rabbitmq:3 을 실행하여 RabbitMQ 컨테이너를 시작합니다(이렇게 하려면 sudo가 필요할 수 있음). 이것은 최신 버전 3 RabbitMQ를 사용하여 Docker 컨테이너를 시작하고 기본 포트 5672를 통해 노출합니다.

마이크로서비스가 있는 Hello World

다음 내용으로 hello.py 라는 파일을 만듭니다.

 from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): return "Hello, {}!".format(name)

Nameko 서비스는 클래스입니다. 이러한 클래스는 확장으로 구현되는 진입점을 노출합니다. 기본 제공 확장에는 RPC 메서드, 이벤트 수신기, HTTP 끝점 또는 타이머를 나타내는 진입점을 만드는 기능이 포함됩니다. PostgreSQL 데이터베이스, Redis 등과 상호 작용하는 데 사용할 수 있는 커뮤니티 확장도 있습니다. 사용자 고유의 확장을 작성할 수 있습니다.

예제를 실행해 보겠습니다. 기본 포트에서 RabbitMQ를 실행하고 있다면 $ nameko run hello 를 실행하기만 하면 됩니다. RabbitMQ를 찾아 자동으로 연결합니다. 그런 다음 서비스를 테스트하기 위해 다른 터미널에서 $ nameko shell 을 실행합니다. 이렇게 하면 동일한 RabbitMQ 인스턴스에 연결할 대화형 셸이 생성됩니다. 가장 좋은 점은 Nameko가 AMQP를 통한 RPC를 사용하여 자동 서비스 검색을 구현한다는 것입니다. RPC 메서드를 호출할 때 nameko는 해당 실행 중인 서비스를 찾으려고 시도합니다.

RabbitMQ RPC를 통해 말하는 두 개의 Nameko 서비스

Nameko 셸을 실행하면 네임스페이스에 n 이라는 특수 개체가 추가됩니다. 이 개체를 사용하면 이벤트를 전달하고 RPC 호출을 수행할 수 있습니다. 서비스에 대한 RPC 호출을 수행하려면 다음을 실행합니다.

 > >> n.rpc.greetingservice.hello(name='world') 'Hello, world!'

동시 통화

이러한 서비스 클래스는 호출이 이루어지는 순간 인스턴스화되고 호출이 완료된 후 소멸됩니다. 따라서 본질적으로 상태 비저장이어야 합니다. 즉, 호출 사이에 개체나 클래스의 상태를 유지하려고 해서는 안 됩니다. 이는 서비스 자체가 상태 비저장이어야 함을 의미합니다. 모든 서비스가 상태 비저장이라는 가정 하에 Nameko는 이벤트렛 그린스레드를 사용하여 동시성을 활용할 수 있습니다. 인스턴스화된 서비스를 "작업자"라고 하며 동시에 실행되는 최대 작업자 수를 구성할 수 있습니다.

실제로 Nameko 동시성을 확인하려면 응답을 반환하기 전에 프로시저 호출에 절전 모드를 추가하여 소스 코드를 수정하십시오.

 from time import sleep from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): sleep(5) return "Hello, {}!".format(name)

비동기가 활성화되지 않은 time 모듈에서 sleep 를 사용하고 있습니다. 그러나 nameko run 을 사용하여 서비스를 실행할 때 sleep(5) 와 같은 호출 차단으로 인한 트리거 결과를 자동으로 패치합니다.

이제 프로시저 호출의 응답 시간은 약 5초가 소요될 것으로 예상됩니다. 그러나 nameko 셸에서 실행할 때 다음 스니펫의 동작은 무엇입니까?

 res = [] for i in range(5): hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) res.append(hello_res) for hello_res in res: print(hello_res.result())

Nameko는 각 RPC 진입점에 대해 비차단 call_async 메서드를 제공하여 결과를 쿼리할 수 있는 프록시 응답 개체를 반환합니다. 응답 프록시에서 호출된 result 메서드는 응답이 반환될 때까지 차단됩니다.

예상대로 이 예제는 약 5초 만에 실행됩니다. 각 작업자는 sleep 호출이 완료될 때까지 기다리는 것이 차단되지만 다른 작업자가 시작되는 것을 중지하지는 않습니다. 예를 들어 이 sleep 호출을 유용한 차단 I/O 데이터베이스 호출로 바꾸면 매우 빠른 동시 서비스를 얻을 수 있습니다.

앞에서 설명한 것처럼 Nameko는 메서드가 호출될 때 작업자를 생성합니다. 최대 작업자 수는 구성할 수 있습니다. 기본적으로 이 숫자는 10으로 설정되어 있습니다. 위 스니펫에서 range(5) 를 예를 들어 range(20)으로 변경하는 것을 테스트할 수 있습니다. 이것은 hello 메소드를 20번 호출할 것입니다. 이제 실행하는 데 10초가 걸립니다.

 > >> res = [] > >> for i in range(20): ... hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) ... res.append(hello_res) > >> for hellores in res: ... print(hello_res.result()) Hello, 0! Hello, 1! Hello, 2! Hello, 3! Hello, 4! Hello, 5! Hello, 6! Hello, 7! Hello, 8! Hello, 9! Hello, 10! Hello, 11! Hello, 12! Hello, 13! Hello, 14! Hello, 15! Hello, 16! Hello, 17! Hello, 18! Hello, 19!

이제 해당 hello 메소드를 호출하는 동시 사용자가 너무 많다(10명 이상)고 가정합니다. 일부 사용자는 응답을 예상한 5초 이상 기다리다가 중단됩니다. 한 가지 해결책은 예를 들어 구성 파일을 사용하여 기본 설정을 재정의하여 작업 수를 늘리는 것이었습니다. 그러나 호출된 메서드가 일부 무거운 데이터베이스 쿼리에 의존하기 때문에 서버가 이미 10명의 작업자로 제한되어 있는 경우 작업자 수를 늘리면 응답 시간이 훨씬 더 늘어날 수 있습니다.

서비스 확장

더 나은 솔루션은 Nameko Microservices 기능을 사용하는 것입니다. 지금까지 우리는 하나의 서버(귀하의 컴퓨터)만을 사용하여 하나의 RabbitMQ 인스턴스와 하나의 서비스 인스턴스를 실행했습니다. 프로덕션 환경에서는 너무 많은 호출을 받는 서비스를 실행하는 노드 수를 임의로 늘리고 싶을 것입니다. 메시지 브로커의 신뢰성을 높이려면 RabbitMQ 클러스터를 구축할 수도 있습니다.

서비스 확장을 시뮬레이션하기 위해 $ nameko run hello 를 사용하여 다른 터미널을 열고 이전과 같이 서비스를 실행할 수 있습니다. 그러면 10명의 작업자를 더 실행할 수 있는 다른 서비스 인스턴스가 시작됩니다. 이제 range(20) 으로 해당 스니펫을 다시 실행해 보세요. 이제 다시 실행하는 데 5초가 걸립니다. 둘 이상의 서비스 인스턴스가 실행 중인 경우 Nameko는 사용 가능한 인스턴스 간에 RPC 요청을 라운드 로빈합니다.

Nameko는 클러스터에서 이러한 메서드 호출을 강력하게 처리하도록 구축되었습니다. 이를 테스트하려면 캡처된 실행을 시도하고 완료되기 전에 Nameko 서비스를 실행하는 터미널 중 하나로 이동하여 Ctrl+C 를 두 번 누릅니다. 이것은 작업자가 완료될 때까지 기다리지 않고 호스트를 종료합니다. Nameko는 사용 가능한 다른 서비스 인스턴스에 대한 호출을 재할당합니다.

실제로 나중에 설명하겠지만 Docker를 사용하여 서비스를 컨테이너화하고 Kubernetes와 같은 오케스트레이션 도구를 사용하여 서비스를 실행하는 노드와 메시지 브로커와 같은 기타 종속성을 관리합니다. 올바르게 수행되면 Kubernetes를 사용하여 예기치 않은 피크에 영향을 받지 않는 강력한 분산 시스템에서 애플리케이션을 효과적으로 변환할 수 있습니다. 또한 Kubernetes는 제로 다운타임 배포를 허용합니다. 따라서 새 버전의 서비스를 배포해도 시스템 가용성에는 영향을 미치지 않습니다.

프로덕션 환경에서는 특히 배포 중에 동일한 서비스의 여러 다른 버전이 동시에 실행될 수 있으므로 이전 버전과의 호환성을 염두에 두고 서비스를 구축하는 것이 중요합니다. Kubernetes를 사용하는 경우 배포 중에 실행 중인 새 컨테이너가 충분할 때만 이전 버전 컨테이너를 모두 종료합니다.

Nameko의 경우 동일한 서비스의 여러 다른 버전을 동시에 실행하는 것은 문제가 되지 않습니다. 라운드 로빈 방식으로 호출을 분배하기 때문에 호출은 이전 버전이나 새 버전을 통과할 수 있습니다. 이를 테스트하려면 이전 버전을 실행하는 서비스가 있는 하나의 터미널을 유지하고 서비스 모듈을 다음과 같이 편집합니다.

 from time import sleep from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): sleep(5) return "Hello, {}! (version 2)".format(name)

다른 터미널에서 해당 서비스를 실행하면 두 가지 버전이 동시에 실행됩니다. 이제 테스트 스니펫을 다시 실행하면 두 버전이 모두 표시되는 것을 볼 수 있습니다.

 > >> res = [] > >> for i in range(5): ... hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) ... res.append(hello_res) > >> for hellores in res: ... print(hello_res.result()) Hello, 0! Hello, 1! (version 2) Hello, 2! Hello, 3! (version 2) Hello, 4!

여러 인스턴스 작업

이제 우리는 Nameko로 효과적으로 작업하는 방법과 확장이 작동하는 방법을 알고 있습니다. 이제 한 단계 더 나아가 Docker 에코시스템의 더 많은 도구인 docker-compose를 사용해 보겠습니다. 이것은 단일 서버에 배포하는 경우 작동합니다. 이는 마이크로서비스 아키텍처의 많은 이점을 활용하지 않을 것이기 때문에 확실히 이상적이지 않습니다. 다시 말하지만, 보다 적합한 인프라를 갖고 싶다면 Kubernetes와 같은 오케스트레이션 도구를 사용하여 컨테이너의 분산 시스템을 관리할 수 있습니다. 따라서 docker-compose를 설치하십시오.

다시 말하지만, 우리가 해야 할 일은 RabbitMQ 인스턴스를 배포하는 것이며 모든 서비스가 해당 RabbitMQ 인스턴스에 액세스할 수 있다는 전제 하에 Nameko가 나머지를 처리합니다. 이 예제의 전체 소스 코드는 이 GitHub 리포지토리에서 사용할 수 있습니다.

Nameko 기능을 테스트하기 위해 간단한 여행 애플리케이션을 빌드해 보겠습니다. 해당 응용 프로그램은 공항 및 여행을 등록할 수 있습니다. 각 공항은 단순히 공항 이름으로 저장되고 여행은 출발지와 목적지 공항의 ID를 저장합니다. 우리 시스템의 아키텍처는 다음과 같습니다.

여행 신청서 그림

이상적으로는 각 마이크로 서비스에는 자체 데이터베이스 인스턴스가 있습니다. 그러나 간단하게 하기 위해 Trips 및 Airports 마이크로서비스가 공유할 단일 Redis 데이터베이스를 만들었습니다. 게이트웨이 마이크로 서비스는 간단한 REST와 같은 API를 통해 HTTP 요청을 수신하고 RPC를 사용하여 공항 및 여행과 통신합니다.

게이트웨이 마이크로서비스부터 시작하겠습니다. 그 구조는 간단하며 Flask와 같은 프레임워크를 사용하는 모든 사람에게 매우 친숙해야 합니다. 기본적으로 GET 및 POST 메서드를 모두 허용하는 두 개의 끝점을 정의합니다.

 import json from nameko.rpc import RpcProxy from nameko.web.handlers import http class GatewayService: name = 'gateway' airports_rpc = RpcProxy('airports_service') trips_rpc = RpcProxy('trips_service') @http('GET', '/airport/<string:airport_id>') def get_airport(self, request, airport_id): airport = self.airports_rpc.get(airport_id) return json.dumps({'airport': airport}) @http('POST', '/airport') def post_airport(self, request): data = json.loads(request.get_data(as_text=True)) airport_id = self.airports_rpc.create(data['airport']) return airport_id @http('GET', '/trip/<string:trip_id>') def get_trip(self, request, trip_id): trip = self.trips_rpc.get(trip_id) return json.dumps({'trip': trip}) @http('POST', '/trip') def post_trip(self, request): data = json.loads(request.get_data(as_text=True)) trip_id = self.trips_rpc.create(data['airport_from'], data['airport_to']) return trip_id

이제 공항 서비스를 살펴보겠습니다. 예상대로 두 가지 RPC 메서드를 노출합니다. get 메소드는 단순히 Redis 데이터베이스를 쿼리하고 주어진 ID에 대한 공항을 반환합니다. create 메소드는 임의의 id를 생성하고 공항 정보를 저장하고 id를 반환합니다:

 import uuid from nameko.rpc import rpc from nameko_redis import Redis class AirportsService: name = "airports_service" redis = Redis('development') @rpc def get(self, airport_id): airport = self.redis.get(airport_id) return airport @rpc def create(self, airport): airport_id = uuid.uuid4().hex self.redis.set(airport_id, airport) return airport_id

nameko_redis 확장을 사용하는 방법에 주목하십시오. 커뮤니티 확장 목록을 살펴보십시오. 확장은 종속성 주입을 사용하는 방식으로 구현됩니다. Nameko는 각 작업자가 사용할 실제 확장 개체의 시작을 처리합니다.

Airports와 Trips 마이크로서비스 사이에는 큰 차이가 없습니다. Trips 마이크로서비스의 모습은 다음과 같습니다.

 import uuid from nameko.rpc import rpc from nameko_redis import Redis class AirportsService: name = "trips_service" redis = Redis('development') @rpc def get(self, trip_id): trip = self.redis.get(trip_id) return trip @rpc def create(self, airport_from_id, airport_to_id): trip_id = uuid.uuid4().hex self.redis.set(trip_id, { "from": airport_from_id, "to": airport_to_id }) return trip_id

각 마이크로 서비스에 대한 Dockerfile 도 매우 간단합니다. 유일한 종속성은 nameko 이며 Airports and Trips 서비스의 경우 nameko-redis 도 설치해야 합니다. 이러한 종속성은 각 서비스의 requirements.txt 에 제공됩니다. 공항 서비스용 Dockerfile은 다음과 같습니다.

 FROM python:3 RUN apt-get update && apt-get -y install netcat && apt-get clean WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY config.yml ./ COPY run.sh ./ COPY airports.py ./ RUN chmod +x ./run.sh CMD ["./run.sh"]

다른 서비스에 대한 Dockerfile과의 유일한 차이점은 소스 파일(이 경우에는 airports.py )이며, 이에 따라 변경되어야 합니다.

run.sh 스크립트는 RabbitMQ와 Airports and Trips 서비스의 경우 Redis 데이터베이스가 준비될 때까지 기다리는 것을 처리합니다. 다음 스니펫은 공항용 run.sh 의 내용을 보여줍니다. 다시 말하지만, 다른 서비스의 경우 aiports 에서 gateway 로 변경하거나 그에 따라 trips 하십시오.

 #!/bin/bash until nc -z ${RABBIT_HOST} ${RABBIT_PORT}; do echo "$(date) - waiting for rabbitmq..." sleep 1 done until nc -z ${REDIS_HOST} ${REDIS_PORT}; do echo "$(date) - waiting for redis..." sleep 1 done nameko run --config config.yml airports

이제 서비스를 실행할 준비가 되었습니다.

$ docker-compose up

시스템을 테스트해 보겠습니다. 다음 명령을 실행합니다.

 $ curl -i -d "{\"airport\": \"first_airport\"}" localhost:8000/airport HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:05:53 GMT f2bddf0e506145f6ba0c28c247c54629

마지막 줄은 우리 공항에 대해 생성된 ID입니다. 작동하는지 테스트하려면 다음을 실행하십시오.

 $curl localhost:8000/airport/f2bddf0e506145f6ba0c28c247c54629 {"airport": "first_airport"} Great, now let's add another airport: $ curl -i -d "{\"airport\": \"second_airport\"}" localhost:8000/airport HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:06:00 GMT 565000adcc774cfda8ca3a806baec6b5

이제 우리는 두 개의 공항을 얻었습니다. 여행을 구성하기에 충분합니다. 이제 여행을 만들어 보겠습니다.

 $ curl -i -d "{\"airport_from\": \"f2bddf0e506145f6ba0c28c247c54629\", \"airport_to\": \"565000adcc774cfda8ca3a806baec6b5\"}" localhost:8000/trip HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:09:10 GMT 34ca60df07bc42e88501178c0b6b95e4

이전과 마찬가지로 마지막 줄은 여행 ID를 나타냅니다. 올바르게 삽입되었는지 확인해 보겠습니다.

 $ curl localhost:8000/trip/34ca60df07bc42e88501178c0b6b95e4 {"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000adcc774cfda8ca3a806baec6b5'}"}

요약

우리는 RabbitMQ의 로컬 실행 인스턴스를 만들고 연결하고 여러 테스트를 수행하여 Nameko가 작동하는 방식을 보았습니다. 그런 다음 얻은 지식을 적용하여 Microservices 아키텍처를 사용하여 간단한 시스템을 만들었습니다.

매우 단순함에도 불구하고 우리 시스템은 프로덕션 준비 배포의 모습에 매우 가깝습니다. Falcon 또는 Flask와 같은 HTTP 요청을 처리하기 위해 다른 프레임워크를 사용하는 것이 좋습니다. 둘 다 훌륭한 옵션이며 예를 들어 게이트웨이 서비스를 중단하려는 경우 다른 HTTP 기반 마이크로서비스를 만드는 데 쉽게 사용할 수 있습니다. Flask에는 이미 Nameko와 상호 작용할 수 있는 플러그인이 있다는 장점이 있지만 모든 프레임워크에서 직접 nameko-proxy를 사용할 수 있습니다.

Nameko는 또한 테스트하기가 매우 쉽습니다. 여기서는 단순성을 위해 테스트를 다루지 않았지만 Nameko의 테스트 문서를 확인하십시오.

마이크로서비스 아키텍처 내부의 모든 움직이는 부분을 통해 강력한 로깅 시스템이 있는지 확인하려고 합니다. 빌드하려면 동료 Toptaler와 Python 개발자: Son Nguyen Kim의 Python Logging: 심층 자습서를 참조하세요.