이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글이며, Look-Aside, Write-Back 패턴 등 Cache Strategy에 대한 내용들은 “Redis : Cache Strategy Pattern” 에서 다루었기 때문에 제외하였습니다.

 

충분히 잘못된 내용이 있을 수 있습니다. 그러한 것들이 있다면 댓글과 채널톡을 통해 피드백 부탁드립니다.

 

 

 

Cache가 필요한 이유?

서비스에서 자주 조회되는 데이터에 대해서 매번 접근하고 조합한 뒤 모델을 만들어 응답하는 것은 생각보다 많은 리소스를 사용하게 합니다. (Disk I/O, Join, Calculate, Sort 등..) 데이터 정규화 수준이 높거나 볼륨이 크고 서비스가 구현된 방식이 동기식이라고 가정한다면, 1개의 Request 당 최소한 1개의 Thread를 사용하기 때문에 연산의 지연 시간에 따라 사용자의 요청 수를 처리량이 따라가지 못해 처리되지 못하는 요청들이 생길 수 있습니다.

SNS의 Timeline, Game Ranking List, Portal Site의 Main Page 등

 

이때 우리는 제공할 데이터 모델을 만들거나 접근한 다음 이 정보를 특정 시간 동안 보관하면서 바로 이용하거나 응답하도록 하고 서비스가 사용하는 리소스 (연산, I/O)를 최소화할 방법을 찾아야 하는데 이것이 바로 Cache입니다.

 

 

 

 

Cache?

Cache란 앞서 설명한 것처럼 반복되는 데이터에 대한 접근 및 연산 비용을 줄이기 위해 좀 더 빠르게 접근 가능한 영역에 완성된 데이터 모델을 보관하는 것이라고 볼 수 있습니다.

 

대표적으로 사용되는 Cache의 Use Case로는 OS Level의 3 Level Cache와 Web Cache가 있습니다.

 

3 Level Cache는 L1 ~ L3 Cache가 계층적인 구조로 배치되어 있는 형태를 말하며, JVM과 같은 VM의 Memory Model 설계에서 동작하는 Thread들이 Memory 영역에 있는 데이터를 Capturing 하여 구성되어 있는 3 Level Cache에 저장하고 데이터 전송으로 발생하는 CPU의 Idle을 최소화해 전체적인 연산 성능을 향상 시킵니다.

이것은 Thread 간의 Visibility 문제가 발생하는 근본 원인이기도 합니다.

 

Web Cache는 Reverse Proxy이나 CDN과 Browser의 Local Cache 등 다양한 요소를 통해 제공되는 Cache이며, HTML Document나 Static Resource (Image, Video 등) 제공 시 실제 서버와의 물리적인 거리로 인한 응답 지연 이슈, 물리 서버가 존재하는 또는 경로에 해당하는 네트워크의 대역폭에 의한 bottleneck, 갑작스럽게 발생하는 Flash Crowds를 대처하기 위해 사용되고 있습니다.

 

이 글에서는 Application Cache에 대한 개념을 정리합니다.

 

 

 

 

Cache design

Cache의 효용성을 뒷받침하는 원칙으로는 전체 결과의 80%가 원인의 20%에 의해 일어나는 현상을 가리키는 Pareto principle이 있습니다.

 

이 원칙을 기반으로 Cache에 저장할 데이터를 선택할 때 모든 데이터를 보관하는 것보단 20%에 해당하는 데이터 모델을 판단하는 것을 기본으로 합니다.

Cache가 효율적으로 동작하려면 Hit rate를 극대화시켜야 하는데 만약 모든 데이터가 골고루 사용되는 서비스에 억지로 작은 Size의 Cache를 도입하려고 한다면, Cache를 사용하지 못하고 실제 데이터를 가진 곳을 접근하고 Cache를 추가하는 Cache Miss가 빈번하게 일어날 수 있습니다.

 

 

Cache의 대상이 되는 데이터는 Request Pattern이 Read에 집중되고 Write가 적거나 없는 또는 결정적인 특성을 가지는 데이터여야 합니다. 만약 Write가 자주 발생하는 데이터를 Caching 한다면, 최신 데이터 보장을 위해 write가 있을 때마다 Cache를 Evition 하고 조회 시 다시 Cache를 등록하는 Overhead가 지속적으로 발생하게 됩니다.

이러한 경우에는 Cache를 활용하지 않거나 실제 응답 결과가 아닌 사용되는 데이터를 Caching 하여야 합니다. ex) id

 

 

Cache에 보관할 데이터 모델을 판단하는 기준으로는 Data Locality principle을 따르는데, 이는 크게 Temporal locality와 Spatial locality로 구분할 수 있습니다.

 

Temporal locality란 요청이 접근한 특정 데이터는 가까운 시일 내에 또 접근할 가능이 높다는 원칙입니다. 이때 데이터는 Loop 등에서 활용되는 Condition 값이나 다른 데이터가 참조하는 Reference id 일 가능성이 높습니다.

loop count, branch condition, fk…

 

Spatial locality란 요청이 접근한 특정 데이터의 주변은 같이 접근할 가능성이 높다는 원칙입니다. 이때 데이터는 메모리 공간을 연속적으로 사용하는 Array 형태일 가능성이 높습니다.

iterate over array or array list (Dynamic array)…

 

 

Cache를 eviction 하는 방식으로는 대표적으로 Expire, LRU, LFU, FIFO 등이 있습니다.

 

Expire 방식은 단순하게 Cache를 생성하였을 때 Memory에 유지될 시간을 지정하는 방식입니다.

 

LRU 방식은 최근에 사용되지 않은지 오래된 Cache를 Timestamp 등을 통해 판단하여 우선적으로 제거하는 방식입니다.

 

LFU 방식은 가장 적게 사용된 Cache를 Cache Hit count 등을 통해 판단하여 우선적으로 제거하는 방식입니다. 이 방식은 앞서 언급한 Pareto principle 분포를 따르는 경우 더욱 효과적입니다.

 

FIFO 방식은 Cache가 저장된 순서대로 제거하는 방식입니다. 이 방식은 일정 시간이 지나면 접근하지 않을 데이터(ex Access Token과 같은 인증 정보) 등을 관리하는데 효과적인 수단이 될 수 있습니다.

 

 

 

 

Cache 종류?

Application에서 Cache를 적용하는 범위는 크게 Local, Global, Distributed Cache 등이 있습니다.

 

Local Cache란 Application마다 가지고 있는 In-Memory, Disk에 구성되어있는 방식을 의미합니다. Cache Abstraction, Caffeine, EhCache…

 

Application에서 바로 접근 가능하기 때문에 성능이 뛰어나지만 동일 Application이 Scale-out 된 경우에는 각 Local Cache가 동기화되지 않기 때문에 데이터의 정합성 문제가 발생하게 됩니다.

 

일반적으로 Local Cache 간의 데이터를 동기화하기 위해 Event를 발행하여 다른 Application에 변경된 사항을 전달하는 방식을 많이 사용하나 동기화 로직이 p2p 형태로 구현되어 있다면 전체 서비스 확장성에 영향을 줄 수 있습니다.

이는 데이터의 특징에 따라 expire를 짧게 두는 것으로 최소화할 수 있고 Message Queue나 Redis를 이용한 Pub/Sub Pattern을 사용할 수도 있지만, 결국 Eveuntual Consistency 문제가 발생하게 됩니다.

 

 

 

Global Cache란 네트워크로 접근 가능한 Cache 서버를 구성하고 Application들이 이를 접근하도록 하는 방식을 의미합니다. Redis Server, MemCached…

 

Local Cache에서 발생하는 데이터 정합성 문제를 해결하는 방법이지만 네트워크를 경유하여 데이터에 접근하기 때문에 Local Cache보다 성능이 낮으며 Application 단위로만 장애가 한정되는 Local Cache에 비해서 Global Cache는 SPoF가 되기 때문에 장애 발생 시 모든 Application에 장애가 전파될 수 있습니다.

이를 방지하기 위해 기본적으로 이중화 구성이 필요하며 약간의 Infra 구성 및 운영 비용이 발생하게 됩니다.

 

 

Distributed Cache란 여러 서버를 Cluster로 묶어 Memory, Disk에 데이터를 분산 저장시키고 서버들이 Hash와 같은 특정 Key를 통해 접근할 서버를 지정하는 방식을 의미합니다. Redis Cluster, Acrus, Hazelcast, Infinispan...

 

새로운 Cache 서버를 쉽게 추가할 수 있어 확장성이 매우 높고 Scale out의 이점을 통해 높은 처리량을 가질 수 있지만 상대적으로 높은 infra 구성 및 운영 비용이 발생하며, 특정 서버의 장애로 인한 일부 데이터 유실이 발생할 수 있고 Consistency Hashing을 사용하는 경우 서버의 요청 부하가 다음 서버로 전달되어 연쇄적인 장애가 발생할 수 있습니다.

 

 

 

 

Cache 종류?

 

 

 

회사에서 개발하는 레거시 프로젝트의 Version Migration 작업을 준비하기 위해 Spring (boot) Version별 변경 내역을 조사하고 있는 내용입니다. 누락되거나 잘못 기술된 내용이 있을 수 있는 점 양해 부탁드립니다.

 

 

Spring Boot 2.1.x

  • Java 11 지원 (Spring Framework 5.1)
  • Spring Boot 2.1 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등 (Junit 4.12 → Junit 5.2, Tomcat 8.5.39 → 9.x, Hibernate 5.2 → 5.3)
  • Spring Framework 5.1
    • Java 8 ~ 11
    • CGLIB 3.2 fork(Java 9+ API로 위임)
    • @Profile에 and, or expression 지원
    • Hibername와 read-only transaction 사용 시 Memory에 Entity Snapshot이 남지 않습니다.
    • 로거 설정 개선, 컴포넌트 스캐닝 성능 개선 용도의 컴포넌트 인덱스 기능 제공, 함수형 프로그래밍 스타일 지원, 코틀린 지원, 리액티브 프로그래밍 모델 지원, DataSize Type 등
  • 포함 프로퍼티 변경 (servlet path, HibernateProperties 별도 분리)
  • Spring JCL - JCL Interface가 잘 사용되지 않음으로써 (Runtime Logger Lookup이 복잡도 등의 문제) JCL을 사용하던 기존 코드를 쉽게 SLF4J로 전환하기 위해 추가한 JCL 용 브리지 모듈이다. (JCL-over-SLF4J 대체)
  • Auto-Configuration Exclusion @EnableAutoConfiguration, @SpringBootApplication, @ImportAutoConfiguration spring.autoconfigure.exclude에서 적용된 제외 설정이 이제 Local 환경에서만 적용되는 것이 아니라 모든 환경에서 일관되게 적용됩니다.
  • Log Group application.property, yml에서 logging.group.{group-name}={Loggers...} 동일한 Logging 구성 정보를 적용할 Logger 들의 그룹을 정의하는 속성이 지원됩니다. loging.level.{group-name}=DEBUG
    • 기본 제공 그룹으로는 org.springframework.web, org.jooq.tools.LoggerListener 등이 있습니다.
  • @EnableAsync, @EnableScheduling, spring-boot-starter-data-jdbc 등 추가 지원
  • Bean Override 비 허용 - 동일한 이름의 Bean을 중복으로 정의하고 ApplicationContext에 등록할 때 기존에 등록된 Bean을 덮어 쓸지 말지 결정하는 옵션입니다. spring.main.allow-bean-definition-overriding = false (default, BeanDefinitionOverrideException 발생)
    • Spring Boot의 Auto Configuration과 사용자의 Configuration, Component 등이 중복됨으로써 발생하는 상황으로 @ConditionalOnMissingBean와 같은 필터링 용 제약을 거는 것을 권장합니다.
  • Spring Data JPA 저장소 초기화에 대해서 Lazy mode 지원 (bootstrap mode)
  • Spring Actuator /info, /health end-point 사용자의 Spring Security 설정으로 인가 처리가 적용되지 않는 한 /info, /health end-point는 인증, 인가 없이 접근 가능한 공개 API가 됩니다.
  • @WebMvcTest and @WebFluxTest Security Configuration @WebMvcTest, @WebFluxTest Slice Test에서 사용자가 정의한 보안 설정인 WebSecurityConfigurer, ServerHttpSecurity bean을 자동으로 구성 정보에 포함합니다.
  • Context ApplicationConversionService Support ApplicationConversionService가 기본적으로 등록되며, @Value Annotation에 사용할 수 있습니다.
  • h2 Compatibility mode 사용 방법 변경 - 이전 버전까지는 spring.jpa.properties.hibernate.dialect 설정에 따라 Compatibility mode가 설정되었으나 현재는 spring.datasource.url=jdbc:h2...;MODE=MYSQL 직접 설정해주어야 mode type이 적용되도록 변경되었습니다.
  • Property Migrator 달라진 property 정보를 반영하지 않아도 정상적으로 Application을 수행할 수 있도록 도와주는 Module입니다. (Runtime에서 동작)

 

 

 

Spring Boot 2.2.x

  • Java 13 지원 (Spring Framework 5.2)
  • Spring Boot 2.2 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등
    • Jakarta EE dependencies 추가 및 기존 JavaEE dependencies 이전
    • ReactiveWebServerApplicationContext 사용 중단
  • Spring Framework 5.2
    • Spring WebFlux Support for Kotlin Coroutines - Spring WebFlux에서 이제 Kotlin의 Coroutines을 지원합니다.
    • RSocket, R2DBC 지원
    • Spring MVC Router Support for Kotlin DSL Web Flux에서의 DSL을 통한 Router 등록 방식과 유사한 모델을 MVC에서도 지원합니다.
    • @Configuration에 대한 Bean Lite Mode 지원 (proxyBeanMethods = false) - Configuration 생성 시 Proxy를 통해 Configuration Instance를 Wrapping 하는 작업을 수행하지 않도록 설정하는 속성으로 기본 제공되는 AutoConfiguration에 적용하기 위해 추가되었습니다.
      • 성능 향상 (구동 시간 감소)가 있으나, Configuration Method를 여러 번 호출할 경우 매번 Instance를 생성해 반환하기 때문에 조심하여야 합니다.
  • Spring Security 5.2
  • Spring Data Moore
    • 선언적 리액티브 트랜잭션 지원
      • PlatformTransactionManager에서 ReactiveTransactionManager로 전환되었습니다.
      • TransactionalOperator@Transactional 구성이 통합되었습니다.
    • 리액티브 QueryDSL 지원
    • 기타 성능 향상
    • What's new in Spring Data Moore?
  • 프로퍼티 변경
  • Spring Actuator HTTP Trace disable by default - 많은 리소스를 소모하고, in-memory에 저장하는(기본 구현) 방식이 Clustering 구조에 맞지 않아 기본적으로 비활성화되었습니다.
  • Prometheus Push Gateway를 위한 https end-point 추가
  • Lazy initialization by Global spring.main.lazy-initialization property를 통해 Application 전역에 Lazy initialization를 지원할 수 있으며, @Lazy(false)LazyInitializationExcludeFilter 를 통해 지연 초기화 대상에서 제외할 수 있습니다.
  • JMX disabled by default spring.jmx.enabled=false
  • spring-boot-starter-test default by JUnit 5 기본 사용하는 Junit Version이 4에서 5로 변경되었습니다.
  • Hibernate Dialect - Hibernate가 감지한 Database dialect를 사용하던 설정에서 사용자 정의를 통해 선택 가능하도록 변경되었습니다.
  • jOOQ RecordListenerProvider 다수의 RecordListenerProvider Bean을 등록하던 구조에서 RecordListenerProvider[] 방식으로 반환하는 Bean을 정의하는 방식으로 변경되었습니다.
  • Shutdown configuration of task execution and scheduling 자동 구성된 TaskExecutor, TaskScheduler의 종료 시 필요한 설정을 적용할 수 있도록 spring.task.execution.shutdown and spring.task.scheduling.shutdown이 추가되었습니다. (gracefully-shotdown)
  • Kubernetes detection @ConditionalOnCloudPlatform을 통해 현재 Application이 Kubernetes 위에서 동작하고 있는 상태인지 감지할 수 있습니다.
  • Test Application Arguments in integration tests 통합 테스트에서 사용할 Application Argument를 정의할 수 있는 ApplicationArguments Bean이 추가되었습니다.
  • @ConfigurationProperties scanning @ConfigurationProperties이 적용된 Class를 Scan 하여 Bean으로 등록할 수 있는 @ConfigurationPropertiesScan, @EnableConfigurationProperties이 추가되었습니다.
  • Immutable @ConfigurationProperties binding @ConfigurationProperties이 붙은 Class를 불변 객체로 선언할 수 있는 @ConstructorBinding을 지원합니다. (이는 Records에도 적용됩니다.)
    • 생성자를 통한 객체 생성을 지원하며 여러 생성자가 존재할 경우 특정 생성자 위에 정의하면 됩니다.
  • Callback for Redis cache configuration ****RedisCacheManager의 불변 설정을 위한 RedisCacheManagerBuilderCustomizer가 추가되었습니다.
  • Qualifier for Spring Batch datasource Spring Batch에서 사용될 DataSource Bean을 설정하기 위한 @BatchDataSource 가 추가되었습니다. ****
  • Idle JDBC connections metrics 현재 대기 상태에 있는 DataSource Connection Pool의 크기를 확인할 수 있는 Metric 정보가 추가되었습니다.

 

 

 

Spring Boot 2.3.x

  • 자바 14 지원
  • Gradle 5.6.x (Legacy), 6.3+
  • Spring Boot 2.3 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등
    • Spring Cloud Connectors starter has been removed
    • Junit Jupiter 5.6
    • Mockito 3.3
    • Jackson 2.11
      • Date, Calendar의 Timezone 표현 방식이 표준에 맞게 변경되었습니다. +00:00
      • 필드 명 없이 JSON Array를 만들 수 없도록 변경되었습니다.
      • JSON 생성 시 Serialization 순서를 결정할 수 있습니다. @JsonProperty(index = )
      • 표준 통화에 대한 객체를 제공하는 Joda Money를 지원하기 위해 새로운 모듈이 추가되었습니다. jackson-datatype-joda-money
  • Spring Integration 5.3
    • RSocket support for Spring Integration spring-integration-rsocket 의존성 추가 및 해당 의존성 사용 시 IntegrationRSocketEndpoint, RSocketOutboundGateway을 통한 end-point 설정이 가능합니다.
  • Spring Security 5.3
    • Oauth 2.0 Client, Resource Server 개선
  • Spring Kafka 2.5, Spring HATEOAS 1.1
  • Spring Data Neumann
    • Cassandra v4 이전 버전과 호환되지 않는 요소들이 다수 추가되었습니다.
    • Couchbase SDK v3 이전 버전과 호환되지 않는 요소들이 다수 추가되었습니다.
    • Elasticsearch 7.5
    • MongoDB 4 - MongoClientSettingsBuilderCustomizer bean이 필수 드라이버(Mongo Operations) 설정에 적용됩니다. spring-boot-starter-data-mongodb-reactive 의존성의 경우 Mongo Operations이 구성되지 않기 때문에 spring-boot-starter-data-mongodb 의존성을 추가하는 것을 권장합니다.
    • Neo4j Neo4j의 Open Session이 기본적으로 Default 됩니다. spring.data.neo4j.open-in-view property를 통해 설정할 수 있습니다.
    • Data JDBC 2.0
      • 낙관적 락 지원, PagingAndSortingRepository 지원, H2 지원 (모든 기능), 식별자 따옴표, Query Derivation 지원 (query method)
    • R2DBC support
      • auto-configuration, health indicator for Connection Factory, @DataR2dbcTest
  • Validation Starter no longer included in web starters - validation 관련 의존성(javax.validation.*)은 spring-boot-strater-web에서 spring-boot-starter-validation라는 별도의 의존성으로 분리되었습니다.
  • Build OCI images with Cloud Native Buildpacks Cloud Native Buildpack을 사용한 Docker Image 구축 방식 지원 및 spring-boot:build-image, bootBuildImage 가 추가되었습니다. (Maven, Gradle)
  • Build layered jars for inclusion in a Docker image Jar에서 자주 변경되는 부분을 식별하여 Layer를 분할해 재 사용할 수 있도록 개선되었습니다. (효율적인 Docker Image build를 위함)
    • JDK → Library → Application Code 순으로 Layer를 생성합니다.
  • Support of wildcard locations for configuration files Configuration File load시 *를 지원합니다.
  • Graceful shutdown embedded Jetty, Reactor Netty, Tomcat, Undertow 및 Servlet, Reactive 기반 Application에서 Gracefully shutdown이 지원됩니다. server.shutdown=graceful, spring.lifecycle.timeout-per-shutdown-phase=10s
  • WebServerInitializedEvent and ContextRefreshedEvent - Graceful shutdown 기능 지원의 일환으로 Web Server의 초기화 시점이 변경되었습니다. (새로 고침 이후 → Application Context 새로 고침 이후)
  • Spring Actuator Liveness and Readiness probes Application의 활성, 처리 가능 상태를 추적할 수 있는 end-point가 추가되었습니다. (kubernetes 위에서 실행될 경우 자동 설정) /actuator/health/readiness, management.health.probes.enabled=true
  • Date-Time conversion in web applications Application.properties, yml을 통해 시간 및 날짜 변환 시 형식을 지정할 수 있습니다. ex) spring.mvc.format.date=iso , spring.mvc.format.date-time, spring.mvc.format.time, spring.webflux.format.date, spring.webflux.format.date-time, spring.webflux.format.time
  • Unique DataSource Name By Default 자동 구성된 DataSource가 고유한 이름으로 생성되며, H2 DB 사용 시 Database URL이 testdb를 참조하지 않아 기존(H2를 자동 구성에 의존하여 사용 중이던) Project에서 해당 Version으로 Migration 수행 시 영향을 받습니다.
  • Embedded Servlet web server threading configuration Embedded Web Server의 Thread를 구성하는 속성이 server.{tomcat, jetty...}.threads 로 이동되었습니다.
  • Changes to the Default Error Page’s Content Error Message 및 Binding Error는 기본 Error Page에 포함되지 않습니다. server.error.include-message, server.error.include-binding-errors 설정을 통해 포함하거나 상황에 따라 제어할 수 있습니다. (always, on-param, never)
  • ApplicationContextRunner disables bean overriding by default SpringApplication을 통한 실행 방식과 일관성을 제공하기 위해 ApplicationContextRunner에서도 Bean Override를 비 허용합니다. withAllowBeanDefinitionOverriding을 통해 허용할 수 있습니다.
  • Activating multiple profiles with @ActiveProfiles @ActiveProfiles에서 쉼표를 통한 다중 Profile을 지원합니다. @ActiveProfiles({"p1","p2"})

 

 

 

Spring Boot 2.4.x

  • 자바 15 지원
  • Spring Boot 2.4 Release Notes · spring-projects/spring-boot Wiki
  • 포함 의존성 변경, 삭제 등
    • Flyway 7 Overall ordering of Java and SQL Callbacks · Issue #2785 · flyway/flyway
    • Logback Configuration Properties Logback properties들이 logging.pattern.rolling-file-namelogging.logback.rollingpolicy.file-name-pattern과 같은 형대로 변경되었습니다.
      • logging.logback.{기존 속성 path}
    • Hazelcast 4 내부 의존성이 Hazelcast 3.2.x → 4.x로 변경되었습니다. 하위 버전이 필요한 경우 hazelcast.version property를 통해 사용할 버전을 설정할 수 있습니다.
    • Neo4 j Spring Data Neo4j
  • 버전 관리 체계 변경 Updates to Spring Versions
  • Spring Framework 5.3
    • Spring Framework 5.3.x가 LTS로 계속 유지되며, 5.4.x를 만들지 않습니다.
    • GraalVM 관련 개선
    • Spring-R2DBC Module 제공
  • Spring Data 2020.0 버전 명명을 Calander 기반으로 변경하였으며, 코드명은 수학자의 이름을 사용하는 것을 유지합니다.
    • Neo4j
    • R2DBC
    • Supported RxJava 3
  • Spring Batch 4.3
  • Spring Security 5.4
  • Spring Integration 5.4
  • Spring Kafka 2.6, Spring HATEOAS 1.2, Spring Session 2020.0, Spring Security 5.4…
  • Config File Processing (application properties and YAML files) application.properties 및 application.yml 파일의 처리 방법이 변경되었습니다. (properties 파일 안에서 여러 profile 정의 가능) 또한 spring.profiles 대신 spring.config.activate.on-profile을 사용하도록 변경되었습니다.
    • 옛날 파일 처리 방식을 사용하려면 spring.config.use-legacy-processing=true를 사용합니다.
  • Supported Configuration Tree spring.config.location, spring.config.import 속성 설정 시 configtree, optional 등의 prefix를 지원합니다.
  • Custom property name support configtree, optional 이외에도 Custem prefix를 정의하여 기능을 확장할 수 있는 방법을 제공합니다. ConfigDataLoader, ConfigDataLocationResolver
  • Layered jar enabled by default Spring Boot 2.3.x 에서 추가되었던 Build layered jars 기능이 기본적으로 활성화되었습니다.
  • Importing Additional Application Config application.properties 또는. yml 파일에서 spring.config.import속성을 통해 Spring 환경을 구성하는 상황에서 같이 참조할 하나 이상의 구성 파일을 지정할 수 있습니다. (spring.config.use-legacy-processing=true를 사용하지 않는 경우)
  • Volume Mounted Config Directory Trees spring.config.import속성을 통해 ConfigMaps와 같은 Key:Value 형식의 Directory Tree를 가져올 수 있습니다.
  • Importing Config Files That Have no File Extension 파일 확장자를 작성할 수 없는 Cloud Platform에서 [.extension] 형식의 Hint를 지정하여 파일의 확장자를 특정할 수 있습니다.
  • Origin Chains
  • Startup Endpoint Actuator에서 Application의 시작 정보를 제공하는 startup end-point를 지원합니다. 제공되는 timeline object의 events array를 통해 생성되는 Bean의 이름이나 시작 및 초기화 지연 시간 등을 식별할 수 있습니다.
  • Docker/Buildpack Support Maven의 spring-boot:build-image, gradle의 bootBuildImage task를 통해 생성된 Docker Image를 Docker Registry에 등록하는 기능이 추가되었습니다.
  • Maven Buildpack Support Maven에서 spring-boot:build-image을 통해 Docker Image를 생성할 때 모든 Project Module Dependency를 Application Layer에 포함합니다. (여러 Project Module이 존재하는 경우 하나의 Layer에 집약됩니다.)
  • Gradle Buildpack Support Gradle에서 bootBuildImage task를 통해 Docker Image를 생성할 때 모든 Project Module Dependency를 Application Layer에 포함합니다. (여러 Project Module이 존재하는 경우 하나의 Layer에 집약됩니다.)
  • Redis Cache Metric Redis Cache를 사용하는 경우 Micrometer가 응답하는 Metric 정보에서 Cache hit, miss, Cache put, Cache evict 등의 모든 통계 정보를 확인할 수 있습니다. spring.cache.redis.enable-statistics=true
  • Web Configuration Properties Spring MVC, Spring WebFlux에서 지정하는 locale 및 Resource path 등의 Properties 속성들이 해당 Module 여부와 상관없이 일관적으로 구성될 수 있도록 변경되었습니다. spring.mvc.locale, spring.mvc.locale-resolver, spring.resources.*spring.web.locale, spring.web.locale-resolver, spring.web.resources.*
  • Register @WebListeners in a way that allows them to register servlets and filters Servlet Container가 자체적으로 @WebListener 이 적용된 클래스를 인스턴스화 합니다. 이를 통해 @WebListener가 붙은 객체에게 제공하던 Spring Container를 통한 의존성 주입이 지원되지 않습니다.
  • Slice Test for Cassandra @DataCassandraTest를 통한 Cassandra Slice Test를 사용할 수 있습니다.
  • Configuration property for H2 Console’s web admin password H2 Console Admin의 Password를 지정 가능한 spring.h2.console.settings.web-admin-password 속성이 추가되었습니다.
  • CqlSession-Based Health Indicators for Apache Cassandra Spring Data Cassandra에서 제공하던 Health Indicator가 지원되지 않으며, CqlSession 기반의 CassandraDriverHealthIndicator, CassandraDriverReactiveHealthIndicator가 추가되었습니다.
  • Filtered Scraping with Prometheus /actuator/prometheus end-point에서 응답하는 Sample을 Filter 하는 용도로 새로운 query parameter가 지원됩니다.? includedNames=jvm_memory_used_bytes
  • Spring Security SAML Configuration Properties SAML2 관련 acs와 decryption 속성이 추가되었습니다.
  • Failure Analyzers 이제 FailureAnalizers가 ApplicationContext 등이 구성되지 않는 경우도 포함합니다. BeanFactoryAware 또는 EnvironmentAware를 구현하여 추가하는 분석기는 ApplicationContext가 만들어지지 않는 경우 사용되지 않습니다.
  • Jar Optimizations Spring Boot jar를 생성할 때 모든 비어있는 (마킹 용도의) starter dependencies들이 제거됩니다. spring-boot-autoconfigure-processor 또한 Build 시에만 유효하며 이후 제거됩니다.
  • Spring Boot Gradle Plugin Gradle bootJar task의 DSL이 설정 속성이 변경되었습니다. mainClassName → mainClass
  • JUnit 5’s Vintage Engine Removed from spring-boot-starter-test Junit4 vintage engine이 해당 의존성에서 제거되었습니다. (org.junit.Test 등 사용 시 Compile Error 발생) 해당 버전에서 Junit4를 사용하려면 별도의 vintage 의존성을 추가하여야 합니다.
  • Support K8s ConfigMap @ConfigurationProperties나 environment 객체를 통해 K8s ConfigMap에 저장된 설정에 접근할 수 있습니다.
  • Embedded database detection in-memory로 띄워진 Database가 아닌 경우 Database 초기화 및 사용자 이름 설정 (default = sa)가 사용되지 않습니다. (Server, Hybrid H2, File based Derby, HSQL 등)
    • spring.datasource.initialization-mode를 통해 데이터베이스 초기화를 하도록 구성할 수 있습니다.
  • User-defined MongoClientSettings no longer customized MongoClientSettings를 Bean으로 정의하여 사용하는 경우 Auto-configuration을 통한 properties가 적용되지 않습니다. 그렇기에 MongoPropertiesClientSettingsBuilderCustomizer를 활용해 properties, environment를 인자로 넘겨 설정한 다음 반환하여야 합니다.
  • Metrics export in integration tests
  • Elasticsearch RestClient - RestClient bean이 더 이상 Auto-configuration 되지 않습니다. RestHighLevelClient bean은 계속 지원됩니다.
  • Default Servlet Registration 해당 버전부터 Servlet Container에서 제공하는 DefaultServlet을 등록하지 않습니다.
    • 사용을 위해서는 server.servlet.register-default-servlet=true property를 정의하여야 합니다.
  • HTTP traces no longer include cookie headers by default
  • Supported Profile Group 특정 Profile에 여러 Profile들을 묶어 Grouping 하여 같이 사용할 수 있습니다. spring.profiles.group.{profile}={sub profiles...}

 

 

 

Spring Boot 2.5.x (WIP)

 

 

 

 

 

참고 자료


이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.



Sharding이 필요한 이유?

서비스의 특성이나 운영되어온 기간에 따라 사용자의 Data, State는 지속적으로 축적되게 됩니다. 이러한 것들이 몇 천만에서 수십 억 또는 그 이상으로 늘어난다면, Index와 같은 요소를 잘 구성되어 있더라도 결국 조회 성능의 하락이나 Data, State를 관리하는 Node에게 큰 부하를 주게 됩니다.

이는 결국 서비스의 지연 발생 빈도 수나 기간이 늘어나는 주요한 원인 중 하나가 됩니다.



이를 대비하기 위해 우리는 Performance와 Scalability 속성을 만족하는 서비스 설계를 진행하여야만 합니다. 그리고 이 속성을 지원하는 설계 방법 중 하나로는 Sharding 구조가 있습니다.




Sharding?

Sharding이란 기본적으로 특정 Attribute(Shard key)를 기준으로 하나의 DataSet을 여러 Shard Node에 분산하여 작은 단위의 DataSet으로 나누고 관리하는 것을 의미합니다.

Sharding은 일반적으로 Horizontal Partitioning과 혼용되는 용어입니다. 명확하게 따지자면, horizontal partitioning은 하나의 Node 안에서 DataSet을 여러 DataSet으로 나누는 것이고, Sharding은 DataSet을 여러 Node에 나누는 것입니다.

저는 이 글에서 Sharding, Horizontal Partitioning을 모두 “Sharding”이라고 서술하겠습니다.



이를 통해 우리는 서비스가 관리하는 Data, State에 접근하는 트래픽을 분산하여 관리하고 있는 Shard Node들의 처리 성능과 서비스의 동시 처리량을 향상시킬 수 있습니다. 또한 각 Shard Node의 장애는 격리되어 다른 Shard Node에 전파되지 않기 때문에 서비스의 High Availability를 향상시킵니다.


이러한 특징으로 인해서 Sharding은 “High Availability : 기본적인 복제 개념과 구현 및 동기화 방식” 에서 언급한 Query Off Loading 구조의 한계를 보완할 수 있습니다.

Replication을 활용한 Query Off Loading 구조를 사용하는 경우 Origin의 Read Query 부하는 분산 가능하나, Write Query에 대한 부하는 분산할 수 없습니다. 즉 Write Query 부하가 가중될수록 Replica Node의 수와 상관없이 Read, Write Query 처리 성능이 하락한다는 것입니다. - Write Heavy Situation

이러한 문제가 발생하는 이유는 Replica Node들 또한 Origin Node가 처리한 Write Query의 Log를 별도의 스레드로 처리하여 Node가 관리하는 상태에 반영하는 구조이기 때문입니다.

이때 Sharding을 도입한다면 하나의 Origin Node가 받던 요청을 여러 Origin Node로 분산시키기 때문에 해당 구조의 한계를 보완할 수 있습니다.



하지만 Sharding이 장점만 가지고 있는 전략은 아닙니다.

  • 시스템 구조의 복잡성이 늘어나는 점 (특히나 직접 구현해야 하는 경우)
  • Sharding 되고 있는 Shard Node 들에 대한 불균형 문제를 고려해야 하는 점
  • Shard Node 추가 및 Rebalancing 작업 등 운영 관리 비용의 증가
  • 다른 Node에 분산된 DataSet 간의 Join, Compose가 어려운 점

이러한 문제점도 많기 때문에 사용할 Sharding 전략과 운영 전략을 잘 세우고 구현해야 합니다.


Sharding은 Redis, MongoDB Cluster, MySQL NDB Cluster, ElasticsearchSolr와 같은 여러 Solution 및 Opensource에서 활용되고 있으며 많은 서비스에서 상황에 따라 Code Level의 Sharding 기법을 구현하고 있습니다.



Sharding 구현 방식?

Sharding 구현 방식으로는 크게 Range, Moduler, Directory, Index Sharding 등이 있습니다.

Range Sharding은 Shard Key로 선정된 Attribute의 일정 범위를 관리할 DataSet을 결정하는 것으로 Rebalancing 없이도 쉽게 새로운 Shard Node를 추가할 수 있습니다. 하지만 Shard Node 간에 DataSet Size가 균등하지 않는 문제가 발생할 수 있습니다. - Hotspot problem

결국 Hotspot problem 문제로 인해 기존 Shard Node를 분할하여 Rebalancing 하는 작업이 필요하고, 반대로 들어오는 요청이 적거나 DataSet이 작은 Shard Node는 Integration 또는 Migration 작업을 통해 운영 비용을 최적화해야 하는 어려움이 존재합니다.

  • 이를 보완하는 방법으로는 MongoDB에서 사용하는 Chunk 단위 분할 방식이 있으며, 해당 방식은 Shard key에 대해 특정 범위를 결정하고 그 범위를 넘어설 때마다 새로운 Chunk를 생성하여 Data를 Migration 하는 방식입니다.

 

Moduler Sharding(Hash Sharding)은 Key로 선정된 값을 현재 존재하는 DB Node 개수로 나누어서 결과 값을 기준으로 저장될 Node를 결정하는 방식입니다. 이 방식은 Range Sharding에 비해서 DataSet을 균등하게 분배할 수 있으나 다른 Shard Node를 추가하였을 때 기존 DataSet를 Rebalancing 하는 것이 어렵습니다.

  • User ID, Client IP, Post number 같은 값을 Hash Function이나 Moduler 연산을 통해 Shard Key로 변환하여 Shard Node를 결정합니다.

Data를 일관적으로 분산하기 위해서는 당연히 Hash Function에 넘겨지는 값을 동일한 Attribute열에서 제공해야 하며, 이 값은 변경되지 않는 정적 값이어야 합니다.

해당 방식의 Rebalancing 비용을 줄이기 위해서 Scale-out 시 2개의 배수로 Shard Node를 추가하는 전략(이를 통해 데이터가 전달될 영역을 특정할 수 있습니다.)과 최대로 늘어날 수 있는 한계 값을 미리 정해 놓고 해당 값을 초과할 시점부터는 Scale-up을 진행하는 전략을 고려하기도 합니다.

 

Consistent HashingModuler(Hash) Sharding와 같이 Hash Function을 사용하지만 상대적으로 Rebalancing 시에 다른 Shard Node로 분산되는 Data를 최소화할 수 있는 방식입니다.

 

기본적으로 환형 큐와 같은 자료 구조(논리적인 구조)에 배포되어 있는 Shard Node의 IP를 Hash하여 자료구조의 Index로 변환하고 Query 요청을 하는 사용자 또는 Client의 IP를 Hash 하여 나온 값과 제일 인접한 Shard Node에 저장하는 식으로 Sharding 규칙을 지정하여 Data를 분산시킬 수 있습니다.

 

새로운 노드가 추가되었을 때에는 앞선 상황과 마찬가지로 해당 Shard Node의 IP를 Hash하여 Index를 지정하고, 나온 Index 값보다 낮은 값을 가진 인접 Shard Node와 자신 사이에 속하는 Data만 Rebalancing을 진행합니다.

  • 해당 사진에서는 761 ~ 1332 안에 속하는 Shard Key를 가진 Node 4의 Data가 새로 추가된 Shard Node5로 Migration 됩니다.

 

하지만 해당 방식을 사용하면 Hash 된 값에 따라 특정 Shard Node(Node 2)에게 넓은 범위가 할당되어 많은 양의 Data가 전달되는 불균등 문제가 발생할 수 있으며, 인접한 Shard Node가 장애 발생이나 부하에 의한 처리 지연으로 정상적인 상태가 아닌 것으로 판단되었을 때 인접한 다음 Shard Node에게 부하를 가중시킬 수 있습니다. 

  • 해당 사진에서는 Shard Node 4의 장애 발생으로 1333 ~ 6661 안에 속하는 모든 Data가 Shard Node 2로 분산됩니다.

 

해당 문제를 방지하기 위해서, (그리고 Rebalancing 단위를 좀 더 최소화 하기 위해서) 기존 Shard Node의 ip에 특정 값을 추가하여 Hash 한 후 이를 Virtual Shard Node로 추가하여 Shard Node마다 관리하는 Shard Key, Data의 수를 조절할 수도 있습니다.

  • 해당 방식을 정리하자면 Consistant Sharding 방식은 Hash Function 을 사용하면서도 Rebalancing 작업 시 이동해야 할 Data를 최소화할 수 있으며, 성능 및 상황에 따라 Shard Node의 부하를 조절할 수 있습니다.
  • 하지만 Shard Node 간의 Data 분산이 균등하지 않은 문제나 인접 Shard Node의 장애에 따른 부하 가중 문제가 발생할 수 있으며, 이를 최소화하기 위해 Virtual Shard Node를 추가할 수 있습니다.

 

Directory Sharding은 Shard Key를 기준으로 어떤 Shard Node가 DataSet를 보유하고 있는지 확인할 수 있는 Static Lookup Table을 만들고 유지하는 방식입니다. 특정 Shard Key로 저장된 DataSet 위치를 지속적으로 기록합니다.

Range Sharding이나 Moduler Sharding과 비교하여 유연한 방식이지만, 해당 방식을 사용하면 모든 Read 또는 Write Query 처리 이전에 먼저 Static Lookup Table에 접근해야 하기 때문에 전체적인 처리 성능을 저하시킬 수 있습니다.

  • Range Sharding은 Shard Key에 대한 범위를 지정해야 한다는 점, Moduler Sharding의 경우 Hash Function 또는 Moduler 로직을 구현해서 사용해야 합니다.

 

Index Sharding(또는 Dynamic Sharding)은 Directory Sharding 방식과 유사하나 Shard Key와 Node를 관리하는 주체를 Table이 아니라 외부 Index 서비스(또는 Locator 서비스)로 제공하는 방법으로 저장할 위치를 알고 있는 서비스를 이용하여 정보를 요청한 Client에게 Lookup Key 또는 Shard node 위치를 알려주는 방식입니다.

이러한 Index 서비스에는 Admin 기능을 제공하여 불균형을 감지한 경우에 DataSet의 특정 값들을 다른 Shard Node로 안전하게 Migration 할 수 있도록 Index 서비스에 요청을 하면 Index 서비스는 Write 요청을 거부하고 Read Query만 허용하는 식으로 안전장치를 마련할 수 있습니다.

  • 하지만 해당 방식을 사용하면 Index 서비스를 따로 구축해야 하는 점, 운영 비용, SPoF가 될 수 있다는 점을 고려하여 상대적으로 복잡한 설계 및 개발이 필요합니다.
  • Index 서비스의 장애 상황 발생 시 전체 서비스의 요청 실패 문제를 완화하기 위해 Index 서비스에 대한 요청이 성공한 경우 Shard Node와 관련된 값을 Local Cache에 저장하고 이후 요청에는 이를 활용하도록 개선할 수도 있습니다. (Index 서비스에 대한 트래픽 완화)



참고 자료


어제저녁부터 시끌시끌한 주제인 Spring4 Shell에 대해서 찾아 정리한 글입니다. 짧은 시간 동안, 얕은 이해도를 가지고 대략적인 부분만 작성하였기에 이 부분을 유의해주시고 피드백해주시면 감사드리겠습니다.

해당 문제가 발생하기 위한 조건

  • Java 9+
  • Spring Core 5.3.17 또는 5.2.19 버전 이하를 사용
  • Post Mapping end-point
  • JSON / XML Converting을 사용하지 않는 API (application/x-www-form-urlencoded 사용)
    • 즉 @ModelAttribute - ModelAttributeMethodProcessor이나 Key=Value Format의 Binding을 사용할 때
  • 외장 Tomcat 9 사용
    • WebAppClassLoaderBase + WebAppClassLoader(. 경로를 구분자로 이용) 기능을 악용합니다.
    • 내장 Tomcat에서 사용하는 LaunchedURLClassLoader (URLClassLoader 구현체)는 위에서 언급한. class를 접근하기 위해서 . 대신 /를 구분자로 사용한 전체 디랙터리 경로를 전달하여야 하기 때문에 현재 공개된 POC 방법을 활용한 접근이 어렵습니다. (그래서 제한적이다 라고 표현하는 것 같습니다.)
  • DataBinder에서 허용하는 패턴을 설정하지 않은 경우 (allow all)

 

 

해당 문제가 발생한 이유.

Java 9에서는 Java Module System이 추가됨에 따라 3계층 Class Loader 중 Application Class Loader(사용자 클래스를 로딩하는 주체)가 Module Path를 통해 Class를 접근, 생성 및 수정하는 것이 가능해졌습니다.

  • 이로 인하여서 빈 정보가 저장되는 객체와 이를 캐싱되는 CachedIntrospectionResults의 초기화 로직에서 getClassLoader(), getProtectDomain() 조건 검증을 통해 ClassLoader에 대한 method를 등록하지 않도록 한 기존 로직을 우회할 방법이 생겼다고 합니다. (이를 통해 ClassLoader에 접근하여 임의 코드를 실행할 수 있습니다.)
    • 우회 가능 형식 ex) class.module.classLoader.~



관련한 매커니즘으로 cve-2010-1622와 cve-2014-0094-apache-struts-security-bypass-vulnerability을 참고하시면 좋을 것 같습니다.

 

 

취약점을 공격하는 방법

Post 요청으로 Query Parameter를 보낼 때 악의적으로 조작된 값을 포함하여 전달합니다.

  • 이때 전달되는 Parameter Value가 Databinder로 넘어갔을 때 사용하는 ClassLoader에 의해 Pipeline mechanism을 수행할 수 있으며 디스크에 존재하는 특정 파일을 접근하거나 생성 및 수정할 수 있게 됩니다.
    • ServletRequestDataBinder.bind() 참조
// AbstractNestablePropertyAccessor 
@Override 
@Nullable 
public Class<?> getPropertyType(String propertyName) throws BeansException {
	try { 
    	PropertyHandler ph = getPropertyHandler(propertyName); 
        if (ph != null) { 
        	return ph.getPropertyType(); 
        } else { 
        	// Maybe an indexed/mapped property... 
            Object value = getPropertyValue(propertyName); 
            if (value != null) { 
            	return value.getClass(); 
            } 
            // Check to see if there is a custom editor, 
            // which might give an indication on the desired target type. 
            Class<?> editorType = guessPropertyTypeFromEditors(propertyName); 
            if (editorType != null) { 
            	return editorType; 
            } 
        } 
    } catch (InvalidPropertyException ex) { 
    	// Consider as not determinable. 
    } 
    return null; 
} 

// BeanWrapperImpl 
@Override 
@Nullable 
protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { 
	PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); 
    return (pd != null ? new BeanPropertyHandler(pd) : null); 
}

 

 

 

해당 문제를 회피하는 방법.

  • Java 9 또는 외장 Tomcat을 사용하지 않습니다.
  • @ModelAttribute를 사용하지 않습니다. (DataBinder)
  • @InitBinder를 정의할 때 setDisallowedFields() 메서드를 이용해 class가 들어가는 패턴을 제외하도록 설정하거나 또는 setAllowedFields() 메서드를 이용해 허용하는 패턴을 정의합니다.
    • 권고) String [] denylist = new String[]{"class.*", "Class.*", ".class.", ".Class."};
  • RequestMappingHandler를 확장하여 createDataBinderInstance를 재정의합니다. (처리는 위 방법과 동일)
  • 취약점이 해결된 버전으로 업데이트 합니다.

 

 

(04-01 추가) Spring4Shell 취약점 해결 버전 배포

Spring4Shell 취약점을 해결하는 버전이 배포되었습니다. ( Spring Core 5.3.18, 5.2.20, Spring Boot 2.6.6, 2.5.12 )

 

 

 

참고 링크

 

 

회사에서 개발하는 레거시 프로젝트의 Version Migration 작업을 준비하기 위해 Java Version별 변경 내역을 조사하고 있는 내용입니다. 누락되거나 잘못 기술된 내용이 있을 수 있는 점 양해 부탁드립니다.

 

회사에서 맡아 진행하는 프로젝트에서 Java 17(Spring Boot는 2.6.2)을 사용하고 있기 때문에 Record나 Switch Expression, 추가된 Util 등에 만족하면서 개발을 하고 있는데요. 그렇기에 가능하다면 다른 레거시 프로젝트 또한 Java 17로 Migration 해보려고 합니다.

 

개인적으로 Spring Boot Version을 올리는 것이 더 까다롭지 않을까 생각하고 있습니다.

 

 

 

Java 9 (~2018년 3월 지원 종료)

  • 자바 모듈화(JDK 모듈 분리 기능), Integration JVM Logging, HTML5 JavaDoc
  • JShell 별도 컴파일 및 선언 없이 코드를 작성하고 테스트 가능한 CLI 도구가 추가되었습니다.
  • try-with-resource 구문 개선 관리하고자 하는 자원 객체를 try 구문 외부에서도 선언 가능합니다.
  • interface private method 추가 default, static method에 대한 공통 로직을 공유하는 용도입니다.
  • diamond operator 개선 (<>) - 익명 클래스에 대한 Generic 문법에서도 타입을 명시하지 않고 <>만 작성 가능하도록 개선되었습니다.
  • Process API 추가 모든, 현재, 자식, 종료 프로세스 정보에 대해 접근 및 관리 가능한 API입니다.
    • Process Stream을 버퍼 공간은 제한적이므로 입, 출력이 잦은 프로세스 요청 시 별도 스레드로 동작시키는 것을 권장한다고 합니다.
    • Java 9 Process API Improvements | Baeldung
  • CompletableFuture 개선 Timeout, Lazy 옵션 및 기능이 추가되었습니다.
  • Reactive Stream API 추가 - Reactive Programming을 위한 Pub / Sub을 지정 및 연계가 가능한 Flow 기능이 추가되었습니다.
  • Compact Strings (String 개선) 기본적으로 사용되는 UTF-16 charset String의 비효율적인 용량을 개선하기 위해 도입된 기능입니다.
    • String 내부에 char [] → byte []로 변경 및 String 생성 시 literal에 영문자만 있는 경우 Latin1 방식을 사용해 각 문자 당 1byte를 차지합니다.
    • benchmark 상 String 처리 속도가 20% 정도 개선되었고, String 으로 인한 Garbage 양이 30% 정도 감소하였습니다.
    • Compact Strings in Java 9 | Baeldung
  • Http Client API 추가 : URLConnection을 개선한 기능 및 명명 규칙을 제공하는 API입니다.
  • Мulti-Resolution Image API 추가 - 여러 해상도를 가지는 이미지들을 단일 해상도로 반환하거나 비교하는 등의 기능을 제공하는 API입니다.

 

 

 

Java 10 (~2018년 9월 지원 종료)

  • 지역 변수 타입 추론 기능 추가 var 제공 (변수 초기화 시 Diamond operation을 통한 Generic 명시)
  • G1 GC 개선 G1 GC의 알고리즘 성능이 향상되었습니다.
  • Unmodifiable Collections 기능 추가 기존 Colleciton을 Unmodifiable Collection으로 복사해주는 copyOf 메서드와 Stream API의 collect()에서 사용되는 Collectors.toUnmodifiableList() 메서드가 추가되었습니다.
  • Optional API 기능 추가 Optional, OptionalDouble, OptionalInt 등에서 사용 가능한 orElseThrow 메서드가 추가되었습니다.
    • 해당 메서드는 기능적으로 Optional.get()을 대체할 수 있습니다.
  • Container Awareness 지원 JVM Process가 Docker Container에서 실행 중임을 인식하여 Container의 구성 정보 값을 접근합니다.
    • Container의 메모리 사용량, CPU 할당량 등
  • Oracle Root Certificates 공개
  • CLI javah 제거, java.security.acl, ava.security.{Certificate,Identity,IdentityScope,Signer Deprecated
  • Graal Compiler 사용 가능 AOT 방식의 Compiler인 Graal Compiler Linux/x64 플랫폼 환경에서 사용할 수 있습니다.

 

 

 

Java 11 (LTS,  ~2023년 9월까지 지원 [연장 - 2026년 9월])

  • HTTP Client API 정식 통합 (JEP 321)
  • CORBA, Java EE, JAXB. JavaFX, JAX-WS Module 제거 - Dependency 정의를 하여야 사용 가능합니다.
  • 지역 변수 타입 추론 기능 개선 - Lambda expression에서 var를 사용 가능합니다.
  • String API 메서드 추가 ****isBlank, lines, strip, stripLeading, stripTrailing, repeat 메서드가 추가되었습니다.
  • Collection to Array 추가 toArray(String []::new), toArray(Integer []::new) 방식으로 사용 가능합니다.
  • Oracle JDK 무료 LTS 종료 JDK LTS의 유료화가 아닌 Oracle JDK에 한정된 사항으로 Oracle이나 다른 벤더에서 제공하는 OpenJDK는 무료 LTS를 지원합니다.
  • Nestmate Reflection API - Reflection API 사용 시 발생하던 중첩 객체의 private method 접근이 허용되도록 수정되었습니다.
  • javac 생략 가능 -. java file을 compile하지 않고 CLI에서 java Sample.java를 입력함으로써 실행할 수 있도록 개선되었습니다.
  • Dynamic Class-File Constants 추가 - 동적으로 생성되는 상수(부트스트랩 메서드를 통한 결과 값)에 대한 생성 비용을 감소시키기 위해 invokedynamic에게 위임되는 constantdynamic과 Constant pool이 추가되었습니다.
  • ARM64, Aarch64에서의 String, Array 내장 함수 최적화 및 Java.lang, Math의 sin, cos, log method 내장 함수 구현 (성능 개선)
  • Epsilon(No-Op) GC 사용 가능 - GC를 하지 않는 성능, 메모리 테스트 용도로 적합한 GC가 실험 기능으로 포함되었습니다.
  • ZGC GC 사용 가능 GC 일시 정지를 최소화하는데 목적을 두고 Colored pointers, Load Barriers, forwarding table 및 Relocation Set을 활용하는 GC인 ZGC를 실험적으로 사용할 수 있게 되었습니다.

 

 

 

Java 12 (~2019년 9월 지원 종료)

 

 

 

Java 13 (~2020년 3월 지원 종료)

  • Switch Expression 개선 - Case 안에서 값을 반환하는 yield keyword가 추가되었습니다.
  • Text Block 여러 줄로 구성된 String literal을 """ ~ """로 묶을 수 있는 기능이 preview로 추가되었습니다. 해당 기능은 literal로 작성하는 Query 구문에 대해서 향상된 경험을 제공하는 것 같습니다.
  • Dynamic CDS Archives - JVM 간의 Class Metadata를 공유할 수 있는 방법인 CDS (Class Data Sharing) 기능을 사용하는 방법이 개선되었습니다.
  • ZGC: Uncommit Unused Memory (JEP 351), Heap 최대 크기 향상 사용되지 않는 Heap Memory 공간을 OS에게 반환하는 기능이 포함되었습니다. 최소 크기에 도달하기 전까지 Heap Memory를 반환합니다. 또한 지원하는 최대 Heap size가 4TB에서 16TB로 향상되었습니다.
  • Reimplement the Legacy Socket API (JEP 353) - java.net.Socket 및 java.net.ServerSocket API의 기본 구현체를 NioSocketImpl로 변경하였습니다.
  • java.nio API 메서드 추가 FileSystem.newFIleSystem() 메서드가 추가되었습니다.

 

 

 

Java 14 (~2020년 9월 지원 종료)

  • Switch Expression 사용 가능 (정식)
  • Pattern Matching for instanceof 명시적인 Casting 없이도 typecasted 시킬 수 있는 기능이 preview 형태로 추가되었습니다.
  • Text Block 개선 escape sequence인 /, /s 가 추가되었습니다.
  • Record 추가 - Data-Transfer-Object, Value-Object 패턴의 POJO에서 발생하는 지속적인 boilerplate를 줄이기 위해 record type이 만들어졌으며, preview로 추가되었습니다.
  • Helpful NullPointerExceptions - Stack trace에 표시되는 NPE 발생 행의 어떤 값이 Null인지 지적하는 기능이 추가되었습니다. 기능을 활성화 함으로써 사용 가능합니다.
  • Foreign Memory Access API 외부 Memory에 접근 가능한 실험적인 Memory API가 Experimental Feature로 추가되었습니다.
  • ZGC on Windows and macOS – Linux 환경에서만 사용 가능한 ZGC가 이제 Windows, macOS에서도 사용 가능해졌습니다.
  • NUMA-Aware Memory Allocation for G1 G1 GC도 Non-uniform memory access를 인식하도록 변경되었습니다.
  • ParallelScavenge + SerialOld GC 방식 제거, Concurrent Mark Sweep(CMS) Garbage Collector 제거

 

 

 

Java 15 (~2021년 3월 지원 종료)

  • ZGC, Shenandoah GC 사용 가능 (정식)
  • Text Block 사용 가능 (정식)
  • Helpful NullPointerExceptions (기본 지원)
  • Record 개선 - Record에 대한 2차 preview로써 Reflection 등으로 Field가 수정될 수 없고, Native Method를 사용하지 못하도록 변경되었습니다.
  • Sealed Classes 추가 접근 제어자 이외에도 상속에 대한 세분화된 제어를 위해 Sealed Classes가 preview로 추가되었습니다. Sub class로 지정할 Type을 permits를 통해 명시적으로 정의할 수 있습니다
  • Hidden Classes 추가 - 동적 클래스의 비효율적인 Memory 사용량 등을 개선하기 위해 Hidden Classes가 preview로 추가되었습니다. 코드에서 직접적으로 접근할 수 없으며 독립적으로 Unload 할 수 있어 필요한 순간에서만 사용하고 GC 시킬 수 있습니다.
    • 명시적인 Reflection API, ClassLoader의 Lookup 메서드로는 찾을 수 없으며, Super-class, Field, Return, Argument type으로 사용할 수 없습니다.
    • Java 15부터 Lambda 기능을 수행하기 위해 만들어지는 Instance는 Hidden Classes입니다.
    • JEP 371: Hidden Classes
  • Pattern Matching Type Checks (Pattern Matching for instanceof) - 해당 기능은 개선 없이 preview 상태를 유지합니다.
  • Foreign Memory Access API - 해당 기능은 Experimental Feature 상태로 유지되고 있으며, Memory Access Handle를 사용자가 정의 가능하도록 VarHandle API를 추가하고 Spliterator Interface를 통한 parallel processing of Memory Segment을 지원합니다.
  • Reimplement the Legacy Socket API - DatagramSocket API 가 재작성되었습니다.

 

 

 

Java 16 (~2021년 9월 지원 종료)

  • Pattern Matching for instanceof 사용 가능 (정식)
  • Record 사용 가능 (정식) 이제 Ineer Class의 상태로써 Reocrd를 정의할 수 있습니다.
  • Sealed Classes 개선 - sealing , non-sealed keyword를 abstract, extends와 유사한 JVM이 인식 가능한 Context keyword로 허용되었으며, Type check 나 Sealed Classes의 Sub Class를 만드는 기능이 제한되었습니다.
  • foreign linker API 추가 JNI를 대체하기 위한 Host System의 Code에 Access 하는 방법인 foreign linker API가 추가되었습니다.
  • Invoke Default Methods From Proxy Instances (JDK-8159746) - Dynamic proxy 정의 시 interface의 default method를 호출할 수 있는 java.lang.reflect.InvocationHandler가 추가되었습니다.
  • Day Period Support (JDK-8247781) DateTimeFormatter에 사용할 수 있는 새로운 기호가 추가되었습니다.
  • Add Stream.toList Method (JDK-8180352) .collect(Collectors.toList()); 와 같은 boilerplate를 줄이기 위해 도입된 기능으로 collect 종결 연산 대신. toList()를 사용할 수 있습니다.
  • Vector API (JEP-338) 추가 스칼라 연산보다 더 최적화된 벡터 연산 방식을 제공하기 위해 Experimental Feature 상태로 도입된 API입니다.
  • Strongly Encapsulate JDK Internals by Default JDK 내부를 직접 접근하는 모든 Library가 접근할 수 없도록 내부 코드가 강하게 Encapsulate 되었습니다.

 

 

 

Java 17 (LTS, ~2026년 9월까지 지원 [연장 - 2029년 9월])

 

 

 

Java 18 (~2022년 9월 지원 종료)

  • Internet-Address Resolution SPI 추가 (JEP-418) Host의 이름이나 주소를 확인할 수 있는 API가 추가되었습니다.
  • Pattern Matching for Switch (JEP 420) Pattern Matching 기능을 Switch에도 확장하기 위한 작업으로 2번째 preview 상태입니다.
  • Reimplement Core Reflection with Method Handles (JEP 416) 기존 핵심 Reflection API의 일부 동작을 다시 구현하여 유지 관리 비용을 줄이기 위한 작업입니다.
  • Simple Web Server 추가 (JEP-408) 정적 리소스 파일을 제공할 수 있는 간단한 웹 서버를 만드는 CLI 도구를 제공하는 작업입니다.
  • UTF-8 by Default (JEP-400) -Java API에서 사용되는 기본 문자 집합으로 UTF-8을 사용하도록 변경한 작업입니다.
    • 특정 문자 집합으로 고정하여 사용하지 않고 Java의 기본 문자 집합을 사용하는 Asia Locale Application에서 String과 같은 데이터의 손상이 발생할 가능성이 높다고 합니다.
    • JEP 400: UTF-8 by Default
  • Vector API (JEP-417) 개선 최적화된 벡터 연산 방식을 도입하기 위한 3번째 incubating 작업으로 CPU 아키텍처에 최적화된 벡터 명령어로 Runtime에 Compile 되는 벡터 계산을 표현하여 동등한 스칼라 계산보다 우수한 성능을 달성하였습니다.
  • Finalizer 제거 예정 (JEP-421) - 보안, 성능, 안정성 및 유지 관리 측면에서 심각한 문제를 일으키는 Finalizer (finalize()) 를 제거하는 작업입니다. try-with-resources 문법이나, Cleaners API를 사용하도록 권장합니다.


이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.


Redis Persistence

Redis는 가지고 있는 데이터를 장애 발생 시 복구할 수 있도록 데이터를 영속화하는 방법을 제공합니다.

Redis 데이터 영속화는 관리하는 비즈니스 데이터의 성향과 중요도에 따라 사용하거나, 하지 않을 수 있습니다. 상황을 고려하지 않고 AOF의 Always 전략 등을 사용하는 행위는 Redis의 성능 하락과 응답 지연 문제를 심화시킬 수 있기 때문에 잘 고려하여야 합니다.

 

영속화를 사용하는 경우 Redis Server의 서비스 중단을 방지하기 위해 Child Background Process를 사용하는 것이 필수적이며, Process fork로 인한 메모리 사용량, 발생하는 Disk I/O 빈도와 저장된 파일 용량 등을 확인하여야 합니다.




RDB : Redis Database Backup

현재 Redis의 메모리에 존재하는 데이터의 스냅샷을 남기는 방식입니다. 즉 key와 value 형태의 값을 그대로 기록하는 방식입니다.

Key1 -> value
count -> 22


이 방식은 AOF 파일보다 작은 용량, 짧은 쓰기 지연 시간과 데이터 복구 시간을 가지고 있는 점이 특징입니다. Redis Replication 수행 시 사용되기도 하는 포맷이며 기본적으로 Origin에서 생성된 RDB 파일을 Replica node에게 전송하고, Replica에서는 임시 RDB 파일을 만들어 파일에 쓴 다음 동기화하는 방식을 취하고 있습니다.

  • RDB 파일은 AOF 파일보다 데이터 유실이 발생하기 쉽습니다. 스냅샷은 일정 주기마다 기록되기 때문에 최대 지정된 주기 만큼의 데이터를 유실할 가능성이 있습니다.
Replication 부하를 줄이기 위해 RDB를 Replica가 설치된 서버에서 수행할 수도 있습니다.


redis-conf에서 save 속성을 설정한 대로 RDB를 주기적으로 수행하며 필요하다면 SAVE, BGSAVE Command를 통해 수동으로 수행할 수 있습니다.

  • SAVE를 사용한 경우에는 모든 사용자 요청 및 작업을 중지하고 현재 동작 중인 프로세스(main)에서 RDB 파일을 생성하게 됩니다. - 비권장
  • BGSAVE를 사용한 경우에는 현재 프로세스는 계속 요청을 처리하고 fork를 통해 자식 프로세스를 생성하여 RDB 파일을 생성합니다. - 권장

 

BGSAVE 방식으로 fork를 진행한 후 데이터를 기록할 때 Linux의 특성상 COW에 의해 메모리 사용량이 최대 2배까지 늘어날 수 있습니다.

이는 Process fork를 통해 동작하여 처음에는 부모와 자식 프로세스가 동일한 메모리 영역을 공유하고 있지만, 부모 프로세스에서 데이터를 변경하면 해당 데이터는 다른 메모리 영역으로 복사되어 “부모에게만 참조되는” 상황 때문입니다.


Redis의 기본 설정에 의해 BGSAVE Command를 통한 RDB 작업이 실패한 경우 Redis Server가 Client의 쓰기 요청을 처리하지 않는 상황이 발생합니다. 이를 방지하기 위해 stop-writes-on-bgsave-error 설정 값을 no로 설정하여야 합니다.

  • no로 설정한 경우에는 Client 요청을 정상적으로 처리하게 되고 실패한 RDB를 다시 수행하지 않습니다.
  • 디스크 용량이나 깨짐, 사용 권한 등의 문제로 RDB가 실패할 수 있음을 인지하고 있어야 합니다.

관련 속성에는 dump 파일 생성 시 압축하도록 하는 rdbcompression (bool), 데이터 유효성 검사가 진행되도록 하는 rdbchecksum (bool), 파일 이름을 지정하는 dbfilename과 파일 저장 위치를 지정하는 dir 속성이 있습니다.



AOF : Append Only File

데이터를 변경하는 모든 Redis Command를 파일에 저장하는 방식입니다. Client에 의해 수행되었던 모든 데이터 추가, 수정 Command를 저장하고 또한 Text 파일 형태로 제공하여 쉽게 식별 가능하고 활용할 수 있기 때문에 장애 발생 시 최신 데이터를 복구함에 있어서 많은 이점을 가지는 방법입니다.

  • 커지는 파일 용량을 잘 고려해야 하며, Disk I/O 횟수나 비용으로 인해 큰 성능 저하를 일으킬 수 있는 방법입니다.
set key1 hello
set key1 value
set count 22
del key2

RDB 동작보다 우선순위가 높아 먼저 수행되고, 매 요청마다 수행됩니다. Client가 업데이트 명령을 요청하면 Redis는 먼저 해당 명령을 파일에 저장하고 파일 쓰기가 완료되었을 때 해당 명령을 실행해서 내용을 추가합니다.

이 방식은 redis-conf에 appendonly를 설정하면 수행하며, 필요하다면 BGREWRITEAOF Command를 통해 수동으로 AOF 파일을 재 작성할 수 있습니다.

또한 Append라는 의미에 맞게 매번 파일에 수행된 Command와 Metadata를 추가합니다. 그렇기에 파일 용량이 시간이 지남에 따라 계속 커지게 됩니다. 만약 OS에서 설정한 파일 용량 임계치를 넘어선다면 AOF 파일 기록이 중단되거나 Redis Server 재 시작 시 I/O 핸들러를 열거나 읽어오는 시간 등 때문에 서비스 제공 시간이 지연될 수 있습니다.

이를 방지하기 위해 rewrite를 지속적으로 수행하여야 하는데 이 작업 또한 Disk I/O 작업이기 때문에 Redis 지연 이슈를 발생시킬 수 있습니다.

  • 위에서 설명한 appendfsync 전략과 auto-aof-rewrite-percentage, auto-aof-rewrite-min-size 속성을 조정하여 이를 알맞게 구성하여야 합니다.


AOF disk write(fsync, fdatasync) 전략에는 Always, Everysec, No가 존재합니다.

  • Always는 write를 수행할 때마다 fsync를 수행하는 방식으로 데이터를 완전히 보전하기 위해 사용하는 전략입니다. 쓰기 방식 중 성능이 제일 좋지 않아 DISK base DBMS를 사용하는 것과 비슷한 처리 성능을 가지게 됩니다.
  • Everysec는 매 초마다 fsync를 수행하는 방식으로 성능과 데이터 보존을 어느 정도 지킬 수 있는 최선의 선택입니다. (권장 방식)
    • 해당 AOF를 사용하고 있다면 최대 1초의 데이터 유실이 발생할 수 있습니다.
  • No는 Redis가 아닌 OS가 fsync를 수행하는 방식으로 Linux의 경우 30초마다 동기화를 수행합니다. 이때 Linux는 fsync가 아닌 fdatasync 방식으로 수행하는데 이는 파일 접근 시간, modify time, 파일 용량 등의 Metadata를 저장하지 않는 방식으로 더 빠른 성능을 보장합니다.



RDB의 경우 백업은 필요하나 데이터 손실이 발생해도 무관한 경우에 활용할 수 있고(단순 콘텐츠 캐싱 등), AOF의 경우 장애 상황 직전까지 모든 데이터가 보장되어야 할 중요한 비즈니스 데이터를 다룰 때 적용하게 됩니다.

비관적으로 한 작업의 실패를 가정하는 경우 RDB & AOF를 동시에 사용하여 안전하게 갈 수도 있습니다.



 

운영 시 주의할 점

비어있는 Master와 데이터를 가진 Slave가 서로 간의 Replication을 수행하지 않도록 할 것

Master와 Slave 사이의 최초 동기화 시점에는 서로 간의 데이터 Sync를 맞추려는 작업을 수행하게 됩니다. 만약 Master가 장애 발생으로 인해 아무 데이터도 없이 복구 조치 되었다면, 이와 연결된 Slave에도 emptyDB라는 함수가 호출되어 가지고 있는 모든 정보를 정리하게 됩니다.

// Sync Process() 
1. main() 
2. initServer()          // network 설정 수행 
3. serverCron()          // 종료, 에러 처리 수행 - Timer 기반, replicationCron() 호출 
4. replicationCron()     // Master와의 Connection 상태 확인 후 복원 수행, connectionWithMaster() 호출 
5. connectionWithMaster()// Replication이 설정된 경우 수행, Master와 실제 연결 시도 
5-1. syncWithMaster()    // Auth 수행 및 Sync 명령 전달, 임시 RDB 파일 생성 
6. readSyncBulkPayload() // Master에게 데이터를 받을 때 수행, sync 완료 후 emptyDB() 호출 
7. emptyDb()             // 현재 메모리 정리 
8. readSyncBulkPayload() // 5-1에서 만든 임시 RDB 파일을 메모리에 로드

이러한 Process가 수행되지 않도록 방지하기 위해 Master에 장애가 발생함을 감지하였을 경우, 해당 Master와 Replication을 수행하던 Slave Node를 Master로 승격 시켜 이러한 문제를 방지하는 것이 좋습니다.

  • Redis 5.0 미만 버전이라면 SLAVEOF NO ONE을 호출합니다.
  • Redis 5.0 이상 버전이라면 REPLICAOF NO ONE을 호출합니다.

 

Linux kernel에 THP 설정이 적용되어 있는지 확인할 것

THP란 대용량 메모리를 사용하는 Linux에서 관리되는 page의 양이 많아져 CPU가 메모리 요청을 할 때 Lookup 하는 TTB의 크기와 TLB 크기가 커지고 Hit ratio가 줄어들어 것을 메모리 접근에 대한 Overhead가 발생하는 상황을 개선하기 위해 추가된 기능입니다. 

 

기본적으로 제공되는 Page Size가 4K라면 해당 설정이 활성화된 상태에선 2MB 혹은 1GB 크기의 Huge page를 동적 할당하도록 변경되는데, CentOS / RHEL 6 이상 버전에는 기본적으로 적용되어 있다고 합니다. 

패치가 되었을지 확인은 못해봤네요

 

하지만 해당 설정은 Redis 운영에 있어서 치명적인 문제를 일으킬 수 있습니다.

  • Redis에서 메모리에 가지고 있는 데이터와 상관없이 큰 사이즈의 Page를 할당하여 Linux Host OOM을 유발할 수 있고 Redis Process의 oom_score 점수를 높일 수 있어 OOM-Killer에게 제거되는 상황이 발생할 수 있습니다.
  • 또한 Fork system call의 지연 시간을 증가시키고 앞서 서술했던 COW 문제를 심화시킬 수 있습니다. Persistence 설정으로 인해 BGSAVE 등이 호출되어 Process Memory를 fork 한다면 대량의 메모리를 2개의 child process가 참조하게 되어 이를 작업하는 시간이 오래 걸리고, 활발한 쓰기 요청을 처리하고 있는 Master Node라면 빈번한 메모리의 재할당 / 참조가 발생하여 정말 큰 메모리 공간을 점유할 수 있습니다. (Node max memory + 1GB[최대 페이지 크기]) x 2

 

해당 설정이 활성화 되었는지에 대한 여부는

// 활성화 시 [always], 비 활성화 시 [never]
/sys/kernel/mm/transparent_hugepage/enabled

파일 정보를 통해 확인할 수 있습니다.

 

해당 내용을 찾아보면서 THP의 이점을 누리려면 Redis 시작 시 최초 메모리 할당 이후에 재 할당이 일어나지 않아야 될 것같다고 생각이 들었는데 그것이 어떤 상황일지 궁금했습니다. 고정된 인원에 대한 리더보드를 관리하는 Redis나 Hyperloglog 등을 사용해 집계를 수행하고 일정 주기로 데이터를 삭제하는 느낌일 것 같은데, 실제로 사용하고 계신 분이 있다면 댓글로 남겨주시길 부탁드립니다! ㅎㅎ

 

 

참고 자료

 

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글이며 기존에 작성했었던 CI / CD 글을 수정한 것입니다.

 

 

 

자동화되지 않은 개발 프로세스의 문제점

상용 서비스, 소프트웨어는 일반적으로 여러 명의 개발자가 협업을 진행하기 때문에 코드의 관리와 배포 후 발생하는 장애 대처 용도로 Git과 같은 버전 관리 도구를 사용하여 각 코드에 대한 이력과 메시지를 남기고 병합하는 과정을 거치게 됩니다.

 

이러한 프로세스를 계속 수행되다 보면 기존 코드(테스트, 스키마 설정 값, 충돌)와 문제가 발생할 수 있지만 이를 빨리 식별하지 못해 운영 환경에서 서비스 장애가 발생하는 경우가 있을 수 있습니다.

 

또한 개발, 운영팀이 수동으로 코드를 빌드하고 테스트 및 FTP 등의 프로토콜을 통한 패키지 전달 및 실행이라는 하나의 배포 흐름을 수작업으로 수행하게 되어 주기적으로 팀의 리소스가 낭비되게 됩니다. (인력, 시간, 돈 등)

  • 이는 서비스의 크기가 커짐에 따라서, 하나의 서비스가 여러 개의 마이크로 서비스로 분리된 상황이나 사용자 트래픽의 급증으로 인해 빨리 서버를 증설해야 할 때 더욱 큰 문제를 야기할 수 있습니다.

 

 

CI / CD 란?

예시로 만든 이미지이며 우측 상단에 있는 이미지는&amp;amp;nbsp;https://www.plutora.com/blog/understanding-ci-cd-pipeline

 

Application 개발 배포 프로세스를 자동화하여 다음 버전을 짧은 주기로 배포하여 사용자에게 제공하는 방법 또는 시스템을 의미합니다. 이를 통해 사용자의 피드백을 받는 주기도 짧아짐으로써 더 나은 서비스를 제공하는데 도움이 됩니다.

  • Application Life Cycle 전체에 걸쳐 지속적인 자동화와 모니터링을 제공한다.
  • CI/ CD를 도입함으로써 개발자들은 비즈니스 요구 사항, 코드 작성에만 집중할 수 있다.
  • CI/ CD 파이프 라인에서 실행되는 회귀, 성능, 유닛, 통합 테스트 등 지속적인 테스트 커버리지와 검증을 요구함으로써 좀 더 안전한 서비스를 제공할 수 있는 여지가 있다.

 

 

 

CI : Continuous Integration

자동화된 프로세스를 통해 애플리케이션 코드 변경 사항이 정기적으로 빌드 및 테스트(유닛, 통합)되어 공유되는 Repository에 병합되는 것을 의미합니다.

  • 개발자 간의 코드 충돌을 방지하기 위한 목적을 가지며, 기존 코드와 신규 코드의 충돌이 발생하면 빠르게 발견하고 수정할 수 있는 식별성을 지닙니다.
  • 통합 프로세스의 자동화와 일관성을 제공하여 사용되는 리소스를 줄이고 요구 사항에 따른 코드 변경을 더 자주 수행하게 끔 촉진합니다.

 

 

 

CI의 필수 구성 요소

  • Repository management
    • CI를 도입하기 위해서는 Version control 도구와 Git-flow와 같은 Branch 전략은 필수적인 요소입니다. Local, Remote, Origin Repository를 각각 관리하여 conflict가 발생할 여지를 줄이고 branch check-out기능과 Commit을 통해 사용 가능한 코드의 식별 및 버전, 이력 관리를 수행해야 합니다.
  • Build-automation
    • 개발, 운영 팀은 Build script (Shell, python) 또는 표준화된 Build tool이나 Open-source (jenkins, gitlab, circle ci 등)를 사용하여 Code Build를 수행해야 하며, 이전 Build version에 대한 백업 버전을 관리하여 예기치 못한 상황에서 복구할 수 있는 여지를 남겨 놓아야 하고 이 모든 것은 자동적으로 수행되어야 합니다.
  • Self-test (Test-automation)
    • 빠른 수정과 장애 상황 방지를 위해서는 기존에 작성된 Unit Test나 Sonarqube와 같은 Third-part library 들을 연동하여 자동화된 테스트 수행 및 커버리지 관리가 되어야 합니다.
  • Source code versioning
    • 정기적인 Source commit을 통해 개발자 간의 이슈 최소화와 작업 상황에 대한 공유가 지속적으로 이루어져야 하고 Commit을 통한 Build를 수행하여 지속적인 Test Cycle을 수행하여야 합니다.

 

 

 

CD : Continuous Delivery / Deployment

해당 용어는 지속적인 서비스 제공 혹은 지속적인 배포를 의미하며, 이 두 가지 용어는 상호 교환적으로 사용됩니다. 최종적으로는 소프트웨어나 서비스를 좀 짧은 주기로 배포, 반영하는 것을 목표로 합니다.

 

이를 통해 지속적으로 발생하는 피드백, 신규 요구 사항에 대해 유기적으로 대처할 수 있습니다.

 

 

Continuous Delivery

지속적인 제공은 개발자들이 적용한 변경 사항이 자동화된 프로세스(Pipeline)를 통해 빌드, 테스트를 거쳐 Repository에 자동으로 업로드되는 것을 뜻하며 운영팀은 이 Repository의 최신 상태를 운영 환경에 실시간으로 빠르게 적용할 수 있습니다.

  • 최소한의 리소스로 새로운 코드를 빠르게 수정하고 온전히 통합하는 것을 목표로 합니다.
    • 각 변경 사항은 작은 단위로 최소화하여 문제가 발생하였을 때 확인해야 하는 영역을 줄입니다.
    • Build, Test, Integration 프로세스를 계속 확인할 수 있으며 문제가 발생한 부분을 빠르게 식별합니다.

 

 

Continuous Deployment

지속적인 배포는 변경 사항이 생긴 Repository에서 고객이 사용하는 환경이나 테스트를 위한 스테이지 서버로 자동으로 배포, 출시하는 것을 뜻하며 이는 지속적인 수작업으로 인해 개발 팀이나 운영 팀의 리소스 소모 또는 프로세스 병목이 발생하는 것을 방지합니다.

  • 최소한의 리소스로 변경된 사항을 빠르게 각각의 환경(Dev, Test, Stage, Production)으로 출시하고 검증하는 것을 목표로 합니다.
  • 통합 테스트, QA, 배포 단계를 자동화하여 사용되던 리소스를 최소화하기에 소프트웨어나 서비스에 집중할 수 있는 리소스가 남게 됩니다. 이는 결국 실제 Code와 Test를 작성하는 것에 집중할 수 있고 Legacy code를 Refactoring, Restructuring 할 수 있는 시간을 만듭니다.

 

 

 

CI / CD Tools

CI / CD를 구성하기 위해 사용하는 도구들을 나열합니다.

  • Source code management tools
    • Git, AWS Code Commit, SVN, Kraken, Source tree
  • Build management tools
    • Maven, Gradle, PyBuilder, make, ANT
  • Test automation, quality tools
    • Selenium, Junit, Cucumber, Sonarqube, Jacoco
  • CI tools
    • Jenkins, Jenkins X, Gitlab, Circle CI, AWS Code Build, Bamboo, Hudson..
  • CD tools
    • Argo CD, AWS Code Deploy..
  • Configuration, Provisioning tools
    • Ansible, Puppet, Chef..
  • Monitoring tools
    • AWS Cloud Watch, Nagios, Ganglia, Sensu..

 

 

도구마다 사용법이나 정의하는 문법 등이 다르기에 도구의 어떠한 점이 중요한 것인가 (Build 서버 구축 여부, Plugin 제공 여부나 생태계의 크기 등)를 고민해보고 있습니다만 우선은 회사에서 사용하는 것이라도 열심히 해야겠습니다. ㅎㅎ 

  • 현재 회사에서는 Gitlab-CI의 Docker base Runner 타입으로 사용하고 있으나 따로 CD가 구성되어 있지는 않습니다. 상황에 알맞은 CD 방법을 고려해보고 도입하는 게 중요한 상황인 것 같습니다.

 

 

참고 자료

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

 

 

복제가 필요한 이유?

우리가 제공하는 API 서비스는 일반적으로 Stateless 한 형태를 준수하여 개발하여야 합니다.

 

이는 State를 다른 서비스(RDBMS, Global Session Storage - Redis 등)에서 관리하게 하고, API 서비스는 필요에 의해 조회하도록 하여 Stateful로 개발하였을 때 발생하는 API 서비스 간의 상태 동기화 이슈나 확장성 저하 문제를 최소화할 수 있기 때문입니다.

 

이러한 구조는 트래픽 급증에 대응하는 Scale-out을 가능케 합니다.

 

 

이런 형태로 개발하게 되면 결국 다수의 API 서비스가 조회하는 State를 관리하는 서비스에는 대량의 부하가 전달되어 자연스레 해당 서비스에 장애가 발생할 확률이 증가하게 됩니다. 이때 저장되어야 할 데이터가 유실되거나 (Transaction 실패, Connection Timeout 등) 또는 서비스가 중단된다면 이는 전체 서비스에 영향을 미치는 큰 문제가 됩니다. (SPoF)

 

이를 대비하기 위해 우리는 High Availability 이라는 속성을 만족하도록 서비스 설계를 진행하여야만 합니다. 그리고 이 속성을 지원하는 설계 방법 중 하나로는 복제 구조가 있습니다.

 

 

 

 

복제?

복제란 기본적으로 네트워크로 연결된 여러 물리 장비에 동일한 데이터를 분산 및 복사하여 저장하는 것을 의미합니다.

 

이를 통해 우리는 서비스 데이터를 좀 더 안정적으로 보관할 수 있으며, 다른 네트워크 간의 복제 시 특정 네트워크의 전체 장애를 대비할 수 있고 또한 동일한 데이터를 가지고 있기 때문에 이를 활용하여 서비스 API의 조회 요청을 여러 장비로 분산시킬 수도 있습니다.

  • 이렇게 서비스 API의 조회 요청을 여러 장비(RDBMS, MongoDB, Redis 등)로 분산 시키는 행위는 Read Query Load Balancing 또는 Query Off Loading이라고 합니다.
이후 설명에서는 Master - Slave 용어가 특정 시기의 차별을 나타냄으로 이제 사용되지 않기에 Origin - Replica로 대체하여 작성하도록 하겠습니다.

MongoDB나 특정 Opensource 에서는 Primary - Secondary로 명명하기도 합니다.

 

 

 

 

복제 구현 방식?

복제 로그 구현 방식으로는 크게 Statement based, Write-ahead logging based, Row based log 3가지가 존재합니다.

 

 

Statement based는 Origin 모든 쓰기 요청의 구문을 특정 영역(버퍼, 큐 등)에 기록하고 write를 수행한 다음 Replica에게 해당 기록을 전송하는 방식입니다.

 

Origin이 INSERT, UPDATE, DELETE 구문 수행 기록을 전송하면 각 Replica는 이를 파싱 하고 실행하여 보유한 데이터를 동일한 상태로 만들어 갑니다.

  • 식별 값 자동 생성, 노드가 보유한 데이터를 의존하는 처리 (UPDATE ~ WHERE) 등은 각 노드마다 정확하게 같은 순서로 실행되어야만 합니다. 인과성의 보장 필요
  • 이 방식의 장점은 복제하는 데이터가 경량이기에 Network I/O 비용이 감소하고, 감사 등의 운영 상 이슈가 발생하였을 때 대처하기 좋다는 장점이 있습니다.
  • 이 방식의 단점은 현재 날짜나 시간, 임의의 숫자를 구하는 비 결정적 함수 혹은 요청에 대해서 각 Replica마다 다른 결과를 얻고 반영하여 데이터 일관성 문제가 발생할 수 있다는 점입니다.
또한 부수 효과를 일으키는 요청 (Trigger, Procedure) 또한 결과가 비 결정적이라면 다른 결과를 얻을 수 있습니다.

 

 

이러한 문제를 해결하기 위해 Origin이 구문을 기록할 때 모든 비결정적 함수 호출과 부수 효과 요청에 대해서 고정 값을 반환하게 끔 할 수 있으나, 여러 예외 케이스가 있기 때문에 일반적으로 다른 복제 방법을 사용하는 것을 선호합니다.

  • MySQL에서는 지속적으로 구문 기반 복제를 사용해왔으며 사용된 구문에 비 결정적 함수가 존재한다면 논리적 로그 기반 복제로 변경하여 사용도록 되어 있습니다.
  • VoltDB도 구문 기반 복제를 사용하고 있지만, MySQL과 다르게 트랜잭션 단위를 결정적이 되게 끔 제약하여 문제를 해결하고 있습니다.
  • Redis 또한 구문 기반의 복제를 사용하고 있습니다.

 

 

 

 

Write ahead logging based는 말 그대로 저 수준의 로그인 WAL을 활용하여 Replica에도 동일한 복제 데이터를 구축할 수 있다는 점을 이용하는 방법입니다. 이 방식은 Postgresql과 Oracle 등에서 사용하고 있습니다.

Origin는 모든 write 요청에 대해서 WAL을 기록하고 Replica에게 Network I/O를 통해 전송합니다. 그리고 Replica는 전송받은 WAL을 그대로 반영하여서 최종적으로 Origin와 동일한 데이터를 구성하게 됩니다.

  • 이 방식의 장점은 Replica에 데이터를 복제할 때의 성능이 다른 방법보다 매우 빠르다는 점입니다. 또한 로그는 인과적으로 저장되는 것을 보장하기 때문에 WAL 동기화 비용 또한 최소화됩니다.
  • 이 방식의 단점은 WAL가 코어 수준에서 사용되는 데이터를 기술하기 때문에 저장소에서 사용하는 엔진에 대한 버전 의존성이 생겨 다른 버전을 사용하는 Origin, Replica 간의 복제가 거의 불가능하다는 점입니다.
이러한 제약 때문에 버전 업데이트 시에는 Replica를 우선 업데이트한 뒤 Origin를 업데이트해야 합니다

 

 

 

 

Row based log는 WAL에서 발생하던 문제점인 저장소 엔진 의존성을 제거하기 위해 구상된 방법으로 추상화된 다른 형태의 로그를 활용하여 데이터를 복제하도록 구현되어 있습니다. 일반적으로 로우 단위로 구성된 데이터베이스 테이블을 기준으로 write 로그를 작성하여 모든 칼럼에 속한 값을 같이 로그로 남깁니다.

 

이렇게 논리적 로그를 데이터와 WAL 중간에 추가함으로써 엔진 간의 하위 호환성을 유지하는 것이 수월해졌으며, 다른 버전이나 다른 저장소 엔진 기반으로 동작하는 노드와의 복제 또한 가능하게 되었습니다.

또한 기록된 로그를 파싱을 하는 것도 수월하여 분석, 로그 시스템, 외부 시스템에 내용 전송 등 여러 측면에서 유용한 방식입니다.

 

 

MySQL에서 사용하는 이진 로그 복제는 해당 방식을 활용합니다.

  • 이 방식의 장점은 WAL의 장점을 대부분 수용하면서도 하위 호환성을 지키고, 복제할 수 있다는 점입니다.
  • 이 방식의 단점은 Origin과 Replica의 테이블 Schema가 동일하여야 한다는 점, 실제 값을 복제하기 때문에 반영하는데 상대적으로 지연 시간이 발생하는 점 그리고 특정 기록이나 대용량의 로그를 반영하기 위해 Bin log로 쓰는데 동시성 문제가 발생할 수 있다는 점입니다.

 

이 방식에서 제공하는 로그 정보는 이렇게 기록됩니다.

  • 삭제된 Row based log는 고유하게 식별하기 위한 메타 데이터를 포함하는데 일반적으로 PK를 사용하고 PK가 없다면 모든 칼럼의 예전 값들을 Logging 하는 방식으로 구현됩니다.
  • 갱신된 Row based log는 모든 칼럼의 새로운 값과 식별하는데 필요한 메타 데이터를 포함하여 기록됩니다.
  • 여러 Row를 동시에 수정하는 Transaction의 경우 우선 그 Row 수만큼 로그 레코드를 생성한 다음 해당 Transaction이 커밋되었음을 나타내는 정보를 같이 표시합니다.

 

 

 

복제 동기화 방식?

복제 동기화 방식으로는 크게 Synchronous, Semi-synchronous, Asynchronous 3가지가 존재합니다.

 

 

Synchronous Replication은 Origin에 데이터를 반영한 직후나 반영하기 전에 Replica node에 먼저 복제 요청을 전송하여 정상 수신이 되었을 때 이후 처리를 진행하는 방식입니다. 이를 통해 분산 저장된 데이터의 일관성과 무결성을 지키는데 집중합니다.

  • 이 방식의 장점은 분산된 장비 간의 데이터 일관성과 무결성을 가지는 것을 보장한다는 점이며, Origin가 작동하지 않는 상황에서도 Replica를 통해 올바른 데이터를 사용할 수 있음을 알 수 있다는 것입니다.
  • 이 방식의 단점은 네트워크 지연 문제, Replica의 처리 지연, 장애 발생 등으로 인해 확인 요청에 대한 응답을 하지 않는다면, 새로운 데이터를 쓰는 것이 불가능한 상태에 빠진다는 점입니다. 이때 기본적으로 Origin은 Replica가 응답하기 전까지 Blocking 상태에 빠집니다.
해당 구조에서는 Origin에 장애가 발생하지 않더라도 Replica 문제로 인해 이를 바라보고 Write 하는 모든 서비스로 장애가 전파될 수 있습니다.

 

 

또한 Scale-out이 될수록 발생하는 응답을 수신하는 지연 시간이 길어지고 위와 같은 상황이 발생할 확률이 증가하여 서비스의 확장성이 매우 떨어지게 됩니다.

 

 

 

Semi-synchronous Replication은 Synchronous Replication을 현실적으로 풀어내는 방법 중 하나입니다. 해당 방식은 한 시점에 최소한 하나의 Replica와 Synchronous Replication를 진행하는데, 해당 Replica가 추가된 Transaction을 반영하면 Origin에 해당 내용을 반영합니다. 이를 통해 최소한의 데이터 무결성을 보장합니다.

  • 그 외 Replica들은 Asynchronous Replication을 수행하는 방식이며, Synchronous Replication를 진행하는 대상은 현재 (Synchronous Replication) 대상에 장애 혹은 처리 지연이 발생하여 변경될 수 있습니다.
  • 이 방식의 장점은 적어도 하나 2개의 서비스에서 데이터 일관성과 무결성을 보장한다는 점이며 그러면서도 Synchronous Replication의 단점을 최소화한다는 것입니다.
  • 이 방식의 단점은 최신의 데이터를 가진, 즉 데이터 일관성과 무결성을 보장하는 Replica가 Synchronous Replication을 수행 중인 Replica가 아닐 수 있다는 점이며 또한 여전히 다른 Replica와 Origin 간의 데이터 불일치가 발생한다는 점입니다.

조회하는 Node에 따라 데이터 일관성 깨질 수 있는 문제는 여전하지만, 현재 Node 개수와 대비하여 읽어올 읽기-쓰기 Node 개수를 조정하면 최종적으로 지연 시간과 일관성을 보완할 수 있습니다.

또한 Synchronous Replication을 통해 데이터를 넘겨받는 Node가 꼭 최신 상태가 아닐 수 있습니다. MySQL의 Bin Log를 활용한 Semi-Sync Replication은 모든 Replica가 동일한 Bin log를 바라보고 있고, 결국에는 동일한 Transaction을 같은 시점에 수신받을 수 있기 때문에 Asynchronous Replication을 수행하는 Replica가 오히려 먼저 최신 상태에 도달할 수 있습니다.

기타 설명된 동작은 구현 방식에 따라 달라질 수 있습니다.

 

 

 

Asynchronous Replication은 Origin에 반영된 데이터 정보를 Replica들이 지속적으로 수집하여 반영하는 High Availability에 좀 더 초점을 맞춘 방식입니다.

  • 이 방식의 장점은 서비스에서 요청한 데이터의 Write를 재시도를 통해 계속 보장할 수 있다는 점이며, Synchronous Replication에 비해 쓰기에 대한 서비스 지연 시간이 짧아지고 확장성이 좋다는 것입니다.
  • 이 방식의 단점은 Origin에 장애가 발생하였을 경우 네트워크 지연 등으로 인해 최신 데이터가 유실될 수 있다는 점과 Origin와 Replica 간의 데이터 일관성이 일정 시간이 지나야 만족된다는 점이 있습니다.(Eventual Consistency)
부가적으로 Asynchronous Replication을 수행하면서 새로운 Origin을 선출하였더라도 이전 Origin이 반영했던 혹은 반영 중이던 데이터를 온전히 가지지 못하게 됩니다.

 

 

이러한 문제를 해결하는 방법은 단순히 무시하고 넘어가는 것이지만 이 경우 서비스의 내구성이 매우 떨어지게 됩니다. 시스템의 확장성이나 HA가 높은 것은 좋지만 저장되어야 할 데이터의 유실은 도메인, 서비스에 따라서 심각한 문제가 되기 때문입니다.

 

그렇기에 지속적으로 좋은 성능과 HA를 제공하면서도 데이터 유실이 없는 복제 방법이 개발되고 있는 상황입니다. 이를 통해 성공적인 결과물로 구현된 방식 중 하나로는 체인 복제 방식이 있습니다.

 

 

 

 

기타

Trigger-Based Replication은 특정 데이터나 이기종 데이터베이스 간의 Replication을 위해 서비스에서 Oracle의 Golden gate, Batch Framework, 데이터베이스의 Trigger나 Procedure를 활용하는 방식입니다.

 

서비스에 복제를 위한 코드를 등록하고 데이터베이스 시스템에서 데이터가 변경이 있을 때 이벤트 등을 받거나 직접 실행하도록 하여 데이터를 가공하고 다른 서비스로 전달시킬 수 있습니다.

  • 이 방식의 장점은 해당 방식은 다른 복제 방식에 비해 유연하다는 점입니다.
  • 이 방식의 단점은 다른 복제 방식에 비해 많은 Overhead를 동반한다는 점이며, 이를 수행하는 서비스 또한 장애 지점이 될 수 있고, 작성된 코드를 통한 버그가 발생할 확률이 매우 높습니다.
Oracle의 Databus나 Postgresql의 Bucardo가 이러한 방식으로 동작합니다.

 

 

 

 

마치면서. 현재 제가 있는 팀에서는 Galera Cluster라는 Opensource를 도입하여 사용하고 있습니다. 이것은 설정에 따라 특정 동작에서 Semi-synchronous Replication과 Asynchronous Replication 방식을 지원하는 MySQL, MariaDB Cluster입니다. 요약하고 넘어가자면,

  • Multi Master 구조의 경우에는 데이터 일관성 유지를 위해 Certification-Based Replication를 사용하게 됩니다. 이는 Dead lock의 주범입니다..
  • Replica들은 기본적으로 Asynchronous Replication을 통해 데이터를 처리하다가 recv queue의 정해진 임계 값을 만족했을 때 해당 Replica와 Flow Control이라는 것을 발생시켜 이때에는 Asynchronous Replication를 중지하고 recv queue에 있는 것을 처리하는 방식으로 구현되어 있습니다.

 

Galera Cluster에서 설명하는 것을 보면 virtually synchronous replication을 수행한다고 합니다. 이는 논리적으로 완전 동기이나 write나 commit이 별개로 수행되고 replication은 비동기로 동작하기 때문에 그렇게 명명되었다고 하네요..

 

 

 

 

참고 자료

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

 

 

장애 감지 알고리즘이 필요한 이유?

우리가 개발하는 서비스가 단일 시스템이 아닌 분산 시스템 구조를 지닌다면 특정 서비스에 장애가 발생하더라도 모든 서비스 노드에게 전파되지 않기 때문에, 사용자 요청으로 인해 식별되기 전까지 모를 수 있습니다.

 

장애가 발생한 서비스에 따라서 시스템에 잘못된 값을 설정하거나 중복이 발생할 수 있고 (Index Sharding Service) 사용자가 보는 캐싱된 데이터가 소실되는 경우도 발생할 수 있습니다. (Feed Cache Service 등)

  • 물론 모든 장애가 서비스 종료, 충돌과 같은 완전 중단 상태를 의미하는 것은 아니며, 상황에 따라 부하로 인한 응답 지연, 네트워크 지연 이슈 혹은 데드락으로 인한 것일 수 있습니다.(이런 상태는 시간이 지나면 해결되는 문제이기 때문입니다.) 그렇기에 좀 더 정확한 감지 방식이 필요합니다.
이러한 장애 상황을 빠르게 대처하기 위해 필요한 것이 장애 감지 알고리즘 혹은 감지 방식 입니다. 이러한 방식은 실제 구현하는 장애 감지 서비스뿐만 아니라, Redis, Zookeeper, Kafka, Cassandra, Akka 등의 Opensource 등에서도 사용되고 있습니다.

 

 

 

 

장애 감지 알고리즘의 속성?

장애 감지 알고리즘의 속성으로는 크게 완전성, 효율성, 정확성이 있으며 이중 효율성과 정확성은 사용하는 방법에 따라서 조정이 가능한 속성입니다.

특정 자료에서는 완성도와 정확도 만을 이야기하기도 합니다.

 

 

완전성

특정 서비스에 장애가 발생하면 모든 서비스는 해당 서비스에 장애가 발생한 사실을 인지하고 이와 관련된 처리를 수행하여 결과를 반환하여야 한다는 속성입니다.

 

 

효율성

얼마나 빨리 장애를 감지하는지 평가하는 속성이자 척도입니다. 이는 장애 상태에 빠진 서비스를 얼마나 빨리 감지하느냐 에 따라서 효율성을 결정하게 됩니다.

 

 

정확성

얼마나 정확하게 장애를 감지했는지 평가하는 속성이자 척도입니다. 이 속성은 위에서 잠깐 언급했던 것과 같이 정상적인 서비스이지만 다량의 요청을 처리하며 발생하는 부하, 네트워크 지연 이슈 혹은 데드락으로 인해 일정 시간 동안 응답하지 못하는 상태의 서비스를 장애로 판단하느냐 에 따라서 정확성을 결정하게 됩니다.

 

 

효율성과 정확성의 트레이드 오프

정확성을 설명하면서 장애 감지 시스템의 결과가 거짓일 수 있다는 점을 알려드렸습니다. 즉 정상 프로세스에 장애가 발생하였다고 잘못 판단하는 경우가 있는데 이러한 경우는 효율성이 높은 알고리즘에서 주로 발생하게 됩니다.

 

효율성을 중시하는 알고리즘일수록 빠르게 결과를 도출하기 위해 검증하는 시간과 로직을 최소한으로 하기 때문입니다. 그렇다면 반대로 정확성을 중시하는 알고리즘은 장애를 감지하고 판단하는데 시간이 오래 걸리지만 좀 더 적은 리소스로 서비스의 상태를 관리할 수 있습니다.

  • 여기서 말하는 리소스는 이는 잘못된 결과로 인해 서비스를 죽이고 다시 배포하는 등의 작업 리소스를 이야기합니다.

 

결국 효율성과 정확성을 완벽하게 충족하는 장애 감지 서비스는 없으며, 상황에 따라서 올바른 감지 시스템을 구성하고 사용하는 것이 중요합니다.

 

 

 

 

Ping과 Heartbeat

Ping과 Heartbeat는 유사한 장애 감지 방식입니다. 장애를 감지하는 서비스는 주기적으로 다른 서비스들에게 메시지를 보내고(broadcasting) 결과를 받아 정상적인 상태임을 판단하게 됩니다.

이 2가지 방식은 Timeout을 사용하느냐(Ping), 하지 않느냐(Heartbeat)로 구분 지을 수 있습니다.

여기에서 설명하는 Heartbeat는 All to All 방식의 Heartbeat입니다. 

 

 

Ping은 메시지를 보내는 주기와 Timeout 설정을 기준으로 감지 정확도가 좌우됩니다. 그렇기에 서비스 A에게 보낸 1번째 메시지의 응답이 2번째 메시지 전송 이후에 도착한다면, 장애 감지 시스템은 이 서비스를 장애가 발생한 서비스로 판단할 수 있습니다.

  • 즉 특정 서비스의 상태를 파악할 수 있는 방법이 아니며, 요청에 대한 결과라는 단순한 지표를 기준으로 장애를 탐지하게 됩니다.
  • 이는 완전성을 지키고 효율성 또한 극대화되었으나, 정확도가 낮은 장애 알고리즘임을 의미합니다.

 

 

Heartbeat는 Timeout을 사용하지 않는 대신에 각각의 서비스가 특정 서비스에 몇 번의 요청을 전송하였는지 기록하여 설정된 임계 값에 도달하였을 때 장애로 판단하는 방식입니다. 그렇기에 정확도를 높이기 위해서는 적절한 임계 값 설정이 필요합니다.

  • 초기에 전송되는 메시지에는 발신자의 정보와 동일 메시지의 중복 전송을 방지하기 위해 고유한 식별자를 기록하게 됩니다.

 

 

 

Heartbeat outsourcing

Heartbeat outsourcing은 기본적으로 Heartbeat의 동작 방식을 따르나 각각의 서비스가 모든 서비스에게 메시지 요청을 보내는 것(broadcasting) 이 아니라 인접한 서비스에만 메시지를 날려 장애를 감지하고 판단하는 방식입니다.

 

그렇기에 각 서비스는 모든 서비스 존재를 알고 있지 않도록 범위를 나누어 메시지 요청으로 인한 장애 감지의 신뢰성과 떨어진 효율성을 어느 정도 보완하게 됩니다.

  • 각 서비스는 주변에 위치하는 서비스들에게만 요청을 전파하는 방식으로 동작합니다.
  • Heartbeat를 사용하는 서비스의 수가 수백 또는 수천 개라고 가정하였을 때, 패킷이 지나는 네트워크의 크기가 커지고 자연스레 네트워크 이슈가 발생할 여지도 증가하여 장애로 잘못 판단하는 경우가 늘어날 수 있기 때문입니다.

 

 

 

φι-accrual detection

https://medium.com/@arpitbhayani/phi-&amp;amp;amp;amp;amp;phi;-accrual-failure-detection-79c21ce53a7a

 

이 방식은 서비스의 상태를 프로세스 충돌 확률을 연속적으로 계산하고(Time series) 이를 통해 장애를 결정하는 방식입니다.

  • cassandra, akka, Hazelcast 등의 opensource가 해당 방식을 사용 및 지원하고 있습니다.

 

우선 특정 배열이나 리스트 형태의 자료구조 또는 TDB에 메시지 응답 도착 시간을 저장하고 Sliding Window 알고리즘을 활용하여 일정 범위의 값을 읽어와 다음 메시지의 응답 시간 또는 응답 확률을 예측한 뒤 실제 메시지를 요청하여 연산된 결과와 비교합니다.

 

그리고 비교한 결과를 통해 Suspicion level을 계산하여 이를 기준으로 노드의 중단, 부하 또는 정상 상태를 판단하게 됩니다.

  • Suspicion level 임계 값을 동적으로 조정할 수 있으며, 다양한 임계 값을 지정하여 여러 가지 행위를 수행할 수도 있습니다.

 

φι-accrual detection을 사용하기 위해서는 지속적인 메시지에 대한 표본을 구하고 이를 저장할 자료구조를 가지고 있어야 하며, 표본에 대한 평균과 분산 값을 계산하여 분포 도를 추정한 뒤 도착할 확률과 프로세스 활성 상태의 정확도를 나타내는 φι를 계산해야 합니다.

  • 또한 특정 임계 값을 지정하여 상태를 결정하는 방법과 상태에 따른 Callback을 구성하여야 합니다.

 

 

 

 

참고 자료

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다. 그림에는 Redis가 표시되어 있지만 Ehcache, Memcached, Hazelcast, Caffeine Cache 등 다양한 구현체로 이를 구현할 수 있습니다. 

 

 

 

Read Cache Strategy

 

Look Aside Cache

서비스에 사용자 조회 요청이 들어오면 우선 Cache에 저장된 데이터가 있는지 확인하는 전략입니다.

  • 초반 Cache 성능 향상 및 트래픽 급증 대비를 위해 (Cache miss 감소) cache warm up을 수행하는 것이 좋습니다.

 

이 방식은 Cache에 장애가 발생하더라도 데이터베이스에 요청을 전달함으로써 Cache 장애로 인한 서비스 중단 발생을 대비할 수 있습니다. 하지만 조회 요청이 많을 경우 Cache에서 발생한 장애가 데이터베이스로 전파되어 전체 장애가 발생할 수 있습니다..

  • 그리고 Cache에서 사용되는 데이터 모델이 데이터베이스의 모델(테이블)과 다른 것 또한 하나의 특징입니다. 예시로 최근에 조회한 글의 조회 수라거나, Query를 통해 만들어진 결과가 Cache에 저장될 수 있습니다.
  • Spring Abstraction Cache module에서 제공하는 기본 구현체가 이 방식을 따르고 있습니다.

 

Cache warm-up or Cache warming : 미리 Cache로 데이터베이스의 데이터를 밀어 넣어두는 작업을 의미합니다. 이 작업을 수행하지 않는다면 서비스 초반, 트래픽 급증 시 발생하는 대량의 Cache miss + Cacheable 작업으로 인해 Cache와 데이터베이스의 부하가 급증하는 Thundering Herd가 발생할 수 있습니다.

물론 warm-up을 통해 추가되었던 Cache가 expire 된다면 다시 Thundering Herd가 발생할 여지가 있으므로 이를 원천적으로 해결하기 위해서는 Probabilistic Early Recomputation 방식 등과 같이 Cache의 TTL이 만료되기 전에 갱신시키는 방법 등을 고려하여야 합니다.

Thundering Herd는 모든 지점에서 발생되는 것은 아니며, 서비스의 첫 페이지와 같은 대부분의 조회가 몰리는 지점에서 주로 발생됩니다.

 

 

 

Read Through (Inline-Cache)

서비스에 사용자 조회 요청이 들어오면 Cache를 조회하고 Cache miss가 났을 때 데이터베이스에 저장된 데이터를 조회하여 Cache를 업데이트한 뒤 값을 반환하는 전략입니다. 

  • 이 방식 또한 서비스 운영 초반에 cache warm up을 수행하는 것이 좋습니다.

 

이 방식은 직접적인 데이터베이스 접근을 최소화하고 Read에 대한 소모되는 Resource를 최소화할 수 있습니다. 하지만 Cache에 장애가 발생하였을 경우 이는 바로 서비스 전체 중단으로 빠질 수 있습니다. 그렇기에 Cache를 제공하는 Redis과 같은 구성 요소를 Replication 또는 Cluster로 구성하여 가용성을 높여야 합니다.

  • 이 방식은 앞서 설명한 Look Aside 방식과는 달리 데이터베이스와 동일한 데이터 구조를 지니며, Query의 대상(모델)은 Cache에 저장되어 있습니다.

 

 

 

 

Write Cache Strategy

 

Write Around

데이터베이스에 데이터를 저장하고 Cache miss가 발생했을 때 Cache로 데이터를 가져오는 쓰기 전략입니다. 이는 Look aside cache의 Cache miss 발생 시 흐름을 이야기합니다.

  • Cache miss가 발생하기 전에 데이터베이스에 저장된 데이터가 수정되었을 때, 사용자가 조회하는 Cache와 데이터베이스 간의 데이터 불일치가 발생하게 됩니다.

 

데이터 불일치를 방지하기 위해서는 데이터베이스에 저장된 데이터가 수정, 삭제될 때마다 저장해둔 Cache 또한 삭제하거나 변경해야 하며 데이터 수정 사항이 빈번할 수록 Cache의 expire를 짧게 조정하는 식으로 대처하여야 합니다.

데이터 수정, 삭제 요청이 너무 빈번한 경우 Cache를 변경하거나 삭제하는 비용이 조회하는 비용보다 커지게 될 수 있기 때문에 잘 고려하여 적용하여야 합니다.

 

 

 

Write Through

데이터베이스와 Cache에 동시에 데이터를 저장하는 전략입니다. 이 방법은 Write Around 전략에서 발생하는 데이터 불일치 문제가 발생하지 않지만 (Cache에도 계속 최신의 데이터가 반영되기 때문에) 매 요청마다 2번의 Write가 발생하게 됨으로써 빈번한 생성, 수정이 발생하는 서비스 영역에서는 성능 이슈(부하)가 가중될 수 있습니다.

  • 이 전략을 Read through 전략과 결합하여 사용한다면 최신 데이터 보장을 통해 Cache의 수정이나 변경을 하지 않으면서도 안정적으로 Read Through 전략의 이점을 끌어올릴 수 있습니다.

 

 

 

Write Back

Cache에 데이터를 저장, 반영하고 일정 주기(스케줄링)의 배치 작업을 통해 데이터를 데이터베이스에 반영하는 전략입니다.

  • 이 방법은 Write가 빈번하면서 Read를 하는데 많은 양의 Resource가 소모되는 서비스 영역의 경우 (data set이 큰 경우) 적용해볼 수 있는 방법입니다. (데이터베이스를 조회하지 않고 지속적인 접근이 가능하도록 합니다.)
  • 데이터의 불일치나 조회 요청으로 인한 서비스 부하는 발생하지 않으나, In-memory에 데이터가 일정 기간 저장되기 때문에 장애가 발생하였을 때 데이터 손실이 발생할 수 있습니다.
    • 이 또한 Replication이나 Cluster 구조를 적용함으로써 Cache 서비스의 가용성을 높여야만 합니다.

 

이 전략을 통해 새로운 데이터를 추가하거나 변경하는 성능을 끌어올릴 수 있고 또한 읽어오는 데이터의 크기를 의식하지 않을 수 있습니다. 부가적으로 데이터베이스 장애가 발생하더라도 지속적인 서비스를 제공할 수 있도록 보장합니다.

 

 

 

 

참고 자료

 

+ Recent posts