Redis Scale up 과정에서 약 45초간 장애가 발생했다. 해당 장애는 서비스의 레디스 클러스터가 스케일업되면서 도메인에 연결된 IP 주소가 변경되었지만, 클라이언트에서 이를 즉시 반영하지 못해 발생했다.
Redis 복제 시간인 15초에 더해 JVM DNS Cache의 기본 설정인 30초 동안 이전 IP를 참조하면서 총 45초간 장애가 이어졌다.
fyi; 관련 코드
Integer tmp = java.security.AccessController.doPrivileged(
new PrivilegedAction<Integer>() {
public Integer run() {
try {
String tmpString = Security.getProperty(cachePolicyProp);
if (tmpString != null) {
return Integer.valueOf(tmpString);
}
} catch (NumberFormatException ignored) {
// Ignore
}
try {
String tmpString = System.getProperty(cachePolicyPropFallback);
if (tmpString != null) {
return Integer.decode(tmpString);
}
} catch (NumberFormatException ignored) {
// Ignore
}
return null;
}
});
java.security.Security에서 networkaddress.cache.ttl
값이 없으면, 시스템 프로퍼티에서 sun.net.inetaddr.ttl
값을 조회하여 TTL 을 설정한다.
값이 없는 경우, 30초를 TTL 로 사용한다.
tmp = java.security.AccessController.doPrivileged(
new PrivilegedAction<Integer>() {
public Integer run() {
try {
String tmpString = Security.getProperty(negativeCachePolicyProp);
if (tmpString != null) {
return Integer.valueOf(tmpString);
}
} catch (NumberFormatException ignored) {
// Ignore
}
try {
String tmpString = System.getProperty(negativeCachePolicyPropFallback);
if (tmpString != null) {
return Integer.decode(tmpString);
}
} catch (NumberFormatException ignored) {
// Ignore
}
return null;
}
});
java.security.Security에서 networkaddress.cache.negative.ttl
값이 없으면, sun.net.inetaddr.negative.ttl
값을 조회하여 TTL 로 사용한다.
# grep 'networkaddress.cache.negative.ttl' /Users/122d6417/Library/Java/JavaVirtualMachines/corretto-22.0.2/Contents/Home/conf/security/java.security
networkaddress.cache.negative.ttl=10
이미 networkaddress.cache.negative.ttl
이 10으로 설정되어있어, sun.net.inetaddr.negative.ttl
으로는 TTL 설정이 불가능하다.
java -Dsun.net.inetaddr.ttl=0 -jar your-application.jar
$JAVA_HOME/conf/security/java.security 파일을 편집하여 DNS Cache 설정을 변경할 수 있다.
networkaddress.cache.ttl=0
networkaddress.cache.negative.ttl=0
InetAddressCachePolicy가 JVM 초기화 시점에 클래스 로딩이 이루어지기 때문에, main 메서드에서 동적으로 설정한 값은 무시된다.
import java.security.Security;
public class ApiApplication {
public static void main(String[] args) {
// 성공한 DNS 조회 결과의 캐시 TTL 설정
Security.setProperty("networkaddress.cache.ttl", "0"); // 0은 캐시 비활성화
// 실패한 DNS 조회 결과의 캐시 TTL 설정
Security.setProperty("networkaddress.cache.negative.ttl", "0"); // 0은 캐시 비활성화
SpringApplication.run(PrivateApiApplication.class, args);
}
}
위 설정은 DNS 조회 결과가 JVM의 전역 캐시에 저장되기 때문에, 같은 JVM에서 실행 중인 모든 애플리케이션과 클라이언트가 영향을 받는다. Redis 클러스터에서만 빠른 IP 감지가 필요하다면 Lettuce 설정을 변경하여 DNS Cache 설정을 변경할 수 있다.
Lettuce에서 DnsAddressResolverGroup을 사용하면 DNS 조회 시 JVM DNS Cache를 우회한다.
Dependency 추가 후에 Configuration 설정을 다음과 같이 해보자.
implementation 'io.netty:netty-resolver-dns:4.1.115.Final'
@Bean
public ClientResources clientResources() {
NioEventLoopGroup dnsEventLoopGroup = new NioEventLoopGroup(1);
DnsAddressResolverGroup dnsAddressResolverGroup = new DnsAddressResolverGroup(
new DnsNameResolverBuilder(dnsEventLoopGroup.next())
.datagramChannelType(NioDatagramChannel.class)
.resolveCache(NoopDnsCache.INSTANCE)
.cnameCache(NoopDnsCnameCache.INSTANCE)
.authoritativeDnsServerCache(NoopAuthoritativeDnsServerCache.INSTANCE)
.consolidateCacheSize(0)
);
return ClientResources.builder()
.addressResolverGroup(dnsAddressResolverGroup)
.eventExecutorGroup(dnsEventLoopGroup)
.build();
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.clientResources(clientResources())
.build();
var redisConnect = new RedisStaticMasterReplicaConfiguration(primaryHost, primaryPort);
redisConnect.addNode(replicaHost, replicaPort);
return new LettuceConnectionFactory(
redisConnect,
clientConfig
);
}
fyi; Lettuce 공식 문서
netty 의존성 추가 시에 lettuce 에서는 DefaultDnsAddressResolverGroupWrapper 을 사용하게 된다. 이 리졸버는 DNS 서버에서 반환하는 TTL 값을 기반으로 캐싱 동작을 수행하므로, 캐싱 동작이 DNS 서버의 설정에 의존적이다. 따라서 캐시를 사용하지 않으려면 커스텀 리졸버를 직접 설정하여 사용해야 한다.
NioEventLoopGroup dnsEventLoopGroup = new NioEventLoopGroup(1);
DnsAddressResolverGroup dnsAddressResolverGroup = new DnsAddressResolverGroup(
new DnsNameResolverBuilder(dnsEventLoopGroup.next())
.datagramChannelType(NioDatagramChannel.class)
.resolveCache(NoopDnsCache.INSTANCE)
.cnameCache(NoopDnsCnameCache.INSTANCE)
.authoritativeDnsServerCache(NoopAuthoritativeDnsServerCache.INSTANCE)
.consolidateCacheSize(0)
);
권한 DNS 서버(Authoritative DNS Server)와 캐싱 DNS 서버(Caching DNS Server)의 차이
권한 DNS 서버는 도메인의 공식 DNS 레코드를 보유하며, 최종적인 응답을 제공한다. 캐싱 DNS 서버는 클라이언트의 DNS 요청을 받아 캐시에 저장된 데이터로 응답한다. 캐시에 데이터가 없는 경우 권한 있는 DNS 서버에 쿼리를 보내 정보를 가져온다.
# nslookup -type=ns example.com
Server: 192.168.1.1
Address: 192.168.1.1#53
Non-authoritative answer:
example.com nameserver = ns1.example.com.
example.com nameserver = ns2.example.com.
Authoritative answers can be found from:
ns1.example.com internet address = 203.0.113.1
ns2.example.com internet address = 203.0.113.2
# dig example.com
; <<>> DiG 9.10.6 <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 1
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 3600 IN A 93.184.216.34
;; AUTHORITY SECTION:
example.com. 172800 IN NS b.iana-servers.net.
example.com. 172800 IN NS a.iana-servers.net.
;; ADDITIONAL SECTION:
a.iana-servers.net. 172800 IN A 199.43.135.53
b.iana-servers.net. 172800 IN A 199.43.133.53
;; Query time: 35 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Dec 20 15:00:00 2024
;; MSG SIZE rcvd: 154