Middleware/Kafka&RabbitMQ 2022. 7. 27. 16:19

spring.cloud.stream.bindings.<channelName>.consumer.concurrency 옵션과 관련해 설명한다.

 

아래 상황이 있다고 가정하자.

 

  • 토픽 이름 : A-topic
  • 파티션 개수 : 4
  • 앱 인스턴스 개수 : 1(concurrency == 2)

 

보통은 위와 같이 설정을 하게 되면, 당연히 하나의 앱에서 2개는 동시 처리 하겠구나 생각을 하기 마련이지만 실제로는 그렇게 동작하지 않을 수 있다. 아래와 같이 컨슈머 그룹이 구성 되어 있다고 생각해보자

 

./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group a-topic-group --describe

GROUP         TOPIC       PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID                                             CLIENT-ID
a-topic-group a-topic     2          18846           18846           0               consumer-a-topic-3-cdb73066-d0de-4079-8fb0-a5663ba9c76d consumer-a-topic-3
a-topic-group a-topic     3          18887           18887           0               consumer-a-topic-3-cdb73066-d0de-4079-8fb0-a5663ba9c76d consumer-a-topic-3
a-topic-group a-topic     0          18838           18838           0               consumer-a-topic-2-2f6ac6eb-35f1-464a-b89e-0ea0579c28b2 consumer-a-topic-2
a-topic-group a-topic     1          19020           19020           0               consumer-a-topic-2-2f6ac6eb-35f1-464a-b89e-0ea0579c28b2 consumer-a-topic-2

 

위 그룹을 보면 2개의 컨슈머(CONSUMER-ID를 기준으로 보자)가 각각 2개의 파티션을 나눠가지고 있는데, 여기서 카프카의 특징을 보아야 한다. "하나의 컨슈머는 요청 하나씩만 처리가능하다" 이 특징에서 우리가 기대한대로 동작이 안하는것인데, 이유는 메시지가 2개가 발행되었고 이 2개의 메시지가 위 파티션중에 0,1번에 들어갔다고 보자. 그러면 우리는 2개가 동시에 처리 될것이라 기대하지만 실제로 컨슈머가 한놈이기때문에 먼저 들어온 것을 처리하고 다음것을 처리한다.(컨슈머 하나는 반드시 한번에 하나의 요청만 처리) 그렇기에 2개가 동시에 실행되지 않는 것이고 만약에 운좋게 서로 다른 컨슈머가 붙은 파티션으로 메시지가 들어가면 2개가 동시에 처리 될것이다.

 

그렇다면 우리가 동시에 처리하고 싶다면? "concurrency == 파티션 개수"로 설정해주면 된다. 그러면 아래와 같이 하나의 앱에 4개의 컨슈머가 각각 다른 파티션에 할당될 것이고 실제로 4개의 메시지가 동시에 처리 될 것이다.

 

 

./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group a-topic-group --describe

GROUP         TOPIC       PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID                                             CLIENT-ID
a-topic-group a-topic     2          18846           18846           0               consumer-a-topic-3-cdb73066-d0de-4079-8fb0-a5663ba9c76d consumer-a-topic-3
a-topic-group a-topic     3          18887           18887           0               consumer-a-topic-3-dfdfad01-d0de-4079-8fb0-a5663ba9c76d consumer-a-topic-3
a-topic-group a-topic     0          18838           18838           0               consumer-a-topic-2-dcvzc023-35f1-464a-b89e-0ea0579c28b2 consumer-a-topic-2
a-topic-group a-topic     1          19020           19020           0               consumer-a-topic-2-2f6ac6eb-35f1-464a-b89e-0ea0579c28b2 consumer-a-topic-2
posted by 여성게
:
Web/Spring Cloud 2019. 8. 25. 18:38

 

2019/02/24 - [Web/Spring Cloud] - Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리

 

Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리

Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리 스프링 클라우드 유레카는 넷플릭스 OSS에서 유래됐다. 자가 등록, 동적 탐색 및 부하 분산에 주로 사용되며, 부하 분산을 위해 내부..

coding-start.tistory.com

우리는 이전 포스팅들에서 Spring Cloud를 다루어보면서 동적인 서비스 등록과 서버사이드 로드밸런싱에 중대한 역할을 하게 되는 Eureka에 대해 다루어 봤었다. 이러한 유레카를 이용하여 우리는 애플리케이션의 무중단 배포도 가능하다. 새로운 애플리케이션을 올리고 이전 버전의 애플리케이션을 죽이는 단순한 과정에서 우리는 중요한 개념을 생각해야 한다. 만약 이전 버전의 애플리케이션이 사용자의 요청을 받아 처리 중이라면? 그냥 애플리케이션을 죽이면 처리중인 요청을 끝까지 처리하지 못하고 데이터 유실이 발생할 것이다. 이럴때 우리는 우아하게 종료할 수 있는 방안이 필요하다. 예를 들면, Apache에서도 프로세스를 재시작하는 명령에 restart / graceful 명령이 존재한다. 전자는 단순히 stop&start이고 후자는 받은 요청을 모두 처리하고 종료하게 된다. 이러한 기능을 Spring boot는 어떻게 제공할까?

 

Actuator를 사용하면 된다. 스프링 액츄에이터는 다양하게 실행 중인 애플리케이션의 모니터링 정보 및 유용한 기능을 제공한다. 이중 shutdown 기능이 있는데, 액츄에이터의 shutdown은 graceful 하게 shutdown을 시켜준다 ! 즉, 데이터 유실 없이 안전하고 우아한 애플리케이션 종료를 제공한다.

posted by 여성게
:
IT이론 2019. 4. 3. 21:57

 

 

 

최근 소프트웨어를 서비스 형태로 제공하는게 일반화 되면서, 웹앱 혹은 SaaS(Software As A Service)라고 부르게 되었다. Twelve-Factor app은 아래 특징을 가진 SaaS 앱을 만들기 위한 방법론이다.

  • 설정 자동화를 위한 절차를 체계화하여 새로운 개발자가 프로젝트에 참여하는데 드는 시간과 비용을 최소화한다.
  • OS에 따라 달라지는 부분을 명확히하고, 실행 환경 사이의 이식성을 극대화한다.(OS에 종속되지 않는 애플리케이션)
  • 클라우드 플랫폼에 적합하고, 서버와 시스템의 관리가 필요없게 된다.
  • 개발 환경과 운영 환경의 차이를 최소화하고 민첩성을 극대화하기 위해 지속적인 배포가 가능하다.
  • 툴, 아키텍쳐, 개발방식을 크게 바꾸지 않고 확장(scale up)할 수 있다.

Twelve-Factor 방법론은 어떤 프로그래밍 언어로 작성된 앱에도 적용할 수 있고, 백엔드 서비스(DB,큐,Mem chache 등)와 다양한 조합으로 사용할 수 있다.

 

 

1. 코드베이스(Code Base)

버전관리되는 하나의 코드베이스와 다양한 배포

 

Twelve-Factor 앱은 항상 깃,서브버전 같은 버전 제어 시스템을 사용하여 변화를 추적하며, 버전 추적 데이터베이스의 사본을 코드 저장소, 줄여서 저장소라고 부른다. 코드베이스는 단일 저장소(서브버전 같은 중앙 집중식 버전 관리 시스템)일 수도 있고, 루트 커밋을 공유하는 여러 저장소(깃 같은 분산 버전 관리 시스템)일수도 있다.

코드베이스와 앱 사이에는 항상 1대1 관계가 성립된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 코드베이스가 여러개 있는 경우, 앱이 아니라 분산 시스템으로 봐야한다. 분산 시스템의 개별 구성요소가 앱이 되며, 개별 앱이 Twelve-Factor를 따른다.
  • 여러개 앱이 동일한 코드를 공유한다면 Twelve-Factor를 위반하는 것이다. 이를 해결하려면 공유하는 코드를 라이브러리화 시키고, 해당 라이브러리를 종속성 매니저로 관리해야한다.

앱의 코드베이스는 한개여야 하지만, 앱 배포는 여러개가 될 수 있다. 배포는 실행중인 앱의 인스턴스를 가리킨다. 보통 운영 사이트와 여러 스테이징 사이트가 여기에 해당된다. 모든 개발자는 자신의 로컬 개발 환경에 실행되는 앱을 가지고 있는데, 이것 역시 하나의 배포로 볼 수 있다.(옆 그림참조)

배포마다 다른 버전이 활성화 될 수 있지만, 코드베이스 자체는 모든 배포에 대해 동일하다. 예를 들어, 개발자는 아직 스테이징 환경에 배포하지 않은 커밋이 있을 수 있으며, 스테이징 환경에는 아직 운영 환경에 배포되지 않은 커밋이 있을 수 있다. 하지만 이 모든 것들이 같은 코드베이스를 공유하고, 같은 앱의 다른 배포라고 할 수 있다.

 

2. 종속성

명시적으로 선언되고 분리된 종속성

 

대부분의 프로그래밍 언어는 라이브러리 배포를 위한 패키징 시스템을 제공하고 있다. 라이브러리는 패키징 시스템을 통해 시스템 전체나 애플리케이션을 포함한 디렉토리에 설치될 수 있다.

Twelve-Factor App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않는다. 종속성 선언 mainifest를 이용하여 모든 종속성을 완전하고 엄격하게 선언한다. 더 나아가, 종속성 분리툴을 사용하여 실행되는 동안 둘러싼 시스템으로 암묵적인 종속성 "유출
"이 발생하지 않는 것을 보장한다. 이런 완전하고 명시적인 종속성의 명시는 개발과 서비스 모두에게 동일하게 적용된다.

 

3. 설정

환경에 저장된 설정

 

애플리케이션의 설정은 배포(스테이징,프로덕션,개발 등)마다 달리질 수 있는 모든 것들이다. 설정에는 다음이 포함된다.

  • 데이터베이스,메모리캐시 등 백엔드 서비스들의 리소스 핸들
  • 외부 서비스 인증정보
  • 배포된 호스트의 정규화된 호스트이름처럼 각 배포마다 달리지는 값

애플리케이션은 종종 설정을 상수로 코드에 저장한다. 이것은 Twelve-Factor를 위반하며, Twelve-Factor는 설정을 코드에서 엄격하게 분리하는 것을 요구한다. 설정은 배치마다 크게 다르지만, 코드는 그렇지 않기 때문이다.

 

애플리케이션의 모든 설정이 정상적으로 코드 바깥으로 분리되어 있는지 확인할 수 있는 간단한 테스트는 어떠한 인정정보도 유출시키지 않고 코드베이스가 지금 당장 오픈소스가 될 수 있는지 확인하는 것이다. 이 "설정"의 정의는 애플리케이션 내부 설정을 포함하지 않는다는 점에 유의해야한다. Spring의 "어떻게 코드 모듈이 연결되는 가"와 같은 설정들은 배치 사이에 변하지 않기 때문에 코드의 내부에 있는 것이 가장 좋다.

 

설정에 대한 또 다른 접근방식은 Rails의 config/database.yaml처럼 버전관리 시스템에 등록되지 않은 설정 파일을 이용하는 것이다. 이 방법은 코드 저장소에 등록된 상수를 사용하는 것에 비하면 매우 큰 발전이지만, 설정 파일이 여러 위치에 여러 포맷으로 흝어지고 모든 설정을 한 곳에서 확인하고 관리하기 어렵게 만드는 경향이 있다.

 

Twelve-Factor App은 설정을 환경변수에 저장한다. 환경 변수는 코드 변경 없이 배포 때마다 쉽게 변경할 수 있다. 설정 파일과 달리, 잘못해서 코드 저장소에 올라갈 가능성도 낮다. 또한, 커스텀 설정 파일이나 Java System Property와 같은 다른 설정 매커니즘과 달리 언어나 OS에 의존하지 않은 표준입니다.

 

설정 관리의 다른 측면은 그룹핑입니다. 종종 애플리케이션은 설정을 명명된 그룹(“environments”라고도 함)으로 구성하기도 합니다. 해당 그룹은 Rails의 ‘development’, ‘test’, ‘production’ environments처럼, 배포의 이름을 따서 명명됩니다. 이 방법은 깔끔하게 확장하기 어렵습니다. 응용 프로그램의 배포가 증가함에 따라, ‘staging’이라던가 ‘qa’같은 새로운 그룹의 이름이 필요하게 됩니다. 프로젝트가 성장함에 따라, 개발자은 자기 자신의 그룹를 추가하게 됩니다. 결과적으로 설정이 각 그룹의 조합으로 폭발하게 되고, 애플리케이션의 배포를 불안정하게 만듭니다.

Twelve-Factor App에서 환경 변수는 매우 정교한 관리이며, 각각의 환경변수는 서로 직교합니다. 환경 변수는 “environments”로 절대 그룹으로 묶이지 않지만, 대신 각 배포마다 독립적으로 관리됩니다. 이 모델은 애플리케이션의 수명주기를 거치는 동안 더 많은 배포로 원활하게 확장해 나갈 수 있습니다.

 

4. 백앤드 서비스

백엔드 서비스를 연결된 리소스로 취급

 

백엔드 서비스는 애플리케이션 정상 동작 중 네트워크를 통해 이용하는 모든 서비스입니다. 예를 들어, 데이터 저장소(예: MySQL, CouchDB), 메시지 큐잉 시스템(예: RabbitMQ, Beanstalkd), 메일을 보내기 위한 SMTP 서비스 (예: Postfix), 캐시 시스템(예: Memcached) 등이 있습니다.

데이터베이스와 같은 백엔드 서비스들은 통상적으로 배포된 애플리케이션과 같은 시스템 관리자에 의해서 관리되고 있었습니다. 애플리케이션은 이런 로컬에서 관리하는 서비스 대신, 서드파티에 의해서 제공되고 관리되는 서비스를 이용할 수 있습니다. 예를 들어, SMTP 서비스 (예: Postmark), 지표 수집 서비스 (예: New Relic, Loggly), 스토리지 서비스 (예: Amazon S3), API로 접근 가능한 소비자 서비스 (예: Twitter, Google Maps, Last.fm)등이 있습니다.

Twelve-Factor App의 코드는 로컬 서비스와 서드파티 서비스를 구별하지 않습니다. 애플리케이션에게는 양 쪽 모두 연결된 리소스이며, 설정에 있는 URL 혹은 다른 로케이터와 인증 정보를 사용해서 접근 됩니다. Twelve-Factor App의 배포는 애플리케이션 코드를 수정하지 않고 로컬에서 관리되는 MySQL DB를 서드파티에서 관리되는 DB(예: Amazon RDS)로 전환할 수 있어야 합니다. 마찬가지로, 로컬 SMTP 서버는 서드파티 SMTP 서비스(예: Postmark)로 코드 수정 없이 전환이 가능해야 합니다. 두 경우 모두 설정에 있는 리소스 핸들만 변경하면 됩니다.

각각의 다른 백엔드 서비스는 리소스입니다. 예를 들어, 하나의 MySQL DB는 하나의 리소스입니다. 애플리케이션 레이어에서 샤딩을 하는 두 개의 MySQL 데이터베이스는 두 개의 서로 다른 리소스라고 볼 수 있습니다. Twelve-Factor App은 이러한 데이터베이스들을 첨부된(Attached) 리소스로 다룹니다. 이는 서로 느슨하게 결합된다는 점을 암시합니다.

리소스는 자유롭게 배포에 연결되거나 분리될 수 있습니다. 예를 들어, 애플리케이션의 데이터베이스가 하드웨어 이슈로 작용이 이상한 경우, 애플리케이션의 관리자는 최신 백업에서 새로운 데이터베이스 서버를 시작시킬 것입니다. 그리고 코드를 전혀 수정하지 않고 현재 운영에 사용하고 있는 데이터베이스를 분리하고 새로운 데이터베이스를 연결할 수 있습니다.

 

5. 빌드, 릴리즈, 실행

철저하게 분리된 빌드와 실행 단계

 

코드베이스는 3 단계를 거쳐 (개발용이 아닌) 배포로 변환됩니다.

  • 빌드 단계는 코드 저장소를 빌드라는 실행 가능한 번들로 변환시키는 단계입니다. 빌드 단계에서는 커밋된 코드 중 배포 프로세스에서 지정된 버전을 사용하며, 종속성을 가져와 바이너리와 에셋들을 컴파일합니다.
  • 릴리즈 단계에서는 빌드 단계에서 만들어진 빌드와 배포의 현재 설정을 결합 합니다. 완성된 릴리즈는 빌드와 설정을 모두 포함하며 실행 환경에서 바로 실행될 수 있도록 준비됩니다.
  • 실행 단계(런타임이라고도 하는)에서는 선택된 릴리즈에 대한 애플리케이션 프로세스의 집합을 시작하여, 애플리케이션을 실행 환경에서 돌아가도록 합니다.

Twelve-Factor App은 빌드, 릴리즈, 실행 단계를 엄격하게 서로 분리합니다. 예를 들어, 실행 단계에서 코드를 변경할 수는 없습니다. 변경을 실행 단계보다 앞에 있는 빌드 단계로 전달할 수 있는 방법이 없기 때문입니다.

배포 도구는 일반적으로 릴리즈 관리 도구를 제공합니다. 특히 주목할만한 점은 이전 릴리즈로 되돌릴 수 있는 롤백 기능입니다. 예를 들어, Capistrano는 배포 툴은 릴리즈를 releases라는 하위 디렉토리에 저장시키고, 현재 릴리즈는 현재 릴리즈 디렉토리로 심볼릭 링크로 연결합니다. 이 툴의 rollback 명령어는 이전 버전으로 쉽고 빠르게 이전 릴리즈로 롤백할 수 있도록 해줍니다. 모든 릴리즈는 항상 유니크한 릴리즈 아이디를 지녀야 합니다. 예를 들어, 릴리즈의 타임 스템프(예: 2011-04-06-20:32:17)나 증가하는 번호(예: v100, v101)가 있습니다. 릴리즈는 추가만 될 수 있으며, 한번 만들어진 릴리즈는 변경될 수 없습니다. 모든 변경은 새로운 릴리즈를 만들어야 합니다.

빌드는 새로운 코드가 배포 될 때마다 개발자에 의해 시작됩니다. 반면, 실행 단계는 서버가 재부팅되거나 충돌이 발생한 프로세스가 프로세스 매니저에 의해 재시작 되었을 때 자동으로 실행될 수 있습니다. 따라서 대응할 수 있는 개발자가 없는 한밤중에 문제가 발생하는 것을 방지하기 위해, 실행 단계는 최대한 변화가 적어야합니다. 빌드 단계는 좀 더 복잡해져도 괜찮습니다. 항상 배포를 진행하고 있는 개발자의 눈 앞에서 에러가 발생하기 때문입니다.

 

6.프로세스

애플리케이션을 하나 혹은 여러개의 무상태 프로세스로 실행

 

실행 환경에서 앱은 하나 이상의 프로세스로 실행됩니다.

가장 간단한 케이스는 코드가 stand-alone 스크립트인 경우입니다. 이 경우, 실행 환경은 개발자의 언어 런타임이 설치된 로컬 노트북이며, 프로세스는 커맨드 라인 명령어에 의해서 실행됩니다.(예: python my_script.py) 복잡한 케이스로는 많은 프로세스 타입별로 여러개의 프로세스가 사용되는 복잡한 애플리케이션이 있습니다.

Twelve-Factor 프로세스는 무상태(stateless)이며, 아무 것도 공유하지 않습니다. 유지될 필요가 있는 모든 데이터는 데이터베이스 같은 안정된 백엔드 서비스에 저장되어야 합니다.

짧은 단일 트랙잭션 내에서 캐시로 프로세스의 메모리 공간이나 파일시스템을 사용해도 됩니다. 예를 들자면 큰 파일을 받고, 해당 파일을 처리하고, 그 결과를 데이터베이스에 저장하는 경우가 있습니다. Twelve-Factor 앱에서 절대로 메모리나 디스크에 캐시된 내용이 미래의 요청이나 작업에서도 유효할 것이라고 가정해서는 안됩니다. 각 프로세스 타입의 프로세스가 여러개 돌아가고 있는 경우, 미래의 요청은 다른 프로세스에 의해서 처리될 가능성이 높습니다. 하나의 프로세스만 돌고 있는 경우에도 여러 요인(코드 배포, 설정 변경, 프로세스를 다른 물리적 장소에 재배치 등)에 의해서 발생하는 재실행은 보통 모든 로컬의 상태(메모리와 파일 시스템 등)를 없애버립니다.

에셋 패키징 도구 (예: Jammit, django-assetpackager)는 컴파일된 에셋을 저장할 캐시로 파일 시스템을 사용합니다. Twelve-Factor App은 이러한 컴파일을 런타임에 진행하기보다는, Rails asset pipeline처럼 빌드 단계에서 수행하는 것을 권장합니다.

웹 시스템 중에서는 “Sticky Session”에 의존하는 것도 있습니다. 이는 유저의 세션 데이터를 앱의 프로세스 메모리에 캐싱하고, 같은 유저의 이후 요청도 같은 프로세스로 전달될 것을 가정하는 것입니다. Sticky Session은 Twelve-Factor에 위반되며, 절대로 사용하거나 의존해서는 안됩니다. 세션 상태 데이터는 Memcached Redis처럼 유효기간을 제공하는 데이터 저장소에 저장하는 것이 적합합니다.

 

7.포트바인딩

포트 바인딩을 사용해서 서비스를 공개함

 

웹앱은 웹서버 컨테이너 내부에서 실행되기도 합니다. 예를 들어, PHP 앱은 Apache HTTPD의 모듈로 실행될 수도 있고, Java 앱은 Tomcat 내부에서 실행될 수도 있습니다.

Twelve-Factor 앱은 완전히 독립적이며 웹서버가 웹 서비스를 만들기 위해 처리하는 실행환경에 대한 런타임 인젝션에 의존하지 않습니다. Twelve-Factor 웹 앱은 포트를 바인딩하여 HTTP 서비스로 공개되며 그 포트로 들어오는 요청을 기다립니다.

로컬 개발 환경에서는 http://localhost:5000과 같은 주소를 통해 개발자가 애플리케이션 서비스에 접근할 수 있습니다. 배포에서는 라우팅 레이어가 외부에 공개된 호스트명으로 들어온 요청을 포트에 바인딩된 웹 프로세스에 전달 합니다.

이는 일반적으로 종속성 선언에 웹서버 라이브러리를 추가함으로써 구현됩니다. 예를 들어, 파이썬의 Tornado나 루비의 Thin이나 자바와 JVM 기반 언어들을 위한 Jetty가 있습니다. 이것들은 전적으로 유저 스페이스 즉, 애플리케이션의 코드 내에서 처리됩니다. 실행 환경과의 규약은 요청을 처리하기 위해 포트를 바인딩하는 것입니다.

포트 바인딩에 의해 공개되는 서비스는 HTTP 뿐만이 아닙니다. 거의 모든 종류의 서버 소프트웨어는 포트를 바인딩하고 요청이 들어오길 기다리는 프로세스를 통해 실행될 수 있습니다. 예를 들면, ejabberd (XMPP을 따름)나 Redis (Redis protocol을 따름) 등이 있습니다.

포트 바인딩을 사용한다는 것은 하나의 앱이 다른 앱을 위한 백엔드 서비스가 될 수 있다는 것을 의미한다는 점에 주목합시다. 백엔드 앱의 URL을 사용할 앱의 설정의 리소스 핸들로 추가하는 방식으로 앱이 다른 앱을 백엔드 서비스로 사용할 수 있습니다.

 

8. 동시성

프로세스 모델을 통한 확장

 

모든 컴퓨터 프로그램은 실행되면 하나 이상의 프로세스로 표현됩니다. 웹 애플리케이션은 다양한 프로세스 실행 형태를 취해왔습니다. 예를 들어, PHP 프로세스는 Apache의 자식 프로세스로 실행되며, request의 양에 따라 필요한 만큼 시작됩니다. 자바 프로세스들은 반대 방향에서의 접근법을 취합니다. JVM은, 시작될 때 큰 시스템 리소스(CPU와 메모리) 블록을 예약하는 하나의 거대한 부모 프로세스를 제공하고, 내부 쓰레드를 통해 동시성(concurrency)을 관리합니다. 두 경우 모두 실행되는 프로세스는 애플리케이션 개발자에게 최소한으로 노출됩니다.

Twelve-Factor App에서 프로세스들은 일급 시민입니다.Twelve-Factor App에서의 프로세스는 서비스 데몬들을 실행하기 위한 유닉스 프로세스 모델에서 큰 힌트를 얻었습니다. 이 모델을 사용하면 개발자는 애플리케이션의 작업을 적절한 프로세스 타입에 할당함으로서 다양한 작업 부하를 처리할 수 있도록 설계할 수 있습니다. 예를 들어, HTTP 요청은 웹 프로세스가 처리하며, 시간이 오래 걸리는 백그라운드 작업은 worker 프로세스가 처리하도록 할 수 있습니다.

이는 런타임 VM 내부의 쓰레드나 EventMachine, Twisted, Node.js에서 구성된 것 처럼 async/evented 모델처럼 개별 프로세스가 내부적으로 동시에 처리하는 것을 금지하는 것은 아닙니다. 하지만 개별 VM이 너무 커질 수 있습니다.(수직 확장) 따라서 애플리케이션은 여러개의 물리적인 머신에서 돌아가는 여러개의 프로세스로 넓게 퍼질 수 있어야만 합니다.

 

프로세스 모델이 진정으로 빛나는 것은 수평적으로 확장하는 경우입니다. 아무것도 공유하지 않고, 수평으로 분할할 수 있는 Twelve-Factor App 프로세스의 성질은 동시성을 높이는 것은 간단하고 안정적인 작업이라는 것을 의미 합니다. 프로세스의 타입과 각 타입별 프로세스의 갯수의 배치를 프로세스 포메이션이라고 합니다.

Twelve-Factor App 프로세스는 절대 데몬화해서는 안되며 PID 파일을 작성해서는 안됩니다. 대신, OS의 프로세스 관리자(예: systemd)나 클라우드 플랫폼의 분산 프로세스 매니저, 혹은 Foreman 같은 툴에 의존하여 아웃풋 스트림을 관리하고, 충돌이 발생한 프로세스에 대응하고, 재시작과 종료를 처리해야 합니다.

 

9. 폐기 가능

빠른 시작과 그레이스풀 셧다운을 통한 안정성 극대화

 

Twelve-Factor App의 프로세스 간단하게 폐기 가능합니다. 즉, 프로세스는 바로 시작하거나 종료될 수 있습니다. 이러한 속성은 신축성 있는 확장과 코드 설정의 변화를 빠르게 배포하는 것을 쉽게 하며, production 배포를 안정성 있게 해줍니다.

프로세스는 시작 시간을 최소화하도록 노력해야합니다. 이상적으로, 프로세스는 실행 커맨드가 실행된 뒤 몇 초만에 요청이나 작업을 받을 수 있도록 준비 됩니다. 짧은 실행 시간은 릴리즈 작업과 확장(scale up)이 더 민첩하게 이루어질 수 있게 합니다. 또한 프로세스 매니저가 필요에 따라 쉽게 프로세스를 새로운 머신으로 프로세스를 옮길 수 있기 때문에 안정성도 높아집니다.

프로세스는 프로세스 매니저로부터 SIGTERM 신호를 받았을 때 그레이스풀 셧다운(graceful shutdown)을 합니다. 웹프로세스의 그레이스풀 셧다운 과정에서는 서비스 포트의 수신을 중지하고(그럼으로써 새로운 요청을 거절함), 현재 처리 중인 요청이 끝나길 기다린 뒤에 프로세스가 종료 되게 됩니다. 이 모델은 암묵적으로 HTTP 요청이 짧다는 가정(기껏해야 몇 초)을 깔고 있습니다. long polling의 경우에는 클라이언트가 연결이 끊긴 시점에 바로 다시 연결을 시도해야 합니다.

worker 프로세스의 경우, 그레이스풀 셧다운은 현재 처리중인 작업을 작업 큐로 되돌리는 방법으로 구현됩니다. 예를 들어, RabbitMQ에서는 worker는 NACK을 메시지큐로 보낼 수 있습니다. Beanstalkd에서는 woker와의 연결이 끊기면 때 자동으로 작업을 큐로 되돌립니다. Delayed Job와 같은 Lock-based 시스템들은 작업 레코드에 걸어놨던 lock을 확실하게 풀어놓을 필요가 있습니다. 이 모델은 암묵적으로 모든 작업은 재입력 가능(reentrant)하다고 가정합니다. 이는 보통, 결과를 트랜잭션으로 감싸거나 요청을 멱등(idempotent)하게 함으로써 구현될 수 있습니다.

프로세스는 하드웨어 에러에 의한 갑작스러운 죽음에도 견고해야합니다. 이러한 사태는 SIGTERM에 의한 그레이스풀 셧다운에 비하면 드문 일이지만, 그럼에도 발생할 수 있습니다. 이런 일에 대한 대책으로 Beanstalkd와 같은 견고한 큐잉 백엔드를 사용하는 것을 권장합니다. 이러한 백엔드는 클라이언트가 접속이 끊기거나, 타임 아웃이 발생했을 때, 작업을 큐로 되돌립니다. Twelve-Factor App은 예기치 못한, 우아하지 않은 종료도 처리할 수 있도록 설계됩니다. Crash-only design에서는 논리적인 결론으로 이러한 컨셉을 가져왔습니다.

 

10. dev/prod 일치

개발,스테이징,프로덕트 환경을 최대한 비슷하게 유지

 

역사적으로, 개발 환경(애플리케이션의 개발자가 직접 수정하는 로컬의 배포)과 production 환경(최종 사용자가 접근하게 되는 실행 중인 배포) 사이에는 큰 차이가 있었습니다. 이러한 차이는 3가지 영역에 걸처 나타납니다.

  • 시간의 차이: 개발자가 작업한 코드는 production에 반영되기까지 며칠, 몇주, 때로는 몇개월이 걸릴 수 있습니다.
  • 담당자의 차이: 개발자가 작성한 코드를 시스템 엔지니어가 배포합니다.
  • 툴의 차이: production 배포는 아파치, MySQL, 리눅스를 사용하는데, 개발자는 Nginx, SQLite, OS X를 사용할 수 있습니다.

Twelve Factor App은 개발 환경과 production 환경의 차이를 작게 유지하여 지속적인 배포가 가능하도록 디자인 되었습니다. 위에서 언급한 3가지 차이에 대한 대응책은 아래와 같습니다.

  • 시간의 차이을 최소화: 개발자가 작성한 코드는 몇 시간, 심지어 몇 분 후에 배포됩니다.
  • 담당자의 차이를 최소화: 코드를 작성한 개발자들이 배포와 production에서의 모니터링에 깊게 관여합니다.
  • 툴의 차이를 최소화: 개발과 production 환경을 최대한 비슷하게 유지합니다.

위의 내용을 표로 요약하면 아래와 같습니다.

전통적인 애플리케이션Twelve-Factor App배포 간의 간격코드 작성자와 코드 배포자개발 환경과 production 환경

몇 주 몇 시간
다른 사람 같은 사람
불일치함 최대한 유사함

데이터베이스, 큐잉 시스템, 캐시와 같은 백엔드 서비스는 dev/prod 일치가 중요한 영역 중 하나 입니다. 많은 언어들은 다른 종류의 서비스에 대한 어댑터를 포함하고 간단하게 백엔드 서비스에 접근할 수 있는 라이브러리들을 제공합니다. 아래의 표에 몇가지 예가 나와있습니다.

종류언어라이브러리어댑터

데이터 베이스 Ruby/Rails ActiveRecord MySQL, PostgreSQL, SQLite
큐(Queue) Python/Django Celery RabbitMQ, Beanstalkd, Redis
캐쉬 Ruby/Rails ActiveSupport::Cache 메모리, 파일시스템, Memcached

production 환경에서는 더 본격적이고 강력한 백엔드 서비스가 사용됨에도 불구하고, 개발자는 자신의 로컬 개발 환경에서는 가벼운 백엔드 서비스를 사용하는 것에 큰 매력을 느낄 수도 있습니다. 예를 들어, 로컬에서는 SQLite를 사용하고 production에서는 PostgreSQL을 사용한다던가, 개발 중에는 로컬 프로세스의 메모리를 캐싱용으로 사용하고 production에서는 Memcached를 사용하는 경우가 있습니다.

Twelve-Factor 개발자는 개발 환경과 production 환경에서 다른 백엔드 서비스를 쓰고 싶은 충동에 저항합니다. 이론적으로는 어댑터가 백엔드 서비스 간의 차이를 추상화해준다고 해도, 백엔드 서비스 간의 약간의 불일치가 개발 환경과 스테이징 환경에서는 동작하고 테스트에 통과된 코드가 production 환경에서 오류를 일으킬 수 있기 때문입니다. 이런 종류의 오류는 지속적인 배포를 방해합니다. 애플리케이션의 생명 주기 전체를 보았을 때, 이러한 방해와 지속적인 배포의 둔화가 발생시키는 손해는 엄청나게 큽니다.

가벼운 로컬 서비스는 예전처럼 필수적인 것은 아닙니다. Memcache, PostgreSQL, RabbitMQ와 같은 현대적인 백엔드 서비스들은 Homebrew apt-get와 같은 현대적인 패키징 시스템 덕분에 설치하고 실행하는데 아무런 어려움도 없습니다. 혹은 Chef and Puppet와 같은 선언적 provisioning 툴과 Vagrant등의 가벼운 가상 환경을 결합하여 로컬 환경을 production 환경과 매우 유사하게 구성할 수 있습니다. dev/prod 일치와 지속적인 배포의 이점에 비하면 이러한 시스템을 설치하고 사용하는 비용은 낮습니다.

여러 백엔드 서비스에 접근할 수 있는 어댑터는 여전히 유용합니다. 새로운 백엔드 서비스를 사용하도록 포팅하는 작업의 고통을 낮춰주기 때문입니다. 하지만, 모든 애플리케이션의 배포들(개발자 환경, 스테이징, production)은 같은 종류, 같은 버전의 백엔드 서비스를 이용해야합니다.

 

11. 로그

로그를 이벤트 스트림으로 취급

 

로그는 실행 중인 app의 동작을 확인할 수 있는 수단입니다. 서버 기반 환경에서 로그는 보통 디스크에 파일(로그 파일)로 저장됩니다. 하지만, 이것은 출력 포맷 중 하나에 불과합니다.

로그는 모든 실행중인 프로세스와 백그라운드 서비스의 아웃풋 스트림으로부터 수집된 이벤트가 시간 순서로 정렬된 스트림입니다. 가공되지 않는 로그는 보통, 하나의 이벤트가 하나의 라인으로 기록된 텍스트 포맷입니다.(예외(exception)에 의한 backtrace는 여러 라인에 걸쳐 있을 수도 있습니다.) 로그는 고정된 시작과 끝이 있는 것이 아니라, app이 실행되는 동안 계속 흐르는 흐름입니다.

Twelve-Factor App은 아웃풋 스트림의 전달이나 저장에 절대 관여하지 않습니다. app은 로그 파일을 작성하거나, 관리하려고 해서는 안됩니다. 대신, 각 프로세스는 이벤트 스트림을 버퍼링 없이 stdout에 출력합니다. 로컬 개발환경에서 작업 중인 개발자는 app의 동작을 관찰하기 원하면 각자의 터미널에 출력되는 이 스트림을 볼 수 있습니다.

스테이징이나 production 배포에서는 각 프로세스의 스트림은 실행 환경에 의해서 수집된 후, 앱의 다른 모든 스트림과 병합되어 열람하거나 보관하기 위한 하나 이상의 최종 목적지로 전달됩니다. 이러한 목적지들은 앱이 열람하거나 설정할 수 없지만, 대신 실행 환경에 의해서 완벽하게 관리됩니다. 이를 위해 오픈 소스 로그 라우터를 사용할 수 있습니다.(예: (Logplex, Fluentd))

앱의 이벤트 스트림은 파일로 보내지거나 터미널에서 실시간으로 보여질 수 있습니다. 가장 중요한 점은 스트림은 Splunk같은 로그 분석 시스템과 Hadoop/Hive같은 범용 데이터 보관소에 보내질 수 있다는 점입니다. 이러한 시스템은 장기간에 걸쳐 앱의 동작을 조사할 수 있는 강력함과 유연성을 가지게 됩니다.

  • 과거의 특정 이벤트를 찾기
  • 트렌드에 대한 거대한 규모의 그래프 (예: 분당 요청 수)
  • 유저가 정의한 휴리스틱에 따른 알림 (예: 분당 오류 수가 임계 값을 넘는 경우 알림을 발생시킴)

12. Admin 프로세스

admin/maintenance 작업을 일회성 프로세스로 실행

 

프로세스 포메이션은 애플리케이션의 일반적인 기능들(예: Web request의 처리)을 처리하기 위한 프로세스들의 집합 입니다. 이와는 별도로, 개발자들은 종종 일회성 관리나 유지 보수 작업이 필요합니다. 그 예는 아래와 같습니다.

  • 데이터베이스 마이그레이션을 실행합니다. (예: Django에서 manage.py migrate, Rail에서 rake db:migrate)
  • 임의의 코드를 실행하거나 라이브 데이터베이스에서 앱의 모델을 조사하기 위해 콘솔(REPL Shell로도 알려져 있는)을 실행합니다. 대부분의 언어에서는 인터프리터를 아무런 인자 없이 실행하거나(예: python, perl) 별도의 명령어로 실행(예: ruby의 irb, rails의 rails console)할 수 있는 REPL를 제공합니다.
  • 애플리케이션 저장소에 커밋된 일회성 스크립트의 실행 (예: php scripts/fix_bad_records.php)

일회성 admin 프로세스는 애플리케이션의 일반적인 오래 실행되는 프로세스들과 동일한 환경에서 실행되어야 합니다. 일회성 admin 프로세스들은 릴리즈를 기반으로 실행되며, 해당 릴리즈를 기반으로 돌아가는 모든 프로세스처럼 같은 코드베이스 설정를 사용해야 합니다. admin 코드는 동기화 문제를 피하기 위해 애플리케이션 코드와 함께 배포되어야 합니다.

모든 프로세스 타입들에는 동일한 종속성 분리 기술이 사용되어야 합니다. 예를 들어, 루비 웹 프로세스가 bundle exec thin start 명령어를 사용한다면, 데이터베이스 마이그레이션은 bundle exec rake db:migrate를 사용해야합니다. 마찬가지로, virtualenv를 사용하는 파이썬 프로그램은 tornado 웹 서버와 모든 manage.py admin 프로세스가 같은 virtualenv에서의 bin/python을 사용해야 합니다.

Twelve-Factor는 별도의 설치나 구성없이 REPL shell을 제공하는 언어를 강하게 선호합니다. 이러한 점은 일회성 스크립트를 실행하기 쉽게 만들어주기 때문입니다. 로컬 배포에서, 개발자는 앱을 체크아웃한 디렉토리에서 일회성 admin 프로세스를 shell 명령어로 바로 실행시킵니다. production 배포에서, 개발자는 ssh나 배포의 실행 환경에서 제공하는 다른 원격 명령어 실행 메커니즘을 사용하여 admin 프로세스를 실행할 수 있습니다.

posted by 여성게
:
Web/Spring Cloud 2019. 2. 25. 00:22

Spring Cloud - Zuul API gateway & Proxy !(Netflix Zuul)


Netflix Zuul 이란 무엇인가?

마이크로서비스 아키텍쳐(MSA)에서 Netflix Zuul은 간단히 API gateway 또는 API Service,Edge Service로 정의된다.

그래서 하는 일이 무엇이냐? 마이크로서비스 아키텍쳐에서 여러 클라이언트 요청을 적절한 서비스로 프록시하거나 라우팅하기 위한 서비스이다.




위의 이미지에서 보이듯, 모든 마이크로서비스의 종단점은 숨기고 모든 요청을 최앞단에서 Zuul이 받아 

적절한 서비스로 분기를 시키게된다. 모든 마이크로서비스의 종단점을 숨겨야하는 이유가 무엇인가?


1) 클라이언트는 일부 마이크로서비스만 필요로한다.

2) 클라이언트별로 적용돼야 할 정책이 있다면 그 정책을 여러 곳에서 분산해 두는 것보단 한곳에 두고 적용하는 것이

더욱안전하다.(크로스오리진 접근정책이 바로 이런 방식의 대표적인 예임) 또한 서비스 단에서 사용자별 분기처리 로직은

구현하기 까다롭다.

3)대역폭이 제한돼 있는 환경에서 데이터 집계가 필요하다면 다수의 클라이언트의 요청이 집중되지 않게 중간에 게이트웨이를

두는것이 좋다.



Netflix Zuul 설계목적?



우선 Zuul은 JVM-based router and Server-side load Balancer이다. Zuul을 사용함으로써 서버사이드에서
동적 라우팅, 모니터링, 회복 탄력성, 보안 기능을 지원한다(Filter를 통한 구현)
또한 Zuul은 다른 기업용 API 게이트웨이 제품과는 달리 개발자가 특정한 요구 사항에 알맞게 설정하고 프로그래밍할 수 있게
개발자에게 완전한 통제권을 준다.


Zuul 프록시는 내부적으로 서비스 탐색을 위해 Eureka(유레카) 서버를 사용하고, 서비스 인스턴스 사이의 부하 분산을 위해 Ribbon(리본)을 사용한다.
위에서도 이야기 했던 것처럼 Zuul은 API계층에서 서비스의 기능을 재정의해서 뒤에 있는 서비스의 동작을 바꿀수 있다.




만약 이전 포스팅에서 Eureka에 대해 읽어 보았다면, 위의 그림만 보아도 Zuul이 어떤식으로

동작하는지 이해가 될것이다.


▶︎▶︎▶︎Spring Cloud - Eureka를 이용한 마이크로서비스 동적등록&탐색&부하분산처리



그렇다면 Zuul은 어떠한 요구사항일때 쓸모가 있을까?


많은 요구사항이 있지만, 아래와 같은 요구사항일때 특히 더 쓸모가 있다.


1) 인증이나 보안을 모든 마이크로서비스 종단점에 각각 적용하는 대신 게이트웨이 한곳에 적용한다. 게이트웨이는

요청을 적절한 서비스에 전달하기 전에 보안 정책 적용, 토큰 처리 등을 수행할 수 있다. 또한 특정 블랙리스트(IP차단) 사용자를

거부할 수 있는 비즈니스 정책 적용이 가능하다.

2) 모니터링, 데이터 집계 등을 마이크로서비스 단에서 처리하는 것이아니라, Zuul에서 처리해 외부로 데이터를

내보낼때 사용할 수 있다.

3)부하 슈레딩(shredding),부하 스로틀링(throttling)이 필요한 상황에서도 유용하다.

4)세밀한 제어를 필요로 하는 부하 분산 처리에 유용하다(Zuul+Eureka+Ribbon)



예제 프로젝트의 구성은 Spring Cloud Config, Eureka, Zuul로 구성되어 있습니다.

▶︎▶︎▶︎Spring Cloud Config

▶︎▶︎▶︎Spring Cloud Eureka




예제프로젝트는 위의 이미지의 구성입니다. 하지만 편의상 Zuul은 하나의 인스턴스만 그리고 2개의 마이크로서비스 인스턴스만

띄울 예정입니다. 그리고 마이크로서비스 인스턴스들은 또한 편의상 Spring Cloud Config를 이용하지 않았습니다.

만약 모든 구성을 스프링클라우드 컨피그로 가신다면 다른 유레카나 주울과 같은 컨피그 구성으로 가시면 됩니다.

그리고 이전 포스팅에서 유레카 서버를 독립설치형이 아닌 클러스터링된 구성으로 진행 할 것입니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#유레카 서버 - 1
spring.application.name=eureka-server1
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/,http://localhost:8899/eureka/
#eureka.instance.hostname=localhost
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
 
#유레카 서버 - 2
spring.application.name=eureka-server2
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/,http://localhost:8899/eureka/
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
 
#주울 
server.port=8060
zuul.routes.search-apigateway.serviceId=eurekaclient
zuul.routes.search-apigateway.path=/api/**
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/,http://localhost:8899/eureka/
cs


위는 깃저장소에 있는 spring cloud config 설정파일입니다.(편의상 하나의 파일로 작성함. 실제로는 따로 파일을 나눠야함)

조금 설명할 점이 있다면, 유레카 독립모드와 클러스터 모드의 차이점입니다. 독립모드는 자신을 유레카서버에 등록하지 않고, 

캐시한 서비스 목록을 패치하지 않습니다. 하지만 클러스터모드에서는 유레카서버들이 서로 통신해야하기 때문에 자신을 서비스로

등록하고, 자신들의 서버목록들을 빠르게 통신하기 위해 캐시합니다. 그리고 defaultZone에 모든 유레카서버의 경로를 ","구분으로

나열합니다.(사실 서로 크로스해서 상대방의 주소만 써도됨. 하지만 나중에 유레카 서버가 많고 서로 하나씩 크로스됬다는 구성에서

만약 하나의 유레카서버가 죽어서 유레카서버끼리의 통신이 단절될 가능성도 있음. 그래서 모든 유레카서버 목록을 나열해서

통신하도록 하는 것이 좋음.)

그리고 주울도 하나의 유레카클라이언트이며 주울 프록시이다. 그래서 defaultZone에 유레카서버들을 나열한다. 그리고 프록시 설정이

한가지 방법만 있는 것은 아닌데 이 설정파일에는 /api/**로 들어오는 요청을 모두 eurekaclient로 보내라는 설정이다.



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
#서비스 - 1, application.properties
server.port=8090
spring.application.name=eurekaclient
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/,http://localhost:8899/eureka/
eureka.client.healthcheck.enabled=true
 
#서비스 - 2, application.properties
server.port=8080
spring.application.name=eurekaclient
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/,http://localhost:8899/eureka/
eureka.client.healthcheck.enabled=true
 
#서비스 1,2 호출하는 클라이언트, application.properties
server.port=8070
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/,http://localhost:8899/eureka/
spring.application.name=eureka-call-client
 
 
#config server, bootstrap.properties
server.port=8888
spring.cloud.config.server.git.uri=https://github.com/yeoseong/spring-cloud-configserver.git
management.security.enabled=false
management.endpoint.env.enabled=true
 
#eureka server - 1, bootstrap.properties
spring.application.name=eureka
spring.profiles.active=server1
server.port=8889
spring.cloud.config.uri=http://localhost:8888
management.security.enabled=false
 
#eureka server - 2, bootstrap.properties
spring.application.name=eureka
spring.profiles.active=server2
server.port=8899
spring.cloud.config.uri=http://localhost:8888
management.security.enabled=false
 
#zuul , bootstrap.properties
spring.application.name=zuulapi
spring.cloud.config.uri=http://localhost:8888
spring.profiles.active=dev
management.security.enabled=false
 
zuul.routes.eurekaclient=/api3/**
 
 
 애플리케이션들의 application.properties,bootstrap.properties 입니다.(스프링클라우드컨피그 사용여부에 따라 다름)
 
cs


나머지 설정들은 이전 포스팅에서 보고 왔다면 모두 이해할수 있다. 마지막 하나만 설명하자면, zuul의 설정이다. 이미 깃에 있는 설정파일에서 하나의

프록시 룰을 정해줬다. 하지만 그 방법말고도 다른방법이 있다.

zuul.routes.serviceId(eureka)=/path/**로도 라우팅 규칙을 정해줄 수 있다.


이제는 소스 설명이다. 유레카 및 컨피그, 마이크로서비스 클라이언트 소스는 이전과 동일하기 때문에 따로 작성하지 않는다.

▶︎▶︎▶︎Spring Cloud Config

▶︎▶︎▶︎Spring Cloud Eureka




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
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulApiApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ZuulApiApplication.class, args);
    }
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @Bean
    public ZuulFilter zuulFilter() {
        return new ZuulCustomFilter();
    }
    
    @RestController
    class ZuulController{
        
        @Autowired
        RestTemplate restTemplate;
        
        @GetMapping("/api2")
        public String zuulProxy() {
            System.out.println("ZuulController.zuulProxy() :::: /api2");
            return restTemplate.getForObject("http://eurekaclient/eureka/client"String.class);
        }
        
    }
}
cs


위의 소스를 설명하면, @EnableZuulProxy로 이 애플리케이션이 주울 프록시임을 명시한다. 그리고 주울도 하나의 유레카 클라이언트임으로

@EnableDiscoveryClient로 명시해준다. 그리고 주울의 특징중 하나는 스프링 기반으로 만들어진 API임으로 개발자가 자신이 커스터마이징해서

사용할 수 있다는 점이다. @RestController로 직접 주울의 엔드포인트를 정의해서 원하는 서비스로 보낼수 있다. 더 세밀한 무엇인가가

필요하다면 이렇게 컨트롤러를 만들어서 커스터마이징해도 좋을 듯싶다. 그리고 주울도 위에서 말했듯이 하나의 유레카 클라이언트고

내부적으로 리본을 사용해 로드벨런싱 한다고 했으니, 컨트롤러에서 라우팅할때 @LoadBalanced된 RestTemplate을 이용해야

로드밸런싱이 된다.(지금까지 총 3가지 라우팅 룰을 다뤘다.)



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
public class ZuulCustomFilter extends ZuulFilter{
    
    private static Logger logger = LoggerFactory.getLogger(ZuulCustomFilter.class);
    
    /**
     * Criteria - 필터 실행 여부를 결정
     */
    @Override
    public boolean shouldFilter() {
        // TODO Auto-generated method stub
        return true;
    }
    
    /**
     * Action - Criteria 만족 시에 실행할 비즈니스 로직
     */
    @Override
    public Object run() throws ZuulException {
        // TODO Auto-generated method stub
        
        logger.info("ZuulCustomFilter :::: {}","pre filter");
        
        return null;
    }
    
    /**
     * Type - pre,route,post
     */
    @Override
    public String filterType() {
        // TODO Auto-generated method stub
        return "pre";
    }
    
    /**
     * Order - 필터 실행 순서를 결정, 숫자가 낮을 수록 우선순위가 높아짐.
     */
    @Override
    public int filterOrder() {
        // TODO Auto-generated method stub
        return 0;
    }
    
}
cs


또한 주울은 필터를 정의해서 필요한 요청,응답에 대한 전/후처리가 가능합니다.

Pre Filter

주로 backend에 보내줄 정보를 RequestContext에 담는 역할

Payco의 AccessToken으로 email을 넘겨주는 경우





























public class QueryParamPreFilter extends ZuulFilter {
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
}

@Override
public String filterType() {
return PRE_TYPE;
}

@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
return "member-api".equals(context.get(SERVICE_ID_KEY));
}

@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String email = paycoTokenToEmail(request);
context.addZuulRequestHeader("X-PAYCO-EMAIL", email);
return null;
}
}

email은 소중한 개인 정보입니다. 다루실 때 주의하시기 바랍니다.

Route Filter

pre filter 이후에 실행되며, 다른 서비스로 보낼 요청을 작성한다

이 필터는 주로 request, response를 client가 요구하는 모델로 변환하는 작업을 수행한다

아래의 예제는 Servlet Request를 OkHttp3 Request로 변환하고, 요청을 실행하고,

OkHttp3 Response를 Servlet Response로 변환하는 작업을 수행한다












































































public class OkHttpRoutingFilter extends ZuulFilter {
@Autowired
private ProxyRequestHelper helper;

@Override
public String filterType() {
return ROUTE_TYPE;
}

@Override
public int filterOrder() {
return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
}

@Override
public Object run() {
OkHttpClient httpClient = new OkHttpClient.Builder()
// customize
.build();

RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();

String method = request.getMethod();

String uri = this.helper.buildZuulRequestURI(request);

Headers.Builder headers = new Headers.Builder();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
Enumeration<String> values = request.getHeaders(name);

while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}

InputStream inputStream = request.getInputStream();

RequestBody requestBody = null;
if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
MediaType mediaType = null;
if (headers.get("Content-Type") != null) {
mediaType = MediaType.parse(headers.get("Content-Type"));
}
requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
}

Request.Builder builder = new Request.Builder()
.headers(headers.build())
.url(uri)
.method(method, requestBody);

Response response = httpClient.newCall(builder.build()).execute();

LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();

for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
responseHeaders.put(entry.getKey(), entry.getValue());
}

this.helper.setResponse(response.code(), response.body().byteStream(),
responseHeaders);
context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
return null;
}
}

Post Filter

Response를 생성하는 작업을 처리한다

아래 예제는 X-Sample 헤더에 임의의 UUID를 넣는 소스이다

























public class AddResponseHeaderFilter extends ZuulFilter {
@Override
public String filterType() {
return POST_TYPE;
}

@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
return null;
}
}

▶︎▶︎▶︎참고


 



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
@EnableDiscoveryClient
@SpringBootApplication
public class Eurekaclient3Application {
 
    
    public static void main(String[] args) {
        SpringApplication.run(Eurekaclient3Application.class, args);
    }
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @RestController
    class EurekaClientController{
        
        @Autowired
        RestTemplate restTemplate;
        
        @GetMapping("/eureka/client")
        public String eurekaClient() {
            
//            String result = restTemplate.getForObject("http://eurekaclient/eureka/client", String.class);
            System.out.println("EurekaClientController :::: /eureka/client");
            String result = restTemplate.getForObject("http://zuulapi/api/eureka/client"String.class);
            
            return result;
        }
        @GetMapping("/eureka/client2")
        public String eurekaClient2() {
            
            System.out.println("EurekaClientController :::: /eureka/client2");
            String result = restTemplate.getForObject("http://zuulapi/api2"String.class);
            
            return result;
        }
        
        @GetMapping("/eureka/client3")
        public String eurekaClient3() {
            
            System.out.println("EurekaClientController :::: /eureka/client3");
            String result = restTemplate.getForObject("http://zuulapi/api3/eureka/client"String.class);
            
            return result;
        }
    }
}
cs


이제 마이크로서비스를 호출하는 클라이언트 소스입니다..(주울호출) 모두 /apin/~으로 주울에게 요청이 갑니다.

그리고 주울에서는 각각의 마이크로서비스로 /apin/을 제외한 나머지 Uri를 해당 마이크로서비스들에게 요청보냅니다.

(물론 설정으로 앞의 프리픽스까지 붙여서 요청보내게 할 수 있음)

postman 이나 curl로 호출이 잘되는지 확인 해보시면 될듯합니다..



<유레카 및 주울 설정 메모>


구글링을 막하다가 유레카 및 주울 설정들을 막 메모한 것들입니다.

정리하기 힘들어서.... 그냥 메모 그대로 올립니다....


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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
================================================================================Zuul=========================================================================================================
#Netflix Zuul 1.x은 외부 API 호출시 클라이언트 사이드 로드 밸런서로 Netflix Ribbon를 사용한다. 
#또한, Netflix Ribbon는 외부 API 서비스의 물리적인 노드 정보를 발견하는 역할로 Netflix Eureka에 의존한다. 
#만약 Netflix Eureka(별도 독립 서비스 구축 필요)를 사용하지 않는다면 ribbon.eureka.enabled 옵션을 false로 설정하면 된다.
#zuul.sensitive-headers에 특정 헤더 이름을 설정하면 라우팅 전에 해당 헤더를 제거할 수 있다. 보안 문제로 라우팅되지 말아야할 헤더가 있을 경우 활용할 수 있다.
#zuul.host.connect-timeout-millis으로 API 요청 후 연결까지의 타임아웃을 설정할 수 있다. 설정된 타임아웃이 초과했을 경우 ZuulException(내부적으로는 java.net.ConnectException) 예외가 발생한다.
#zuul.host.socket-timeout-millis으로 API 요청 후 응답까지의 타임아웃을 설정할 수 있다. 설정된 타임아웃이 초과했을 경우 ZuulException(내부적으로는 java.net.SocketTimeoutException) 예외가 발생한다.
#zuul.routes.url을 직접적으로 명시하면 Netflix Ribbon을 사용하지 않는다.
#zuul.routes.stripPrefix를 false로 설정하면 라우팅시 url에 path가 그대로 보존되어 결합된다. 인지적으로 가장 자연스러운 설정이다. true(기본값)로 설정시에는 url에서 path 부분은 제거되고 나머지 부분이 추가되어 라우팅된다.
 
#구성 등록 정보 zuul.max.host.connections는 
#두 개의 새 등록 정보 zuul.host.maxTotalConnections 및 zuul.host.maxPerRouteConnections로 대체되었습니다. 
#기본값은 각각 200 및 20입니다.
 
#모든 경로의 기본 Hystrix 격리 패턴 (ExecutionIsolationStrategy)은 SEMAPHORE입니다. 
#zuul.ribbonIsolationStrategy는 격리 패턴이 선호되는 경우 THREAD로 변경할 수 있습니다.
#THREAD일때, WAS의 스레드로 API요청을 받는 것이 아니라, Hystrix의 별도의 스레드를 이용하여 
#WAS의 스레드와 격리한다.
 
#프록시는 리본을 사용하여 검색을 통해 전달할 인스턴스를 찾습니다. 
#모든 요청은 hystrix 명령으로 실행되므로 실패는 Hystrix 메트릭에 나타납니다. 
#회선이 열리면 프록시는 서비스에 접속하려고 시도하지 않습니다.
 
#서비스가 자동으로 추가되는 것을 건너 뛰려면 zuul.ignored-services를 서비스 ID 패턴 목록으로 설정하십시오. 
#zuul.ignored-services='*'
 
#zuul.routes.eurekaclient=/api3/**
 
#서비스아이디가 아니라 직접 URL을 등록할 수 있지만, 이것은 클라우드의 로드벨런싱 효과를 얻을 수 없다.
 
#zuul.strip-prefix=true 으로 설정된 prefix를 붙여서 요청을 보낸다.
 
 
#zuul:
#  routes:
#    users:
#      path: /myusers/**
#      serviceId: users
#
#ribbon:
#  eureka:
#    enabled: false
#
#users:
#  ribbon:
#    listOfServers: example.com,google.com
#위와 같이 직접 리본으로 라우팅할 리스트를 작성할 수있다. 이것을 사용하려면 eureka 사용을 비활성화해야한다.
 
#만약 X-Forwarded-Host 헤더같은 것이 요청에 들어갔을 경우,
#헤더값을 추가못하게 설정할수 있다.
#zuul.add-proxy-headers=false
 
 
#기본 경로 (/)를 설정하면 @EnableZuulProxy가있는 응용 프로그램이 독립 실행 형 서버로 작동 할 수 있습니다. 
#예를 들어, zuul.route.home : /는 모든 트래픽 ( "/ **")을 "home"서비스로 라우팅합니다.
 
#zuul:
#  routes:
#    users:
#      path: /myusers/**
#      sensitiveHeaders: Cookie,Set-Cookie,Authorization
#      url: https://downstream
# 위처럼 bypass시킬 헤더값 목록을 지정
 
# zuul:
#  routes:
#    users:
#      path: /myusers/**
#      sensitiveHeaders:
#      url: https://downstream
# 모든 헤더값을 bypass
 
#zuul.ignored-headers=Cookie,Set-Cookie
#Cookie,Set-Cookie 헤더는 버린다.
 
 
#spring security를 쓰고 시큐리티헤더를 통과시키려면
#zuul.ignore-security-headers=false
 
#zuul:
#  forceOriginalQueryStringEncoding: true
# 원래 인코딩값으로 강제로 바꾼다.
 
#Zuul이 서비스 검색을 사용하는 경우 ribbon.ReadTimeout 및 ribbon.SocketTimeout 리본 등록 정보로 이러한 시간 초과를 구성해야합니다.
#URL을 지정하여 Zuul 경로를 구성한 경우 zuul.host.connect-timeout-millis 및 zuul.host.socket-timeout-millis를 사용해야합니다.
 
#기본적으로 Zuul은 모든 Cross Origin Request (CORS)를 서비스로 라우팅합니다. 
#대신 Zuul이 이러한 요청을 처리하기 원하는 경우 사용자 지정 WebMvcConfigurer bean을 제공하여 수행 할 수 있습니다.
#@Bean
#public WebMvcConfigurer corsConfigurer() {
#    return new WebMvcConfigurer() {
#        public void addCorsMappings(CorsRegistry registry) {
#            registry.addMapping("/path-1/**")
#                    .allowedOrigins("http://allowed-origin.com")
#                    .allowedMethods("GET", "POST");
#        }
#    };
#}
================================================================================Zuul=========================================================================================================
================================================================================Eureka=========================================================================================================
#<Eureka 등장 용어 정리>
#    <Eureka 행동 관련>
#        Service Registration: 서비스가 자기 자신의 정보를 Eureka에 등록하는 행동
#        Service Registry: 서비스가 스스로 등록한 정보들의 목록, 가용한 서비스들의 위치 정보로 갱신됨
#        Service Discovery: 서비스 클라이언트가 요청을 보내고자 하는 대상의 정보를 Service Registry를 통해 발견하는 과정
#    <Eureka 구성 요소 관련>
#        Eureka Client: 서비스들의 위치 정보를 알아내기 위해 Eureka에 질의하는 서비스를 가리킴 (like Service consumer)
#        Eureka Service: Eureka Client에 의해 발견의 대상이 되도록 Eureka에 등록을 요청한 서비스를 가리킴 (like Service provider)
#        Eureka Server: Eureka Service가 자기 자신을 등록(Service Registration)하는 서버이자 Eureka Client가 가용한 서비스 목록(Service Registry)을 요청하는 서버
#        Eureka Instance: Eureka에 등록되어 목록에서 조회 가능한 Eureka Service를 의미
#
#<Eureka Client 동작과 Server간 Communication>
#    <Self-Identification & Registration>
#        Eureka Client는 어떻게 Eureka Server로부터 서비스 목록을 받아올까?
#        REST endpoint /eureka/apps를 통해 등록된 인스턴스 정보를 확인할 수 있다.
#        
#        Traffic을 받을 준비가 되면 Eureka Instance의 status가 STARTING → UP으로 바뀐다
#        status:STARTING은 Eureka Instance가 초기화 작업을 진행 중인 상태로 Traffic을 받을 준비가 안되었다는 의미이다
#        eureka.instance.instance-enabled-onit 설정값을 통해 Startup 후 Traffic 받을 준비가 되었을 때 status:UP이 되도록 할 수 있다 (default: false)
#        
#        등록 이후 heartbeat은 eureka.instance.lease-renewal-interval-in-seconds에 설정된 주기마다 스케쥴러가 실행된다 (default: 30)
#        
#        Eureka Server는 interval에 따라 Eureka Service의 status(UP/DOWN/..)를 판단하고 
#        가장 최근 heartbeat 시점 + interval 이후에 heartbeat을 받지 못하면 
#        eureka.instance.lease-expiration-duration-in-seconds에 설정된 시간만큼 기다렸다가 
#        해당 Eureka Instance를 Registry에서 제거한다 (default: 90, 단, Eureka Instance가 정상적으로 종료된 경우 Registry에서 바로 제거된다)
#        위의 값은 lease-renewal-interval-in-seconds보다는 커야한다.
#        
#        등록 이후 Instance 정보가 변경 되었을 때 Registry 정보를 갱신하기 위한 REST를 
#        eureka.client.instance-info-replication-interval-seconds에 설정된 주기마다 호출한다 (default: 30)
#        eureka.client.initial-instance-info-replication-interval-seconds (default: 40)
#        
#        Eureka Server 추가, 변경, 삭제가 일어날 때 Eureka Client가 얼마나 자주 service urls를 갱신할 것인지 
#        eureka.client.eureka-service-url-poll-interval-seconds 값으로 조정할 수 있다 
#        #default: 0, 단 DNS를 통해 service urls를 가져오는 경우)
#    
#    <Service Discovery>
#        -Instance Startup 시점
#        Eureka로부터 Registry 정보를 fetch한다
#        Instance Startup 이후 Fetch Registry
#        등록 이후 Eureka Client는 eureka.client.registry-fetch-interval-seconds에 설정된 주기마다 Local Cache Registry 정보를 갱신한다 (default: 30)
    
    
#<Eureka Server 동작과 Peer Server간 Communication>    
#    <Self-Identification & Registration>
#        -Instance Startup 시점
#            Peer nodes를 찾아서 Registry 정보 등 Sync 맞추는 작업을 한다
#            eureka.server.registry-sync-retrires 값을 통해 Peer nodes로부터 Registry 정보를 얻기 위한 재시도 횟수를 조정할 수 있다 (default: 5)
#            Eureka Server가 시작되고 Peer nodes로부터 Instance들을 가져올 수 없을 때 얼마나 
#            기다릴 것인지 eureka.server.wait-time-in-ms-when-sync-empty 시간(milliseconds)을 조정할 수 있다 (default: 3000)
#
#            나머지 과정은 Server도 Eureka Client이기 때문에 'Eureka Client > Self-Identification & Registration > Instance Startup 시점'에 설명한 바와 같이 동일하게 동작한다
#            Standalone으로 구성하는 경우 Peer nodes가 없기 때문에 eureka.client.register-with-eureka: false 설정을 통해 등록 과정을 생략할 수 있다
 
 
#<Eureka Server Response Cache 설정>  
#    Eureka server에서 eureka client에게 자신의 registry 정보를 제공 시 사용하는 cache.  
#    client에게 더 빠른 registry 정보 제공을 위해 실제 registry 값이 아닌 cache의 값을 제공 함.  
#    eureka.server.response-cache-update-interval-ms: 3000 # 기본 30초
 
#<Eureka Client Cache 설정> 
#    Eureka client에 존재하는 cache로 eureka server에 서비스 정보 요청 시 이 cache의 값을 이용 한다.   
#    eureka.client.fetchRegistry 값이 false이면 client cache는 적용되지 않는다.   
#    eureka.client.registryFetchIntervalSeconds: 3 # 기본 30초
 
#어떤 경우에는 유레카가 호스트 이름보다는 서비스의 IP 주소를 광고하는 것이 바람직합니다.
#eureka.instance.preferIpAddress를 true로 설정하고 응용 프로그램이 eureka에 등록하면 호스트 이름 대신 IP 주소를 사용합니다.
 
#spring-boot-starter-security를 ​​통해 서버의 classpath에 Spring Security를 ​​추가하기 만하면 유레카 서버를 보호 할 수 있습니다. 
#기본적으로 Spring Security가 classpath에있을 때, 모든 요청에 ​​대해 유효한 CSRF 토큰을 앱에 보내야합니다. 
#유레카 고객은 일반적으로 유효한 CSRF (cross site request forgery) 토큰을 보유하지 않으므로 / eureka / ** 엔드 포인트에 대해이 요구 사항을 비활성화해야합니다.
#@EnableWebSecurity
#class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#
#    @Override
#    protected void configure(HttpSecurity http) throws Exception {
#        http.csrf().ignoringAntMatchers("/eureka/**");
#        super.configure(http);
#    }
#}
 
#Eureka Discovery Client를 사용하지 않으려면 eureka.client.enabled를 false로 설정할 수 있습니다. 
#Eureka Discovery Client는 spring.cloud.discovery.enabled가 false로 설정된 경우에도 비활성화됩니다.
 
#eureka.client.serviceUrl.defaultZone URL 중 하나에 자격 증명이 포함되어 있으면 HTTP 기본 인증이 자동으로 유레카 클라이언트에 추가됩니다 
#(컬 스타일, http : // user : password @ localhost : 8761 / eureka). 
#보다 복잡한 요구를 위해, DiscoveryClientOptionalArgs 타입의 @Bean을 생성하고 ClientFilter 인스턴스를 클라이언트에 삽입 할 수 있습니다.
#이 인스턴스는 모두 클라이언트에서 서버로의 호출에 적용됩니다.
 
#Eureka 인스턴스의 상태 페이지 및 상태 표시기는 각각 Spring Boot Actuator 응용 프로그램의 유용한 끝점의 기본 위치 인 / info 및 / health로 기본 설정됩니다.
#eureka:
#  instance:
#    statusPageUrlPath: ${server.servletPath}/info
#    healthCheckUrlPath: ${server.servletPath}/health
 
#HTTPS를 통해 앱과 연락하려는 경우 EurekaInstanceConfig에서 다음과 같은 두 가지 플래그를 설정할 수 있습니다.
#eureka.instance.[nonSecurePortEnabled]=[false]
#eureka.instance.[securePortEnabled]=[true]
 
#eureka.hostname == eureka.instance.hostname
 
#기본적으로 Eureka는 클라이언트 하트 비트를 사용하여 클라이언트가 작동 중인지 확인합니다.
#별도로 지정하지 않는 한, Discovery Client는 Spring Boot Actuator에 따라 응용 프로그램의 현재 상태 검사 상태를 전파하지 않습니다. 
#따라서 성공적으로 등록한 후 Eureka는 응용 프로그램이 항상 UP 상태임을 발표합니다. 
#이 동작은 Eureka 상태 점검을 활성화하여 응용 프로그램 상태를 Eureka에 전파함으로써 변경 될 수 있습니다. 
#결과적으로 다른 모든 응용 프로그램은 'UP'이외의 상태로 응용 프로그램에 트래픽을 보내지 않습니다.
#반드시 actuator 의존이 필요
 
#상태 검사를 더 많이 제어해야하는 경우에는 com.netflix.appinfo.HealthCheckHandler를 직접 구현하는 것이 좋습니다.
 
#라우터를 사용하는 경우 (권장 또는 필수, 플랫폼 설정 방식에 따라 다름) 라우터를 사용하도록 명시 적으로 호스트 이름과 포트 번호 (보안 또는 비보안)를 설정해야합니다.
#eureka:
#  client:
#    healthcheck:
#      enabled: true
#만약 bootstrap.properties에 등록하면 UNKNOWN등의 비정상적인 상태값이 나올수 있다.(반드시 application.xxx에 설정하자)
 
#Cloud Foundry에는 글로벌 라우터가있어 동일한 앱의 모든 인스턴스가 동일한 호스트 이름을 갖는다면,
#이것은 반드시 유레카 사용의 문제가 있지는 않다.
#그러나 라우터를 사용하는 경우 (권장 또는 필수, 플랫폼 설정 방식에 따라 다름) 라우터를 사용하도록 명시 적으로 호스트 이름과 포트 번호 (보안 또는 비보안)를 설정해야합니다.
#eureka:
#  instance:
#    hostname: ${vcap.application.uris[0]}
#    nonSecurePort: 80
 
 
 
 
 
#########################################################################################################################################
#Register
#    eureka.instance, eureka.client 설정값을 바탕으로 Eureka에 등록하기 위한 Eureka Instance 정보를 만듦
#    Client가 eureka 서버로 첫 hearbeat 전송 시 Eureka Instance 정보를 등록
#    등록된 instance 정보는 eureka dashboard나  http://eurekaserver/eureka/apps를 통해 확인할 수 있음
#Renew
#    Client는 eureka에 등록 이후 설정된 주기마다 heatbeat를 전송하여 자신의 존재를 알림
#    eureka.instance.lease-renewal-interval-in-seconds (default: 30)
#    설정된 시간동안 heartbeat를 받지 못하면 해당 Eureka Instance를 Registry에서 제거
#    eureka.instance.lease-expiration-duration-in-seconds (default: 90)
#    renew 관련 interval은 변경하지 않는것을 권장 함(서버 내부적으로 client를 관리하는 로직 때문)
#Fetch Registry
#    Client는 Server로부터 Registry(서버에 등록된 인스턴스 목록) 정보를 가져와서 로컬에 캐시
#    캐시 된 정보는 설정된 주기마다 업데이트 됨
#    eureka.client.registryFetchIntervalSeconds (default: 30)
#Cancel
#    Client가 shutdown될 때 cancel 요청을 eureka 서버로 보내서 registry에서 제거 하게 됨
#Time Lag
#    Eureka server와 client의 registry 관련 캐시 사용으로 인해 client가 호출 하려는 다른 instance 정보가 최신으로 갱신되는데 약간의 시간 차가 있음
 
 
#Peering
#    여러대의 eureka server를 사용하여 서로 peering 구성이 가능하다.
#    Eureka server는 설정에 정의된 peer nodes를 찾아서 Registry 정보 등 Sync 맞추는 작업을 한다 .
#    
#    관련 설정
#        Standalone으로 구성하려면 아래 처럼 설정
#            eureka.client.register-with-eureka: false
#        Peer nodes 로부터 registry를 갱신할 수 없을 때 재시도 횟수
#            eureka.server.registry-sync-retrires (default: 5)
#        Peer nodes 로부터 registry를 갱신할 수 없을때 재시도를 기다리는 시간
#            eureka.server.wait-time-in-ms-when-sync-empty (default: 3000) milliseconds
 
#Self-Preservation Mode(자가보존모드)
#    Eureka 서버는 등록된 instance로부터 heartbeat를 주기적으로 받는다.
#    하지만 네트워크 단절 등의 상황으로 hearbeat를 받을 수 없는 경우 보통 registry에서 해당 instance를 제거 한다.
#    Eureka로의 네트워크는 단절되었지만, 해당 서비스 API를 호출하는데 문제가 없는 경우가 있을수 있어서,
#    self-preservation 을 사용하여 registry에서 문제된 instance를 정해진 기간 동안 제거하지 않을 수 있다.
#    EvictionTask가 매분 마다 Expected heartbeats 수와 Actual heartbeats 수를 비교하여 Self-Preservation 모드 여부를 결정한다.
#        eureka.server.eviction-interval-timer-in-ms (default: 60 * 1000)
 
#Expected heartbeats updating scheduler
#    기본 매 15분(renewal-threshold-update-interval-ms) 마다 수행되며 preservation mode로 가기 위한 임계값을 계산한다.
#    예를 들어 인스턴스 개수가 N개이고, renewal-percent-threshold값이 0.85이면 계산식은 아래와 같다.
#    최소 1분이내 받아야 할 heartbeat 총 수 = 2  N  0.85  
#    위 값은 아래 설정으로 변경 가능 
#        eureka.instance.lease-renewal-interval-in-seconds (default: 30)
#        eureka.server.renewal-percent-threshold (default: 0.85)
#        scheduler 수행 주기 설정 eureka.server.renewal-threshold-update-interval-ms (default: 15  60  1000)
 
#Actual heartbeats calculation scheduler
#    기본 매 1분 마다 수행되며 실제 받은 heartbeats 횟수를 계산하다.
 
#eureka
#    instance:
#         preferIpAddress: true # 서비스간 통신 시 hostname 보다 ip 를 우선 사용 함
 
#server:
#  port: 8761
#
#eureak:
#  server:
#    enable-self-preservation: true
#  client:
#    registerWithEureka: true      
#    fetchRegistry: true           
#
#---
#
#spring:
#  profiles: eureka1
#eureka:
#  instance:
#    hostname: eureka1
#  client:
#    serviceUrl:
#      defaultZone: http://eureka2:8761/eureka/
#
#---
#spring:
#  profiles: eureka2
#eureka:
#  instance:
#    hostname: eureka2
#  client:
#    serviceUrl:
#      defaultZone: http://eureka1:8761/eureka/
 
#동일서버에서 실행하는 경우 instance hostname은 unique하게 설정되어야 한다.
#registerWithEureka true로 설정
#    true설정시 서버 자신도 유레카 클라이언트로 등록한다.
#fetchRegistry true로 설정
#    defaultZone의 유레카 서버에서 클라이언트 정보를 가져온다(registerWithEureka가 true로 설정되어야 동작함)
#profile 추가하여 서로 참조하도록 serviceUrl.defaultZone 설정
#self preservation
 
#spring:
#  application:
#    name: customer-service
#
#eureka:
#  client:
#    serviceUrl:
#      defaultZone: http://eureka1:8761/eureka/,http://eureka2:8761/eureka/
#    enabled: true
#eureka.client.serviceUrl.defaultZone에 clustering한 유레카 서버 모두 입력
#    heart-beat는 defaultZone의 very first 항목인 eureka1에 만 전송
#여러개의 Eureka에 등록할 경우 defaultZone에 ,(comma)로 구분하여 입력한다.
 
#유레카 (Eureka)는 CAP 정리의 관점에서 AP 시스템입니다. 그러면 레지스트리의 정보가 네트워크 파티션 동안 서버간에 일치하지 않게됩니다. 자체 보존 기능은 이러한 불일치를 최소화하기위한 노력입니다.
#
#자기 보존 정의
#    자체 보존은 Eureka 서버가 특정 임계 값 이상으로 하트 비트 (피어 및 ​​클라이언트 마이크로 서비스에서)를 수신하지 않을 때 레지스트리에서 만료 인스턴스를 중지하는 기능 입니다.
================================================================================Eureka=========================================================================================================
 
cs


posted by 여성게
:
Web/Spring Cloud 2019. 2. 24. 01:04

Spring Cloud - Spring Cloud Bus



Spring Cloud Bus 는 분산 시스템에 존재하는 노드들을 경량 메시지 브로커(rabbitmq, kafka etc)와 연결하는 역할을 합니다.

구성 변경과 같은 상태변경, 기타관리 등을 브로드캐스트하는데 사용이 가능합니다.

현재 AMQP 브로커를 전송으로 사용하지만 Kafka, Redis도 사용 할 수 있습니다. 그 외의 전송은 아직 지원되지 않습니다.



1. 개요

Spring Cloud Config Server를 구축하게 되면 각 어플리케이션에 대한 설정정보(ex: applicatoin.yml)를 한 곳에서 관리 할 수 있습니다.

  • 하지만 해당 정보가 수정 될 경우 클라이언트 어플리케이션을 재기동해야 하는 것은 변함이 없습니다.

  • 이러한 방식은 이상적이지 않기 때문에 spring-boot-actuator와 @RefreshScope 어노테이션을 추가한 이후에 해당 클라이언트에 아래와 같은 명령을 보내어 재기동 없이 설정정보를 다시 읽어오게 할 수 있습니다.

    $ curl -x POST http://[ip]:[port]/refresh
  • 하지만 클라우드 환경에서는 모든 actuator endpoint에 접근하여 모든 클라이언트 어플리케이션을 refresh 해야 하는 번거로움이 존재합니다. 이러한 문제는 Spring Cloud Bus를 통해서 해결 할 수 있습니다.

  • 아래와 같은 서버(또는 브로커)를 만들도록 하겠습니다.

    • hello-act-client

    • config-server

    • RabbitMQ(Docker)

2. hello-act-client 구축


해당 어플리케이션은 GET request 를 통해 간단한 문자열을 출력하는 어플리케이션입니다.

먼저 간단한 dependency를 추가합니다. (해당 프로젝트는 spring-boot-starter-parent:2.0.2.RELEASE를 사용합니다.)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

application.yml을 작성합니다. 여기서 message.act 는 GET request 호출 시 출력할 문자열입니다.

server:
port: 8090
message:
act: "act"
spring:
application:
name: hello-act

Controller를 작성합니다.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloActController {
@Value("${message.act}")
private String message;
@GetMapping("/")
public String getMessage() {
return message;
}
}

서버를 기동하고 terminal에서 GET request를 호출하면 "act" 라는 메시지가 출력 되는 것을 확인 할 수 있습니다.

$ curl -X GET http://localhost:8090/
act

3. config-server 구축

해당 message를 config 서버에서 읽어와서 출력하기 위해서는 config-server 구축이 필요합니다. 새로운 프로젝트를 생성하고 아래와 같은 dependency를 추가합니다.

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
...
</dependencies>

해당 어플리케이션이 config-server임을 알리기 위해서 @EnablieConfigServer 어노테이션을 추가합니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}

해당 서버가 바라보는 config 저장소를 application.yml에 추가합니다. 해당 예제에서는 git 주소를 로컬 git으로 하였습니다.

server:
port: 8091
spring:
cloud:
config:
server:
git:
uri: file:/Users/bristol/bradley/configure

이제 /Users/bristol/bradley/configure 경로에 hello-act-client에서 사용 할 설정 정보를 가져옵니다.

hello-act.yml을 만들어 아래와 같은 설정 정보를 넣습니다.

server:
port: 8090
message:
act: "act"

이 후에 commit 을 해주도록 합니다.

$ git add.
$ git commit -m 'init yml'



4. hello-act-client 수정

config-server를 구축하였으므로 이제 포트정보와 message 정보는 config-server를 통해서 가져오도록 하겠습니다.

먼저 cloud-config를 사용 할 수 있도록 dependency를 추가합니다.

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
...
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
...
</dependencies>

기존의 application.yml 내용 중 아래의 내용을 bootstrap.yml으로 이동하고 application.yml을 삭제합니다.

spring:
application:
name: hello-act
cloud:
config:
uri: http://localhost:8091

해당 서버를 재기동 한 이후 아래와 같이 명령어를 보내면 act 라는 문자열이 출력되는 것을 볼 수 있습니다.

$ curl -X GET http://localhost:8090/
act

configure 폴더에서 메시지 정보를 수정합니다.

message:
act: "hello-act"

다시 commit을 합니다.

$ git add .
$ git commit -m 'change message'

서버 재 기동 없이 아래와 같이 명령어를 보내면 여전히 act 라는 문자열이 출력되는 것을 볼 수 있습니다.

$ curl -X GET http://localhost:8090/
act

5. RefreshScope

환경설정을 바꿨다고 해서 서버를 재기동하는 것은 불필요한 행위입니다. 따라서 서버 재기동 없이 환경설정을 읽어오는 방법을 알아보겠습니다.

먼저 hello-act-client 어플리케이션에 dependency를 추가합니다.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

@RefreshScope 어노테이션을 추가합니다. @RefreshScope로 표시된 Spring Bean은 사용시 초기화 되는 lazy proxy로 범위는 초기화 된 캐쉬 값으로 작동합니다.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope
public class HelloActController {
@Value("${message.act}")
private String message;
@GetMapping("/")
public String getMessage() {
return message;
}
}

그리고 bootstrap.yml에 아래와 같은 설정을 추가합니다. (기본적으로 actuator로 추가 된 민감한 엔드포인트는 보안에 묶여 있습니다. 아래와 같은 설정을 하거나 특정 url을 노출 시킬 수 있습니다.)

management:
security:
enabled: false

서버를 재기동하고 아래와 같이 명령어를 보내면 hello-act가 출력됩니다.

$ curl -X GET http://localhost:8090/
hello-act

config저장소에서 hello-act.yml을 열어 hello-sds로 변경하고 commit을 합니다.

message:
act: "hello-sds"
$ git add.
$ git commit -m 'change sds'

이제 서버 재기동 없이 터미널에서 아래와 같은 명령어를 보냅니다.

$ curl -X POST http://localhost:8090/actuator/refresh

해당 명령어를 보내면 변경된 프로퍼티가 출력 됩니다. 다시 아래와 같은 명령어를 날리게 되면 변경된 메시지가 출력 되는 것을 확인 할 수 있습니다.

$ curl -X GET http://localhost:8090/
hello-sds



6. Spring Cloud Bus

변경된 설정 값이 반영 되는 것을 확인하였으나 이와 같은 방법은 클라우드환경에서 endpoint 가 늘어날 수록 번거로울수 밖에 없습니다. 따라서 Spring Cloud Bus를 사용해보도록 합니다.

먼저 rabbitmq를 docker로 실행합니다.

$ docker run -d \
--hostname rabbit \
--name rabbit \
-p 15672:15672 \
-p 5672:5672 \
rabbitmq:3.7.5-management

클라이언트 어플리케이션에 아래와 같은 dependency를 추가합니다.

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

config 저장소에 hello-act.yml을 아래의 구문을 추가합니다.

spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

이제 config-server 설정을 변경합니다.

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-monitor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

다음으로 config-server의 application.yml을 수정합니다.

spring:
...
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: "bus-refresh"

이제 모든 서버를 재 기동 한 이후에 hello-act.yml의 메시지 값을 다시 변경합니다.

...
message:
act: "hello-sds-act"
...

기존에는 hello-act 서버에 리퀘스트를 보냈으나 이제는 config-server에 리퀘스트를 보냅니다.

$ curl -X POST http://localhost:8091/actuator/bus-refresh

이후에 hello-act 서버에 GET 리퀘스트를 보내면 변경된 메시지를 확인 할 수 있습니다.

$ curl http://localhost:8090/
hello-sds-act

이러한 방법을 사용하면 코드 저장소(github, gitlab, bitbucket 등)에 webhook 기능을 사용하여 설정파일 변경이후에 commit, push가 일어날 때마다 자동으로 모든 클라우드 노드의 refrshscope 가 적용된 어플리케이션이 환경설정을 다시 읽게 할 수 있습니다.

posted by 여성게
:
Web/Spring Cloud 2019. 2. 24. 00:20

Spring Cloud - Eureka를 이용한 마이크로서비스 

동적등록&탐색&부하분산처리


스프링 클라우드 유레카는 넷플릭스 OSS에서 유래됐다. 

자가 등록, 동적 탐색 및 부하 분산에 주로 사용되며, 부하 분산을 위해 내부적으로 리본을 사용한다.

마이크로서비스의 장점 중 하나인 동적인 서비스 증설 및 축소를 유레카를 이용하면

아주 쉽게 가능하다.




위의 그림과 같이 사용자의 사용이 급격하게 많아졌다고 가정해보자.

그렇다면 위와 같이 서비스 인스턴스를 증설할 것이다. 

여기에서 유레카를 사용한다면 마이크로서비스 인스턴스를 하나 추가하면

자가 등록을 통해 유레카서버에 자신의 서비스를 등록한다.

그러면 동적으로 추가된 인스턴스를 탐색할 수 있게 되고 내부적으로 리본에 의해

같은 인스턴스 4개가 부하 분산(로드밸런싱) 처리가 될 것이다.


만약 유레카와 같은 것을 사용하지 않았다면? 개발자가 수동으로 전부다 등록해야하고 

그렇게 함으로써 추가된 인스턴스만 배포하는 것이 아니라, 관련된 다른 인스턴스까지 추가로 

재배포가 필요할 수도 있을 것이다.


위의 구성에 대해서 간단히 설명하자면 유레카는 서버와 클라이언트 컴포넌트로 이루어져있다.

서버 컴포넌트는 모든 마이크로서비스가 자신의 가용성을 등록하는 레지스트리이다.

등록되는 정보는 일반적으로 서비스 ID&URL이 포함된다.

마이크로서비스 인스턴스는 유레카 클라이언트를 이용해서 자기 자신의 가용성을 유레카 서버의 레지스트리에 

등록한다. 등록된 마이크로서비스를 호출해서 사용하는 컴포넌트도 유레카 클라이언트를 이용해서 

필요한 서비스를 탐색한다.


마이크로서비스가 시작되면 유레카 서버에 접근해 서비스 ID&URL 등의 정보를 등록하고 자신이

기동되었다는 것을 알린다.(통신은 모두 REST) 일단 등록이 되면 유레카 서버의 레지스트리에 

30초 간격으로 ping을 날리면서 자신의 status가 정상이다라는 것을 알린다.

만약 이 ping요청이 제대로 이루어지지 않는다면 유레카서버는 서비스가 죽은 것으로 

판단하여 레지스트리에서 제거한다.


유레카 클라이언트는 서비스의 정보를 받기 위하여 매번 유레카 서버에서 요청을 보내지않고

한번 받으면 로컬캐시에 저장을 해둔다. 그리고 기본 30초마다 계속 서버에 요청을 보내서

서비스의 목록을 들여다보며 변경이 있다면 로컬캐시에 저장된 것을 갱신시킨다.

(로컬캐시와 서버에 있는 서비스 정보를 비교해차이가 있는 것을 가져오는 Delta Updates 방식으로 갱신)



예제로 만들어볼 소스는 우선 Spring Cloud Config를 이용할 것이다.

만약 스프링 클라우드 컨피그에 대한 개념을 모른다면 아래 링크를 통해 한번 보고와도 좋을 것같다.


▶︎▶︎▶︎Spring Cloud Config






우선 유레카 서버로 이용할 스프링 부트 프로젝트를 생성한다.


Cloud Config>Config Client

Cloud Discovery>Eureka Server

Ops>Actuator


를 체크하여 프로젝트를 생성해준다.




1
2
3
4
spring.application.name=eureka
spring.profiles.active=server1
server.port=8889
spring.cloud.config.uri=http://localhost:8888
cs



spring.application.name=eureka,spring.profiles.active=server1는 

클라우드 컨피그에서 가져올 프로퍼티 파일명을 뜻한다.

> eureka-server1.properties

나머지설정은 위의 클라우드 컨피그 링크에서 참조하면 될 것같다.



유레카 서버는 Standard alone과 cluster mode 모두가 가능하다. 하지만

이번 예제에서는 Standard alone mode로 진행할 것이다.




1
2
3
4
spring.application.name=eureka-server1
eureka.client.serviceUrl.defaultZone=http://localhost:8899/eureka/
eureka.client.registerWithEureka=false
eureka.client.fetchRegistry=false
cs



위의 설정정보는 git에 저장된 eureka-server1.properties에 작성될 설정정보이다.

유레카서버는 서버임과 동시에 클라이언트가 될수 있다. 즉, 유레카서버도 결국은 유레카 클라이언트로

동작하는 것이다.(유레카 서버가 여러대일때, peer 관계에 있는 유레카서버의 서비스 목록을 가져오기 위하여

자신의 클라이언트를 이용해서 가져온다. 하지만 지금은 일단 클라이언트들과 동일한 동작이 계속 시도되지 않도록 false로 한것이다.) 

eureka.client.serviceUrl.defaultZone 설정으로 Zone을 지정해준다.

그리고 eureka.client.registerWithEureka=false로 자기자신을 서비스로 등록하지 않는다.

마지막으로 eureka.client.fetchRegistry=false로 마이크로서비스인스턴스 목록을 로컬에 캐시할 것인지의

여부로 등록한다. 하지만 여기서 유레카서버는 동적 서비스 탐색등의 목적으로

사용되지는 않음으로 밑의 두개의 설정은 false로 등록한다.(즉,Standard alone이면 두개다 false)

만약 registerWithEureka를 true로 동작하면 자기 자신에게 계속 health check 요청 및 다른 유레카 클라이언트가 보내는 요청을

자기스스로에게 보내게 될것이다.



1
2
3
4
5
6
7
8
9
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(EurekaserverApplication.class, args);
    }
 
}
cs


@EnableEurekaServer 어노테이션으로 자기자신이 유레카서버임을 명시한다.

http://localhost:8889 로 접속하면 유레카 관리페이지가 나온다.

현재는 아무런 서비스도 등록되어 있지않은 상태이다.



나머지 유레카 클라이언트들의 코드는 편의상 클라우드 컨피그를 이용하지 않았다.



1
2
3
server.port=8070
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/
spring.application.name=eureka-call-client
cs


위의 설정파일은 마이크로서비스 인스턴스들을 호출할 하나의 클라이언트 설정이다. 유레카클라이언트는 defaultZone 속성값이 같은

다른 유레카 클라이언트와 동료관계를 형성하므로, 해당 애플리케이션의 defaultZone설정으로 유레카서버와 동일하게 작성한다.

그 다음 spring.application.name 설정은 유레카서버에 등록될 서비스이름이다.

유레카서버에게 동적서비스 등록을 하고,

동적탐색의 대상이 되는 어떠한 서비스들을 호출하기 위한 애플리케이션도 유레카 클라이언트이어야한다.



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
@EnableDiscoveryClient
@SpringBootApplication
public class Eurekaclient3Application {
 
    
    public static void main(String[] args) {
        SpringApplication.run(Eurekaclient3Application.class, args);
    }
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    @RestController
    class EurekaClientController{
        
        @Autowired
        RestTemplate restTemplate;
        
        @GetMapping("/eureka/client")
        public String eurekaClient() {
            
            String result = restTemplate.getForObject("http://eurekaclient/eureka/client"String.class);
            
            return result;
        }
    }
}
cs


위의 소스를 설명하자면 우선 애플리케이션이 유레카 클라이언트임을 @EnableDiscoveryClient 어노테이션으로 명시한다.

그리고 우리가 많이 사용하던 RestTemplate을 빈으로 등록할때 @LoadBalanced 어노테이션을 등록하여

Ribbon에 의해 로드벨런싱할 RestTemplate임을 명시한다.(@LoadBalanced 때문에 서비스로 등록된 마이크로서비스 인스턴스 등을 호출할때

라운드로빈 방식으로 분산으로 요청이 가게된다.)


그런데 RestTemplate을 사용하는 메소드 안의 URL정보가 조금 특이하다. eurekaclient? 우리는 로컬환경이고

따로 호스트를 등록하지도 않았는데, localhost가 아니고 다른 DNS명으로 호출하고 있다.

해답은 다음 과정에 나오게 된다.


다음은 서비스로 등록될 마이크로서비스 인스턴스 애플리케이션 2개이다.(2개의 애플리케이션 코드는 동일하고 설정정보만 조금 다르니, 소스코드는

하나의 애플리케이션만 명시한다.)



1
2
3
server.port=8090
spring.application.name=eurekaclient
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/
cs


1
2
3
server.port=8080
spring.application.name=eurekaclient
eureka.client.serviceUrl.defaultZone=http://localhost:8889/eureka/
cs


두개의 마이크로서비스 인스턴스의 설정정보이다. 다른 것은 서버포트 하나뿐이다. 그리고 해당 애플리케이션들은

같은 애플리케이션은 증설한 상황이다. 즉, 서비스 이용자 입장에서는 같은 인스턴스인 것이다. 그렇기 때문에

spring.application.name을 eurekaclient로 동일하게 등록한다. 어? 이건 이 인스턴스들을

호출한 클라이언트에서 RestTemplate의 메소드의 DNS였는데? 맞다. 그것이다.


즉, 유레카서버에 등록한 서비스 이름으로 RestTemplate 요청을 보내는 것이다. 그런 다음 해당 서비스 이름으로

서비스가 등록되어있는지 확인하고 있다면 Ribbon이 로드밸런싱(라운드로빈 방식) 해줘서 요청이 가게된다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaclientApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(EurekaclientApplication.class, args);
    }
    
    @RestController
    class EurekaClientController{
        
        @GetMapping("/eureka/client")
        public String eurekaClient() {
            return "eureka client - 1";
        }
    }
}
cs


마이크로서비스 인스턴스의 소스이다. 애플리케이션을 하나더 생성하여 위의 소스에서 반환값만 수정하자.


그런다음 유레카 관리자 페이지를 들어가보자.



총 3개의 서비스가 등록되어 있는 것을 볼 수 있다.(Eureka-call-client(1),EurekaClient(2))


마지막으로 postman 툴이나 curl로 Eureka-call-client의 "/eureka/client"를 호출해보자


계속해서 반환되는 값이 "eureka client - 1" , "eureka client - 2" 로 번갈아가면서

반환될것이다. 지금은 로컬에서 나자신 혼자만 요청을 보내니 라운드로빈방식으로 각각 번갈아가면서

한번씩 호출된다.


이렇게 독립모드형 유레카서버,클라이언트 구성을 해보았다.


마지막으로 간단한 유레카서버,클라이언트 용어 및 부트설정 설명이다.



1
2
3
4
5
6
7
8
9
10
#<Eureka 등장 용어 정리>
#    <Eureka 행동 관련>
#        Service Registration: 서비스가 자기 자신의 정보를 Eureka에 등록하는 행동
#        Service Registry: 서비스가 스스로 등록한 정보들의 목록, 가용한 서비스들의 위치 정보로 갱신됨
#        Service Discovery: 서비스 클라이언트가 요청을 보내고자 하는 대상의 정보를 Service Registry를 통해 발견하는 과정
#    <Eureka 구성 요소 관련>
#        Eureka Client: 서비스들의 위치 정보를 알아내기 위해 Eureka에 질의하는 서비스를 가리킴 (like Service consumer)
#        Eureka Service: Eureka Client에 의해 발견의 대상이 되도록 Eureka에 등록을 요청한 서비스를 가리킴 (like Service provider)
#        Eureka Server: Eureka Service가 자기 자신을 등록(Service Registration)하는 서버이자 Eureka Client가 가용한 서비스 목록(Service Registry)을 요청하는 서버
#        Eureka Instance: Eureka에 등록되어 목록에서 조회 가능한 Eureka Service를 의미
cs



일단 이번 포스팅은 간단하게 유레카의 사용법을 익혀봤다. 다음 포스팅에서는 더 다양한 유레카 설정과 유레카 서버를 클러스터구성으로

예제를 진행할것이다. 이번에는 대략적인 유레카의 사용법을 익히는 것으로 간다.

posted by 여성게
:
Web/Spring Cloud 2019. 2. 23. 12:46

Spring Cloud - Spring Cloud Config(스프링 클라우드 컨피그)


Spring cloud Config(스프링 클라우드 컨피그) 서버는 애플리케이션과 서비스의 모든 환경설정 속성 정보를 저장하고, 

조회하고 관리할 수 있게 해주는 외부화된 환경설정 서버다. 스프링 컨피그는 환경설정 정보의 버전 관리 기능도 지원한다. 

환경설정 속성 정보를 애플리케이션 배포 패키지에서 분리해 외부화하고 외부 소스에서 설정 정보를 읽어노는 방법이다.


위의 그림과 같이 스프링 클라우드 컨피그 서버가 모든 마이크로서비스의 환경설정정보를 가지고 있고,

설정 배포의 작업을 진행한다. 하지만 항상 서버에 접근해서 설정정보를 가져오지는 않고,

첫 애플리케이션 구동 단계에서 설정정보를 가져와 로컬에 캐시를 해둔다. 그리고 만약 컨피그 서버에서

설정정보의 변경이 이루어 진다면 모든 마이크로서비스에 변경 사항을 전파하고, 모든 마이크로서비스는

변경사항을 로컬캐시에 반영한다. 컨피그 서버는 개발 환경별 프로파일 기능도 제공한다.


이번 포스팅은 스프링 클라우드 컨비그 서버의 환경설정 정보를 GitHub에 저장하고 원격에서 설정정보를

불러올 것이다. (SVN 등을 이용해도 무관)




만약 Git 사용법이 익숙하지 않다면 밑의 링크에서 Git사용법을 익히고 와도 좋다.


▶︎▶︎▶︎GitHub - 간단한 Git사용법(로컬 레포지토리,원격 레포지토리)

▶︎▶︎▶︎GitHub - Git 사용법 2 (branch, checkout, reset 등)

▶︎▶︎▶︎Github - eclipse(이클립스)와 local repository(로컬레포지토리) 연동





스프링부트프로젝트를 생성할 때, 위 이미지와 같이 Config Server & Actuator를 체크해준다.


기존에 기본으로 생성됬던 application.properties를 bootstrap.properties로 변경해준다.

스프링 클라우드 컨피그는 application.properties가 아닌 bootstrap.properties를 이용한다.



1
2
3
4
server.port=8888
spring.cloud.config.server.git.uri=https://github.com/yeoseong/spring-cloud-configserver.git
management.security.enabled=false
management.endpoint.env.enabled=true
cs



bootstrap.properties를 위와같이 작성한다. 간단히 설명하면

컨피그 서버의 포트는 8888, 환경설정 파일을 관리하는 원격 Git을 서버는 ~.git이라는 것이다.

나머지는 액츄에이터에 대한 설정파일이다.



1
2
3
4
5
6
7
8
9
@EnableConfigServer
@SpringBootApplication
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
 
}
cs


@EnableConfigServer 어노테이션을 붙여서 해당 애플리케이션은 컨피그서버임을 명시해준다.


그 다음은 해당 컨피그 서버를 이용하는 클라이언트 작성이다.


새로운 스프링부트 프로젝트를 생성하고, Config Client와 Actuator,WEB를 체크해준 후 위의 과정과 동일하게

application.properties -> bootstrap.properties로 바꿔준다.



1
2
3
spring.application.name=configclient
spring.profiles.active=dev
spring.cloud.config.uri=http://localhost:8888
cs



해당 애플리케이션 이름은 configclient고, 프로파일은 dev이며, 설정정보를 받아올 컨피그 서버의 주소는 ~:8888이라는 뜻이다.

이 설정들은 다 의미가 있고 중요한 설정이다. 우선 애플리케이션 이름은 깃에 저장될 프로퍼티 파일의 이름이고, 프로파일은 해당 프로퍼티의

프로파일이라는 뜻이다. 즉, 깃허브에는 {name}-{profile}.properties라는 환경설정 파일이 저장되어 있고,

이 설정파일을 불러오기위한 설정이라고 생각하면된다. 

애플리케이션 별로 환경설정 파일을 분리하고, 한 애플리케이션에 대한 설정이지만 프로파일 별로 설정파일을 유지할 수도 있는 것이다.




이제 깃허브에 configclient-dev.properties라는 파일을 작성하고 예제로 설정파일에 밑의 이미지와 같은 내용을 기입한다.






마지막으로 예제 애플리케이션을 작성한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootApplication
public class ConfigclientApplication implements CommandLineRunner{
    
    @Value("${application.service.name}")
    private String serviceName;
    
    @Override
    public void run(String... args) throws Exception {
        // TODO Auto-generated method stub
        
        System.out.println(serviceName);
        
        
    }
 
    public static void main(String[] args) {
        SpringApplication.run(ConfigclientApplication.class, args);
    }
 
}
cs



해당 예제는 CommandLineRunner를 이용하여 애플리케이션 구동시점에 간단히 콘솔에 

프로퍼티에 작성한 내용을 출력하였다.


▶︎▶︎▶︎CommandLineRunner란?


이제 애플리케이션을 구동해보자, 아마 Console에 "configclient2"라는 문자열이 찍혔을 것이다.


그 다음은 만약 환경설정 파일이 변경이 되었다면


해당 인스턴스에게 refresh요청을 보낸다.

파라미터는 필요없다.

>http://localhost:8080/refresh(POST)



1
2
3
4
5
6
7
8
9
10
11
12
    @RefreshScope
    @RestController
    class ConfigClientController {
        
        @Value("${application.service.name}")
        private String serviceName;
        
        @GetMapping("/config")
        public String config() {
            return serviceName;
        }
    }
cs


하지만 이미 주입된 프로퍼티값을 변경하기 위해서는 @RefreshScope라는

어노테이션을 붙여준다. 이렇게하면 애플리케이션 재시작 없이 환경설정 값을 바꿔줄 수 있다.


하지만 여기에서 마이크로서비스가 여러개이고, 관리되는 환경설정이 여러개라면 일일이 

위의 요청을 보내야 할까? 아니다.

>http://localhost:8080/bus/refresh(POST)


하나의 인스턴스에게 위의 요청을 한번만 보내면 모든 인스턴스에게 환경설정 변경정보가 전파된다.


만약 위의 요청을 사용하려면

management.security.enabled=false

설정을 꼭 해줘야한다.


설정파일을 적절한 값으로 변경하고 refresh 한후에 해당 컨트롤러에 다시 요청을 보내보자.

아마 변경된 프로퍼티를 읽어서 반환해줄 것이다.

posted by 여성게
: