1. 비동기 메시징
동기적, 비동기적 통신 방식에 대한 설명은 생략한다. 비동기 메시징 방식을 이용하면 서버와 클라이언트 사이의 메시지 큐에 요청 또는 응답 내용을 저장하고 있으므로 혹시나 서버나 클라이언트에서 문제가 발생하더라도 통신 내용을 메시지 큐에 안전하게 저장해놓을 수 있다는 장점이 있다.
레디스 사용 실습
실습으로 레디스 컨테이너를 실행해본다.
docker network create ch21
docker container run -d --name redis --network ch21 diamol/redis
docker container logs redis --tail 1
메시지를 보내본다. 메시지큐는 일반적으로 최적화된 전용 통신 프로토콜을 갖고 따로 메시지에 가공을 하지 않는 형태이기 때문에 보통 초당 수천개 정도의 메시지를 처리할 수 있다.
아래 명령어를 통해 실행하는 컨테이너는 channel21 이라는 채널에 메시지 'ping'을 5초에 한번씩 총 50번 반복 전송한다.
docker run -d --name publisher --network ch21 diamol/redis-cli -r 50 -i 5 PUBLISH channel21 ping
docker logs publisher
또 다른 레디스 컨테이너를 위에서 정의한 channel21을 주시하도록하여 메시지를 구독받는 형태로 실행한다. 그럼 5초마다 한번씩 메시지를 받는다.
docker run -it --network ch21 --name subscriber diamol/redis-cli SUBSCRIBE channel21
보통 레디스와 같은 메시지큐 도구들은 웹용 프레임워크나 각 언어의 SDK의 주요 플랫폼에서 제공된다. 그런 도구들을 컨테이너로 실행하면 이렇게 명령행 도구를 통해 따로 실행하는 것과 동일한 기능을 제공하면서 각 환경에 맞게 최적화된 메시지 큐 도구들로 활용할 수 있다.
2. 클라우드 네이티브 메시지 큐 사용(NATS)
앞서 여러 번 다뤘던 to-do 애플리케이션은 웹-SQL 데이터베이스로 구성됐다. 모든 요청과 응답이 동기적으로 이루어지는데, 이 상황에서 만약 많은 수의 사용자의 요청이 들어와서 DB connection이 일어나기 시작한다면 금세 데이터베이스의 최대 커넥션 수를 넘길 수 있다. 여기서 메시지큐를 도입하여 비동기적으로 처리하면 접속의 지속 시간이 짧아지기 때문에 어지간해서는 처리 한계에 도달하지 않는다.
퍼블리셔 생성
레디스를 또 사용해도 되지만, 이번에는 CNCF(클라우드 네이티브 재단)에서 관리하는 NATS 프로젝트로 구성해본다.
cd ch21/exercises/todo-list
docker-compose up -d message-queue
docker container logs todo-list_message-queue_1
# 현재 메시지큐의 클라이언트 수 확인
curl http://localhost:8222/connz
이 비동기 메시징을 적용하기 위해서 기존 닷넷 코어로 작성된 todo 애플리케이션에서도 약간의 수정이 필요하다.
public void AddToDo(ToDo todo)
{
MessageQueue.Publish(new NewItemEvent(todo));
_NewTasksCounter.Inc();
}
NATS에는 채널 개념이 없고 대신 서브젝트(subject)를 통해 메시지의 유형을 구분한다. 예제에서는 events.todo.newitem 이라는 서브젝트를 적용한다. 나중에 이 메시지를 구독하기 위한 채널명이라고 생각하면 된다.
다시 위 메시지 publishing 기능이 적용된 애플리케이션을 실행한다.
docker-compose up -d todo-web todo-db
그리고 localhost:8080에 접근하여 todo-list 페이지에서 add item을 해본다.
브라우저 캐싱 오류 처리
만약 8080 포트에 접속했는데 todo-list 페이지가 아니라 이전에 학습한 traefik 등의 페이지가 나온다면, todo-list 컨테이너가 정상적으로 8080 포트를 바라보고 실행하고 있는지를 확인해야한다. 그리고 만약 다른 컨테이너가 8080포트를 주시하여 todo-list 컨테이너가 제대로 실행되지 않았다면 다른 컨테이너를 종료하고 다시 실행한다.
그 이후에도 동일한 현상이라면 브라우저가 주소-화면을 캐싱하고 있어서 일 수 있다. 나의 경우 크롬 브라우저라서, 도구 -> 인터넷 사용 기록 삭제 -> 캐시된 이미지 및 파일 삭제 처리를 통해 해결했다.
new item 페이지에서 할 일을 추가해도 정상적으로 리스트에 조회되지 않는다. 이것은 새로운 todo 아이템을 publisher를 통해서 메시지큐로 보냈기 때문이다. 이제 이 메시지들을 수신해서 처리할 subscriber 컨테이너를 실행한다. 이 컨테이너는 위에서 설정한 events.todo.newitem 서브젝트를 구독한다.
서브스크라이버 생성
docker container run -d --name todo-sub --network todo-list_app-net diamol/nats-sub events.todo.newitem
docker container logs todo-sub
#브라우저에서 localhost:8080에 접근하여 새 할일 추가
docker container logs todo-sub
정상적으로 실행됬다면 아래처럼 subscriber에 로그가 찍힌다.
메시지 핸들러 추가
이제 구독한 메시지를 받아서 애플리케이션에서 처리하고 데이터베이스에 그 내용을 추가할 메시지 핸들러를 추가해야한다.
# 메시지 핸들러 컨테이너 시작
docker-compose up -d save-handler
docker logs todo-list_save-handler_1
# localhost:8080에 새로운 할 일 추가
docker logs todo-list_save-handler_1
로그가 정상적으로 찍혔다면 이후 브라우저를 새로고침했을 때 새 할일 목록이 정상적으로 추가된다. 다만 약간의 시간차가 발생한다.
결과적 일관성(eventual consistency)
https://swdevelopment.tistory.com/147
시간차가 발생하는 것은 결과적 일관성을 위해 polling대신 push 방식을 적용했기 때문이다. 보통 클라이언트-서버간 요청 응답 구조가 polling 방식으로 클라이언트에서 서버에 주기적으로 계속 요청을 하는 방식을 사용한다. 그러다가 서버에서 어떤 이벤트가 발생되어 데이터가 변경되거나 상태가 변경되면 polling을 멈추고 변경된 데이터를 사용자에게 보여준다. 그러나 polling 방식은 서버에 부하를 줄 수 있고 실시간성을 요구하는 서비스의 경우 요청 주기가 짧아져서 부하가 매우 커질 수 있기 때문에 주의해야한다.
이에 반에 push 방식을 이용하면 클라이언트에서 요청이 오더라도 일단 HTTP 요청에 대한 응답을 하지 않고 있다가, 서버에서의 처리가 모두 끝나고 데이터의 일관성이 확보되면 그제서야 클라이언트에게 응답하는 방식이다.
메시지큐의 경우 여러 메시지가 비동기적으로 들어와서 처리되다보니 결과의 일관성이 떨어질 수 있다. 예를 들어 상품 2개 중 1개의 메시지큐에서 1개를 소비하고, 나머지 메시지큐에서 1개를 소모하는 요청이 들어왔는데, 첫 번째 메시지큐의 결과만 갖고 클라이언트에게 결과를 표시하면 9개인 상태로 새로운 요청을 받게되어 사용자에게 잘못된 상품 갯수 정보를 전달할 수 있을 것이다. 그래서 모든 메시지큐의 결과가 정리된 뒤에(결과적 일관성을 확보한 뒤에) 푸시를 하는 푸시 모델을 적용했다. 그렇기 때문에 상기 약간의 시간차가 발생한 것이다.
수평적 Scaling - 로드 밸런싱
핸들러의 숫자를 늘리는 것만으로 로드 밸런싱을 할 수 있다. NATS는 라운드 로빈 방식(요청을 서버의 순서대로 쭉 할당하는 것)의 로드 밸런싱을 제공한다.
docker-compose up -d --sacle save-handler=3
docker logs todo-list_save-handler_2
# localhost:8080에서 new item 추가
docker-compose logs --tail=1 save-handler
마지막 로그를 보면 처음과 다른 컨테이너에 의해서 메시지가 처리되었음을 볼 수 있다.
3. 핸들러로 기능추가
메시지큐를 비동기적으로 구성한 것은 어떤 요청을 담을 그릇을 만든 것과 같다. 이 요청을 기존 애플리케이션과 소통하도록 하는 것이 주 목적이였지만 다른 애플리케이션에 전달하는 기능을 핸들러에 추가하면 또 다른 기능을 하도록 만들 수 있다. 교재에서 설명하기로는 이 메시지의 데이터를 일래스틱서치에 저장하여 키바나에서 검색하게 할 수도 있고, 새 할 일 정보를 구글 캘린더에 추가할수도 있다고 한다.
docker-compose -f docker-compose.yml -f docker-compose-audit.yml up -d --scale save-handler=3
docker logs todo-list_audit-handler_1
# localhost:8080에서 새로운 할 일 추가
docker logs todo-list_audit-handler_1
위 명령어는 새롭게 추가되는 할 일을 로그로 출력하는 간단한 메시지 핸들러이다. 기존 컨테이너의 변경은 없고 핸들러의 설정만 변경되었으므로 무중단 배포가 되었다.
이런 방식으로 처리하면 새 할 일을 추가했을 때 두 가지의 처리가 함께 일어난다. DB에 저장도 하고, 로그로 출력도 하는 것이다. 서로 다른 컨테이너에서 동작하며 웹 UI는 이들을 기다릴 필요가 없다. 처리 시간도 다르므로 사용자 경험에 영향을 미치지 않는 아키텍처이다.
Publish-Subscribe, Request-Response 패턴
여태 배웠던 패턴은 publish-subscribe(pub-sub) 패턴이였다. 웹이 메시지큐에 요청을 퍼블리시하면, 웹은 핸들러의 존재를 알지 못한채 메시지가 핸들러에 전달되어 처리되는 방식이였다.
이외에 request-reponse 패턴은 웹이 메시지큐에 요청을 전달하고, 핸들러에서 처리된 응답을 다시 웹에 전달하는 방식이다. 원래 동기적으로 처리되는 요청-응답을 메시지큐에 일단 전달하여 비동기적으로 처리하는 것이다.
대부분의 메시지 큐 기술은 이런 패턴들과 약간의 변종 정도이다.