In 이 시리즈의 첫 번째 편에서 마이크로서비스를 만드는 원동력과 Couchbase가 마이크로서비스 아키텍처에 사용하기에 완벽한 데이터스토어인 이유에 대해 설명했습니다. 상태 저장소가 없기 때문에 어디에나 배포할 수 있고 필요에 따라 수평적으로 확장할 수 있습니다. 어떤 언어로든 마이크로서비스를 작성할 수 있지만, 기능을 빠르게 제공해야 하는 애자일 워크플로우에 맞추려면 대부분의 개발자가 알고 있고 신속한 개발이 가능한 언어를 선택해야 합니다. Python과 JavaScript는 널리 사용되는 두 가지 언어이므로 두 언어 중 하나를 선택하는 것이 좋습니다. 이 블로그 시리즈에서는 Python에 초점을 맞추고 있습니다.
분산 성능 고려 사항
애플리케이션 성능 요구사항은 중요하지만 때로는 정량화하기 어려운 경우가 있습니다. 간단한 예로, X시간에 실행되는 보고서가 비즈니스 요구사항에 따라 Y분 안에 실행되어야 한다고 가정해 보겠습니다. 이 예는 정량화하기 쉽고 명확한 개선 목표를 제공합니다. 지리적으로 분산된 아키텍처의 경우, 구성 요소가 초당 X개의 요청을 지원할 수 있다고 해도 이는 아무런 의미가 없습니다. 지리적으로 분산된 아키텍처에서 변수의 수를 고려하면 분산 애플리케이션예를 들어 스마트폰 앱이 X초 안에 완전히 로드되기를 원하거나 웹 페이지가 Y초 안에 완전히 로드되기를 원한다면 한 걸음 물러나서 정량화할 수 있는 것부터 시작해야 합니다. 그런 다음 이를 실현하기 위해 무엇이 필요한지 거꾸로 생각해 보세요.
Python 마이크로서비스 성능 고려 사항
Python으로 마이크로서비스를 만들 때는 코딩을 시작하기 전에 고려해야 할 몇 가지 옵션이 있습니다. 모든 Python 코드를 직접 작성하여 서비스를 만들거나 Flask 또는 FastAPI와 같은 Python API 프레임워크를 사용할 수 있습니다. In 이 시리즈의 첫 번째 편에서 첫 번째 옵션의 예를 제공했습니다. 이를 "전체 코드" 옵션이라고 부르겠습니다. 이 시리즈의 두 번째 파트에서는 FastAPI를 사용하여 간단한 사용자 프로필 서비스를 구현하는 방법을 소개하겠습니다. 이 블로그에서 Flask 대신 FastAPI를 선택한 이유는 대부분 더 빠르다고 생각하기 때문이며, 한번 사용해 보는 것도 재미있을 것 같았기 때문입니다.
하지만 먼저 원래의 "전체 코드" 예제에 집중해 보겠습니다. 파이썬의 HTTPServer 클래스를 사용하여 API 호출에 응답하는 기본 웹 서버를 만들었습니다. API에서는 빠르고 구문 분석이 쉬운 경로(매개변수나 JSON 본문 게시 대신)를 사용하기로 결정했습니다. 간단한 사용자 프로필 API는 많은 것을 제공할 필요 없이 사용자 프로필을 조회하고 데이터를 가져오는 몇 가지 방법만 제공합니다. 다음 기준으로 조회할 수 있는 옵션을 포함했습니다. ID, 닉네임 또는 사용자 이름. 실제로는 업스트림 애플리케이션의 설계 방식에 따라 요구 사항이 달라집니다.
전체 코드 프로그램에는 한 번 실행되는 코드와 각 요청마다 실행되는 코드, 즉 do_GET 함수의 두 가지 논리 영역이 있습니다. 여기서는 편의상 제한된 실행 코드에는 초점을 맞추지 않고 do_GET 함수와 그 위성 함수에 초점을 맞추겠습니다. 파이썬의 HTTPServer 클래스에서 이 함수는 각 요청과 함께 호출됩니다. 요청 경로는 클래스에 있으며 다음을 통해 액세스할 수 있습니다. self.path 헤더는 self.headers. 파이썬을 이제 막 시작하는 경우, self 는 Java에서 이와 같이 클래스의 호출 인스턴스를 참조합니다.
서비스는 적절한 조회를 수행하고 데이터를 반환할 수 있도록 경로 문자열의 내용을 반복해야 합니다. Couchbase의 JSON 네이티브 설계의 장점 덕분에 데이터를 전송하기 전에 많은 작업을 할 필요가 없습니다. 따라서 경로를 검사하는 방법에 집중하겠습니다. Python에는 문자열 처리를 위한 많은 기본 제공 옵션이 있어 예쁜 코드를 작성할 수 있지만 반드시 가장 빠른 코드는 아닙니다. 파이썬은 해석된 언어(소스에서 직접 실행)이기 때문에 명령문이 차이를 만듭니다.
경로 문자열 처리를 위한 두 가지 옵션을 살펴보겠습니다. 시작 그리고 분할 메서드.
1 2 3 4 5 6 7 |
% python3 -m timeit -s 'text="/api/v1/id/4"' 'text.startswith("/api/v1/id/")' 2000000 루프, 최고 의 5: 111 nsec 당 loop % python3 -m timeit -s 'text="/api/v1/id/4"' 'text.split("/")[-1]' 1000000 루프, 최고 의 5: 205 nsec 당 loop |
분할은 비용이 더 많이 들지만 어차피 해야 하므로 한 번만 수행하는 것이 가장 좋습니다. 그런 다음 분할에서 반환된 배열을 사용하여 다른 것을 호출하지 않을 수 있습니다. 시작.
1 2 3 |
% python3 -m timeit -s '1 == 5이면 참, 그렇지 않으면 거짓' 50000000 루프, 최고 의 5: 6.07 nsec 당 loop |
조건문은 빠르므로 보기에는 좋지 않을 수 있지만 단일 분할을 수행한 다음 if...elif...else 구조를 사용하여 경로를 반복할 수 있습니다. 쿼리 또는 키-값 가져오기를 수행하는 짧은 헬퍼 함수를 작성하여 최소한의 처리로 요청자에게 JSON 데이터를 반환합니다.
또한 마이크로서비스의 보안을 강화하기 위해 무기명 토큰을 추가할 것입니다. 실제 환경에서는 무기명 토큰 및 JWT 토큰과 함께 OAuth와 같은 것을 사용할 것입니다. 이 예에서는 이를 크게 단순화하여 고정 토큰으로 스키마에 컬렉션을 추가하겠습니다. 서비스는 시작 시 이 토큰을 쿼리하고 이 토큰을 무기명 토큰으로 제공하는 요청에 대해서만 응답합니다. 마지막으로, 필요한 경우 상태 확인 경로를 추가하여 HTTP 200 그래서 저희 서비스가 건강하다는 것을 알고 있습니다.
1 2 3 4 5 6 7 8 9 10 |
def do_GET(self): 경로_벡터 = self.경로.분할('/') 경로_벡터_길이 = len(경로_벡터) 만약 경로_벡터_길이 == 5 그리고 경로_벡터[3] == 'id': 만약 not self.v1_check_auth_token(self.헤더): self.승인되지 않은() 반환 요청_파라미터 = 경로_벡터[4] 레코드 = self.v1_get_by_id('user_data', 요청_파라미터) self.v1_responder(레코드) |
Python 마이크로서비스 컨테이너화
저는 서비스를 테스트하기 위해 Kubernetes를 사용하기로 결정했기 때문에 API의 다양한 구현이 포함된 컨테이너를 구축해야 했습니다. 기본으로 사용할 수 있는 공개된 Python 컨테이너가 있습니다. 일부 OS 필수 구성 요소는 필수 Python 패키지보다 먼저 설치해야 합니다. Python 컨테이너는 Debian을 기반으로 하므로 필수 패키지는 APT와 함께 설치할 수 있습니다. 그런 다음 pip 를 호출하여 필요한 파이썬 패키지를 설치할 수 있습니다. 서비스 포트를 노출해야 하며, 마지막으로 명령줄에서 실행할 때와 마찬가지로 서비스를 실행할 수 있습니다. 서비스를 컨테이너화하려면 환경 변수를 지원하기 위해 추가 수정이 필요하며, 이는 컨테이너에 매개 변수를 전달하는 데 선호되는 방법이기 때문입니다.
다음은 전체 코드 서비스에 대한 Docker파일의 예입니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
FROM python:3.9-불스아이 RUN apt 업데이트 RUN apt 설치 elpa-magit -y RUN apt 설치 git-모두 python3-dev python3-pip python3-설정 도구 cmake 빌드-필수 libssl-dev -y WORKDIR /usr/src/앱 ADD . /usr/src/앱 RUN pip 설치 --아니요-캐시-dir -r 요구 사항.txt EXPOSE 8080 CMD ./마이크로-svc-데모.py 그리고 서비스 can 다음 be 실행 통과 의 매개변수 통해 의 환경: 도커 실행 -d --이름 마이크로서비스 \ -p 8080:8080 \ -e COUCHBASE_HOST=$COUCHBASE_HOST \ -e COUCHBASE_USER=$COUCHBASE_USER \ -e couchbase_password=$COUCHBASE_PASS \ -e 카우치베이스_버킷=$Couchbase_버킷 \ testsvc |
FastAPI
얼마 전 파이썬 웹 프레임워크용 WSGI(웹 서버 게이트웨이 인터페이스)가 만들어졌습니다. 이를 통해 개발자는 웹 서버에 필요한 다른 모든 하위 수준 작업 대신 웹 애플리케이션 구축에만 집중할 수 있었습니다. 이 표준은 비동기 Python 프로그래밍을 지원하는 ASGI(비동기 서버 게이트웨이 인터페이스)로 확장되어 REST API와 같은 상태 비저장 애플리케이션에 매우 적합합니다.
유비콘 는 Python용 ASGI 웹 서버 구현이며, FastAPI는 Uvicron과 통합되어 신속한 API 개발 플랫폼을 만듭니다. 저는 이를 사용하여 전체 코드 버전과 비교할 두 번째 API 구현을 만들기로 결정했습니다. 비동기 파이썬을 완벽하게 지원하기 때문에 비동기 프로그래밍을 완벽하게 지원하는 Couchbase 파이썬 SDK와도 잘 작동합니다.
이 프레임워크를 사용하면 전체 코드 버전보다 훨씬 적은 코드가 필요하므로 개발 속도가 빨라집니다. Couchbase에 연결하기 위한 일부 함수가 필요하지만, 그 외에는 데이터를 가져오고 반환하기 위해 최소한의 코드 세그먼트를 호출하는 FastAPI 인스턴스와 상호 작용하는 데 장식된 앱 메서드가 사용됩니다. 전체 코드 버전과 마찬가지로 서비스는 Couchbase에 한 번 연결하고 결과 수집 메서드를 사용하여 데이터를 가져옵니다. 그리고 on_event 메서드는 시작 시 Couchbase에 연결하고, 인증 토큰을 검색하고, 필요한 모든 변수를 설정하는 데 사용됩니다.
1 2 3 4 5 6 7 8 9 10 |
@앱.on_event("startup") 비동기 def 서비스 초기화(): key_id = '1' 클러스터[1] = 기다림 get_cluster() 컬렉션['서비스_인증'] = 기다림 get_collection(클러스터[1], '서비스_인증') doc_id = f"서비스_인증:{키_ID}" 결과 = 기다림 컬렉션['서비스_인증'].lookup_in(doc_id, [SD.get('토큰')]) auth_token[1] = 결과.content_as[str](0) 컬렉션['user_data'] = 기다림 get_collection(클러스터[1], 'user_data') 컬렉션['user_images'] = 기다림 get_collection(클러스터[1], 'user_images') |
시작 작업이 완료되면 가능한 각 요청 경로에 대한 짧은 함수가 앱 메서드 호출을 통해 호출됩니다. 경로 매개변수는 경로에서 추출되어 함수에 전달되며, 인증 토큰을 확인하기 위해 함수에 대한 종속성과 함께 전달됩니다. 이 구현에서는 연결 매개변수를 전달하는 데 환경 변수만 사용됩니다.
1 2 3 4 5 |
@앱.get("/api/v1/id/{문서}", 응답_모델=프로필) 비동기 def get_by_id(문서: str, 승인됨: bool = 의존성(verify_token)): 만약 승인됨: 프로필 = 기다림 get_profile(컬렉션=컬렉션['user_data'], 컬렉션_이름='user_data', 문서=문서) 반환 프로필 |
이 구현을 위한 컨테이너는 전체 코드 버전과 동일한 베이스를 사용하고 동일한 종속 요소를 설치할 수 있지만, 몇 가지 추가 Python 패키지 요구 사항이 있으며 Uvicorn을 통해 서비스가 호출됩니다.
1 2 3 4 5 6 7 8 9 |
FROM python:3.9-불스아이 RUN apt 업데이트 RUN apt 설치 elpa-magit -y RUN apt 설치 git-모두 python3-dev python3-pip python3-설정 도구 cmake 빌드-필수 libssl-dev -y WORKDIR /usr/src/앱 ADD . /usr/src/앱 RUN pip 설치 --아니요-캐시-dir -r 요구 사항.txt EXPOSE 8080 CMD uvicorn 서비스:앱 --호스트 0.0.0.0 --포트 8080 |
엔드포인트를 테스트하기 위해 Node.js 설정하기
이 블로그 게시물은 Python에 관한 것이지만, API에 대해 Python이 아닌 것과 비교하는 것이 도움이 될 것 같아서 비동기식이며 API와 잘 작동하는 Node.js를 사용하기로 결정했습니다. Node.js 구현은 Express 모듈을 사용하여 웹 서버를 생성하며, FastAPI와 비슷한 방식으로 app.get 메서드를 호출합니다. 먼저 인증 토큰을 확인하는 함수를 호출하고 성공하면 요청된 데이터를 가져오는 함수를 호출합니다.
1 2 3 4 5 6 |
앱.get('/api/v1/닉네임/:닉네임', 체크토큰, getRESTAPINickname); 앱.get('/api/v1/사용자명/:사용자명', 체크토큰, getRESTAPI사용자 이름); 앱.get('/api/v1/id/:id', 체크토큰, getRESTAPIId); 앱.get('/api/v1/picture/record/:id', 체크토큰, getRESTAPIPictureId); 앱.get('/api/v1/picture/raw/:id', 체크토큰, getRESTAPIImageData); 앱.get('/healthz', getHealthCheckPage); |
카우치베이스 함수를 위한 모듈은 자바스크립트 파일에 있으며, 지원되는 API 호출을 위한 함수 역시 별도의 자바스크립트 파일에 모듈로 들어 있습니다. 파이썬과 마찬가지로 기본으로 사용되는 노드 컨테이너가 있으며, NPM 유틸리티가 종속성을 유지 관리하고 서비스를 시작합니다.
1 2 3 4 5 6 7 8 |
FROM 노드:16.14.2 WORKDIR /앱 ADD . /앱 RUN rm -rf /앱/node_modules RUN npm 설치 -g npm@최신 RUN npm 설치 EXPOSE 8080 CMD npm 시작 |
자율적으로 카우치베이스를 스핀업하는 쿠버네티스
앞서 언급했듯이 서비스 구현을 테스트하기 위해 Kubernetes를 선택했습니다. 다양한 테스트 시나리오에 맞게 서비스를 신속하게 배포하고 확장할 수 있기 때문에 테스트 속도를 높일 수 있었습니다. Kubernetes와 함께 Couchbase를 사용하는 데는 두 가지 옵션이 있습니다. Couchbase 자율 운영자를 사용하여 Couchbase를 Kubernetes 환경에 배포하거나 서비스를 외부 클러스터에 연결할 수 있습니다. 이 서비스는 동일한 클라우드 VPC에 배포된 외부 클러스터로 테스트되었습니다. 모든 노드는 동일한 클라우드 지역에 있었고, 실제 배포에서 볼 수 있는 상황을 시뮬레이션하기 위해 가용성 영역에 걸쳐 Couchbase 클러스터 노드와 Kubernetes 노드를 모두 배포했습니다.
세 가지 구현을 배포하기 위해 세 개의 배포 YAML 파일이 만들어졌습니다. 각 배포 YAML은 서비스에 대한 네임스페이스를 만듭니다. 이 파일은 Couchbase 비밀번호에 비밀을 사용합니다. 서비스는 처음에 4개의 복제본으로 배포됩니다. 상태 비저장 마이크로서비스이므로 필요에 따라 확장 및 축소할 수 있습니다. 트래픽은 로드 밸런서를 통해 서비스로 전달됩니다. 사용된 Kubernetes 환경이 클라우드 공급자와 통합되었기 때문에 각 배포는 서비스에 대한 클라우드 로드 밸런서도 프로비저닝했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
apiVersion: v1 종류: 네임스페이스 메타데이터: 이름: demopy --- apiVersion: v1 종류: 비밀 메타데이터: 이름: demopy-비밀 네임스페이스: demopy 유형: 불투명 데이터: 관리자 비밀번호: aBcDeFgH= --- apiVersion: 앱/v1 종류: 배포 메타데이터: 이름: demopy 네임스페이스: demopy 사양: 복제본: 4 선택기: matchLabels: 앱: demopy 전략: 유형: 롤링 업데이트 롤링업데이트: 최대 사용할 수 없음: 25% 최대 서지: 1 템플릿: 메타데이터: 레이블: 앱: demopy 사양: 컨테이너: - 이름: demopy 이미지: mminichino/demopy:1.0.5 이미지 풀 정책: 항상 포트: - 이름: 앱-포트 컨테이너 포트: 8080 환경: - 이름: 터치베이스_호스트 값: 1.2.3.4 - 이름: 터치베이스_USER 값: 관리자 - 이름: 터치베이스_비밀번호 valueFrom: secretKeyRef: 이름: demopy-비밀 키: 관리자 비밀번호 - 이름: 터치베이스_버킷 값: sample_app --- apiVersion: v1 종류: 서비스 메타데이터: 이름: demopy-서비스 네임스페이스: demopy 레이블: 앱: demopy 사양: 선택기: 앱: demopy 포트: - 이름: http 포트: 8080 대상 포트: 8080 유형: 로드밸런서 |
배포 YAML 파일을 사용하여 Kubernetes CLI로 필요에 따라 서비스를 배포하고 확장할 수 있습니다. 실제 프로덕션 환경이라면 자동 확장 및 고급 로드 밸런싱과 같은 도구를 사용하여 배포를 제어하고 액세스할 수 있습니다.
1 2 |
$ kubectl 신청하기 -f demopy.yaml $ kubectl 규모 배포 --복제본=8 demopy -n demopy |
클러스터 성능 결과
서비스를 테스트하기 전에 기준선을 만들기 위해 Kubernetes 클러스터에서 Couchbase 클러스터를 테스트했습니다. YCSB 워크로드 B가 사용되었습니다(주로 키-값 get 작업)을 수행한 결과 156,094 작업/초를 기록했습니다. API 테스트는 Apache JMeter로 수행되었습니다. ID API 호출은 간단하게 유지하기 위해 사용되었으며, JMeter 난수 생성기를 활용하여 임의의 사용자 프로필에 대한 테스트 실행을 생성했습니다. 테스트 시나리오는 무작위 사용자 프로필을 요청하는 로드 밸런서 서비스에 대해 램프업 없이 무제한 부하를 생성하는 실행 시간 3분으로 시간 제한이 있었습니다(전체 테스트 기간 동안 부하가 일정함).
첫 번째 테스트 세트의 경우, JMeter 테스트 매개변수는 변경되지 않았으며, 세 가지 API 구현의 규모만 변경되었습니다. 테스트는 각 구현 배포에 대해 4개의 파드로 시작하여 8개, 최종적으로 16개의 파드로 확장되었습니다. 모든 구현은 배포에서 파드가 확장됨에 따라 처리량을 확장했습니다.
이 테스트 전략에서는 평균 지연 시간이 가장 낮았던 Node.js가 가장 좋은 결과를 보였습니다. 1밀리초는 많은 지연 시간이 아니며 12밀리초도 마찬가지입니다. 하지만 고정된 수의 제너레이터 스레드가 3분 동안 1백만 건 이상의 요청을 생성하는 경우 밀리초는 누적 효과가 있습니다. 하지만 이는 극단적인 테스트라는 점을 염두에 두시기 바랍니다. 이는 단지 데이터 포인트일 뿐입니다. 놀라웠던 점은 전체 코드 Python 서비스가 FastAPI 구현에 보조를 맞추고 있다는 점입니다.
첫 번째 테스트 시나리오에서 전체 코드 Python과 FastAPI 구현이 모두 확장 가능함을 입증했기 때문에, 두 번째 테스트에서는 32개의 고정된 서비스 파드로 요청 스레드 수를 확장했습니다. 이 테스트 시나리오를 통해 Python 기반 서비스는 초당 10,000개에 가까운 요청으로 확장할 수 있었습니다.
결론
저는 파이썬이 중간 정도의 부하 서비스를 위한 훌륭한 옵션이라고 생각합니다. 모든 테스트를 수행한 결과, Kubernetes 클러스터 노드에는 사용 가능한 CPU와 메모리가 충분했기 때문에 필요에 따라 서비스를 확장할 수 있는 충분한 여유가 있었습니다. 가장 낮은 지연 시간으로 대규모 확장이 필요한 구현의 경우 Node.js가 더 나은 옵션일 수 있습니다. Couchbase는 널리 사용되는 모든 언어를 지원하므로 제가 3개의 마이크로서비스 구현을 쉽게 코딩할 수 있었던 것처럼 누구나 여러 언어와 프레임워크를 사용하고 Couchbase를 쉽게 통합할 수 있습니다.
다음 단계
이 블로그 시리즈의 다음 글에서는 마이크로서비스 스키마에 대한 무작위 테스트 데이터를 생성하는 방법에 대해 설명합니다. 다음은 이 게시물에서 언급된 리소스에 대한 링크입니다:
- 카우치베이스로 파이썬 마이크로서비스 구축 - 1부
- 2부 - 사용자 프로필을 위한 마이크로서비스 소스 코드
- 카펠라 카우치베이스 클라우드 서비스
- 카우치베이스 자율 운영자
재미있는 사실
HTTP 응답 코드는 프로토콜 사양에 따라 정의됩니다. 400 범위는 클라이언트에 의해 오류가 발생한 것으로 보이는 상황을 위해 예약되어 있습니다. HTTP 418은 "나는 주전자다" 오류로, 사양에 따르면 "나는 주전자다 클라이언트 오류 응답 코드는 서버가 영구적으로 주전자이기 때문에 커피 추출을 거부한다는 것을 나타냅니다."라고 명시되어 있습니다. 일시적으로 커피가 없는 커피/주전자는 대신 503을 반환해야 합니다."라고 명시되어 있습니다.
멋진 게시물입니다!
패스트파이가 기본 직렬화기로 인해 많은 어려움을 겪고 있다고 생각하며, 또한 상당히 빠르지만 출력 데이터의 유효성 검사를 수행하고 있으며 아마도 많은 시간이 걸릴 것입니다.