터미널에서 중간중간 나오는 confirm 창에 yes, no만 선택하며 여러 생각이 들었다.
클라우드로 경력을 시작해 개발을 넘어서, 나름 DevOps 엔지니어(철학이긴 하지만..) 를 지망하고, 몇번의 실패를 넘어 더 나은 서비스 운영을 위해 Root Cause를 확인하고 계속해서 기술을 파고들었지만 부질없는 느낌이 들었다.
과연 AI 시대에 개발자, 엔지니어는 어떤걸 목표로 해야 할까?
답을 찾기위해서,, 내가 지금까지 뭘해왔는지 생각을 해봤는데, 여러가지 업무를 맡았던 특성 상 아래 네가지가 겹쳐있다.
- DevOps
- SRE
- Platform Engineering
- SA
여러 프로젝트를 하면서 일단 코드가 돌아가야하니, Jenkins, Shell 등을 통해 배포를 시켰고, 계약서 상 명시된 서비스 수준 협약(99%)를 지키기 위해 모니터링 대시보드도 구성했었다.
타 업체 개발자들이 사용하기 편하도록 cicd 구조를 변경하기도 했었고, 최근엔 K8s 관련 발표, SR 리뷰 및 트러블슈팅을 진행하면서 SA 업무또한 병행하고 있다.
나열해둔 위 업무를 AI 도입으로 하나씩 따져봤다.
AI Agent를 도입하게 되면 위에 만들었던 Azure Terraform 코드와 같이 초기 구성이 필요한 Jenkins Pipeline, Script는 충분히 대체될 수 있다. 또한 단순 PoC 업무도 사람이 하는 건 효율적이지 않다.
학습할 데이터 예제도 많고 난이도가 낮다면 AI는 할 수 있다고 판단해야 할듯하다.
그러나 SLA 99%를 지키기 위해 vLLM Pod나 Application 콜드스타트 문제를 고민하고, HPA 정책을 테스트하며 수립하고 Root Cause를 찾으며 초기 구성된 폼에서 문제 소지를 찾아내는 등의 업무는 대체할수 없을 것이다. 책임자 간의 협의, 책임 범위와 대화 대상(AI가 됐던, 사람이 됐던,.)을 이해시키고 합의를 끌어내는 몫은 늘어날 것이다.
이를 위해 필요한 기술적 이해도는 심화될 수밖에 없다.
결국 수동 제봉틀에서 자동 제봉틀로 바뀐거다. 바느질은 기계가하는데,, 옷 디자인, 섬유선택, 품질판단은 인간이 한다.
AI의 발전은 업무의 효율화에 장점이 있는 것 같다. 나에게 주어진 업무시간이 8시간이라면, 단순 업무에 투입되는 시간을 획기적으로 줄이고, 남는 시간을 더 깊고 어려운 판단이 필요한 작업들을 진행할 수 있지 않을까 싶다.
확실한 것은 배워야 할 건 더 늘어났다는 것이고, 단순히 할 수 있다를 넘어서 이해했고, 이해시킬 수 있고, 책임질 수 있다 가 더 중요해지는듯 하다.
고민중에 삼성클라우드에서 작성된 글 중, DevOps, SRE, Platform Engineering 을 비교한 글을 읽었다. 내가 목표로 정한 문장이 있어서 인용한다. 열심히 하자.
아이언소프트웨어(Iron Software)의 CEO 카메론 리밍턴(Cameron Rimington)은 자사의 팀이 어떻게 성장하면서 각 역할을 분리해 갔는지 구체적으로 밝혔습니다. “처음에는 개발자가 5명뿐이라 내가 데브옵스 역할을 겸임했다. CI/CD 파이프라인을 설정하고 배포를 관리했다. 팀이 15명으로 늘자 SRE를 채용했고, 모니터링과 인시던트 대응 시스템을 구축해 팀의 월평균 다운타임을 4시간에서 30분으로 줄였다. 현재 팀이 40명이 되자 플랫폼 엔지니어를 채용해, 개발자가 몇 분 안에 테스트 환경을 만들 수 있는 내부 API를 구축했다” 고 했습니다.
Multus란? 2.1 Primary CNI가 필요한 이유 2.2 kubelet은 CNI를 모른다 — CRI 위임 2.3 containerd의 conf 선택 로직
whereabouts IPAM 이란? 3.1 IP 중복 할당을 어떻게 막는가 — Lease 기반 분산 락
Multus와 whereabouts의 관계
결론
1. 개요
최근 Harvester 관련 SR 트러블슈팅 중에, 흥미로운 장애를 접했다. 상세 내용은 공개하기 어려우나, 원인 추적 중 Multus와 whereabouts 간의 상관관계를 코드 레벨까지 파악해야만 했다.
표면적으로 드러난 증상이 가리키는 컴포넌트와, 실제 원인이 있던 컴포넌트는 완전히 달랐다.
2. Multus 란?
Multus는 OpenSource CNI이며, 단일 Pod에 여러 NIC을 할당해 줄 수 있다.
Pod에 할당할 여러 CIDR 대역은 Kubernetes CRD Object로 관리가 가능하다.
이를 구현하기 위해선 Pod 기본 NIC인 eth0를 할당해 줄 Flannel, Calico, Cilium 등의 Default(primary) CNI가 필요하다. 이후 추가로 붙는 NIC(net1, net2)는 보통 macvlan, ipvlan, bridge, host-device, SR-IOV 같은 별개 플러그인이 담당한다.
NetworkAttachmentDefinition CRD가 바로 Multus의 CIDR 대역을 관리하는 Object 이다.
그렇다면 Multus 가 어떻게 Pod 내부에 NIC을 여러 개 붙일 수 있는지 좀 더 내려가 보자.
2.1 Primary CNI가 필요한 이유
위 Multus 개요 글에서, 구현하기 위해선 Pod 기본 NIC인 eth0를 할당해 줄 Flannel, Calico, Cilium 등의 Default(primary) CNI가 필요 라고 작성해 두었다.
이는 오해의 소지가 다분한 문장이다.
이유는 Kubernetes에서 Pod 내부에 NIC을 할당하기 위해, kubelet이 Pod를 만들 때, 사실 CNI를 딱 하나만 호출한다. /etc/cni/net.d 경로의 conflist 하나만 읽는다. Calico + macvlan, Cilium + SR-IOV 이렇게는 안된다는거다.
실제로 Multus가 붙은 K8s Cluster의 00-multus-conf를 확인해 보면, 아래와 같다.
...
[plugins.'io.containerd.cri.v1.runtime'.cni]
# DEPRECATED, use `bin_dirs` instead (since containerd v2.1).
bin_dir = ''
bin_dirs = ['/opt/cni/bin']
conf_dir = '/etc/cni/net.d' # cni conf list 위치
max_conf_num = 1 # 최대 CNI conf List 개수
setup_serially = false
conf_template = ''
ip_pref = ''
use_internal_loopback = false
...
이때 선택과 실행은 다른 단계다.
- Load()가 c.networks에 무엇을 담을지 정하고(선택),
- Setup()이 담긴 것을 병렬로 attach한다(실행).
슬라이스가 병렬 순회된다고 해서 conf가 여러 개 로드되는 건 아니다. 참고로 containerd는 WithLoNetwork로 loopback 네트워크를 하나 더 넣으므로 실제c.networks는 [lo, eth0] 2개이며, WithMinNetworkCount(2)로 최소 개수도 2로 올려 잡는다.
조금더 설명을 하자면, loopback interface는 하드코딩되서 들어간다. 그리고 나머지 한개 conf는 루프에서 돌면서 주입시켜주게 된다.
macvlan, ipvlan, SR-IOV 같은 delegate 플러그인이 파드에 secondary NIC을 붙일 때, 그 인터페이스에 부여할 IP를 결정하는 역할을 맡는다. NAD의 ipam 섹션에 "type": "whereabouts"로 지정하면 호출된다.
나의 환경에선 Multus가 위 플러그인을 사용하기 때문에, whereabouts IPAM을 통해 IP를 부여한다.
기존 host-local IPAM은 할당 상태를 각 노드의 로컬 파일에 저장하기 때문에 노드끼리 서로의 할당 내역을 알 수 없고, 다중 노드 환경에서 IP가 중복 할당된다. dhcp IPAM은 이를 해결하지만 외부 DHCP 서버라는 별도 인프라를 요구한다.
whereabouts는 할당 상태를 IPPool, OverlappingRangeIPReservation CRD(whereabouts.cni.cncf.io/v1alpha1)로 etcd에 저장한다. 클러스터 전체가 단일 할당 테이블을 공유하므로, DHCP 서버 없이도 노드 간 IP 중복 없이 동적 할당이 가능하다.
kubectl get ippools.whereabouts.cni.cncf.io -n kube-system
NAME AGE
10.10.0.0-24 12d
물론 Multus의 IPAM(IP Address Management)으로 whereabouts을 꼭 사용해야 하는 것은 아니다.
IPAM을 아예 사용하지 않아도 되며, 다른 플러그인을 선택할 수도 있다.
이는 모두 Multus 의 NAD CRD에 설정이 가능하다!
"ipam": { "type": "..." }
Type
사용 사례
제약사항
{} (IPAM 아예 없음)
L2 통신만 필요하거나, 워크로드가 알아서 IP를 잡는 경우
IP 미할당
static
파드 수가 적고 IP가 고정
파드마다 수동 지정
host-local
단일 노드, 또는 노드별로 대역을 쪼개 NAD를 따로 만든 경우
다중 노드에서 중복
dhcp
해당 VLAN에 이미 DHCP 서버가 있는 경우
노드마다 dhcp 데몬 필요
whereabouts
다중 노드 + 동적 할당 + 외부 DHCP 없음
CRD/etcd 의존
3.1 whereabouts는 IP 중복 할당을 어떻게 막는가 — Lease 기반 분산 락
IPAM에서 가장 중요한 로직이 있다. IP가 중복할당되면 안된다는거다.
이를 위해 whereabouts은 Kubernetes Lease 오브젝트로 Lock을 구현하여 해결한다.
기본적으로 whereabouts은 DS으로 생성된다.
만약 여러 노드에 Pod가 동시에 생성되었다 쳤을 때, 각 노드의 whereabouts 프로세스가 같은 IPPool을 수정(할당하려고)하려 한다. 이때 whereabouts은 Kubernetes Lease를 이용한 분산 락(생성시도, 생성성공 시 성공한 프로세스만 다음로직 진행 - IP 할당)으로 직렬화 한다.
따라서 whereabouts은 Kubernetes API 서버 접근이 가능해야 하며, 이를 위해 kubeconfig 파일을 생성해두고 관리하는 로직을 품고있다.
실제 DS 구성을 보면 아래와 같다.
#whereabouts version 0.7.0 기준. 0.9.0 이상은 kubeconfig 유효성 검증 로직이 추가되었다.
즉 whereabouts의 kubeconfig 파일이 유효하지 않다면 IP 할당이 불가능하며 secondary interface는 붙지 않고, Pod 또한 생성되지 않는다. CNI 로직의 체인은 하나라도 실패하면 sandbox(pause) 전체가 실패하는 로직을 갖고 있기 때문이다.
4. Multus와 whereabouts의 관계
지금까지 작성한 내용을 바탕으로 생각해 보았을 때, 둘의 관계성이 정의된다.
Multus는 Pod에 NIC 인터페이스를 붙이는 데에만 관여하며, 그 인터페이스에 IP를 할당하는 주체는 IPAM(whereabouts) 플러그인이다. 따라서 Multus를 쓴다 해서 whereabouts를 무조건 써야 하는 것은 아니다.
whereabouts가 필요해지는 것은 다중 노드 환경에서 secondary 인터페이스에 IP를 동적으로 할당해야 하고, 외부 DHCP 서버는 없는 경우다. 이번에 확인한 SR이 정확히 그 조건에 해당했다.
5. 결론
이번 SR을 경험하면서, 표면적 장애는 믿으면 안된다는걸 다시 한번 확인했다.
상세 장애 내용을 작성하긴 어려우나, 표면적으로 보인 이유는 다른 곳에 있었다. 그러나 심화된 깊은 진짜 이유는 CNI 호출 경로 깊숙한 곳에 숨어있었고, 이를 끌어내기 위해선 전체 로직을 이해하는 과정이 필수적이었다.
kubelet이 CRI에 어떤것을 위임하고, containerd가 CNI를 어떤 기준으로 선택하는지, Multus와 whereabouts은 어떤 상관관계를 가지며 동작하는지 전부 파악해야만 했다.
장애는 빠르게 해결되어야 한다. 하지만 빠른 복구와 재발 방지는 다른 일이다.
Root Cause를 파악하기 위해 로그와 이벤트는 이슈 추적의 출발점일 뿐이다. 그것이 왜 발생하고 어디가 문제인지 진단하며 진짜 해결을 위해 어떤행동을 해야할지 차후 진행 방향을 수립하기 위해선 전체 로직의 과정을 이해해야 한다.
주제 : K8s 환경의 Rancher Prime 확장성과 SUSE Virtualization 기반의 GPU Multi-Tenant K8s 환경 구현
위치 : 안다즈 서울 강남
발표 시간 : 2시간 30분
발표한지 5개월이 지나서야 블로그에 남긴다,
아마 지금껏 햇던 세미나발표자리중 가장 큰 자리가 아니었나 싶다.
SUSE 솔루션(Rancher Prime, Virtualization(Harvester), NeuVector 등..)에 많은 관심이 있던 고객사가 주를 이루었으며, 제조, 금융, 통신 등 대규모 프로덕션 환경을 운영 중인 분들이 많이 오셨다.
기존에 Rancher를 사용 중인 고객이 많았기 때문에, 단순히 기능 소개에 그칠 수 없었으며 AI 관련된 관심도가 높아짐에 따라 GPU 관련된 내용도 보강해야 했다.
세션 진행중..
내가 맡은 세션이 총 두번이었기 때문에, 두가지 트랙으로 주제 방향을 잡앗다.
1. K8s Production 환경의 Rancher Prime 확장 방향성
2. 제한된 GPU 환경에SUSE Virtualization 기반의 GPU Multi-Tenant 환경 구현
첫번째 세션에서는, 아래와 같은 내용을 발표했다,
- 단순 Multi Cluster K8s 환경을 UI기반으로 관리하는것을 넘어 GitOps를 도입한다. 이후 Fleet(like Argo)과 Rancher를 마치 Multi Cluster의 Proxy처럼 사용하며 DownStream 리소스를 관리한다.
- 이는 K8s 환경에서 중단없는 마이그레이션이 필요할 때에도 사용이 가능하다.
두번째 세션에서는, 아래 내용을 전달했다,
- 사내 GPU를 할당받은 환경에서, vGPU를 지원하지 않는 L4 GPU(Non-License) 만으로 다수 팀에게 격리된 K8s + GPU 환경을 제공해야 한다.
- SUSE Virtualization의 PCI Pass-through로 VM 계층에서 GPU를 격리하고, VM 내부 K8s에서 GPU Operator의 Time-Slicing으로 논리 분할한 뒤 vCluster와 ResourceQuota로 팀별 테넌트를 나누는 다층 구조를 설계했다.
- 팀 구성 변경과 GPU 증설 같은 수명주기 작업은 Helm + Git + ArgoCD 기반 GitOps로 자동화해, 데모에서는 팀 추가부터 GPU 확장·제한까지 전 과정을 라이브로 시연했다.
// etcd/server/etcdserver/v3_server.go
const (
// In the health case, there might be a small gap (10s of entries)
// between the applied index and committed index.
// However, if the committed entries are very heavy to apply,
// the gap might grow.
// We should stop accepting new proposals if the gap growing
// to a certain point.
maxGapBetweenApplyAndCommitIndex = 5000
)
func (s *EtcdServer) processInternalRaftRequestOnce(...) {
ai := s.getAppliedIndex()
ci := s.getCommittedIndex()
if ci > ai+maxGapBetweenApplyAndCommitIndex {
return nil, errors.ErrTooManyRequests // 503 에러!
}
// ...
}
3. Node Network Buffer의 한계
Default Buffer Size는 4096개
MsgApp, SnapShot, HeartBeat 패킷은 4096개가 넘어갈 경우 Drop
# ETCD 설정
etcd-arg:
# Snapshot 생성 빈도 증가
- "snapshot-count=50000"
# 기본: 100,000개
# 변경: 50,000개
# 목적:
# 1. 디스크 공간 절약 (WAL 파일 정리 빈도 증가)
# 2. 메모리 사용량 감소 (메모리 내 로그 개수 감소)
# 3. 재시작 시 복구 속도 개선 (WAL 재생 개수 평균 감소)
# 주의:
# - Snapshot 생성 시 CPU/디스크 I/O 발생
# Heartbeat 전송 간격 증가
- "heartbeat-interval=200"
# 기본: 100ms
# 변경: 200ms
# 목적:
# 1. Heartbeat 메시지 빈도 50% 감소
# 2. Network Buffer 공간 확보로 OverFlow 압박 완화
# 3. 네트워크 일시적 지연 허용
# 주의:
# - 장애 감지 시간 100ms → 200ms로 증가
# Election Timeout 증가
- "election-timeout=2000"
# 기본: 1000ms
# 변경: 2000ms
# 목적:
# 1. Leader 일시적 과부하 시 불필요한 Election 방지
# 2. 버퍼 오버플로우로 인한 Heartbeat 드롭 허용
# 3. 클러스터 안정성 증가
# 주의:
# - 실제 Leader 장애 시 복구 시간 1초 증가
직접적 원인은 파악했다. 그럼 데이터 재분배 시에는 트래픽 인입을 끊어야하기 때문에, Application 계층에 설정한 Actuator가 어떻게 동작하는지 알아야 했다.
기본적으로 Spring Boot Acutator 의존성을 Boot Application에 추가하게 되면,, 기본 헬스 체크 엔드포인트가 활성화 된다.
위처럼 actuator를 추가해 줬다.
Spring Boot 3.5.6 기준으로 기본 Health Check 경로는 /actuator/health 이다.
해당 경로는 변경이 가능하며, 바로 사용이 가능하다. 따라서 의존성을 추가만 하면 바로 Spring Application이 정상적으로 동작하고 있는지 여부를 Response Body 형태로 받아볼 수 있는 것이다.
그렇다면 여기서, 어떤 상태가 정상이라 할 수 있을까? 어떤 기준으로 서버가 정상, 비정상인지 확인하는 걸까?
이를 알아야 Health Check를 제대로 수행할 수 있을 것이다.
위를 파악하기 위해, 받아왔던 Response Body에 상세 정보를 포함해서 받을 수 있다. 방법은 간단한데, properties에 management.endpoint.health.show-details 설정을 always로 변경해 보자. default 값은 never이기 때문에, 초기 설정에는 위 사진처럼 단순한 상태 결과만 확인 할 수 있는것이다.
다시 Health Check 경로로 요청하게 되면,, 다음과 같은 응답을 확인할 수 있다.
이처럼 Actuator는 특정 의존성 패키지를 넣었을 경우, 자동으로 생성되는것을 확인할 수 있었다.
그렇다면 수동 추가는 불가능한것일까? 여러개의 Health Check를 수행한 경우 하나만 실패하면 어떻게 되는걸까?
기본적으로 Actuator는 커스텀으로 생성하는 것이 가능하다.
실제로 spring-boot-actuator-autoconfigure:3.5.6 의존성 패키지 코드를 까보면,, HealthIndicator 라는 interface를 상속받아서 HealthCheck 로직이 수행된다는것을 쉽게 알 수 있다.
아래는 HelthIndicator Interface 이며,
package org.springframework.boot.actuate.health;
@FunctionalInterface
public interface HealthIndicator extends HealthContributor {
default Health getHealth(boolean includeDetails) {
Health health = this.health();
return includeDetails ? health : health.withoutDetails();
}
Health health();
}
이를 상속받아 구현한 구현채들의 목록이다. 20개 정도 확인된다.
이중 PingHealthIndicator Class를 들어가서 확인해보면, 우리가 봤던 Response Body 중 Ping은 이친구가 만들어 준다는 것을 알 수 있다..
package org.springframework.boot.actuate.health;
public class PingHealthIndicator extends AbstractHealthIndicator {
public PingHealthIndicator() {
}
protected void doHealthCheck(Health.Builder builder) throws Exception {
builder.up();
}
}
이 코드는 단순히 SpringBoot Application 프로세스가 살아 있다 만을 체크한다.
그래서 AbstractHealthIndicator를 확장하거나 HealthIndicator 인터페이스를 구현하여 Custom으로 만들 수 있다.
*만드는건 따로 작성할 예정
그리고 가장 중요한 것은.. Down 상태를 반환한 HealthIndicator 가 하나라도 존재할 경우, 서비스 상태를 Down으로 생각하여 503을 반환하게 된다는 점이다.
HealthEndpointSupport 클래스 내부에 getAggregateStatus 메서드에서, 등록된 HealthIndicator 들을 오름차순으로 정렬하여 순환 검사를 진행하기 때문이다.
// SimpleStatusAggregator.java
public Status getAggregateStatus(Set<Status> statuses) {
return statuses.stream()
.filter(this::contains)
.min(this.comparator) // ← 여기서 최소값(가장 심각한 상태) 선택
.orElse(Status.UNKNOWN);
}
// 우선순위 (오름차순)
// 1. DOWN
// 2. OUT_OF_SERVICE
// 3. UP
// 4. UNKNOWN
이제 어떻게 HealthCheck가 동작하는지는 파악했다.
그럼 나의 문제와 어떤 상관관계가 있을까?
2. Wildfly Infinispan <-> Spring Boot Actuator
문제는 Spring Boot Actuator가 Wildfly의 Infinispan 상태를 모른다는 점이다.
위 테스트 내역과 같이.. 기본 healthCheck 경로에는 Infinispan 관련 메서드가 없다.
또한 기본 의존성으로 생성되지도 않기 때문에, 직접 Custom Health Indicator를 만들어야 한다.
왜 없을까?
- Spring Boot에서 자동 생성되는 Health Indicator는. Bean에 등록되는 영역만 검사한다.
- Session Clustering을 위해 사용한 Wildfly의 Infinispan은, Application 외부 영역이다.
- 따라서 자동으로 HealthIndicator가 생성되지 않는다.
원인 & 장애 타임라인
위 두가지 분석을 통해 도출된 해당 이슈의 타임라인은과 원인은 다음과 같다.
1. 상황 : 트래픽 급증으로 인해 HPA 트리거
결과 : Pod 스케일 업
2. 상황 : Startup Probe 통과
결과 : Actuator Up
3. 상황 : Wildfly Infinispan 클러스터 조인 진행
결과 : 데이터 리벨런싱 진행 .. 약 30 ~ 60초 소요
4. 상황 : Readiness Probe 통과 (Actuator는 Infinispan 상태를 모르므로 UP 반환)
결과 : 리벨런싱 완료되지 않은 WAS로 트래픽 인입
5. 상황 : 세션 생성 시도
결과 : ClassCastException 발생(장애)
Spring은 정상으로 보이나, Infinispan의 distributed-cache 모드에서는 Consistent Hash Ring 재구성이 필요하다. 이때 리밸런싱 중 세션 캐시 불일치가 발생하였다.
해결 방안
옵션 1. Graceful Startup : Custom HealthIndicator와 Probe 이용
Application HealthCheck를 WAS 계층 상태까지 확인하는 방법이다.
이는 이상적인 방법인듯 싶으나, 생각해보면 불완전한 방법이라는 것을 알 수 있다.
예시 코드는 다음과 같다.
기본적으로 Spring Application은 JBoos 상태를 갖고올 수 없기 때문에,, EmbeddedCacheManager 메서드로 JBoos 캐시 컨테이너를 가져와야 한다.
package com.example.demo.indicator;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.remoting.transport.Address;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import javax.naming.InitialContext;
import java.util.List;
public class InfinispanHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
InitialContext ctx = new InitialContext();
/**
* web 이라는 이름의 Infinispan 캐시 컨테이너 찾기
* JNDI 기반으로 리소스를 찾는다.
*/
EmbeddedCacheManager cm = (EmbeddedCacheManager)
ctx.lookup("java:jboss/infinispan/container/web");
if (cm.getStatus() == ComponentStatus.RUNNING) {
List<Address> members = cm.getMembers();
builder.up()
.withDetail("status", "RUNNING")
.withDetail("clusterSize", members.size());
} else {
builder.down()
.withDetail("status", cm.getStatus());
}
}
@Override
public Health getHealth(boolean includeDetails) {
return super.getHealth(includeDetails);
}
}
그러나 Application Bean과 Wildfly 계층의 차이로 인해 이는 완벽하지 않다..
이유는 다음과 같다.
Wildfly Infinispan을 통한 세션 클러스터링이 완전하기 위해서, 아래 두가지를 충족해야 한다.
1. Wildfly Cluster로 증가된 WAS가 Join 되어야 함.
2. 데이터 재분배가 완료되어야 함.
첫번째는 Application Warm-up을 통해 해결이 가능할 듯 하나, 두번째는 Kubernetes liveness Probe 주기보다 조금더 빠르게 지속적으로 확인하는 로직이 있어야만 한다.(WAS가 추가될 경우에도 데이터 재분배가 일어나기 때문에..)