Middleware/Kafka&RabbitMQ 2020. 7. 26. 17:32

 

 

yoonyeoseong/rabbitmq-sample

Contribute to yoonyeoseong/rabbitmq-sample development by creating an account on GitHub.

github.com

 

오늘 포스팅할 내용은 래빗엠큐이다. 그 동안에는 카프카를 사용할 일이 많아 카프카에 대한 포스팅이 주였는데, 이번에 래빗엠큐를 사용할 일이 생겨 간단히 래빗엠큐에 대해 간단히 다루어 볼것이다.(예제 코드는 위 깃헙에 올려놓았습니다.)

 

비동기 작업에 있어 큐를 사용하려면 중간에 메시지 브로커라는 개념이 존재하는데, 이러한 메시지 브로커에는 RabbitMQ, Kafka 등이 대표적으로 있다. 해당 포스트에서는 표준 MQ프로토콜인 AMQP를 구현한 RabbitMQ(래빗엠큐)에 대해 다루어볼 것이다.

 

간단하게 메시지큐는 아래 그림과 같은 워크 플로우로 이루어져있다. 

 

 

대부분의 메시지큐는 프로듀서가 있고, 해당 프로듀서가 브로커로 메시지를 발행하면, 적절한 컨슈머가 해당 메시지를 구독(읽다)하는 구조이다. 그렇다면 래빗엠큐는 상세하게 어떠한 구조로 되어있을까?

 

래빗엠큐는 단순히 프로듀서가 브로커로 메시지를 전달하여 컨슈머가 큐를 읽어가는 구조라는 면에서는 동일하지만, 프로듀싱하는 과정에서 조금 더 복잡한 개념이 들어간다. 바로 exchange와 route, queue라는 개념이다.

 

간단하게 워크 플로우를 설명하자면 아래와 같다.

 

  1. Producer는 Message를 Exchange에게 보내게 됩니다.
    1. Exchange를 생성할때 Exchange의 Type을 정해야 합니다.
  2. Exchange는 Routing Key를 사용하여 적절한 Queue로 Routing을 진행합니다.
    1. Routing은 Exchange Type에 따라 전략이 바뀌게 됩니다.
  3. Exchange - Queue와 Binding이 완료된 모습을 볼 수 있습니다.
    1. Message 속성에 따라 적절한 Queue로 Routing이 됩니다.
  4. Message는 Consumer가 소비할때까지 Queue에 대기하게 됩니다.
  5. Consumer는 Message를 소비하게 됩니다.

 

위에서 1번에 Exchange라는 개념이 등장하는데, Exchange는 정해진 규칙으로 메시지를 라우팅하는 기능을 가지고 있다. 여기서 정해진 규칙은 크게 4가지가 존재하는데, 규칙들은 아래와 같다.

 

exchange routeing 전략

  • Direct Exchange
    • Message의 Routing Key와 정확히 일치하는 Binding된 Queue로 Routing
  • Fanout Exchange
    • Binding된 모든 Queue에 Message를 Routing
  • Topic Exchange
    • 특정 Routing Pattern이 일치하는 Queue로 Routing
  • Headers Exchange
    • key-value로 정의된 Header 속성을 통한 Routing

 

 

 

익스체인지는 위와 같은 전략으로 메시지를 라우팅하게 된다. 그리고 자주 사용되는 Exchange의 옵션으로는 아래와 같이 있다.

 

exchange options
  • Durability
    • 브로커가 재시작 될 때 남아 있는지 여부
    • durable -> 재시작해도 유지가능
    • transient -> 재시작하면 사라집니다.
  • Auto-delete
    • 마지막 Queue 연결이 해제되면 삭제

마지막으로 래빗엠큐에서 사용되는 용어가 있다.

 

  • Vhost(virutal host)
    • Virtual Host를 통해서 하나의 RabbitMQ 인스턴스 안에 사용하고 있는 Application을 분리할 수 있습니다.
  • Connection
    • 물리적인 TCP Connection, HTTPS -> TLS(SSL) Connection을 사용
  • Channel
    • 하나의 물리적인 Connection 내에 생성되는 가상의 Connection
    • Consumer의 process나 thread는 각자 Channel을 통해 Queue에 연결 될 수 있습니다.

 

여기까지 간단하게 래빗엠큐의 동작과 용어에 대해서 다루어봤으니 실제 코드 레벨로 실습을 진행해본다.

 

docker rabbitmq 설치
sudo docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 \
	--restart=unless-stopped -e RABBITMQ_DEFAULT_USER=username \
    -e RABBITMQ_DEFAULT_PASS=password rabbitmq:management

 

위처럼 도커로 래빗엠큐를 설치해주고, http://localhost:15672(username/password)로 접속해보자.

 

springboot rabbitmq 예제

build.gradle

plugins {
    id 'org.springframework.boot' version '2.3.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.levi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '14'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-amqp'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.amqp:spring-rabbit-test'
}

test {
    useJUnitPlatform()
}

 

application.yml

rabbitmq:
  test:
    username: username
    password: password
    host: localhost
    port: 5672
    virtualHost: levi.vhost
    routeKey: test.route
    exchangeName: test.exchange
    queueName: testQueue
    deadLetterExchange: dead.letter.exchange
    deadLetterRouteKey: dead.letter.route

 

application.yml에서 프로퍼티를 읽어오는 클래스를 아래 하나 만들었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@ConfigurationProperties(prefix = "rabbitmq")
@Getter
@Setter
public class RabbitMqProperty {
    private RabbitMqDetailProperty test;
 
    @Getter
    @Setter
    public static class RabbitMqDetailProperty {
        private String username;
        private String password;
        private String host;
        private int port;
        private String virtualHost;
        private String routeKey;
        private String exchangeName;
        private String queueName;
        private String deadLetterExchange;
        private String deadLetterRouteKey;
    }
}
cs

 

 

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
@SpringBootApplication(exclude = {
        RabbitAutoConfiguration.class
})
@EnableRabbit
@RestController
@RequiredArgsConstructor
public class RabbitmqApplication {
    private final RabbitTemplate testTemplate;
 
    public static void main(String[] args) {
        SpringApplication.run(RabbitmqApplication.class, args);
    }
 
    @GetMapping("/")
    public String inQueue() {
        testTemplate.convertAndSend(SampleMessage.of("message!!"));
        return "success";
    }
 
    @Data
    @AllArgsConstructor(staticName = "of")
    @NoArgsConstructor
    public static class SampleMessage {
        private String message;
    }
}
cs

 

래빗엠큐 auto configuration을 꺼주었고, 메시지를 인큐하기 위한 컨트롤러를 하나 만들어 주었다.

 

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
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RabbitMqConfig {
    private final RabbitMqProperty rabbitMqProperty;
 
    @Bean
    public ConnectionFactory testConnectionFactory() {
        final RabbitMqProperty.RabbitMqDetailProperty test = rabbitMqProperty.getTest();
        final CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(test.getHost());
        factory.setPort(test.getPort());
        factory.setUsername(test.getUsername());
        factory.setPassword(test.getPassword());
        factory.setVirtualHost(test.getVirtualHost());
 
        return factory;
    }
 
    @Bean
    public SimpleRabbitListenerContainerFactory testContainer(final ConnectionFactory testConnectionFactory) {
        final SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(testConnectionFactory);
        factory.setConnectionFactory(testConnectionFactory);
        factory.setErrorHandler(t -> log.error("ErrorHandler = {}", t.getMessage()));
        factory.setDefaultRequeueRejected(false); //true일 경우 리스너에서 예외가 발생시 다시 큐에 메시지가 쌓이게 된다.(예외상황 해결안될 시 무한루프)
        factory.setMessageConverter(jacksonConverter());
        //예외 발생시 recover할 수 있는 옵션, 위 에러 핸들러와 잘 구분해서 사용해야할듯
//위 핸들러와 적용 순서등도 테스트가 필요(혹은 둘다 동시에 적용되나?)
//        factory.setAdviceChain(
//                RetryInterceptorBuilder
//                .stateless()
//                .maxAttempts(3) //최대 재시도 횟수
//                .recoverer() //recover handler
//                .backOffOptions(2000, 4, 10000) //2초 간격으로, 4번, 최대 10초 내에 재시도
//                .build()
//        );
        return factory;
    }
 
    @Bean
    public RabbitTemplate testTemplate(final ConnectionFactory testConnectionFactory, final MessageConverter jacksonConverter) {
        final RabbitMqProperty.RabbitMqDetailProperty property = rabbitMqProperty.getTest();
        final RabbitTemplate template = new RabbitTemplate();
        template.setConnectionFactory(testConnectionFactory);
        template.setMessageConverter(jacksonConverter);
        template.setExchange(property.getExchangeName());
        template.setRoutingKey(property.getRouteKey());
        return template;
    }
 
    @Bean
    public MessageConverter jacksonConverter() {
        return new Jackson2JsonMessageConverter();
    }

//app에서 큐만들거나 바인딩 하기 위해서는 RabbitAdmin 필요
    @Bean
    public RabbitAdmin testAdmin(final ConnectionFactory testConnectionFactory) {
        return new RabbitAdmin(testConnectionFactory);
    }
}
cs

 

브로커 연결 및 메시지를 프로듀싱하기 위한 RabbitTemplate 설정이다. 이중 RabbitAdmin 빈을 만들고 있는 수동이 아니라, 앱에서 큐를 만들고 바인딩 하기 위해서는 RabbitAdmin이 빈으로 떠있어야한다. 기타 설정은 주석을 참고하자 !

 

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
@Configuration
@RequiredArgsConstructor
public class RabbitMqBindingConfig {
    private final RabbitMqProperty rabbitMqProperty;
 
    @Bean
    public TopicExchange testTopicExchange() {
        return ExchangeBuilder
                .topicExchange(rabbitMqProperty.getTest().getExchangeName())
                .durable(true)
                .build();
    }
 
    @Bean
    public TopicExchange dlExchange() {
        return ExchangeBuilder
                .topicExchange(rabbitMqProperty.getTest().getDeadLetterExchange())
                .durable(true)
                .build();
    }
 
    @Bean
    public Queue testQueue() {
        final RabbitMqProperty.RabbitMqDetailProperty testDetail = rabbitMqProperty.getTest();
        return QueueBuilder
                .durable(testDetail.getQueueName())
                .deadLetterExchange(testDetail.getDeadLetterExchange())
                .deadLetterRoutingKey(testDetail.getDeadLetterRouteKey())
                .build();
    }
 
    @Bean
    public Queue dlq() {
        return QueueBuilder
                .durable("DEAD_LETTER_QUEUE")
                .build();
    }
 
    @Bean
    public Binding testBinding(final Queue testQueue, final TopicExchange testTopicExchange) {
        return BindingBuilder
                .bind(testQueue)
                .to(testTopicExchange)
                .with(rabbitMqProperty.getTest().getRouteKey());
    }
 
    @Bean
    public Binding dlBinding(final Queue dlq, final TopicExchange dlExchange) {
        return BindingBuilder
                .bind(dlq)
                .to(dlExchange)
                .with(rabbitMqProperty.getTest().getDeadLetterRouteKey());
    }
}
cs

 

 

프로듀싱과 컨슈밍을 위해 브로커에 exchange와 queue를 binding하는 설정이다. 하나 추가적으로는 컨슈밍에서 예외가 발생하였을 때, Recover를 위한 Dead Letter Queue를 선언하였다.

 

이제 앱을 실행시키고, localhost:8080으로 요청을 보내보자 ! 요청을 보내면 메시지를 프로듀싱 할것이고, 해당 메시지를 컨슈밍해서 로그를 찍게 될 것이다. 여기까지 간단하게 래빗엠큐에 대해 다루어봤다.

 

 

 

https://jonnung.dev/rabbitmq/2019/02/06/about-amqp-implementtation-of-rabbitmq/

 

조은우 개발 블로그

 

jonnung.dev

https://velog.io/@hellozin/Spring-Boot%EC%99%80-RabbitMQ-%EC%B4%88%EA%B0%84%EB%8B%A8-%EC%84%A4%EB%AA%85%EC%84%9C

 

Spring Boot와 RabbitMQ 초간단 설명서

이번 포스트에서는 Spring boot 프로젝트에서 RabbitMQ를 사용하는 간단한 방법을 알아보겠습니다. Consumer 코드와 Producer 코드는 GitHub에 있습니다. 먼저 RabbitMQ 서버를 실행해야 하는데 Docker를 사용하�

velog.io

https://nesoy.github.io/articles/2019-02/RabbitMQ

 

RabbitMQ에 대해

 

nesoy.github.io

 

posted by 여성게
:
Web/gRPC 2020. 5. 3. 14:41

이전 포스팅에서는 grpc를 이용하여 간단하게 client-server 애플리케이션을 작성하였다. 간단하게 감을 익혀봤으니, 세세한 부분을 스터디 해본다. 오늘은 Protobuf(proto3)에 대해 다루어본다.

 

메시지 유형 정의

간단한 예를 보자. 검색 요청 메시지이며, 쿼리 문자열과 페이징을 위한 필드를 가지고 있다.

 

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

 

첫 줄은 proto3 syntax를 사용하고 있음을 지정한다. default는 proto2이다. 그리고 message SearchRequest를 정의한다. message는 자바로 비교하면 하나의 dto(model) 클래스라고 생각하면 좋다. 그리고 해당 message안에 필요한 필드를 선언한다. 위의 모든 필드는 scala type으로 지정되어 있지만, enum 및 다른 message 유형을 참조하여 복합 유형을 지정할 수도 있다. 

 

그리고 하나의 proto 파일에 여러 message 유형을 정의할 수 있다.

 

필드 번호

message의 각 필드에는 번호가 할당되어 있다. 이 필드 번호는 message가 이진형식으로 직렬화될때 필드를 식별하는데 사용되는 번호이다.

 

주석

proto 파일은 아래와 같이 주석 작성이 가능하다.

 

syntax = "proto3";

/*
검색 요청을 위한 요청 객체를 표현하는 message 정의
 */
message SearchRequest {
  string query = 1; //쿼리 문자열
  int32 page_number = 2;
  int32 result_per_page = 3;
}

 

Scala value types

스칼라 타입 유형은 아래와 같이 지원하고 있다.

 

TYPE

Notes

Java

Python

double

 

double

float

float

 

float

float

int32

Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.

int

int

int64

Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.

long

int/long[3]

uint32

Uses variable-length encoding.

int[1]

int/long[3]

uint64

Uses variable-length encoding.

long[1]

int/long[3]

sint32

Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.

int

int

sint64

Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.

long

int/long[3]

fixed32

Always four bytes. More efficient than uint32 if values are often greater than 228.

int[1]

int/long[3]

fixed64

Always eight bytes. More efficient than uint64 if values are often greater than 256.

long[1]

int/long[3]

sfixed32

Always four bytes.

int

int

sfixed64

Always eight bytes.

long

int/long[3]

bool

 

boolean

bool

string

A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.

String

str/unicode[4]

bytes

May contain any arbitrary sequence of bytes no longer than 232.

ByteString

str

 

Default Value

messagefmf deserialize할때, 값이 없다면 각 타입에 대해 아래와 같은 기본 값을 가진다.

 

Type Default Value
string ""
bytes empty bytes
boolean false
numeric 0
enums 첫번째 정의된 enum value(must be 0)

 

Enum Type

protobuf에서도 자바와 같은 Enum 타입을 생성할 수 있다.

 

syntax = "proto3";

/*
검색 요청을 위한 요청 객체를 표현하는 message 정의
 */
message SearchRequest {
  string query = 1; //쿼리 문자열
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}

enum Corpus {
  UNIVERSAL = 0;
  WEB = 1;
  IMAGES = 2;
  LOCAL = 3;
  NEWS = 4;
  PRODUCTS = 5;
  VIDEO = 6;
}

 

열거형은 반드시 시작을 0으로 시작해야한다. 그래야 역직렬화할때, 값이 존재하지 않을경우 0에 할당된 값을 기본값으로 쓰기 때문이다. 

 

다른 message 유형 사용

다른 message 타입을 필드 타입으로 사용할 수 있다.

 

message SearchResponse {
  SearchResult results = 1;
}

message SearchResult {
  string url = 1;
  string title = 2;
}

 

그렇다면 다른 .proto 파일에 있는 message 타입을 사용하려면 어떻게 해야할까?

 

#SearchResult.proto
syntax = "proto3";

option java_multiple_files = true;
option java_outer_classname = "SearchResultProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

message SearchResult {
  string url = 1;
  string title = 2;
}

#SearchProto.proto
syntax = "proto3";
import "SearchResult.proto";

option java_multiple_files = true;
option java_outer_classname = "SearchProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

/*
검색 요청을 위한 요청 객체를 표현하는 message 정의
 */
message SearchRequest {
  string query = 1; //쿼리 문자열
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}

enum Corpus {
  UNIVERSAL = 0;
  WEB = 1;
  IMAGES = 2;
  LOCAL = 3;
  NEWS = 4;
  PRODUCTS = 5;
  VIDEO = 6;
}

message SearchResponse {
  SearchResult results = 1;
}

 

import 문을 통해서 다른 .proto 파일을 가져와서 사용할 수 있다.

 

중첩 타입

message 타입을 중첩해서 사용가능하다. 마치 자바에서 클래스 안에 클래스를 가지듯 !

 

syntax = "proto3";

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
  }
  repeated Result results = 1;
}

 

Map type

필드 타입으로 Map을 사용할 수 있다.

 

syntax = "proto3";

option java_multiple_files = true;
option java_outer_classname = "ExampleRequestProto";
option java_package = "com.levi.yoon.proto";

package grpc.sample;

message ExampleRequest {
  map<string, string> requests = 1;
}

 

해당 필드를 set/get 할때는 아래와 같은 메서드를 지원한다.

 

ExampleRequest exampleRequest = ExampleRequest.newBuilder()
        .putRequests("a", "a")
        .putRequests("b", "b")
        .build();
exampleRequest.getRequestsMap();
exampleRequest.getRequestsOrDefault("a", "defaultValue");
exampleRequest.getRequestsOrThrow("a");

 

주의해야할 점은 Map 타입은 repeated를 사용할 수 없다.

 

Package

protobuf message 타입간 이름 충돌을 피하기 위해 파일에 선택적으로 package 지정자를 추가할 수 있다.

 

#ExampleRequestProto1.proto
syntax = "proto3";

package grpc.sample1;

message ExampleRequest {
  map<string, string> requests = 1;
}

#ExampleRequestProto2.proto
syntax = "proto3";

package grpc.sample2;

message ExampleRequest {
  map<string, string> requests = 1;
}

 

서비스 정의

client에서 stub 객체로 호출할 원격 프로시져(서비스)를 정의할 수 있다.

 

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

 

우리가 정의한 SearchRequest message를 매개변수로 받고, 응답으로 SearchResponse를 반환하는 서비스를 정의하였다. 해당 interface를 gRPC server에서 override하여 구현하고, client는 Stub 객체 안에 해당 서비스이름의 메서드를 콜하여 원격 gRPC service를 호출하게 된다.

 

기타 옵션

option java_multiple_files = true; -> proto 파일안의 message와 enum, service 등이 각 java 파일로 생성된다.

option java_outer_classname = "SearchProto"-> 생성될 자바코드의 클래스명이 된다.

option java_package = "com.levi.yoon.proto"-> 생성된 자바코드의 package 경로가 된다.

 

여기까지 간단하게 Protobuf에 대한 기능들 몇가지를 다루어보았다. 미쳐 다루지 못한 것은 실제 grpc 실습에서 다루어본다.

'Web > gRPC' 카테고리의 다른 글

gRPC - convert proto generate java to jsonString  (0) 2020.07.15
gRPC - java gRPC 간단한 사용법  (0) 2020.05.03
gRPC - gRPC란 무엇인가?  (0) 2020.05.02
posted by 여성게
:
Web/gRPC 2020. 5. 2. 22:14

오늘은 gRPC가 무엇인지 알아본다. 그동안 스터디한다고 마음만 먹고 매일 미루기만 했는데, 황금연휴에 맘잡고 gRPC에 대해 다루어 볼 것이다.

 

개요

gRPC를 사용하면 클라이언트 애플리케이션에서 마치 자신의 메서드를 호출하는 것처럼 원격서버(gRPC서버)의 메서드를 직접 호출 할 수 있으므로 MSA환경의 서비스를 보다 쉽게 만들 수 있다. 여타 다른 RPC와 마찬가지로 gRPC는 IDL(Interface Definition Language)를 이용하여 서비스를 정의하고 페이로드를 정의하며 gRPC서버는 이 인터페이스를 구현하고 클라이언트 호출을 처리하기 위해 gRPC서버를 실행한다. 클라이언트 측에서 클라이언트는 서버와 동일한 인터페이스를 가지는 스텁 객체를 가지고 있다.

 

 

ProtoBuf

gRPC는 기본적으로 구조화된 데이터를 직렬화하기 위해 Google의 ProtoBuf를 사용한다. 

 

ProtoBuf로 작업할 때는 첫번째로 .proto 파일에 직렬화하려는 데이터의 구조를 정의한다. 메시지라는 오브젝트(?)로 구성되며, 각 메시지는 데이터 타입과 key&value쌍을 이루는 필드를 하나이상 가지고 있다.

 

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

 

그 이후 protoc를 사용하여 원하는 언어로 컴파일이 가능하다. 컴파일된 소스는 각 필드에 대한 간단한 setter/getter를 제공한다. 또한 .proto 파일을 이용하여 위 proto message가 매개변수, 리턴 값으로 사용된 service를 정의할 수 있다.

 

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

 

 

일반 RPC와 다를게 없을 수도 있는데, gRPC가 왜 좋은가? 

 

성능

gRPC 메시지는 효율적인 이진 메시지 형식인 ProtoBuf를 사용하여 직렬화된다. protobuf는 서버와 클라이언트에서 엄청 빠르게 직렬화된다. 또한 적은 용량의 페이로드로 형성되어 있어, 제한된 대역폭에서 아주 중요한 역할을 한다.

 

코드 생성

모든 gRPC 프레임워크는 코드 생성에 대한 최고 수준의 지원을 제공한다. gRPC 개발에 대한 핵심 파일은 gRPC 서비스 및 메시지의 계약을 정의하는 .proto file이다. 이 파일에서 gRPC 프레임워크는 서비스 기본 클래스, 메시지 및 전체 클라이언트를 코드 생성한다.

서버와 클라이언트 간에 proto 파일을 공유하여 메시지와 클라이언트 코드를 종단 간에 생성할 수 있다. 클라이언트의 코드 생성은 클라이언트와 서버에서 메시지의 중복을 제거하고 강력한 형식의 클라이언트를 만든다. 클라이언트를 작성하지 않아도 되므로 많은 서비스를 갖춘 응용 프로그램의 개발 시간이 상당히 절감된다.

 

엄격한 사양

일반적으로 JSON을 주고 받는 HTTP는 엄격한 사양이 존재하지 않는다. 그래서 개발자끼리 URL, HTTP payload, 응답코드 등을 논의해야한다. 하지만 gRPC는 gRPC가 따라야하는 명확한 형식이 있기 때문에 HTTP와 같이 큰 논의가 필요없다. 단순히 생성된 코드를 사양에 맞게 사용하면 된다.

 

그렇다면 gRPC는 어떠한 상황에서 사용하는 것이 좋을까?

 

  • 마이크로 서비스 - gRPC는 대기 시간이 짧고 처리량이 높은 통신을 위해 설계되었습니다. gRPC는 효율성이 중요한 경량 마이크로 서비스에 적합합니다.
  • gRPC – 지점 간 실시간 통신은 양방향 스트리밍을 위한 뛰어난 지원 기능을 제공합니다. gRPC 서비스는 폴링을 사용하지 않고 실시간으로 메시지를 푸시할 수 있습니다.
  • Polyglot 환경 – gRPC 도구는 널리 사용되는 모든 개발 언어를 지원하며, 따라서 gRPC는 다중 언어 환경에 적합합니다.
  • 네트워크 제한 환경 – gRPC 메시지는 경량 메시지 형식인 Protobuf를 사용하여 직렬화됩니다. gRPC 메시지는 항상 해당하는 JSON 메시지보다 작습니다.

 

여기까지 gRPC가 무엇이고, 어떠한 특징을 가지며 장점이 무엇인지 다루어보았다. 다음 포스팅부턴 제대로된 실습을 해본다.

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 여성게
:
Middleware/Kafka&RabbitMQ 2019. 3. 12. 22:30

Kafka - Kafka(카프카)의 동작 방식과 원리


 

 

Kafka는 기본적으로 메시징 서버로 동작합니다. 여기서 메시징 시스템에 대해 간단히 살펴보자면 

메시지라고 불리는 데이터 단위를 보내는 측(publisher,producer)에서 카프카에 토픽이라는 각각의 메시지 저장소에 데이터를 저장하면, 

가져가는 측(subscriber, consumer)이 원하는 토픽에서 데이터를 가져가게 되어 있습니다. 즉, 메시지 시스템은 중앙에 메시징 시스템 서버를

두고 이렇게 메시지를 보내고(publish) 받는(subscriber) 형태의 통신 형태인 pub/sub 모델의 통신구조입니다.

 

여기서 미담이지만, 카프카의 창시자인 제이 크렙스는 대학 시절 문학 수업을 들으며 소설가 프란츠 카프카에 심취했습니다. 자신의 팀이 새로 개발할 시스템이

데이터 저장과 기록, 즉 쓰기에 최적화된 시스템이었기에 이런 시스템은 글을 쓰는 작가의 이름을 사용하는 것이 좋겠다 생각하여 

카프카라는 이름으로 애플리케이션이 탄생하게 되었습니다.

 

 

 

 

pub/sub은 비동기 메시징 전송 방식으로서, 발신자의 메시지에는 수신자가 정해져 있지 않은 상태로 발행합니다. 구독을 신청한 수신자만이 정해진

메시지를 받을 수 있습니다. 또한 수신자는 발신자 정보가 없어도 원하는 메시지만을 수신가능합니다.

 

 

펍/섭 구조가 아닌 일반적인 통신은

 

A ---------------> B

C ---------------> D

 

형태로 통신을 하게 됩니다. 이럴 경우 빠른 전송 속도와 전송 결과를 신속하게 알 수 있는 장점이 있지만, 장애가 발생한 경우

적절한 처리를 수신자 측에서 해주지 않는다면 문제가 될 수 있습니다. 그리고 통신에 참여하는 개체수가 많아 질 수록 

일일이 다 연결하고 데이터를 전송해야 하기 때문에 확장성이 떨어집니다.

 

하지만 펍/섭구조는 

 

         ==========

A ---------------->=메시지서버= ---------->B

C ---------------->========== ---------->D

 

프로듀서가 메시지를 컨슈머에게 직접 전달하는 방식이 아니라 중간의 메시징 시스템에 전달합니다. 이때 메시지 데이터와 수신처 ID를

포함시킵니다. 메시징 시스템의 교환기가 메시지의 수신처ID 값을 확인한 다음 컨슈머들의 큐에 전달합니다. 컨슈머는 자신들의

큐를 모니터링하고 있다가, 큐에 메시지가 전달되면 이 값을 가져갑니다. 이렇게 구성할 경우 장점은 혹시나 통신 개체가 하나 빠지거나 수신 불가능 상태가 되었을 

때에도, 메시징 시스템만 살아 있다면 프로듀서에서 전달된 메시지는 유실되지 않고, 장애에서 복구한 통신 개체가 살아나면 다시 메시지를

안전하게 전달할 수 있습니다. 그리고 메시징 시스템을 중심으로 연결되기 때문에 확장성이 용이합니다. 물론 직접 통신 하지 않기 때문에

메시지가 정확하게 전달되었는지 확인하는 등의 코드가 들어가 복잡해지고, 메시징 서버를 한번 거쳐서 가기에 통신속도가 느린 단점도 있습니다.

하지만 메시징 시스템이 주는 장점때문에 이러한 단점은 상쇄될만큼 크지 않다고 생각합니다.(비동기가 주는 장점 등)

 

(밑의 설명을 두고 넣은 그림은 아님)

 

 

기존 메시징 시스템을 사용하는 펍/섭 모델은 대규모 데이터(데이터 단건당 수MB 이상)를 메시징 시스템을 통해 전달하기 보다는 간단한 이벤트(수KB정도)를 서로 전송하는데 주로 사용되었습니다. 왜냐하면 메시징 시스템 내부의 교환기의 부하, 각 컨슈머들의 큐관리, 큐에 전달되고 가져가는 메시지의 정합성, 전달 결과를

정확하게 관리하기 위한 내부 프로세스가 아주 다양하고 복잡했기 때문입니다.

즉, 기존 메시징 시스템은 속도와 용량보단 메시지의 보관,교환,전달 과정에서의 신뢰성을 보장하는 것에 중점을 두었습니다.

 

카프카는 메시징 시스템이 지닌 성능의 단점을 극복하기 위해, 메시지 교환 전달의 신뢰성 관리를 프로듀서와 컨슈머 쪽으로 넘기고, 부하가 많이 걸리는 교환기 기능

역시 컨슈머가 만들 수 있게 함으로써 메시징 시스템 내에서의 적업량을 줄이고 이렇게 절약한 작업량을 메시지 전달 성능에 집중시켜 

고성능 메시징 시스템을 만들어 냈습니다.

 

 

 

 

 

 

카프카의 특징

프로듀서와 컨슈머의 분리 - 카프카는 메시징 전송 방식 중 메시지를 보내는 역할과 받는 역할이 완벽하게 분리된 펍/섭 방식을 적용했습니다. 각 서비스서버들은 다른 시스템의 상태 유무와 관계없이 카프카로 메시지를 보내는 역할만 하면 되고, 마찬가지로 메시지를 받는 시스템도서비스 서버들의 상태유무와 관계없이 카프카에 저장되어 있는 메시지만 가져오면 됩니다.
멀티 프로듀서, 멀티 컨슈머 - 카프카는 하나의 토픽에 여러 프로듀서 또는 컨슈머들이 접근 가능한 구조로 되어있습니다. 하나의 프로듀서가 하나의토픽에만 메시지를 보내는 것이 아니라, 하나 또는 하나 이상의 토픽으로 메시지를 보낼 수 있습니다. 컨슈머는 역시 하나의 토픽에서만메시지를 가져오는 것이 아니라, 하나 또는 하나 이상의 토픽으로부터 메시지를 가져올 수 있습니다. 이렇게 멀티 프로듀서와 멀티 컨슈머를구성할 수 있기 때문에 카프카는 중앙 집중형 구조로 구성할 수 있게 됩니다.
디스크에 메시지 저장 - 카프카가 기존의 메시징 시스템과 가장 다른 특징 중 하나는 메모리에 데이터를 쓰는 것이 아니라, 바로 디스크에 메시지를 저장하고유지하는 것입니다. 일반적인 메시징 시스템들은 컨슈머가 메시지를 읽어가면 큐에서 바로 삭제하는데, 카프카는 컨슈머가 데이터를 가져가도 일정 기간동안데이터를 유지합니다. 트래픽이 급증해도 컨슈머는 디스크에 안전하게 저장된 데이터를 손실 없이 가져갈 수 있습니다. 또한 앞서 멀티 컨슈머가 가능한 것도이렇게 디스크에 데이터를 쓰기 때문입니다. 디스크에 데이터를 쓰기 때문에 처리가 느릴 것이라는 생각을 할 수 있기도한대, 기존의 디스크 사용법과는다르게 디스크를 나눠서 쓰는 것이 아니라, 특정 영역의 디스크에 순차적으로 쓰기 때문에 읽어가는 디스크의 영역의 범위를 확 줄일 수 있어 읽어가는 속도는조금은 빨라집니다. 그리고 카프카는 무조건 데이터를 디스크에 쓴다기 보다는 VM메모리의 일부를 페이지 캐싱으로 사용하기 때문에 속도가 빠릅니다.
확장성 - 카프카는 확장이 매우 용이하도록 설계되어 있습니다. 하나의 카프카 클러스터는 3대부터 수십대의 브로커로도 메시지 서버의 중지없이 확장 가능합니다.
높은성능 - 카프카는 내부적으로 고성능을 유지하기 위해 분산 처리, 배치 처리 등 다양한 기법을 사용하고 있습니다.(병렬 처리를 위한 토픽 파티셔닝 기능)
이러한 카프카의 사용으로 연동되는 애플리케이션들의 느슨한 결합도를 유지할 수 있게 됩니다. 이번 포스팅은 간단한 카프카의 소개였고,다음 포스팅부터는 진짜 카프카를 이용한 예제 포스팅이 될 것 같습니다.

 

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. 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 여성게
: