티스토리 뷰
서로 통신할 수 있는 독립적인 컴포넌트로 애플리케이션을 개발한다.
모놀리식 접근 방식
모놀리식 애플리케이션이란 거대한 단일 구조의 애플리케이션을 뜻
장점
- 단일 코드 기반(Single code base)이므로, 개발 초기에는 모든 것이 단순
- 테스트 coverage를 높이기 쉽다.
- 코드는 깨끗이 구조화된 형태로 유지됨
- 하나의 데이터베이스에 모든 데이터를 저장함으로써 데이터 모델과 쿼리 역시 쉽게 변경할 수 있기 때문에
- 애플리케이션 개발이 단순
- 모놀리식으로 프로젝트를 시작하는 것은 쉽다
- 중앙 집중화된 데이터베이스는 데이터 설계와 구성이 단순
- 단일 애플리케이션 배포는 간단
단점
- 어떤 기능에 대한 코드 수정이, 관련되지 않은 다른 기능에도 영향을 미친다.
- 해당 기능에 문제가 생기면 전체 애플리케이션이 잘못될 수도 있다.
- 애플리케이션 확장에 제약이 있다.
- 여러 개의 인스턴스를 배포하는 방법이 있지만, 애플리케이션의 특정 기능이 대부분의 리소스를 사용한다면 결국 전체 시스템에 영향을 미친다.
- 코드가 증가하면서 깨끗이 유지하기가 어렵고 이해하기도 힘들어 진다.
마이크로서비스 접근 방식
동일한 애플리케이션을 마이크로서비스 방식으로 개발한다면 각각의 프로세스로 실행되는 분리된 컴포넌트로 코드를 구성할 수 있다. 따라서 모든 것을 처리하는 단일 애플리케이션 대신, 여러 개의 마이크로서비스로 분리가 가능하다.
마이크로서비스는 명확하게 정의된 계약으로 축소된 기능 목록을 제공하는 가벼운 애플리케이션이다. 이 애플리케이션은 독립적인 개발과 배포가 가능한 단일 책임(single responsibility)을 가진다.
장점
- 위험 분리
- 작은 프로젝트
- 확장 및 배포의 다양한 옵션
- 하나의 팀이 각 마이크로서비스를 독립적으로 개발할 수 있으며, 선호하는 기술을 사용할 수 있다.
- 릴리스 주기 역시 원하는 대로 정할 수 있다. 제공하는 HTTP API만 명확히 정의해두면 그 외의 작업은 자유도가 높다
- 애플리케이션의 복잡성을 논리적인 컴포넌트로 분리한다.
- 각 마이크로서비스는 한 가지 일을 잘하는 데만 집중한다.
- 마이크로서비스는 독립형 애플리케이션이므로 배포를 효과적으로 제어할 수 있다. 따라서 확장하기도 편하다.
위험 분리
분리된 팀에서 독립적으로 서비스를 개발할 수 있다
각 마이크로서비스는 오로지 하나의 역할에만 집중하게 만들어야 한다.
작은 프로젝트
프로젝트의 복잡성을 감소시키는 것. 작은 프로젝트는 애플리케이션을 개선할 때 발생 가능한 위험을 감소시킨다. 팀에서 새로운 프로그래밍 언어나 프레임워크를 사용해보고 싶다면 빠르게 프로토타입을 만들어 사용해본 다음, 도입 여부를 결정할 수 있다.
확장 및 배포의 다양한 옵션
상황에 따른 확장이 쉽다.
단점
- 비논리적인 분할
- 네트워크 연동 증가
- 데이터 저장과 공유
- 호환성 이슈
- 테스트
- 애플리케이션을 성급하게 마이크로서비스로 분리하는 것은 아키텍처 문제를 유발
- 마이크로서비스 사이의 네트워크 연동은 약점이 될 수 있고 부가적인 오버헤드를 발생
- 테스트 및 배포가 복잡할 수 있다.
- 서비스 간의 데이터 공유가 어렵다(가장 큰 단점)
비논리적인 분할
마이크로서비스의 첫 번째 문제는 "어떻게 설계할 것인가?"다.
설계는 시도와 실패를 반복해 가면서 진행해야 한다. 더구나 마이크로서비스를 추가 하거나 제거하는 것은 모놀리식 애플리케이션을 리팩토링하는 것보다 어렵다.
분할 여부가 확실하지 않다면 분할하지 않는 것이 좋다. 나중에 코드를 떼 내어 새로운 마이크로서비스를 만들면 된다.
네트워크 연동 증가
백엔드 서비스 연동은 특별한 주의가 필요하며, 다음과 같은 질문에 답할 수 있어야 한다.
- 네트워크 끊김이나 지연으로 인해 예약 UI 서비스가 PDF 생성 서비스와 연결 할 수 없다면 어떻게 처리해야 하는가?
- 예약 UI 서비스가 다른 서비스를 호출하는 방식은 동기와 비동기 중 어느 방식을 사용해야 하는가?
- 응답 시간에 어떤 영향을 주는가?
데이터 저장과 공유
마이크로서비스는 다른 서비스에 대해 독립적이어야 하므로 데이터베이스를 공유하지 않는 것이 좋다.
- 모든 데이터베이스에서 동일한 사용자 ID를 사용해야 하는가? 아니면 각 서비스마다 고유한 ID를 생성해야 하는가?
- 새로운 사용자가 시스템에 추가되면 사용자 정보를 다른 서비스의 데이터베이스에 복제해야 하는가?
- 데이터 제거는 어떻게 다뤄야 하는가?
마이크로서비스 기반의 애플리케이션을 설계하는 데 있어 가장 중요한 과제 중 하나는, 데이터 중복을 최대한 피하면서 독립적으로 마이크로서비스를 유지하는 것이다.
호환성 이슈
기능 변경이 여러 마이크로서비스에 영향을 미칠 때 또 다른 문제가 발생
테스트
마지막으로 엔드 투 엔드(end-to-end) 테스트나 전체 앱을 배포할 때 많은 장애물을 만날 수 있다.
효과적으로 해결하려면 애자일 개발 프로세스를 따르는 것이 좋다. 개발할 때부터 전체 애플리케이션을 실행하고 연동할 수 있어야 한다. 일부만으로는 전체 테스트를 할 수 없다.
마이크로서비스는 배포 도구의 혁신을 촉진시켰고, 배포 도구의 발전은 마이크로서비스 아키텍처가 빠르고 널리 보급되는 데 큰 역할을 했다.
파이썬으로 마이크로 서비스 구현
WSGI 표준
이 표준에 따라 개발된 애플리케이션은 uwsig나 mod_wsgi 같은 WSGI 확장을 사용해서 아파치, nginx 등의 웹 서버에서 실행할 수 있다.
import json
import time
def application(environ, start_response):
headers = [('Content-type', 'application/json')]
start_response('200 OK', headers)
retrun [bytes(json.dumps({'time': time.time()}), 'utf8')]
if __name__ == '__main__':
from wsgiref.simple_server import make_server
srv = make_server('localhost', 8080, application)
srv.serve_forever()
WSGI의 가장 큰 문제는 동기(sync) 방식인데, 함수가 한 번 호출되면 응답을 반환할 때까지 블록된다.
따라서 마이크로서비스에 적용하면 연동된 서비스가 응답을 보내 줄 때까지 항상 대기해야 할 것이다. 바꿔 말하면 애플리케이션은 유후 상태가 되며, 모든 준비가 완료될 때까지 클라이언트가 블록된다.
WSGI 서버는 스레드 풀을 사용해서 여러 개의 동시 요청을 다룬다. 그러나 수 천개를 돌릴 수는 없다. 또한 풀이 꽉 차면 클라이언트 요청은 블록되며, 설사 마이크로 서비스가 아무것도 하지 않더라고 유휴 상태가 돼 백엔드 서비스의 응답을 대기한다.
이 문제는 반대로 Twisted나 Tornado 같은 비WSGI 프레임워크와 자바스크립트 진영의 node.js가 큰 성공을 거둔 이유 중 하나다. 이들은 완전한 비동기(async) 방식이기 때문이다.
Twisted에서 코딩할 때는 응답을 만들기 위해 콜백을 사용해서 수행한 작업을 잠시 멈춘 후 재개한다. 따라서 곧바로 새로운 요청을 받고 처리할 수 있다.
WSGI 표준으로는 이와 비슷하게 처리할 수 있는 간단한 방법이 없다. 커뮤니티는 수년 동안 논쟁을 벌였지만 합의를 도출하는 데 실패했다.
한편 한 개의 요청을 스레드 한 개에서 처리한다는 WSGI 표준의 제약을 따른다면 동기 프레임워크로 마이크로서비스를 구축하는 건 충분히 가능하며, 좋은 방법이다.
동기식 웹 애플리케이션의 성능을 향상시키기 위한 기술로 Greenlet이 있다.
Greenlet과 Gevent
비동기 프로그래밍의 일반적인 원칙은 프로세스에서 여러 스레드를 실행해서 병렬성(parallelism)을 흉내 내는 것이다.
비동기 애플리케이션은 이벤트가 발생할 때 실행 컨텍스트를 잠깐 중지했다가 재시작하는 이벤트 루프를 사용한다. 오직 하나의 컨텍스트만 활서화돼 순서대로 사용된다. 코드의 명시적인 명령을 통해 이벤트 루프가 실행을 멈출 수 있는 위치를 나타낸다. 실행이 멈추면 프로세스는 재시작할 다른 지연된 작업을 찾는다. 나중에 프로세스는 해당 함수로 다시 돌아와 멈췄던 곳에서 시작한다. 이처럼 실행되는 컨텍스트가 전환되는 것을 Context Switching이라고 한다.
Greenlet 프로젝트는 Stackless 프로젝트에 기반을 둔 패키지다. 특히 CPython 구현인 greenlets를 제공한다.
https://greenlet.readthedocs.io/en/latest/
Greenlet의 의사 스레드(pseudo-threads)는 실제 스레드와 다르게 매우 작은 비용으로 인스턴스를 만들어 파이썬 함수를 호출한다. 이 함수 내에서 switch를 사용해서 다른 함수를 조절할 수 있다. 이벤트 루프에서 스위칭이 완료되면 스레드 같은 인터페이스 패러다임을 사용해서 비동기 애플리케이션을 만들 수 있다.
from greenlet import greenlet
def test1(x, y):
z = gr2.switch(x+y)
print(z)
def test2(u):
print(u)
gr1.switch(42)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch("hello", "world")
WSGI 표준에 기반을 둔 마이크로서비스를 만들기 위해 greenlet을 사용하면 I/O 요청처럼 요청이 블록되는 경우 다른 쪽으로 switch해서 여러 개의 동시 요청을 받게 할 수 있다.
하지만 greenlet에서 스위칭은 명시적이어야 하므로, 코드가 지저분해지고 이해하기 어려울 수 있다. 이런 면에서 Gevent는 매우 유용하다.
Gevent 프로젝트는 Greenlet 기반으로 만들었으면 greenlet 간의 암시적이고 자동화된 스위칭 방법을 제공한다.
Gevent는 greenlet을 사용해서 자동으로 멈추고 소켓에서 데이터 사용이 가능해지면 실행을 재개하는 소켓 모듈의 협업 버전을 제공한다. 또한 monkey patch라고 부르는, 표준 라이브러리 소켓을 Gevent 버전으로 자동 대체하는 기능이 있다.
from gevent import monkey; monkey.patch_all()
def application(environ, start_response):
headers = [('content-type', 'application/json')]
start_response('200 OK', headers)
# 소켓으로 필요한 작업을 한다.
return result
Gevent가 잘 동작하려면 모든 기본 코드가 Gevent 패치와 호환돼야 한다. 특히 C 확장을 사용하거나 Gevent가 패치한 일부 기능을 우회하는 경우 다른 패키지 일부가 계속 차단돼 예상치 못한 결과가 발생할 수 있다.
하지만 대부분의 경우 잘 동작하고 Gevent와 잘 동작하는 프로젝트들은 green이라 불린다.
트위스티와 토네이도
동시 요청의 수가 증가하고 이를 처리하는 것이 중요하다면 WSGI 표준을 포기하고 tornado나 twisted 같은 비동기 프레임워크를 사용할 수 있다.
import time
from twisted.web import server, resource
from twisted.internet import reactor, endpoints
class Simple(resource.Resource):
isLeaf = True
def render_GET(self, request):
request.responseHeaders.addRawHeader(b"content-type", b"application/json")
return bytes(json.dumps({'time': time.time()}), 'utf8')
site = server.Site(Simple())
endpoint = endpoints.TCP4ServerEndpoint(reactor, 8080)
endpoint.listen(site)
reactor.run()
HTTP 마이크로서비스로 사용 하는데 문제
- Resource 클래스를 상속받는 클래스로 각 마이크로서비스의 endpoint를 구현해야 하며, 필요한 함수도 구현해야 한다. 몇 개의 단순한 API를 만들기 위해 많은 장황한 코드가 추가된다.
- 트위스트 코드는 비동기적인 성질 때문에 이해하기 어렵고 디버그가 힘들다.
- 트리거를 연달아 발생시키기 위해 너무 많은 함수를 연결하면 callback hell에 빠지기 쉽다. 코드 역시 지저분해진다.
- 트위스티드 애플리케이션은 제대로 테스트하기 어렵다. 트위스티드에 종속된 단위 테스트 모델을 사용해야 한다.
asyncio
파이썬 핵심 개발자들이 asyncio를 코딩한 방법, 그리고 coroutine을 구현하기 위해 async와 await 키워드를 사용해서 언어를 확장한 방법은 순수 파이썬으로 구현한 비동기 애플리케이션 코드가 매우 우아하며, 동기식 코드에 가깝게 보이게 한다.
코루틴은 실행이 일시적으로 중지됐다가 다시 시작되는 함수를 말한다.
이렇게 되면서 Node.js나 트위스티드 애플리케이션에서 가끔씩 보이는 지저분한 콜백 구문을 피할 수 있게 됐다.
코루틴 이외에도 파이썬 3는 비동기 애플리케이션을 만드는 데 필요한 여러 가지 기능을 asyncio 패키지에서 제공하고 있다.
https://docs.python.org/3/library/asyncio.html
이러한 기능을 채택한 몇 개의 프레임워크가 있으며, aiohttp는 그런 프레임워크 중 하나다.
https://aiohttp.readthedocs.io/en/stable/
from aiohttp import web
import time
async def handle(request):
return web.json_response({'time': time.time()})
if __name__ == '__main__':
app = web.Application()
app.router.add_get('/', handle)
web.run_app(app)
asyncio를 사용한 또 다른 프로젝트인 aiopg는 PostgreSQL용 라이브러리다
https://aiopg.readthedocs.io/en/stable/
import asyncio
import aiopg
dsn = 'dbname=aiopg user=aiopg password=passwd host=127.0.0.1'
async def go():
pool = await aiopg.create_pool(dsn)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
ret = []
async for row in cur:
ret.append(row)
assert ret == [(1, )]
loop = asyncio.get_event_loop()
loop.run_until_complete(go())
파이썬에서는 Bottle, Pyramid, Cornice, Flast 같은 동기식 프레임워크를 사용해서 마이크로서비스를 개발할 수 있다.
https://cornice.readthedocs.io/en/latest/
https://flask.palletsprojects.com/en/1.1.x/
언어 성능
마이크로서비스는 생애 주기 대부분을 다른 서비스로부터 네트워크 응답이 오기를 기다리면서 보낸다. 이 속도는 보통 SQL 쿼리가 Postgres 서버로부터 결과를 얻는 속도보다 덜 중요하다. 후자가 응답을 돌려줄 때 소용되는 전체 시간의 대부분을 차지하기 때문이다.
GIL(Global Interpreter Lock)은 동일 프로세스에서 여러 코어의 사용를 방지하는 것 외에도 뮤텍스로 인한 시스템 호출 오버헤드 때문에 높은 부하에서 약간의 성능 저하가 있다.
def myfunc(data):
for value in data:
yield value + 1
import dis
dis.dis(myfunc)
정적으로 컴파일되는 언어로 이 함수를 만들면 동일한 결과를 만드는 데 필요한 작업수가 크게 감소한다.
PyPy 인터프리터를 사용해서 애플리케이션 실행 속도를 높이는 단순한 방법이 있다.
PyPy는 JIT(Just-in-Time) 컴파일러를 구현한다. 이 컴파일러는 런타임에 파이썬 코드를 CPU가 곧바로 사용할 수 있는 기계어로 변환한다. JIT의 목적은 실행에 앞어 실시간으로 언제 어떻게 작업을 처리할지 탐지하는 것이다.
REST(REpresentational State Transfer)
HATEOAS(Hypermedia as the engine of application state)
하이퍼미디어란 하나의 콘텐트가 텍스트나 이미지, 사운드와 같은 다양한 포맷의 다양한 콘텐트를 링크하는 개념
https://martinfowler.com/articles/richardsonMaturityModel.html
http://restcookbook.com/HTTP%20Methods/idempotency/
https://httpd.apache.org/docs/2.4/mod/mod_proxy.html
http://stateless.co/hal_specification.html
https://www.reportlab.com/opensource/
https://www.tiobe.com/tiobe-index/
'Microservices' 카테고리의 다른 글
코딩, 테스트, 문서화: 선순환 (0) | 2019.07.23 |
---|