기존에 feign을 resilience4j와 사용하면서 생긴 이슈

 

1. 클라이언트 인터페이스와 연결된 실제 서버에서 응답의 이슈가 있었음(DecodeException)

2. 해당 오류는 에러디코드에서 제대로 해석되지 않았기에,서킷에서 무시하지 않고 집계됨

3. 해당 영향으로 연결된 서비스가 정상적인 상태임에도 불구하고, 서킷이 오픈됨

4. 결과적으로 연결된 전체 서비스의 요청에 폴백으로 넘어감

@Bean
fun feignDecorator(
    circuitBreakerRegistry: CircuitBreakerRegistry,
): FeignDecorator {
    val circuitBreakerConfig = CircuitBreakerConfig.from(circuitBreakerRegistry.defaultConfig)
        .ignoreException { throwable ->
            if (throwable is CustomClientException) {
                throwable.code != -1
            } else {
                false
            }
        }
        .build()
    val circuitBreaker = circuitBreakerRegistry.circuitBreaker("clientCircuit", circuitBreakerConfig)

    return FeignDecorators.builder()
        .withCircuitBreaker(circuitBreaker)
        .withFallbackFactory {
            ClientFallbackFactory().create(it)
        }
        .build()
}

@Bean
fun clientBuilder(
    feignDecorator: FeignDecorator,
): Feign.Builder {
    return Feign.builder()
        .client(ApacheHttp5Client())
        .options(feignRequestOptions())
        .addCapability(Resilience4jFeign.capability(feignDecorator))
}

 

변경사항

1. 의도했든 안했든, 연결된 서비스의 응답이 이슈는 있으나, 이게 서킷에 영향을 줄만한 사항은 아님

2. 해당 오류 발생시 서킷에서 무시할 수 있도록 수정

@Bean
fun feignDecorator(
    circuitBreakerRegistry: CircuitBreakerRegistry,
): FeignDecorator {
    val circuitBreakerConfig = CircuitBreakerConfig.from(circuitBreakerRegistry.defaultConfig)
        .ignoreException { throwable ->
            when (throwable) {
                is CustomClientException -> {
                    throwable.code != -1
                }

                is DecodeException -> {
                    true
                }

                else -> false
            }
        }
        .build()
    val circuitBreaker = circuitBreakerRegistry.circuitBreaker("clientCircuit", circuitBreakerConfig)

    return FeignDecorators.builder()
        .withCircuitBreaker(circuitBreaker)
        .withFallbackFactory {
            ClientFallbackFactory().create(it)
        }
        .build()
}

@Bean
fun clientBuilder(
    feignDecorator: FeignDecorator,
): Feign.Builder {
    return Feign.builder()
        .client(ApacheHttp5Client())
        .options(feignRequestOptions())
        .addCapability(Resilience4jFeign.capability(feignDecorator))
}

 

이렇게 또 운영이슈를 하나 얻어가는구나

Call By Something

 

배경

  • 서비스에서 약 백오십만의 유저에게 알림을 전달하는 새로운 시스템에 대해 이야기를 하다, 어떤 방식으로 이걸 풀어야 하는지 생각하게 됨
  • API로 받아줄 경우 대상을 매번 추리는데 리소스가 크게 들거라 생각
  • 동시성 모델로 해당 대상 목록을 주기적으로 관리하며 알림을 전달하는건 어떨까 고민하게됨

 

잘못된 문제 인식 및 커뮤니케이션 오류

  • 동시성 모델로 선형적 자료형을 관리하고, 호출시마다 해당 자료형을 청크로 찍어내어 알림을 전달한다.
    • 예상된 문제는 값복사가 일어나서 메모리 사용량이 늘어날 거라 생각됨.
    • 대상을 추리기 위해서는 값복사 후 청크를 해야하나 vs 값복사 없이 청크를 말까
  • 안해도 된다고 해도 자바는 함수호출시 매개변수 전달로 인해 값복사가 일어나는데, 이건 문제가 아닐까?

 

체크

  • 오늘 생각하다보니 이게 잘못된거 같다는 생각이 들었고, 자바는 call by value인걸 인지는 하고 있지만, 그게 값복사가 항상 발생하는건 아닌듯 하여 확인하고자 함

 

과정

 

우선 아래 코드를 통해 150만개의 대상을 만들고 이걸 리스트에 저장함

val callBySomething = CallBySomething()
val list = callBySomething.list
println("hash=" + System.identityHashCode(list))
for(i in 1500000..3000000){
    list.add(BigInteger.valueOf(i.toLong()))
}

 

그리고 메모리 사용량을 체크

fun getMemoryUsage(): Long {
    val runtime = Runtime.getRuntime()
    return runtime.totalMemory() - runtime.freeMemory()
}
# 결과
hash=258952499
Memory used by myList: 120 MB

 

그리고 해당 객체를 매개변수로 사용하는 함수를 콜하여 해시값과 다시 한번 메모리 사용량을 체크

callBySomething(list)
val memoryAfter2 = getMemoryUsage()
println("Memory used by myList: ${(memoryAfter2 - memoryBefore)/1024/1024} MB")

fun callBySomething(list: MutableList<BigInteger>): MutableList<BigInteger> {
    println("hash=" + System.identityHashCode(list))
    return list
}
# 결과
hash=258952499
Memory used by myList: 120 MB
# 동일한 해시값과 변화없는 메모리 사용량

이번에는 깊은 복사를 해보자

val newList = copy(list)
val memoryAfter3 = getMemoryUsage()
println("Memory used by myList: ${(memoryAfter3 - memoryBefore)/1024/1024} MB")

fun copy(list: MutableList<BigInteger>): List<BigInteger> {
    return list.map { BigInteger(it.toByteArray())  }.also {
        println("hash=" + System.identityHashCode(it))
    }
}
# 결과
hash=1149319664
Memory used by myList: 236 MB
# 당연히 변경된 해시값과 그리고 증가한 메모리양

만약 copy의 코드가 아래와 같다면 list는 새로운 객체를 가리키겠지만, 그 안의 객체는 동일한 객체를 사용하여, 메모리 사용량에 차이가 거의 없음

// 왜냐하면 BigInteger도 객체이기 때문
// 새로운 list를 생성시 기존 BigInteger의 참조를 기반으로 생성
fun copy(list: MutableList<BigInteger>): List<BigInteger> {
    return list.map { it }.also {
        println("hash=" + System.identityHashCode(it))
    }
}

자, 그럼 이제 더 헷갈리는 건, 자바는 분명 call by value인데, 왜 마치 call by reference처럼 보이는걸까?

  • 분명한건 원시형 자료를 전달할때는 값복사를 한다는 점
  • 하지만 그게 아닐 경우에는 값의 참조를 복사하여 전달한다는 점
  • 하지만 그렇다고해서 포인터처럼 값의 참조 자체를 전달하는건 아님
  • 이게 어떤 차이가 있느냐, 아래에서 확인해보자
    fun referenceOfValue(list: MutableList<BigInteger>) {
        var list2 = list
        println("list2 hash=" + System.identityHashCode(list2))
        list2 = mutableListOf()
        println("list2 hash=" + System.identityHashCode(list2))
        println("list=${list.size}")
    }
    # 결과
    list2 hash=258952499
    list2 hash=2093631819
    list=1500001
  • 일단 코틀린은 매개변수가 불변이라 직접 수정은 못 하기에, 따로 변수에 할당하여 동일한 참조를 같는 변수를 만들어서 수정을 함
  • 만약 참조 자체를 전달하는거라면 해당 참조변수를 재선언 했을대, 참조주소 자체에 값을 할당을 하기에 변화가 일어난다
  • 하지만 자바는 재선언시 참조에 새로운 값을 할당하는게 아니라, 객체를 새로 생성하고 해당 객체의 참조를 재선언하는 필드에 할당하다보니 포인터를 사용하는것과는 차이가 있다
  • 이걸 한번 간단하게 포인터가 사용가능한 golang으로 확인해보자
func main() {
    str := "Hello, Go!"
    printString(&str)
    fmt.Println(str)
}

func printString(org *string) {
    fmt.Println(*org)
    cop := org
    fmt.Println(*cop)
    *cop = "Hello, Go2!"
    fmt.Println(*cop)
}
# 결과
Hello, Go!
Hello, Go!
Hello, Go2!
Hello, Go2!
  • 위에서 확인 할 수 있듯이, 원본을 값을 주소 연산자를 통해 받고 그걸 복사 후 포인터로 참조하여 값을 변경하면 원본도 변경된다

결론

  • 자바는 call by value이지만, 값의 참조를 복사하여 전달한다
  • 그렇기에 선언된 값을 매개변수로 사용한다고해도 값의 참조를 복사하여 전달하기에 메모리 사용량이 증가하지 않는다
  • 더불어 동시성모델로 관리하여 청크를 통해 실행한다면 BigInteger 또한 객체이기에 청크시 동일한 BigIneger의 참조값을 활용하기에 메모리 이슈도 없을거라 예상된다

“언제나 그렇듯이, 코드 정리에 대한 커밋과 동작 변경에 대한 커밋은 분리해야 합니다.“ …

'프로그래밍 > 기타' 카테고리의 다른 글

[Mac] sshkey 등록 확인 및 영구 등록  (0) 2021.12.23

+ Recent posts