분류없음2019.04.14 00:27

스프링에서 가장 쉽게 프로퍼티를 읽어오는 방법은 @Value를 이용하는 것이다.

- @Value(value="${app.base.name}") 를 이용해서 application의 프로퍼티 하나를 읽어오거나
- @Value("#{systemProperties['os.name']}") 처럼 이용해서 시스템 값을 가져오거나
- @Value("${spring.profiles.active:default}") 를 이용해서 프로파일값을 가져 올 수 있지만,

expression 을 이용하거나, 몇몇 한정된 경우에만 쓰이고 실제로는 @ConfigurationProperties를 더 많이 이용한다.
(링크 : https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html )

ConfigurationProperties를 를 이용해서 외부 설정값을 application 내부에 사용하기 위해서는 spring bean으로 등록해야 한다.

첫번째로 명시적으로 @Bean 으로 명시적으로 생성 하기

@Configuration
public class AppConfigurare {
    @Bean
    public BistrosAppProperties bistrosAppProperties() {
        return new BistrosAppProperties();
    }
}

두번째로는 간단하기 해당 클래스에 바로 @Configuration을 붙여도 된다.

@Configuration
@ConfigurationProperties(prefix = "app.base")
public class BistrosAppProperties {
...
}

둘다 동작은 하지만, Intellij IDE 에서는  아래와 같은 화면을 보여준다. 

이 꼴보기 싫은 빨간줄을 없애려면 가이드대로 @EnableConfigratuinProperties를 사용해야 한다.
그래서 order-eda-gateway 는 4개의 @ConfigurationProperties 클래스가 존재하지만  @EnableConfigurationProperties 도 3개나 선언했다.  (패키지별로의 커플링을 위해서 모듈화 되어 있다)

왜 이럴까 갑자기 궁금해져서 찾아보니.... https://youtrack.jetbrains.com/issue/IDEA-204153

 

Spring: Incorrect error when combining @Configuration and @ConfigurationProperties annotations : IDEA-204153

What steps will reproduce the issue? 1. Start a Spring Boot project 2. Create a class with both the "@Configuration" and "@ConfigurationProperties" annotations (see screenshot) 3. See the red squiggle…

youtrack.jetbrains.com

@Component 는 사용하면 빨간줄이 안나오도록 수정되었고, @Service는 여전히 에러라고 표시된다.   
@Configuration 이나 @Service 어노테이션을 사용 할 경우에 에러라고 나오는 것은 문제이고 '경고'로 나와야 할것 같지만, 그건 좀 어려운것 같다. 정도로 이해했다. 그러니까... 그냥 이렇게 쓰자 '_';;

ps. 내 스프링 기억은 2.5에서 끝인데 끝도 없이 뭐가 생기네 https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html 를 보면 properties 의 적용 순서의 첫번째가 devtools global properties이다. 쓸일도 없고 써본적도 없는 devtools ....

Posted by dev.bistro
분류없음2019.04.11 15:59

Spring Security OAuth2  인증 순서.

1. https://localhost:8443/login  으로 로그인을 하려고 하면 security의 `anyRequest().authenticated()` 코드에 의해 인증이 필요한것으로 파악하여 302응답이 돌아온다.
    Status Code : 302
    Location: https://www.facebook.com/dialog/oauth?client_id=394182450695217&redirect_uri=https://localhost:8443/login&response_type=code&state=X6kWlA

해당 Server Log

...더보기

2019-04-11 15:17:37.327 DEBUG 52044 --- [nio-8443-exec-6] o.s.security.web.FilterChainProxy        : /login at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2019-04-11 15:17:37.327 DEBUG 52044 --- [nio-8443-exec-6] w.c.HttpSessionSecurityContextRepository : HttpSession returned null object for SPRING_SECURITY_CONTEXT
2019-04-11 15:17:37.327 DEBUG 52044 --- [nio-8443-exec-6] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@16504e11. A new one will be created.

2019-04-11 15:17:37.327 DEBUG 52044 --- [nio-8443-exec-6] o.s.security.web.FilterChainProxy        : /login at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
2019-04-11 15:17:37.327 DEBUG 52044 --- [nio-8443-exec-6] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/login'

2019-04-11 15:17:37.327 DEBUG 52044 --- [nio-8443-exec-6] uth2ClientAuthenticationProcessingFilter : Request is to process authentication

2019-04-11 15:17:37.329 DEBUG 52044 --- [nio-8443-exec-6] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2019-04-11 15:17:37.329 DEBUG 52044 --- [nio-8443-exec-6] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
2019-04-11 15:17:37.329 DEBUG 52044 --- [nio-8443-exec-6] o.s.s.web.DefaultRedirectStrategy        : Redirecting to 'https://www.facebook.com/dialog/oauth?client_id=394182450695217&redirect_uri=https://localhost:8443/login&response_type=code&state=X6kWlA'

 

2.  해당 링크에서 다시 한번 페이스북 로그인을 요청하기 위해 302:redirect를 한다.

https://www.facebook.com/login.php?skip_api_login .. 이때 cancel_url 과 redirect_url 파라미터를 추가로 붙인다. 거기서 성공하면 다시 한번 https://www.facebook.com/dialog/oauth 를 호출하게 되고 302응답과 함께 code 값을 받아온다.

 

해당 값으로 access token을 얻기 위해 호출한다. ->  https://git.io/fjqur 에서 OAuth2RestTeamplte 를 이용해서 access token을 얻어온다는 https://git.io/fjquP 에서 context에 저장한다. 

Posted by dev.bistro
분류없음2019.03.14 21:54


Netty를 학습하기 위해서 Debug 레벨로 로그를 보고 싶음.

모든 튜토리얼에서 처음에 나오는 클래스인 ServerBootstrap ( https://git.io/fjeuN ) 를 살펴보면  InternalLoggerFactory 클래스를 이용해서 인스턴스를 획득하는 것을 볼 수 있다. 해당 클래스는 abstract class 이고 다양한 로그 라이브러를 지원하고 있음을 알 수 있다.


그 중에서 slf4j - logback 콤보를 이용하기 위해 아래 디펜던시를 추가한다.


<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>


그리고 classpath에 아래와 같은 기본적인 logback.xml 을 설정한다

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}                      - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="console"/>
</root>
</configuration>


아래와 같은 콘솔 로그를 확인 할 수 있다.

 io.netty.util.internal.logging.InternalLoggerFactory - Using SLF4J as the default logging framework
 io.netty.channel.MultithreadEventLoopGroup - -Dio.netty.eventLoopThreads: 24
 io.netty.util.internal.PlatformDependent0 - -Dio.netty.noUnsafe: false
 io.netty.util.internal.PlatformDependent0 - Java version: 8
 io.netty.util.internal.PlatformDependent0 - sun.misc.Unsafe.theUnsafe: available
 io.netty.util.internal.PlatformDependent0 - sun.misc.Unsafe.copyMemory: available
 io.netty.util.internal.PlatformDependent0 - java.nio.Buffer.address: available
 io.netty.util.internal.PlatformDependent0 - direct buffer constructor: available
 io.netty.util.internal.PlatformDependent0 - java.nio.Bits.unaligned: available, true
 io.netty.util.internal.PlatformDependent0 - jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable prior to Java9
 io.netty.util.internal.PlatformDependent0 - java.nio.DirectByteBuffer.(long, int): available
 io.netty.util.internal.PlatformDependent - sun.misc.Unsafe: available
 io.netty.util.internal.PlatformDependent - -Dio.netty.tmpdir: /var/folders/sr/grtv_wmn3wj092488tqh7slr0000gn/T (java.io.tmpdir)
 io.netty.util.internal.PlatformDependent - -Dio.netty.bitMode: 64 (sun.arch.data.model)
 io.netty.util.internal.PlatformDependent - -Dio.netty.noPreferDirect: false
 io.netty.util.internal.PlatformDependent - -Dio.netty.maxDirectMemory: 3817865216 bytes
 io.netty.util.internal.PlatformDependent - -Dio.netty.uninitializedArrayAllocationThreshold: -1
 io.netty.util.internal.CleanerJava6 - java.nio.ByteBuffer.cleaner(): available
 io.netty.channel.nio.NioEventLoop - -Dio.netty.noKeySetOptimization: false
 io.netty.channel.nio.NioEventLoop - -Dio.netty.selectorAutoRebuildThreshold: 512
 io.netty.util.internal.PlatformDependent - org.jctools-core.MpscChunkedArrayQueue: available
 io.netty.channel.DefaultChannelId - -Dio.netty.processId: 78164 (auto-detected)
 io.netty.util.NetUtil - -Djava.net.preferIPv4Stack: false
 io.netty.util.NetUtil - -Djava.net.preferIPv6Addresses: false
 io.netty.util.NetUtil - Loopback interface: lo0 (lo0, 0:0:0:0:0:0:0:1%lo0)
 io.netty.util.NetUtil - Failed to get SOMAXCONN from sysctl and file /proc/sys/net/core/somaxconn. Default: 128
 io.netty.channel.DefaultChannelId - -Dio.netty.machineId: xx:1c:xx:ff:fe:00:00:09 (auto-detected)
 io.netty.util.ResourceLeakDetector - -Dio.netty.leakDetection.level: simple
 io.netty.util.ResourceLeakDetector - -Dio.netty.leakDetection.maxRecords: 4
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.numHeapArenas: 24
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.numDirectArenas: 24
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.pageSize: 8192
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxOrder: 11
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.chunkSize: 16777216
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.tinyCacheSize: 512
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.smallCacheSize: 256
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.normalCacheSize: 64
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedBufferCapacity: 32768
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimInterval: 8192
 io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.useCacheForAllThreads: true
 io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled
 io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 65536
 io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384


1라인 : slf4j 를 로깅 프레임웍으로 쓰고 있는 것을 확인 
2라인 : 내 맥북의 사용가능 CPU가 12개 여서 24 ( https://git.io/fjez0 )


주의) 혹시나 java.lang.ClassNotFoundException: jdk.internal.misc.Unsafe 오류가 보인다면 Netty 4.11 을 사용하고 있는지 확인하고 4.12+ 로 버전 변경 한다 ( https://github.com/netty/netty/issues/6901 ) 


Posted by dev.bistro
TAG logback, Netty
분류없음2018.11.09 12:09


logstash 에는 이미 kafka input 모듈이 있기 때문에 쉽게 카프카 메시지를 엘라스틱에 저장할 수 있다.
( https://www.elastic.co/guide/en/logstash/current/plugins-inputs-kafka.html )

몇가지 주의할 점


1) 주문번호등을 Partition Key로 이용할 경우에, 데이터가 어느 파티션에 들어가 있는지, Offset은 얼마인지도 중요한 디버깅 요소가 된다. 이러한 데이터도 함께 elasticsearch 에 넣으려면 add_field를 이용한다.


우선 input 쪽에 아래의 설정을 추가하여 메타 정보를 사용할 수 있게 한다

input {

kafka {

decoreate_events => true

}

}


그리고 filter 영역에 아래처럼 추가하여 Kafka Message의 Offset, Partition, EventTime을 ES document에 추가한다.

filter {

mutate {

"partition" => "%{[@metadata][kafka][partition]}"

"offset" => "%{[@metadata][kafka][offset]}"

"eventtime" => "%{[@metadata][kafka][timestamp]}"

}

}



2)  logstash를 이용하여 데이터를 주입 받을 경우, Document의 생성시간은 logstash의 서버시간에 의존적이다. 하지만 Kafka Message를 모니터링 하기 위해서는 EventTime을 기반으로 index time을 설정하는 것이 편리했다.
아래처럼 Kafka Header에 존재하는 EventTime을 @timestamp 로 변경한다.

filter {

data {

match => ["eventtime", "UNIX_MS"]

target => "@timestmap"

}

}

이제 kibana에서 @timestamp로 index-pattern을 생성한다면, 그것은 kafka의 eventtime을 기반으로 보는것이다.


3) 카프카를 사용하다보면 실제로는 Avro Protocols을 사용하는 경우가 많다. 이 경우에는 외부 플러그인을 통해서 엘라스틱 서치로 데이터를 저장 할 수 있다.

https://github.com/revpoint/logstash-codec-avro_schema_registry 를 설치한다.

kafka input을 아래처럼 수정한다.

input {

kafka {

...

codec => avro_schema_registry {

endpoint => "http://registry:8081"

}

value_deserializer_class => "org.apache.kafka.common.serialization.ByteArrayDeserializer"

}

}


Posted by dev.bistro
분류없음2018.09.04 13:12

모던 랭귀지 중에서 현업에서 써봤거나,  대충은 해봤던 언어는 Java Scala Groovy Kotlin Swift python go... 이중에서 Go는 원래 태생이 그래서 제외한다면 제일 구닥다리는 Java.

그 중에 Optional에 대한 사용은 항상 고민이 된다.


http://blog.joda.org/2015/08/java-se-8-optional-pragmatic-approach.html

https://stackoverflow.com/questions/31922866/why-should-java-8s-optional-not-be-used-in-arguments


1. Optional로 인스턴스 변수를 사용하지 말 것

2. null로 내부 private scope 클래스의 데이터 유무를 나타낸다. (첨언 : 자바에는 Null이 있어서 내부적으로 null이 Optional.empty를 표현할 수 있다. 이렇게 하고, 외부로 노출하는 getMethod에는 null이 아닌 empty를 보여주면 된다)

3. getter에는 사용

4. set메소드, 생성자에서는 사용하지 말 것

5. 다른 로직에서 Optional 에 따른 비즈니스 로직이 있을 때에는 사용



즉 Class 내에서 private variable로 Optional을 가지지 말라

장점으로는 

1. 이 객체는 Application 내에서 자주 사용될것이고 그 life-cycle동안 길것이다. 그에 반헤 Getter만 Optional이라면 그 라이프 사이클은 훨씬 짧아진다. GC에 대해 이로울 것이고

2. Optional 이 있다면 Serializable  할수 없다. (jackson-modules-java8도 결국엔 Optional.empty를 null로 만든다.)

3. setter나 생성자에서 Optional을 사용하는 것 자체가 경험에 비추어 유용하지 않았다. 



단점으로는 이렇게 생성되는 Class가  beans가 아니라는 것이다 (private variable + getter + setter로 이루어진) 

일부 IDE에서 문제가 발생할 수도 있으니 확인해봐야한다. 하지만 이런식으로 된다면 사용하는 쪽에서 null에

te 고민을 없앨 수 있다. 혹시나 Optional.get()을 호출하고 있다면 뭔가 이상하다고 생각할 것




Posted by dev.bistro
TAG Java, Optional
분류없음2018.07.30 19:33

현재 업무에서 메지시의 상태는 하나의 필드로 정의하고 있다.  예를 들어 결제 요청이 들어오면 APPROVAL_REQUESTED:type 을 정의하여 payment topic에 적재하고 이 후 저 메시지를 컨슘하여 결제가 완료된다면 '동일한 토픽에' APPROVAL_COMPLETED:type 으로 하나의 메시지를 추가한다.

즉, APPROVAL_REQUESTED 상태는 APPROVAL_COMPLETED or APPROVAL_FAILED 상태로 변경 가능하다.

각각은 Kafka에 의해 Header에 event time이 기록되어 있지만, 별개로 actBy라는 timestamp 필드도 추가하였다.  REQUESTED 상태에서 COMPLETED 상태로 변경되기까지의 시간을 어떻게 측정할 것인가?


무난한 방법으로 하나의 Streams Application을 작성할 수도 있고, KSQL을 통해서 볼 수도 있다. 또는 COMPLTED 메시지가 기록될 때 프로그램적으로 기록 할수도 있고... 내가 선택한 방법은 기존에 모니터링을 위해 사용중인 logstash의 plugin 이다.


https://www.elastic.co/guide/en/logstash/current/plugins-filters-elapsed.html

하나의 필드를 기준으로 2개의 message 사이이 시간을 측정해서 소요시간을 측정하는 것이다.

2번째 메시지에 elapsed_time 필드를 add_field 줄수도 있고, 원본 이벤트는 그대로 나둔채 하나의 추가 이벤트를 발행해서 elasticsearch에 기록 할 수 있다. (이 경우에는 elapsed_match 라는 tag를 가진 document이다) 이 설정은 new_event_on_match 를 통해서 설정 가능하다.


개인적으로 셋팅한 내용은 아래와 같다.

   elapsed {

      start_tag => "APPROVAL_REQUESTED"

      end_tag => "APPROVAL_COMPLETED"

      unique_id_field => "id"

      timeout => 10

      new_event_on_match => false

    }


기준이 되는 필드는 'id' 이고  동일한 ID를 가진 이벤트의 APPROVAL_REQUESTED,  APPROVAL_COMPLETED 사이의 시간을  APPROVAL_COMPLETED tag를 가지는 메시지에 elapsed_time 필드로 기록 해준다. 만약 10초 이내에  동일한 id를 가지는 APPROVAL_COMPLETED 메시지를 수신하지 못한다면 elapsed_expired_error 태그를 가지는 메시지를 발행한다.


이를 통해서 손쉽게 


1카프카 주문 메시지 발행 - 2컨슘 - 3Remote API호출 - 4주문 요청 결과 메시지 발행  

1~4단계의 소요 시간을 측정 할 수 있다.



(물론 이것은 카프카 메시지 간의 소요시간이지, 그 외 측정구간은 또 다른 방법을 이용 했다)

Posted by dev.bistro
분류없음2018.07.27 16:04


사내 시스템은 특정 계정으로만 관리된다. 이 계정의 home directory 에 npm package를 설치 하는 방법이다.


1. .bashrc 설정

NPM_PACKAGES="$HOME/.npm-packages"
PATH="$NPM_PACKAGES/bin:$PATH"
unset MANPATH
MANPATH="$NPM_PACKAGES/share/man:$(manpath)"
NODE_PATH="$NPM_PACKAGES/lib/node_modules:$NODE_PATH"

2. 커맨드라인에서 다음의 명령을 수행

$) mkdir -p "$NPM_PACKAGES"
$) echo "prefix = $NPM_PACKAGES" >> ~/.npmrc

참고1. https://stackoverflow.com/questions/10081293/install-npm-into-home-directory-with-distribution-nodejs-package-ubuntu

참고2. https://github.com/sindresorhus/guides/blob/master/npm-global-without-sudo.md

Posted by dev.bistro
분류없음2018.06.29 17:43

6월 gradle 4.8이 릴리즈 되었다. (현재 4.8.1) 가장 눈에 들어오는 점은  nebula의 gradle-dependency-lock-plugin 으로 사용하고 있던 `Dependency locking` 이거다. 

> ./gradlew build --write-locks


사용하기 위해서는 build.gradle 에서 해당 내용을 활성화 시켜야 한다.
(참고 : https://docs.gradle.org/4.8.1/release-notes.html#locking-of-dynamic-dependencies )

dependencyLocking {
    lockAllConfigurations()
}


처럼 실행하면 ./gradle/dependency-locks 에 현재 실행된 task 들의 각 단계에서 필요한 dependencies 정보가 lockfile로 생성된다. 예를 들어 build를 실행하니, 아래와 같이 생성되었다.

annotationProcessor.lockfile
checkstyle.lockfile
compileClasspath.lockfile
cpd.lockfile
jacocoAgent.lockfile
pmd.lockfile
runtimeClasspath.lockfile
testAnnotationProcessor.lockfile
testCompileClasspath.lockfile
testRuntimeClasspath.lockfile

만약 dependencies tasks를 실행할 때 --write-locks 옵션을 붙인다음 좀 더 많은 lockfile이 생긴다.

annotationProcessor.lockfile
archives.lockfile
bootArchives.lockfile
checkstyle.lockfile
compile.lockfile
compileClasspath.lockfile
compileOnly.lockfile
cpd.lockfile
default.lockfile
jacocoAgent.lockfile
jacocoAnt.lockfile
pmd.lockfile
runtime.lockfile
runtimeClasspath.lockfile
testAnnotationProcessor.lockfile
testCompile.lockfile
testCompileClasspath.lockfile
testCompileOnly.lockfile
testRuntime.lockfile
testRuntimeClasspath.lockfile

이 후 디펜던시 버전을 변경하여 다시 실행하고자 하면 아래와 같은 메시지를 확인 할 수 있을 것이다.


FAILURE: Build failed with an exception.


* What went wrong:
Could not determine the dependencies of task ':cpdCheck'.
> Could not resolve all dependencies for configuration ':cpd'.
   > Dependency lock state for configuration 'cpd' is out of date:
       - Did not resolve 'net.sourceforge.pmd:pmd-php:5.6.1' which is part of the lock state
       생략


Posted by dev.bistro
분류없음2018.06.27 15:58

마이크로 미터는 2017년  4월 중순  Jon Schneider (슈나이더?) 에 의해 시작되었다. 원래 넷플릭스 직원이었고, 우리도 애용하고 있는 nebula의 몇몇 플러그인 개발자 였다. 하여튼 (동일한 시기에 Pivotal로 이직을 해서인지?, 어떤 이유에서인지)  슈나이더는 micrometer를 시작하였고 "Experimental ground for Spring metrics work" 라는 최초의 커밋을 남겼다.

https://github.com/micrometer-metrics/micrometer/commit/4df27760


그가 micrometer에 대해서 소개한 글이다. 대충 필요하다고 생각되는 내용만 옮겨 적음

https://spring.io/blog/2018/03/16/micrometer-spring-boot-2-s-new-application-metrics-collector



What is it?

마이크로미터(Micrometer)는 벤더중립(vender neutral) API를 이용해서 내 코드를 시간, 카운트, 계측 할 수 있는 차원 우선 계측 집합의 facade이다. (dimensional-first metrics collection facade) 클래스패스나 설정을 통해서 하나 또는 이상의 모니터링 시스템에 메트릭 데이터를 보낼수(export) 할 수 있다.  메트릭 계의 SLF4J 라고 생각하면 된다. 

마이크로미터는 스프링부트1에 존재했던 카운터, 게이지에 더 풍족한 기본 요소를 추가하였다. 예로 단일 마이크로미터 타이머(Timer)는  처리량(throughput), 총 시간(total time), 샘플의 최대 지연시간(maximum latency of recent samples),  퍼센테이지(& 히스토그램)등을 생성 할 수 있다.

차원 측정(dimensional metrics) 에 포커싱을 하고 있지만, 마이크로미터는 계층명(hierarchical names)에 매핑하여 Ganglia 나 JMX 같은 오래된 모니터링 솔루션을 계속 지원한다. 마이크로미터로 변경하게 된 이유는 좀 더 발전된 차원 모니터링 시스템(Prometheus, Datadog, Wavefront, SignalFx, Influx) 때문이다. 

스프링 프레임워크의 장점은 추상화를 이용한 선택을 할 수 있다는 것이다. 마이크로미터를 적용한 이후 하나 이상의 모니터링 시스템을 선택하고, 나중에 metrics 수집부분을 재작성 하지 않고도 모니터링 시스템을 교체할 수 있다.



What do I get out of the box?

스프링 부트 2는 몇가지 메트릭을 포함하고 있다.

* JVM, CPU, Spring Request, RestTemplate, Cache, DataSource, Logback, Tomcat 등등



Which monitoring systems does Micrometer support ?

마이크로미터는 벤더중립적인 메트릭 수집 API(io.micrometer.core.instrument.MeterRegistry) 를 제공하며, 여러 구현체도 가지고 있다.

* Netflix Atlas, Datadog, Influx, JMX, NewRelic, Prometheus 등등

마이크로미터 1.1.0 에서는 다음도 지원할 예정이다.

* AppOptics, Azure Application Insights, Dynatrace, Elasticsearch, StackDriver


스프링부트2는 여러개의 registry implementations 를 등록할 수 있는 MeterRegistry 를 복합(composite) 설정하여 여러개의 모니터링 시스템으로 전송 할 수 있다.  MeterRegistryCustomizer 를 이용한다면 한번에 레지스트리 셋트나 개별 구현을 커스텀마이즈 할 수 있다.

예를 들어 1) Prometheus , CloudWatch로 각각 메트릭을 보내거나 2) 호스트와 Application Tag같은 공통 태그 집합을 추가하거나 3) CloudWatch에는 일부 메트릭에 whitelist를 추가하거나 등등이다.



The distinction between metrics and tracing

메트릭(metrics) 이란 시스템의 성능을 추록할 수 있는 정보 클래스를 뜻한다.  중요한 점은 single request가 여러 구성 요소를 지날때의 대기 시간(total latency) 는 제외(excldue)된다. 이것은 Spring Cloud Sleuth 와 같은 분산 트레이싱 수집기의 역할입니다.

분산 트레이싱 시스템은 subsystem latency 에 대한 자세한 정보를 제공하지만 일반적으로 확장을 위해서 샘플링을 한다. (예를 들어 Sleuthsms 디폴트로 10%만 샘플링한다) 메트릭 데이터는 일반적으로 사전 집계(pre aggregated) 하지만 샘플링을 하지 않는다.


그래서 B와 상호작용을 하는 A의 100,000건의 요청에 대해서

- 메트릭 관점에서 보면 A서비스의 스루풋은 100K, B 처리량은 60K,  A 전체 평균 지연 시간은 100ms, B는 50ms 등을 알 수 있다. 또한 최대 지연 시간 및 다른 통계등도 확인 할 수 있다

- 트레이싱 시스템은 특정한 요청(particular request / 샘플링이기 때문에 전체가 아니다)이 A에서 50ms B에서 90ms가 걸렸을 알려줄 수 있다.



The importance of dimensionality

스프링 부트1 의 메트릭은 계층적 ( hierarchical ) 이었다. 이 말은 수집된 메트릭은 이름으로 인해서 식별이 가능했다. 그래서 jvm.memory.used 라는 메트릭이 있을 수 있다. 단일 어플리케이션 인스턴스에서 메트릭을 확인 하는 경우 적합하지만, 10개의 인스턴스일 경우는 어떻할 것인가? 메모리 소비량이 예상외로 급증하면 어떻게 구별할 것인가? 일반적으로는 이름에 접두/접미사를 추가한다 따라서 이름을 {HOST}.jvm.memory.used로 변경 할 수 있다. 그리고 일반적인 계층 모니터링 시스템(hierarchical monitoring system) 에서는 이름을 와일드카드로 지정(*.jvm.memory.used)해서 메모리의 합계를 확인 할 수 있다. 

이런식으로 region 까지 확장해 나간다면 *.*.jvm.memory.used 라고 사용 할 수는 있다. 하지만 이렇게 새로운 메트릭을 설정해서 전부 배포되기 전까지 제대로 메트릭 정보를 얻을 수 없다. 이 예시는 기존의 계층 모니터링 시스템의 제한 중 하나의 예에 불과하다.

차원 모니터링 시스템(Dimensional monitoring systems) 은 자연스럽게 jvm.memory.used를 하나이상의 태그를 drill 하기전까지 모든 태그에 걸쳐 자연스럽게 표시한다. 우선 이름(jvm.memory.used)를 선택하고  이후 태그에 의한 필터링을 한다 문제가 되는 이전 시나리오처럼 기존 차트가 생성된 다음 나중에 region tag를 추가한 경우에는 기존 메트릭도 중단없이 계속 동작 한다. 




Meter filters

Meter 필터를 이용하면 미터가 등록되는 방법,시기,통계의 종류 등을 제어 할 수 있다. 미터는 3가지 기본 기능을 제공한다.

. 거부 : 미터 등록을 거부/승인 한다.
. 변환 : 이름, 태그의 추가 삭제, 설명이나 기본단위 변경 등을 바꿀 수 있다.
. 설정 : 백분위 수, 타이머 SLA등의 통계 구성을 설정 할 수 있다.


스프링 부트2는 속성을 통해 미터 필터에 바인딩 할 수 있다.

management.metrics.enable.jvm=false
management.metrics.distribution.percentiles-histogram.http.server.requests=true
management.metrics.distribution.sla.http.server.requests=1ms,5ms

위의 설정은 jvm metrics을 해제하고, SpringBoot에 의 http-request 백분위 메트릭을 설정하고, sla 주기를 1ms, 5ms 이하로 셋팅한 것이다.



Why the /actuator/metrics endpoint changed in Spring Boot 2

스프링 부트1 에서는 단지 카운터/게이지만 있었기 때문에 하나의 REST 포인트에 계층적으로 제공하는 것이 간단했습니다. 하지만 차원 모니터링 시스템으로 변경되면서 모든 정보를 하나의 페이로드에 출력할 수 있는 방법이 없다는 사실을 알았다. 

커스텀 된 UI를 원한다면 UI에 필요한 데이터만 표시하는 컴포넌트를 구성하는 것이 좋다. MeterRegistry 를 컴포넌트에 삽입하여 find, get 메소드를 이용해서 필요한 메트릭을 expose 한 뒤 용도에 맞게 직렬화해서 사용하라.



Posted by dev.bistro
분류없음2018.06.18 14:51

필요한 수준까지 이해한듯 해서, 메모로 남김

원래의 카프카는 데이터 주입에 대한 순서가 중요하지 않았다.  (정확히는 어느 정도의 버퍼를 카프카 브로커가 가지면서 Event Time에 대한 조정 작업을 알아서 해준다)

하지만 producer의 명등성 옵션과 순서 보장에 대한 이슈가 중요해졌고 max.in.flight.request.per.connection = 1 로 셋팅하면서 순서 꼬임을 좀 방어하고자 했는데...

1로 설정하고 idempotence 옵션이 켜져있고 retry가 가능한 상태일때 
전송하다가 OutOfOrderSequence 예외가 발생한다면 클라이언트 영역에서 이 Sequence Number를 잘 처리해야한다. (재전송 하거나, 다음 idempotence 전송에서 써야한다)


이게 어렵다... 그래서 그냥 하드 코딩으로 각 Topic Partition의 5개의 메타데이터를 들고 있게한것이다. 

if (!inflightBatchesBySequence.containsKey(batch.topicPartition)) {
inflightBatchesBySequence.put(batch.topicPartition, new PriorityQueue<>(5, new Comparator<ProducerBatch>() {
@Override
public int compare(ProducerBatch o1, ProducerBatch o2) {
return o1.baseSequence() - o2.baseSequence();
}
}));

그래서 kafka 문서의 "enable.idempotence" 항목을 보면 Note that enabling idempotence requires max.in.flight.requests.per.connection to be less than or equal to 5 라고 나온다.


idempotence 켜져있고 내부의 매직 넘버인 5보다 크게 셋팅한다면 sequence를 보장하지 못할 수 있기 때문이다.

실제로도 

        if (idempotenceEnabled && 5 < config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION)) {

            throw new ConfigException("Must set " + ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION + " to at most 5" +

                    " to use the idempotent producer.");

        }

코드에 의해서 실행 자체를 막아놨다.



요약
1. 그냥 디폴트로 쓰자.
2. 프로듀서에서 동기식으로 사용한다면 (.get() 호출) 더더욱 상관하지 않아도 된다.


Posted by dev.bistro