resistance는 membership, dedicated, business, logging 총 4개의 마이크로 서비스로 구성되는 MSA 서비스이다.
아직 모니터링은 설치도 안했는데 배포하니까 총 Pod 64개가 나왔었다.
흠 4일정도 내내 틀어두니까 22$ 나왔다
NAT Gateway의 Data process보다 시간별 비용이 더 나온 것으로 보아 성능적 이슈는 안보였다. 근데도 오류 해결할때는 노드 성능탓만 했다ㅎㅎ
대략 게시글 7000개의 dummy 데이터를 넣어서 진행해봤는데, 무의미하게 데이터양만 늘린거 같다.
백엔드에서 동시 다발적으로 트래픽 과부하에 걸릴 정도의 병목에 버티느냐가 관건인거 같다.
운영 모니터링 - Prometheus for k8s
Helm repo를 설치해서 Prometheus로 metrics 데이터를 수집했다.
for k8s 레포지토리에선 시각화를 위한 Grafana도 함께 제공해서 편리했다.
로그 데이터 수집을 위한 loki나 성능 테스트를 위한 컴포넌트들도 찾아보면 서비스 운영에 더 용이할 거라 생각해서 찾아보고 있다.
이놈은 모니터링하는데만 11개를 쓰는거여??
아무튼 service 중 grafana-loadbalancer-svc
의 external IP:3000으로 접속하면 Grafana Dashboard가 뜬다.
Grafana의 초기 계정은 admin, admin으로 최초 접속시 비밀번호 변경을 요구한다.
dashboard에서 kubernetes/cluster
모니터링 사진
흠 예상대로 kafka-cluster의 리소스가 메모리를 가장 많이 잡아먹었고, 모니터링 역시 마찬가지로 꽤나 메모리를 차지하고 있다.
따로 커스텀하진 않고 기존에 제공되는 대시보드들만 확인하면서 필요한 영역을 가져왔다.
k8s 배포 과정의 시행착오와 오류 해결
1. 환경 변수를 찾을 수 없는 오류(configmap not found)
어떤 namespace로 지정해줘야 읽어줄래..??
namespace=dev
인 config와 secret 둘다 잘 잇는데 왜 인식못해
argo-rollout 에서 deployment 리소스를 custom한 Rollout 을 제공한다.
다음과 같이 env 파일과 환경 변수를 설정해서 내부 컨테이너로 환경변수를 알려줄 수 있다.
envFrom:
- configMapRef:
name: {{ .Chart.Name }}-config
namespace: dev
- secretRef:
name: {{ .Chart.Name }}-secret
namespace: dev
chart.name= membership-chart
라서 prefix를 사용해서 불러왔다.
env 리소스
apiVersion: v1
kind: ConfigMap
metadata:
name: membership-chart-config
data:
SPRING_JPA_HIBERNATE_DDL_AUTO: update
KAFKA_CLUSTERS_BOOTSTRAPSERVERS: kafka-cluster-bootstrap:9092
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_REPOSITORIES_ENABLED: "false"
EXPIRE_DEFAULTTIME: "18000"
---
apiVersion: v1
kind: Secret
metadata:
name: membership-chart-secret
type: Opaque
data:
SPRING_DATASOURCE_USERNAME: base64
SPRING_DATASOURCE_PASSWORD: base64=
SPRING_DATA_REDIS_PASSWORD: base64=
SPRING_CLOUD_VAULT_TOKEN: base64..
namespace를 지정하지 않고, ConfigMap이나 Secret 리소스의 namespace 설정을 rollout 리소스와 동일하게 진행해도 된다.
애플리케이션 지우고 다시 만드니까 이제야 잘된다.
결국 환경변수가 되는 ConfigMap과 Secret은 namespace를 배포하는 ArgoCD 애플리케이션의 Destination namespace
로 지정해서 Pod 컨테이너가 읽을 수 있게 하는게 관건이었다.
2. ArgoCD에서 Rollout.argoproj.io "svc" not found 오류
일반적으로 Argo Rollouts CRD
가 클러스터에 설치되지 않았거나, 리소스 정의가 잘못되었을 때 발생하는오류라고 한다.
나같은 경우는 ArgoCD가 Rollout 리소스를 찾지 못해서 발생한 오류이다.
ubuntu:~/environment/helm-chart/k8s-resource (dev) $
kubectl apply -f argo-rollouts-v1.6.2-install.yaml
namespace 설정을 잘못했었다.
켜두면 계속 비용이 나가다보니 리소스 지우고 생성하기를 반복하다보니 헷갈린거 같다.
삽질 한번 했으니 이제 안 헷갈려!!
3. 같은 Service인데 될거면 다되던가.. Pod 2개만 되는건 뭐야..?
오류 로그
- Readiness probe failed: Get "http://10.0.4.230:8080/actuator/health": dial tcp 10.0.4.230:8080: connect: connection refused
- Liveness probe failed: Get "http://10.0.4.230:8080/actuator/health": dial tcp 10.0.4.230:8080: connect: connection refused
Tomcat started on port 8080 (http) with context path ''
Started MembershipApplication in 44.277 seconds (process running for 47.107)
Closing JPA EntityManagerFactory for persistence unit 'default'
HikariPool-1 - Shutdown initiated...
HikariPool-1 - Shutdown completed.
다시 Sync하니까 이제 잘된다.
흠
4. Back-off restarting failed container in pod 오류
난 아무 잘못 없으므로 노드의 CPU 부족에서 기인한 현상으로 판단.
인스턴스 유형을 향상시켜서 해결했다.
무턱대고 Autoscaling Group
의 desired node
수를 늘리거나 인스턴스 유형을 t3.medium 으로 올려서 해결해버렸다.
5. Kafka Consumer의 중복 소비와 (feat. CORS 오류)
엥 난 CORS 이미 처리했었는데 왜?????
삽질 끝에..... kafka 문제였다.
작업을 요청한 membership Pod가 아닌 다른 Pod에서 응답하고 있었고
응답의 출처가 다르니 CORS 오류로 뜬 것이었다.
그리고 membership에서 만든 회원 정보를 토대로 business-service에서 게임 데이터를 중복으로 생성하고 있었다.
해결한 방법은 다음과 같다.
- membership pod들은 각각 자신의 pod 이름을 consumer-group로 가진다.
- membership에서 kafka로 발행된 작업 처리 메시지는 business pod들이 단일 consumer-group으로 작업을 처리한다.
어떻게 각 Pod들의 consumer-group이 고유한 id를 갖도록 할까?
각 Pod들의 생성된 이름이 고유값을 가진다는 점을 이용해서 각 Pod들의 metadata.name
을 가져오도록 설계해봤다.
kafka로 IPC하는 각 마이크로서비스의 Consumer 로직 설정은 다음과 같다.
public ResultConsumer(@Value("${kafka.clusters.bootstrapservers}") String bootstrapServers,
@Value("${task.result.topic}")String topic,
@Value("${consumer.group}") String groupId) {
Properties props = new Properties();
props.put("bootstrap.servers", bootstrapServers);
props.put("group.id", groupId);
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
this.consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList(topic));
log.info("current membership Pod's consumer-group : "+groupId);
...
}
@Value
를 통해 환경변수를 주입받을때 consumer.group
도 받도록 한다.
그럼 그 환경변수는 어디서 주입받는가? rollout 리소스(deployment) 생성시 주입해준다.
env:
- name: CONSUMER_GROUP
valueFrom:
fieldRef:
fieldPath: metadata.name
consumer-group을 각 Pod별로 독립적으로 두고 싶지 않다면 metadata.name
대신에 고유한 my-group 등을 넣어서 쓸 수 있다.
pod 생성시 로그를 확인해보면 각 Pod의 이름을 넣어서 고유한 consumer-group을 가지게 되었음을 알 수 있다.
결과적으로, 중복 소비되던 문제는 consumer-group을 동일하게 설정해서 해결했고, CORS 오류는 consumer-group을 각자 고유한 값으로 설정해서 해결할 수 있었다.
6. 비즈니스 로직의 문제로 발생한 504 Gateway Timeout 오류
처음에 참고한 504 Gateway Timeout 관련 자료들
alb.ingress.kubernetes.io/manage-backend-security-group-rules
어노테이션을 추가로 등록해서 해결해볼려 했다.
아니였다. 내가 틀렸다.
너무 노드의 성능 탓만 하는거 같긴한데, 노드 메모리가 딸려서 오래걸리는건가? Autoscaling group 노드 늘려주니까 잘된거 같다.
아니였다. 내가 틀렸따.
천천히 다시 디버깅하면서 로직을 분석해보겠다.
비즈니스 로직을 다시 보자.
String encryptedAddress = vaultAdapter.encrypt(command.getAddress());
MembershipJpaEntity jpaEntity = registerMembershipPort.createMembership(command);
Task task = Task.builder()
.membershipId(jpaEntity.getMembershipId())
.build();
sendTaskPort.sendTaskPort(task);
countDownLatchManager.addCountDownLatch(task.getTaskID());
try {
countDownLatchManager.getCountDownLatch(task.getTaskID()).await();
}catch (InterruptedException e){
throw new RuntimeException(e);
}
String result = countDownLatchManager.getDataForKey(task.getTaskID());
membership-service에서 kafka로 데이터를 요청하는 메시지를 보내고 business-service에서 해당 토픽을 받는동안 await하는 구조이다.
이때 각 Pod들이 여러 개라고 하면 membership-service-0에서 요청을 보내고 기다리는 동안 복수 개의 business-service에서는 kafka topic을 받아서 요청을 처리한 뒤, result topic에 메시지를 전송한다.
그러면 이 result topic을 기다리고 있던 membership-service-0
이 그대로 해당 메시지를 받도록 해야한다.
kafka 오류라 생각해서, 가상머신에서 kakfa-consumer의 result topic을 모니터링하면서 로직을 진행해봤다.
task topic : {"taskID":"b285d7ca-4cc1-4aae-bcff-55c0f1f335bc","taskName":"GetUserDatas Task","membershipId":"52","subTaskList":[{"membersrhipId":"52","subTaskName":"GetMemberDataTask : MembershipId validation, transfer UserData.","taskType":"register","status":"ready","data":"asdf"}]}
task result topic : {"taskID":"b285d7ca-4cc1-4aae-bcff-55c0f1f335bc","taskName":"GetUserDatas Task","membershipId":"52","subTaskList":[{"membersrhipId":"52","subTaskName":"GetMemberDataTask : MembershipId validation, transfer UserData.","taskType":"register","status":"success","data":"asdf"}]}
작업 요청 메시지(task topic)와 요청된 작업 완료를 알리는 메시지(task result topic
)가 동일한 taskID를 가지고 이뤄졌음을 알 수 있었다.
원하던 로직이 잘 이뤄지고 있는 상태인데 흠.
근데 왜 await()가 오래 걸려서 504 Gateway Timeout이 일어난걸까?
각고의 삽질끝에... 겨우 디버깅에서 흔적을 찾아냈다.
아래는 해당 마이크로 서비스의 초기화 과정 중 로직이다.
App info kafka.consumer for consumer-task-topic-1 unregistered
Exception in thread "Thread-0" java.lang.NullPointerException:
Cannot invoke "java.util.concurrent.CountDownLatch.countDown()" because the return value of "com.ns.common.CountDownLatchManager.getCountDownLatch(String)" is null
at com.ns.membership.adapter.in.kafka.ResultConsumer.lambda$new$0(ResultConsumer.java:77)
at java.base/java.lang.Thread.run(Thread.java:831)
씽.. INFO로 로깅하면 내가 어떻게 보냐고!!! 당연히 kafka init
로그에 밀려서 못보잖아.
CountDownLatchManager.getCountDownLatch(String)
의 반환값이 null이 되면서 kafka consumer-poll batch
가 종료된 것이었다.
Consumer에서 CountDownLatch
를 초기화하는데 신경써서 예외 처리로 해결했다.
'project > resistance' 카테고리의 다른 글
게임 서비스에 대한 취약점 모의해킹 이벤트 (1) | 2024.12.17 |
---|---|
Kubernetes 배포를 위한 클라우드 환경 구축 (0) | 2024.11.27 |
웹서버 마이그레이션, MSA 설계하기 (1) | 2024.11.27 |
환율 변동과 환전 시스템 구현 (Redis 캐싱으로 성능 개선) (0) | 2024.11.23 |