Skip to content

Commit 323af98

Browse files
committed
feat: new post
1 parent 96bffb5 commit 323af98

5 files changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: 작성대기
3+
author: rojae
4+
date: 2025-07-05 16:20:15 +0900
5+
published: false
6+
categories: [backend]
7+
tags: [java, spring, dns, connection-pool, service-outage]
8+
mermaid: true
9+
image:
10+
path: /assets/img/posts/2025-07-05-jvm-dns-caching-meets-connection-pools/jvm-dns-caching-meets-connection-pools.jpg
11+
---
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
---
2+
title: JVM의 DNS 캐싱과 연결 풀 - (IP 변경으로 인한 프로덕션 서비스 장애)
3+
author: rojae
4+
date: 2025-07-05 16:20:15 +0900
5+
published: true
6+
categories: [backend]
7+
tags: [java, spring, dns, connection-pool, service-outage]
8+
mermaid: true
9+
image:
10+
path: /assets/img/posts/2025-07-05-jvm-dns-caching-meets-connection-pools/jvm-dns-caching-meets-connection-pools.jpg
11+
---
12+
> 운영 중인 서비스에서 IP 변경이 어떻게 장애로 이어질 수 있었는지, 이를 해결하며 얻은 인사이트를 정리해 둡니다.
13+
> + 영문 버전은 [Medium](https://medium.com/p/0b1cadd08ba2/edit)에서 확인하실 수 있습니다.
14+
{: .prompt-info }
15+
16+
---
17+
18+
## **“문제는 예고 없이 찾아온다”**
19+
20+
얼마 전 실 서비스 중에 문제를 겪었다.
21+
운영 중인 세션 서비스의 라우팅 IP를 변경했지만, DNS 이름은 그대로 유지했었다.
22+
23+
나를 포함한 팀원들은 별다른 문제없이 새 IP로 잘 붙을 거라고 예상했는데, 그게 아니었다.
24+
25+
동료가 세션 서비스의 IP를 바꾼 뒤에도 DNS 이름이 같았던 탓에, 체크아웃 서비스 쪽에서 504 게이트웨이 타임아웃이 발생했고, 다른 팀에서도 장애를 호소했다.
26+
결국 원인을 살펴보니, 체크아웃 서비스가 새 IP를 받아오지 못하고 예전 IP로 계속 요청을 보내고 있었던 거다.
27+
28+
바로 체크아웃 서비스의 파드를 롤아웃해서 다시 띄우자 정상적으로 새 IP로 연결되었고, 곧바로 서비스도 복구됐다.
29+
30+
---
31+
32+
## **"팀원들과 논의"**
33+
<figure style="text-align: center;">
34+
<img
35+
src="/assets/img/posts/2025-07-05-jvm-dns-caching-meets-connection-pools/discussion.jpg"
36+
alt="서로 눈치보는 논의"
37+
width="500"
38+
style="border-radius: 8px;">
39+
<figcaption style="margin-top: 0.5rem; font-size: 0.95rem; color: #666;">
40+
we are team
41+
</figcaption>
42+
</figure>
43+
44+
*나: "혹시, 세션 서비스의 라우트 IP를 왜 변경했는지 설명해줄 수 있을까요?"*
45+
46+
*팀원 1: "라우트 IP를 바꿔도 문제없이 잘 동작할 거라고 생각했어요. DNS는 동일하니까요"*
47+
48+
*팀원 2: "HTTP 옵션, 예를 들어 keep-alive 같은 부분도 신경 써야 하지 않을까요?"*
49+
50+
51+
---
52+
53+
## **"지금 다시 생각해보면서, 정리해보니.."**
54+
55+
```mermaid
56+
sequenceDiagram
57+
participant Checkout as Checkout Service (Pod)
58+
participant DNS as session.address.com (DNS)
59+
participant SessionA as Session Service (IP A)
60+
participant SessionB as Session Service (IP B)
61+
62+
Checkout->>DNS: Resolve session.address.com
63+
DNS-->>Checkout: Returns IP A
64+
Checkout->>SessionA: Sends requests
65+
Note over Checkout,SessionA: Communication OK (200)
66+
67+
Note over SessionA: Teammate changes to IP B
68+
SessionA-->>DNS: Updates to IP B
69+
70+
Checkout->>SessionA: Sends requests
71+
Note over Checkout,SessionA: Communication failed (504)
72+
73+
Checkout->>SessionB: Sends requests
74+
Note over Checkout,SessionB: Communication restored after rolling out the Checkout Service
75+
```
76+
77+
음… 😕
78+
79+
정리하자면, “팀원이 세션 서비스의 라우트 IP를 변경했지만 DNS 이름은 그대로 뒀다”는 거다.
80+
81+
사실 나도.. 체크아웃 서비스가 DNS를 통해 새 IP를 자동으로 찾아갈 거라고 예상했는데, 왜 계속 타임아웃이 났을까?
82+
83+
DNS가 새 주소를 받아야 정상일 텐데.
84+
아마 캐시 문제나 TTL(수명)이 너무 길어서 이전 IP를 계속 쓰고 있었던 것 같기도 하다.
85+
DNS 레코드는 멀쩡해 보여도, 그 때문에 체크아웃 서비스가 세션 서비스에 도달하지 못했을 가능성이 있을 것 같다.
86+
87+
---
88+
89+
## **"Java에서 DNS가 어떻게 해석되는가?"**
90+
<figure style="text-align: center;">
91+
<img
92+
src="/assets/img/posts/2025-07-05-jvm-dns-caching-meets-connection-pools/InetAddress.png"
93+
alt="InetAddress.java"
94+
width="650"
95+
style="border-radius: 8px;">
96+
<figcaption style="margin-top: 0.5rem; font-size: 0.95rem; color: #666;">
97+
InetAddress.java
98+
</figcaption>
99+
</figure>
100+
101+
이 부분은 `디버깅`을 통해서 확인이 가능했다.
102+
Java Core에 포함된 `InetAddress.java`가 DNS(호스트)를 IP 주소로 해석해주는 역할을 한다.
103+
104+
**그러나 아래 2가지 현상 때문에 의아했다.**
105+
106+
1. *DNS 해석이 매번 정상적으로 동작하지 않았다.*
107+
2. *통신 오류가 발생한 이후에도, 리졸버가 새 IP를 안정적으로 받아오지 못하고 계속 예전 IP로만 연결을 시도했다.*
108+
109+
원인은 같아 보이지만, 사실상 **완전히 다른 문제**라고 볼 수 있다.
110+
111+
---
112+
113+
## **"networkaddress.cache.* in JVM"**
114+
- 1. *DNS 해석이 매번 정상적으로 동작하지 않았다.*
115+
→ 답은 `/java/security` 파일(JVM Option)에 있었다.
116+
117+
대부분의 `Java API 통신 라이브러리`**Java Core의 DNS 해석 메커니즘**을 그대로 사용한다.
118+
이때, `DNS에서 반환된 IP 주소`**메모리에 캐싱**해서 애플리케이션의 성능을 높인다.
119+
120+
주요 옵션으로는 다음과 같다.
121+
- networkaddress.cache.ttl : 긍정적(정상) 캐시의 TTL
122+
- networkaddress.cache.negative.ttl : 부정적(실패) 캐시의 TTL
123+
124+
---
125+
126+
## **"HTTP Connection Pool Considerations"**
127+
- 2. *InetAddress.java 안에 있는 리졸버가 새 IP를 안정적으로 가져오지 못했다. 통신 실패가 발생한 이후에도 계속 같은 IP로만 연결을 시도했다.*
128+
→ 답은 Http Connection Pool의 설정이 잘못되어 있었기 때문이다.
129+
130+
요청서버(`checkout-server`)는 `spring-cloud-feign`을 사용하고 있었고 아래와 같은 방식이였다.
131+
132+
```java
133+
@Configuration
134+
public class FeignHttpClientConfig {
135+
136+
@Bean
137+
public Client feignClient() {
138+
return new ApacheHttpClient(HttpClientBuilder.create().build());
139+
}
140+
141+
}
142+
```
143+
144+
하지만 이 방식은 안전하지 않다.
145+
일부 서버가 200 OK를 반환하지 않더라도, 동일한 IP에만 계속 연결을 시도하고 DNS를 다시 조회하지 않는다.
146+
147+
그래서 504 오류가 발생했을 때 연결을 선제적으로 끊는 등의 폴백(fallback) 메커니즘이 분명히 필요하다는 점이 명확해졌다.
148+
149+
**아래는 원인 분석을 끝낸 뒤 작성한 최종 다이어그램이다.**
150+
151+
```mermaid
152+
sequenceDiagram
153+
participant Checkout as Checkout Service (Pod)
154+
participant DNS as session.address.com (DNS)
155+
participant SessionA as Session Service (IP A)
156+
participant SessionB as Session Service (IP B)
157+
158+
Checkout->>DNS: Resolve session.address.com
159+
DNS-->>Checkout: Returns IP A
160+
Checkout->>SessionA: Sends requests
161+
Note over Checkout,SessionA: Communication OK
162+
163+
Note over SessionA: teammate changes to IP B
164+
SessionA-->>DNS: Updates to IP B
165+
166+
Checkout->>SessionA: Continues using cached IP A
167+
SessionA-->>Checkout: Connection fails
168+
Note over Checkout: JVM still caches IP A
169+
170+
Checkout->>DNS: Eventually queries again because of connection pool configuration
171+
DNS-->>Checkout: Returns IP B
172+
Checkout->>SessionB: Sends requests
173+
Note over Checkout,SessionB: Recovered after pod restart
174+
```
175+
176+
결국 이 문제는 JVM의 IP 캐싱과 HTTP 커넥션 풀 때문에 발생했다.
177+
178+
## **"라이브 환경에서 IP 주소를 변경할 때"**
179+
아래 **두 가지 요소**를 반드시 기억해야 한다.
180+
- JVM TTL 옵션 (java/security 파일의 networkaddress.cache.*)
181+
- HTTP 커넥션 풀 팩토리 (애플리케이션 설정)
182+
183+
**결론적으로,** JVM의 DNS 캐싱은 흔히 간과되지만 서비스 가용성에 치명적인 영향을 줄 수 있다.
184+
커넥션 풀과 결합되면, 오래된 DNS 레코드가 훨씬 더 오래 유지될 수 있으며, 그 결과 IP가 변경될 경우 예기치 못한 장애로 이어질 수 있다.
185+
186+
187+
---
188+
189+
## **"느낀 점"**
190+
191+
JVM의 DNS 캐싱은 의외로 사람들이 많이 간과한다.
192+
193+
운영 환경에서는 DNS가 바뀌거나, 로드밸런서가 교체되거나, 라우팅이 바뀌는 일이 흔하다.
194+
근데 JVM이 너무 똑똑하게(?) 예전 IP를 계속 기억하고 있으면, 그게 그대로 장애로 터질 수 있다.
195+
196+
커넥션 풀도 마찬가지다.
197+
198+
“재사용”은 성능 면에서는 좋지만, 장애가 발생하면 그 재사용이 오히려 문제를 키울 수 있다.
199+
200+
- `JVM``networkaddress.cache.ttl`에 대해서 어느정도 인식하고 있자.
201+
- `Connection pool`은 장애 상황을 고려해서 `fallback` 로직을 반드시 설계하자.
202+
203+
*IP가 변경되면 “즉시” 반영될 거라고 쉽게 믿지 말하야 한다.*
204+
205+
**JVM DNS 캐싱 + Connection Pool** 이 두 가지가 얽히면
206+
207+
**장애가 터질 수 있다는 걸** 이번에 제대로 배웠다.
208+
209+
---
210+
211+
## **"샘플 코드"**
212+
이 코드는 테스트 편의를 위해 작성된 샘플입니다.
213+
> [feign-dns-lookup-test](https://github.com/rojae/feign-dns-lookup-test)
214+
215+
---
216+
217+
218+
## **"참고한 내용들"**
219+
220+
### Standard JVM `networkaddress.cache.*` Properties with Java version
221+
222+
| Property | Description | Default | Java Version Introduced | Status | Java Proposal Reference |
223+
| ----------------------------------- | --------------------------------------------------------------------------------- | ------- | ----------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224+
| `networkaddress.cache.ttl` | Positive cache TTL (seconds) for successful name resolutions. `-1` means forever. | `-1` | JDK 1.4+ | Standard | introduced in JDK 1.4, no JEP (java.security standard) |
225+
| `networkaddress.cache.negative.ttl` | Negative cache TTL (seconds) for failed name resolutions. `-1` means forever. | `10` | JDK 1.4+ | Standard | introduced in JDK 1.4, no JEP (java.security standard) |
226+
| `networkaddress.cache.stale.ttl` | TTL (seconds) for allowing stale (expired) cache reuse if fresh lookup fails. | `0` | JDK 17+ | Standard | [JDK-8306653 (Reuse stale data to improve DNS resolver resiliency)](https://bugs.openjdk.org/browse/JDK-8306653) AND [Oracle-Docs Java-24](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/net/doc-files/net-properties.html#address-cache-heading) |
227+
| `sun.net.inetaddr.ttl` | Legacy alias for positive TTL (same as `networkaddress.cache.ttl`). | n/a | pre-JDK 1.4 | Deprecated | no JEP |
228+
| `sun.net.inetaddr.negative.ttl` | Legacy alias for negative TTL (same as `networkaddress.cache.negative.ttl`). | n/a | pre-JDK 1.4 | Deprecated | no JEP |
229+
230+
### Shutdown OS Cache
231+
232+
| OS | Command | Description |
233+
| ----------- | ------------------------------------------------------------------- | ------------------------------------------ |
234+
| **macOS** | `sudo killall -HUP mDNSResponder` | Flush macOS DNS cache immediately |
235+
| **Linux** | `sudo systemd-resolve --flush-caches`<br>OR<br>`sudo nscd -i hosts` | Flush Linux systemd-resolved or nscd cache |
236+
| **Windows** | `ipconfig /flushdns` | Flush Windows DNS client cache |
237+
238+
### Reference Links
239+
240+
#### Oracle Docs
241+
242+
- [Oracle-Docs Java-24](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/net/doc-files/net-properties.html#address-cache-heading)
243+
- [Oracle-Docs Java-17](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/doc-files/net-properties.html)
244+
- [Oracle-Docs Java-11](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/doc-files/net-properties.html)
245+
- [Oracle-Docs Java-8](https://docs.oracle.com/javase/8/docs/technotes/guides/net/properties.html)
246+
247+
#### Open JDK
248+
249+
- [JDK-8306653 (Reuse stale data to improve DNS resolver resiliency)](https://bugs.openjdk.org/browse/JDK-8306653)
250+
- [JDK-8219993 (OpenJDK bug)](https://bugs.openjdk.org/browse/JDK-8219993)
251+
- [JDK-8323089 (OpenJDK bug)](https://bugs.openjdk.org/browse/JDK-8323089)
252+
253+
#### RFC
254+
255+
- [RFC-8767 (Serving Stale Data to Improve DNS Resiliency)](https://www.rfc-editor.org/rfc/rfc8767)
132 KB
Loading
105 KB
Loading
307 KB
Loading

0 commit comments

Comments
 (0)