이 글에서는 리눅스 커널 기능인 Control Groups(이하 cgroups)에 대해서 간단히 알아보고, Kubernetes(이하 k8s)가 cgroups를 어떻게 사용하는지 살펴보겠습니다. Kubernetes와 cgroups 간의 관계를 이해하는 데 도움이 되기를 바랍니다. cgroups란 cgroups는 시스템에서 실행되는 여러 프로세스를 그룹으로 묶고, 각 그룹이 사용할 수 있는 CPU, 메모리, I/O, 네트워크 등의 자원 사용을 제한하고 격리하는 리눅스 커널 기능입니다. 이 글에서는 여러 자원 중 k8s와 관련이 깊은 CPU와 메모리 자원에 대해 살펴보겠습니다. 출처: How I Used CGroups to Manage System Resources 리눅스에서 cgroups를 사용하는 방법에는 여러 가지가 있지만 여기에서는 간단한 cgroupfs를 통해서 진행해 보겠습니다.(이 글에서는 cgroups v1을 이용합니다.) 셸에서 mount 명령어를 실행하면 다음과 같이 cgroups를 사용하기 위한 가상의 파일 시스템이 있는 것을 볼 수 있습니다. 디렉터리를 만들거나 파일 내용을 수정하는 것으로 cgroups의 기능을 사용할 수 있습니다. $ mount ... cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) ... /sys/fs/cgroup 하위 디렉터리를 보면 다음과 같이 다양한 자원을 볼 수 있습니다. 이 글에서는 이 중에서 cpu,cpuacct와 memory 디렉터리만을 사용하겠습니다. $ ll /sys/fs/cgroup dr-xr-xr-x 6 root root 0 6월 19 15:11 blkio lrwxrwxrwx 1 root root 11 6월 19 15:11 cpu -> cpu,cpuacct dr-xr-xr-x 6 root root 0 6월 19 15:11 cpu,cpuacct lrwxrwxrwx 1 root root 11 6월 19 15:11 cpuacct -> cpu,cpuacct dr-xr-xr-x 3 root root 0 6월 19 15:11 cpuset dr-xr-xr-x 6 root root 0 6월 19 15:11 devices dr-xr-xr-x 3 root root 0 6월 19 15:11 freezer dr-xr-xr-x 3 root root 0 6월 19 15:11 hugetlb dr-xr-xr-x 6 root root 0 6월 19 15:11 memory lrwxrwxrwx 1 root root 16 6월 19 15:11 net_cls -> net_cls,net_prio dr-xr-xr-x 3 root root 0 6월 19 15:11 net_cls,net_prio … 메모리 설정 우선 간단히 설정할 수 있는 메모리부터 알아보겠습니다. /sys/fs/cgroup/memory 하위에 test1 디렉터리를 만들었는데요, 이것만으로 하나의 cgroup이 만들어집니다. 디렉터리 안의 내용을 보면 여러 설정값이 있습니다. $ sudo mkdir /sys/fs/cgroup/memory/test1 $ ll /sys/fs/cgroup/memory/test1 … -rw-r--r-- 1 root root 0 8월 6 15:23 cgroup.clone_children --w--w--w- 1 root root 0 8월 6 15:23 cgroup.event_control -rw-r--r-- 1 root root 0 8월 6 15:23 cgroup.procs -rw-r--r-- 1 root root 0 8월 6 15:23 memory.failcnt --w------- 1 root root 0 8월 6 15:23 memory.force_empty -rw-r--r-- 1 root root 0 8월 6 15:23 memory.kmem.failcnt -rw-r--r-- 1 root root 0 8월 6 15:23 memory.kmem.limit_in_bytes -rw-r--r-- 1 root root 0 8월 6 15:23 memory.kmem.max_usage_in_bytes -r--r--r-- 1 root root 0 8월 6 15:23 memory.kmem.slabinfo -rw-r--r-- 1 root root 0 8월 6 15:23 memory.kmem.tcp.failcnt -rw-r--r-- 1 root root 0 8월 6 15:23 memory.kmem.tcp.limit_in_bytes -rw-r--r-- 1 root root 0 8월 6 15:23 memory.kmem.tcp.max_usage_in_bytes -r--r--r-- 1 root root 0 8월 6 15:23 memory.kmem.tcp.usage_in_bytes -r--r--r-- 1 root root 0 8월 6 15:23 memory.kmem.usage_in_bytes -rw-r--r-- 1 root root 0 8월 6 15:23 memory.limit_in_bytes -rw-r--r-- 1 root root 0 8월 6 15:23 memory.max_usage_in_bytes -rw-r--r-- 1 root root 0 8월 6 15:23 memory.memsw.failcnt ... 이 중에서 k8s와 관련된 값은 다음 3가지입니다. memory.limit_in_bytes: 프로세스의 메모리 사용량이 이 값을 초과하면 시스템이 해당 프로세스의 작업을 중단시키거나 오류 발생(기본값은 uint64 max) memory.soft_limit_in_bytes: 프로세스의 메모리 사용량이 이 값을 일시적으로 초과하는 것은 허용, 지속적인 초과는 금지(기본값은 uint64 max) tasks: cgroup에 속한 프로세스 ID(기본값은 없음) 위 설정값을 변경하면서 어떻게 동작하는지 실험해보겠습니다. 우선 100MB의 메모리를 차지하는 프로세스를 백그라운드로 하나 생성합니다. $ stress --vm 1 --vm-bytes 100M & 이제 memory.limit_in_bytes 값을 1MB로 설정하고, tasks에는 이 프로세스의 ID를 써보겠습니다. 파일 내용을 수정하면 cgroup 설정이 변경되므로 매우 편리합니다. OOM이 발생하여 프로세스가 종료되는 것을 볼 수 있습니다. memory.limit_in_bytes 값을 초과하는 프로세스는 바로 중단되기 때문입니다. $ echo 1048576 | sudo tee memory.limit_in_bytes $ echo {PROCESS ID} | sudo tee tasks $ stress: FAIL: [42715] (415) <-- worker 42716 got signal 9 stress: WARN: [42715] (417) now reaping child worker processes stress: FAIL: [42715] (451) failed run completed in 15s [1]+ Exit 1 stress --vm 1 --vm-bytes 100M 그렇다면 k8s에서 메모리 설정은 cgroup 설정과 어떤 관련이 있을까요? 실험을 위해 간단한 Pod를 하나 실행해 보겠습니다. apiVersion: v1 kind: Pod ... resources: requests: memory: "512Mi" limits: memory: "768Mi" k8s는 위 Pod를 위해서 아래 위치에 cgroup 하나를 생성합니다.(위치는 k8s 버전에 따라 다를 수 있습니다.) /sys/fs/cgroup/memory/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podbea85cd8_f91e_45e8_8570_505487b16d77.slice/crio-d8bad6a478b6b509661a9ad2c76d189b0659c75eb2ee5ca5ba93fe87ab093b65.scope/container 위 디렉터리로 이동해서 메모리 관련 croup 설정이 어떻게 변경되었는지 살펴보겠습니다. memory.limit_in_bytes의 값은 limits.memory에 설정된 768Mi입니다. limits 값을 초과하면 Pod이 종료되어야 한다는 것을 생각해보면 당연합니다. $ cat memory.limit_in_bytes 805306368 <- 768Mi 그렇다면 memory.soft_limit_in_bytes 값은 어떻게 설정되었을까요? limits.requests에 설정된 512Mi로 예상하셨겠지만 의외로 기본값인 uint64 max가 들어 있습니다. docker는 --memory-reservation 옵션으로 memory.soft_limit_in_bytes 값을 설정할 수 있지만, k8s는 이를 cgroup 설정에 사용하지 않고 Pod 스케쥴링 시에만 참고한다고 합니다. $ cat memory.soft_limit_in_bytes 9223372036854771712 <- (uint64 max) CPU 설정 다음으로 CPU 설정 테스트를 위해 /sys/fs/cpu,cpuacct 하위에 test2 디렉터리를 만들어서 또 다른 cgroup을 만들어 보겠습니다. $ sudo mkdir /sys/fs/cpu,cpuacct/test2 $ ll /sys/fs/cpu,cpuacct/test2 -rw-r--r-- 1 root root 0 7월 4 18:38 cgroup.clone_children -rw-r--r-- 1 root root 0 7월 4 18:38 cgroup.procs -rw-r--r-- 1 root root 0 7월 4 18:38 cpu.cfs_period_us -rw-r--r-- 1 root root 0 7월 4 18:38 cpu.cfs_quota_us -rw-r--r-- 1 root root 0 7월 4 18:38 cpu.rt_period_us -rw-r--r-- 1 root root 0 7월 4 18:38 cpu.rt_runtime_us -rw-r--r-- 1 root root 0 7월 4 18:38 cpu.shares -r--r--r-- 1 root root 0 7월 4 18:38 cpu.stat -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.stat -rw-r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage_all -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage_percpu -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage_percpu_sys -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage_percpu_user -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage_sys -r--r--r-- 1 root root 0 7월 4 18:38 cpuacct.usage_user -rw-r--r-- 1 root root 0 7월 4 18:38 notify_on_release -rw-r--r-- 1 root root 0 7월 4 18:38 tasks 이번에도 많은 설정이 보이지만 이중에서 k8s와 관련된 4가지 값만 알아보겠습니다. cpu.cfs_period_us: CPU 자원에 대한 액세스를 재할당하는 주기(기본값 100000 = 0.1초) cpu.cfs_quota_us: 할당된 CPU 사용 시간(기본값 -1). 할당된 CPU 사용 시간을 모두 사용한 경우 나머지 시간 동안 CPU 사용이 제한됨(CPU 스로틀링). cpu.shares: 다른 group에 비해 상대적인 CPU 사용량(기본값 1024) tasks: cgroup에 속한 프로세스 ID(기본값은 없음) cpu.cfs_period_us에 설정된 시간 안에는 cpu.cfs_quota_us에 설정된 시간 동안만 CPU 자원을 제한 없이 사용할 수 있고 나머지 시간에는 CPU 사용량이 제한됩니다. 예를 들어 cpu.cfs_period_us가 100000(100ms)이고 cpu.cfs_quota_us가 50000(50ms)이면, 100ms 중 50ms 동안은 CPU 자원을 제한 없이 쓸 수 있고 나머지 50ms 동안은 절반의 CPU 자원만 쓸 수 있습니다. 이 설정이 잘 적용되는지 테스트해 보겠습니다. 테스트에 사용한 장비는 4코어입니다. 메모리 테스트에서 사용한 stress 프로그램으로 4코어 장비에서 4개 코어를 모두 쓰도록 프로세스를 4개 실행했습니다. 4개 코어 모두 100% 사용되는 것을 볼 수 있습니다. 이 상태에서 프로세스 하나만 100000μs(100ms) 중 50000μs(50ms) 동안만 CPU를 쓸 수 있도록 변경해 보겠습니다. $ echo 100000 | sudo tee cpu.cfs_period_us $ echo 50000 | sudo tee cpu.cfs_quota_us $ echo {SECOND PROCESS ID} | sudo tee tasks 2번째 프로세스가 CPU 자원을 절반만 사용하는 것을 볼 수 있습니다. CPU 관련 설정은 하나 더 있습니다. cpu.shares는 cgroup 간의 상대적인 CPU 사용량을 나타냅니다. 예를 들어 cgroup이 4개 있는데 모두 cpu.shares 값이 1024라면 각 cgroup은 CPU 자원을 1/4씩 사용합니다. 1024/(1024+1024+1024+1024) = 25% 만약 다음 그림처럼 특정 cgroup의 cpu.shares 값을 1536으로 증가시키면 해당 cgroup은 1536/(1536+512+1024+1024) = 37.5%의 CPU 자원을 사용할 수 있습니다. 다시 실험을 해보겠습니다. 4코어 장비에 cgroup을 2개 만들고 각 cgroup마다 4개의 프로세스를 실행해서 전체 CPU 자원을 다 소모할 정도로 부하를 준 다음 다음과 같이 설정을 변경했습니다.(cpu.cfs_period_us와 cpu.cfs_quota_us는 기본값인 100000과 -1 사용) $ echo 1024 | sudo tee cpu.shares $ echo {FOUR PROCESS IDS} | sudo tee tasks $ echo 512 | sudo tee cpu.shares $ echo {OTHER FOUR PROCESS IDS} | sudo tee tasks CPU 사용률이 1024/(512+1024) = 66.6%와 512/(512+1024) = 33.3%로 나뉘는 것을 볼 수 있습니다. 그렇다면 CPU와 관련된 cgroup 설정은 k8s에서 어떻게 적용될까요? Pod을 실행해서 cgroup 설정을 비교해 보겠습니다. cpu.cfs_period_us의 k8s 기본값은 100000(100ms)이고, Pod manifest로는 이 값을 변경할 수 없습니다. cpu.cfs_quota_us 값은 limits.cpu 값으로 결정됩니다. 예를 들어 limits.cpu 값을 "4000m"로 설정하면 최대 4코어의 CPU 자원을 사용할 수 있으며 cpu.cfs_quota_us 값은 400000(400ms)이 됩니다. 만약 limits.cpu를 설정하지 않으면 cpu.cfs_quota_us는 -1이 됩니다. 이는 제한이 없다는 의미입니다. cpu.shares 값은 requests.cpu 밀리코어 값을 1000으로 나누고 1024를 곱한 값입니다. 예를 들어 requests.cpu를 "2000m"로 설정하면 cpu.shares 값은 2000/1000*1024=2048이 됩니다. apiVersion: v1 kind: Pod ... resources: requests: cpu: ”2000m" <-- cpu.shares 2048 limits: cpu: ”4000m" <-- cpu.cfs_period_us 100000 (k8s 기본값) cpu.cfs_quota_us 400000 (만약 4코어 장비라면 -1로 설정한것과 같음) 이 밖에도 CPU의 특정 코어만 독점해서 쓸 수 있는 cpuset 기능도 있지만 이 글에서는 생략하겠습니다. 관심 있는 분은 아래 문서를 참고해 주세요. CPUSETS Kubernetes v1.31: New Kubernetes CPUManager Static Policy: Distribute CPUs Across Cores 마치며 k8s가 cgroups를 어떻게 사용하는지 간단히 알아보았습니다. 끝으로 짧은 k8s 운영상의 경험을 말씀드리면서 글을 마무리하고자 합니다. requests.memory 값은 limits.memory 값과 같게 설정합니다. 두 값을 다르게 한다고 자원을 아껴 쓸 수 있는 것은 아닙니다. API 서버처럼 latency가 중요한 경우 CPU 사용량 제한 때문에 응답 시간이 늦어지면 안 되므로 limits.cpu 값을 설정하지 않습니다. 이렇게 해도 request.cpu(cpu.shares) 비율대로 Pod 간 CPU 자원이 배분되므로 다른 프로세스의 자원을 과도하게 빼앗지는 않습니다.
기술 블로그 모음
국내 IT 기업들의 기술 블로그 글을 한 곳에서 모아보세요


AWS Parallel Cluster를 생성해 보도록 하겠습니다. The post AWS Parallel Cluster 생성 appeared first on NDS Cloud Tech Blog.

이 블로그에서는 AWS Systems Manager의 주요 기능과 그 활용 방안을 단계별로 살펴보겠습니다. The post AWS Systems Manager란? appeared first on NDS Cloud Tech Blog.

Helm은 Kubernetes 클러스터에서 애플리케이션을 설치하고 관리하는 데 사용되는 패키지 관리자입니다. The post EKS에서 Helm 사용하기 appeared first on NDS Cloud Tech Blog.

이번 글에서는 AWS CLI의 프로파일 설정과 활용법을 실습과 함께 자세히 알아보고, 효율적인 사용을 위한 팁을 공유하겠습니다. The post AWS CLI 프로파일(Profile) 설정 및 활용법 appeared first on NDS Cloud Tech Blog.

AWS ECS를 통해 고양이&강아지 사진이 랜덤으로 뜨는 웹 애플리케이션을 배포해보는 실습입니다. The post AWS ECS로 웹 애플리케이션 배포하기 appeared first on NDS Cloud Tech Blog.

AWS Shield는 DDoS 방어와 고급 보안을 제공하며, AWS WAF 및 Firewall Manager로 웹 애플리케이션 보호와 정책 관리를 지원합니다. The post AWS Shield란? appeared first on NDS Cloud Tech Blog.

클라우드 환경에서 비용을 절감하기 위한 EC2 스케줄링 방법을 살펴보겠습니다. The post AWS EC2 중지/시작 자동화: 사람도 퇴근, 서버도 퇴근! 태그 기반 EC2 스케줄링 appeared first on NDS Cloud Tech Blog.
1. 들어가며 안녕하세요. 쏘카 데이터엔지니어링팀 삐약, 루디입니다. 내용을 시작하기에 앞서, 저희 팀의 업무와 역할에 대해 간략히 소개해 드리겠습니다. 데이터엔지니어링팀은 신뢰할 수 있는 데이터를 쏘카 구성원들이 안정적으로 활용할 수 있도록 기반을 마련하고, 이를 실제 비즈니스에 적용할 수 있는 서비스를 개발하며 환경을 구축하고 있습니다. 데이터 마...

시작하며 안녕하세요. SRE(site reliability engineering, 사이트 안정성 엔지니어링) 업무를 맡고 있는 Enablement Engineering 팀 어다희,...

2월 20일에 열리는 AWS Developer Day에 참여하세요! 이 가상 이벤트는 개발자와 팀이 개발 라이프사이클 전반에 걸쳐 최첨단의 책임감 있는 생성형 AI를 통합하여 혁신을 가속화할 수 있도록 디자인되었습니다. AWS Evangelism의 Jeff Barr 부사장은 기조 연설에서 생성형 AI를 기준으로 하는 차세대 소프트웨어 개발, 변화하는 ...

AWS CloudTrail에서 Amazon Virtual Private Cloud(Amazon VPC) 엔드포인트용 네트워크 활동 이벤트를 정식 출시합니다. 이 기능을 사용하면 VPC 엔드포인트를 통과하는 AWS API 활동을 로깅하고 모니터링할 수 있으므로 데이터 경계를 강화하고 더 효과적인 탐지 제어를 구현할 수 있습니다. 이전에는 잠재적인 데이터...

LLMOps란 무엇인가요? 최근 GPT-4와 같은 대규모 언어 모델(large language model, 이하 LLM)의 사용이 보편화되면서 이를 활용한 애플리케이션이 활발히 개...
데브시스터즈에서 적용하고 있는 장애 대응 원칙과 방법을 공개합니다.
RADIUS 네트워크 인증 환경에서는 보안 강화를 위해 공인인증서를 사용하며, 공인인증서는 매해 갱신 작업이 필요합니다. 하지만 인증서 갱신 과정에서 예상치 못한 클라이언트 인증 실패가 발생하였습니다. 이 글에서는 공인인증서(Globalsign) 갱신 이후 발생한 RADIUS 인증 실패 사례를 분석하고, 원인과 해결 과정을 공유합니다. 공인인증서 갱신 후 RADIUS 인증 실패 문제를 경험했거나, RADIUS 인증 프로세스, 인증서 체계에 관심 있는 독자에게 […] The post RADIUS 인증 실패 분석 및 해결 사례 first appeared on 우아한형제들 기술블로그.

 ## 들어가며 다양한 산업 분야에서 클라우드 컴퓨팅의 확...

토스증권의 실시간 데이터팀은 Active-Active 구성에서 Consumer Offset Sync를 어떻게 하고 있을까요?
.png)
토스증권은 현재 Active-Active 구성으로 Kafka를 운영하고 있는데요. 오늘은 Active-Active를 유지하기 위해 필요한 양방향 데이터 미러링에 대해 소개하려고 합니다.

안녕하세요, 리멤버 플랫폼 서버 파트의 노아론입니다. 이번 글에서는 특정 유저군을 타겟팅하는 과정에서 Redis의 SET 구조 대신 Bitmap 구조를 이용하여 어떻게 메모리를 절약할 수 있었는지에 대해 이야기하려고 합니다.리멤버 리서치에선 설문 조건에 맞는 유저를 타겟팅하여 응답을 수집하고, 참여한 유저에겐 소정의 리워드를 지급하고 있습니다. 특정 ...

배치 작업을 VM 서버에서 실행해 동시 실행에 어려움을 겪은 적이 있나요? 이 글에서는 Kubernetes Job을 활용해, 기존에는 VM 서버에서 실행되던 배치 작업이 클러스터에서 실행되도록 아키텍처를 변경해 작업의 효율성을 높이고, Kubernetes 커스텀 컨트롤러로 Job 스케줄러를 구현해 Job 실행을 더 유연하게 관리한 방법을 공유하고자 합니다. 동시에 실행하고 싶은 배치가 너무 많다 프로젝트 초기에는 실행해야 하는 배치 수가 적기 때문에 간편하게 VM 서버 한 대에서 모든 작업을 처리할 수 있습니다. 하지만 프로젝트를 운영해 갈수록 실행해야 하는 배치 수는 점점 늘어나고 VM 서버 한 대로는 해결하기 힘든 상황이 옵니다. 예를 들어보겠습니다. VM 서버 한 대로 특정 시간에 사용자에게 다양한 알림을 보내고 싶다면 어떻게 해야 할까요? 일반적인 운영 환경에서는 VM 장비 한 대로는 CPU, 메모리 등 자원 할당의 문제로 여러 배치 작업을 동시에 실행하기 힘들기 때문에 서로 연관이 없는 독립적인 배치 작업이라도 동시에 실행하지 못하고 하나의 작업이 끝날 때까지 기다렸다가 다른 작업을 시작해야 합니다. 만약 동시에 여러 작업을 실행하고 싶다면 서버를 추가해서 각각 실행해야 합니다. 하지만 서버를 추가하면 비용이 증가할 뿐 아니라 관리 포인트가 늘어나고, 이 경우 특정 시간에만 작업을 실행하기 때문에 대부분의 시간에 서버가 사용되지 않아서 자원을 효율적으로 사용할 수 없습니다. 그렇다고 해서 이 작업을 위해 특정 시간에만 새로운 서버를 설정하는 방법은 확장성이 떨어집니다. 그래서 이런 문제점을 해결하기 위해 Kubernetes Job을 활용해 배치 작업을 클러스터에서 실행할 수 있게 했습니다. 클러스터에서 일회성 작업을 실행할 수 있는 Kubernetes Job 오브젝트 Kubernetes Job은 Kubernetes 클러스터에서 일회성 작업을 실행하기 위한 오브젝트입니다. 의존성 없는 다수의 배치 작업을 각각 Job으로 실행하면 하나 이상의 컨테이너에서 배치 작업을 독립적으로 수행할 수 있어 전체 배치 작업의 실행 시간이 단축되고 시스템 전체의 효율이 향상됩니다. 뿐만 아니라 Kubernetes의 자동 확장 및 복구 기능은 시스템의 안정성을 높이고 유지 보수 부담을 줄여줍니다. 또한 한 번 Job을 구성해두면 클러스터만 변경해 실행이 가능하기 때문에 이중화에도 효과적입니다. 그럼 Kubernetes Job이 어떤 식으로 동작을 하는지 간단하게 알아보고, Job 템플릿을 생성 후 실제로 실행하고 모니터링해 보겠습니다. Kubernetes Job 동작 방식 Kubernetes Job은 클러스터 내에서 자원을 조정하고 작업을 스케줄링하는 컨트롤 플레인을 통해 파드에 스케줄링받아 실행됩니다. 출처: Kubernetes Components 사용자가 Job 생성을 요청하면 Kubernetes API 서버가 요청을 받아서, 클러스터의 모든 상태 정보를 저장하는 etcd에 리소스를 저장합니다. etcd에 리소스가 저장되면, 해당 리소스의 이벤트를 감시하는 컨트롤러 매니저가 리소스 생성을 감지하고 리소스를 가져와 사용자가 의도한 Job Spec에 맞게 파드를 생성합니다. 여기서 생성된 파드는 대기(pending) 상태이며, 스케줄러가 클러스터의 상태를 고려해 적절한 노드에 파드를 할당하면서 작업이 실행됩니다. 이런 실행 과정을 거치기 때문에 사용자가 요청하는 각각의 Job은 모두 독립적인 파드에서 병렬 처리될 수 있습니다. Kubernetes Job 생성 그럼 이제 Job 템플릿을 하나 생성해서 실행해보겠습니다. Kubernetes 리소스 정의 파일인 YAML 파일을 이용해서 Kubernetes 클러스터에 Job 생성을 요청할 수 있습니다. 이 YAML 파일에는 Job 생성에 필요한 모든 설정이 포함되어 있습니다. 기본적인 Job 템플릿 YAML 파일은 다음과 같습니다. Job의 메타데이터와 사용자의 의도가 담기는 spec을 정의할 수 있습니다. apiVersion: batch/v1 kind: Job metadata: name: my-job spec: template: metadata: labels: app: name spec: containers: - name: my-container image: my-container-image restartPolicy: OnFailure 템플릿을 실행하면 spec에 정의된 내용에 맞는 파드가 생성되고, 적절한 노드에 할당되어 실행됩니다. 이 Job 템플릿만으로도 클러스터에 Job을 생성할 수 있지만, 여기에서는 Helm을 이용해서 Job을 실행해보겠습니다. Helm은 Kubernetes의 패키지 매니저로, 파라미터나 설정값을 쉽게 패키징해 클러스터에 배포할 수 있게 도와줍니다. Helm 차트를 생성하면 나오는 기본 파일 구조를 보겠습니다. job-chart/ ├── charts/ ├── templates/ │ ├── job.yaml ├── env/ │ ├── dev/ │ ├── values.yaml │ ├── real/ │── ├── values.yaml templates 디렉터리 하위에 정의되어 있는 Job 템플릿 YAML 파일에서 values.yaml 파일의 값을 참조하여 Kubernetes 리소스를 생성합니다. 위치에 맞게 job.yaml 템플릿 파일을 생성하고, values.yaml 파일에 Job 실행에 필요한 기본 설정값을 정의하겠습니다. apiVersion: batch/v1 kind: Job metadata: name: my-job spec: template: metadata: labels: app: name spec: containers: - name: my-container image: {{ .Values.image.name }}:{{ .Values.image.tag }} restartPolicy: OnFailure resources: limits: cpu: "8" memory: "8Gi" command: \["sh", "-c"\] args: \["실행 명령어"\] image: name: my-image config: javaopts: -server -Xms4096m -Xmx8192m ... ... Helm 차트를 배포하면 values.yaml 파일의 값을 참조하여 동적으로 Kubernetes 리소스가 생성됩니다. 그리고 리소스를 생성할 때 동적으로 파라미터를 주입하고 싶다면 다음과 같이 Helm 명령어 파라미터로 --set을 이용해 값을 전달할 수 있습니다. helm upgrade --install test-group-scheduled-1 ./ --values=./env/dev/values.yaml --set metadata.labels.order=1 ... 그럼 Helm 차트를 이용해 Job 여러 건을 동시에 실행해보겠습니다. 정상적으로 파드가 생성되고, 각각의 노드에 할당되어 작업이 수행되는 것을 확인할 수 있습니다. 클러스터 내에서 배치성 작업을 독립적으로 실행할 수 있게 되어, 서로 연관 없는 작업이 불필요하게 다른 작업이 끝날 때까지 기다릴 필요가 없어졌고, 불필요한 의존성을 제거함으로써 전체 작업의 효율성이 높아졌습니다. Kubernetes 커스텀 컨트롤러를 이용한 Job 가변적 스케줄러 구현 앞에서 Job을 활용해 의존성 없는 배치 작업들을 병렬 처리함으로써 작업 효율성을 크게 높일 수 있었습니다. 하지만 배치 작업 중에는 서로 의존성이 있어서 순차적으로 처리해야 하는 경우도 있습니다. 이런 작업을 Job으로 지연 없이 순차 처리하려고 할 때 Job의 한계점이 드러납니다. Job은 파드에서 독립적으로 실행되는 특성상, Job 간의 실행 상태를 클러스터 외부에서 실시간으로 알기 어렵습니다. 그래서 Jenkins같이 외부에서 배치를 실행하고 있다면 상태를 알 수 없기 때문에 순차 처리가 쉽지 않습니다. 배치 작업을 지연 없이 순차 처리하고 싶다면 클러스터 내부에서 Job의 실행 상태를 파악하면서 스케줄링해야 합니다. 이런 문제를 해결하기 위해서, Kubernetes 커스텀 컨트롤러를 이용해 Job의 실행 상태를 유연하게 관리할 수 있는 스케줄러를 구현해보겠습니다. 물론, 스케줄링을 위해 커스텀 컨트롤러를 반드시 구현해야 하는 것은 아닙니다. 필요한 기능을 제공하는 오픈소스가 있다면 이를 사용하는 것이 좋습니다. 그러나 오픈소스를 사용할 수 없는 환경이거나 특수한 요구 사항이 있는 경우, 또는 학습 목적으로 활용하려는 경우에는 직접 구현을 고려해볼 수 있습니다. Kubernetes 커스텀 컨트롤러의 이해 우선 스케줄러를 구현하기에 앞서, 커스텀 컨트롤러에 대해 알아보겠습니다. Kubernetess 커스텀 컨트롤러는 사용자가 정의한 커스텀 리소스의 상태를 관리하며, 리소스의 현재 상태를 지속적으로 모니터링하면서 사용자가 의도한 상태가 되도록 동작하는 컴포넌트입니다. Kubernetes에서 컨트롤러가 동작하는 방식은 다음과 같습니다. 출처: client-go under the hood 컨트롤러는 리소스의 상태를 확인하고 의도한 상태가 될 때까지 동일한 작업을 하는 Reconcile Loop 동작을 수행합니다. 그런데 만약 컨트롤러가 직접 Kubernetes API 서버와 통신을 하면서 필요한 데이터를 조회한다면 서버에 과도한 부하를 주게 됩니다. 이를 방지하기 위해, Kubernetes는 client-go 라이브러리를 활용해 API 서버와의 통신을 효율적으로 처리하는 컴포넌트를 제공합니다. client-go의 주요 컴포넌트를 보겠습니다. Reflector 컴포넌트는 서버와 통신하며 리소스를 감시(watch)합니다. 리소스의 이벤트가 발생하면 로컬 캐시에 동기화해서 최신 정보를 유지하고 리소스 검색 시에 캐시에서 검색되게 해서 서버에 부하가 가지 않게 합니다. Informer 컴포넌트는 발생한 리소스 이벤트 종류에 맞는 이벤트 핸들러를 호출해서 컨트롤러 workQueue에 리소스를 전달합니다. 이제 컨트롤러의 동작 과정을 보겠습니다. workQueue에 저장된 리소스를 순서에 맞게 꺼내 처리하는데, 이 과정이 Process Item입니다. 이 과정 중에 Reconcile 메서드가 호출되어 리소스의 현재 상태가 의도한 상태가 될 때까지 상태를 조정합니다. 즉 우리가 커스텀 컨트롤러를 구현하기 위해서는, 커스텀 리소스를 등록하고 해당 리소스의 이벤트 발생 시 동작할 Reconcile 메서드 로직과 Informer를 사용한 리소스 이벤트 감지 등이 필요하다는 것을 알 수 있습니다. 그럼 이 커스텀 컨트롤러를 어떻게 생성하면 좋을까요? 여러 가지 방법이 있지만 여기에서는 커스텀 컨트롤러를 쉽게 구현할 수 있게 도와주는 Kubebuilder 프레임워크를 이용해 구현하겠습니다. Kubernetes 커스텀 컨트롤러의 생성 Kubebuilder는 어떻게 커스텀 컨트롤러 구현을 쉽게 해줄 수 있을까요? Kubebuilder의 아키텍처는 다음과 같습니다. 출처: Architecture - The Kubebuilder Book main.go 파일을 통해 프로세스가 실행되면 컨트롤러 매니저가 클러스터에 배포되고, 컨트롤러 매니저 내부에서는 Informer를 통해 Kubernetes API 서버와 통신해 커스텀 리소스에 대한 이벤트를 감지합니다. 커스텀 리소스에 대한 이벤트가 감지되면 해당 이벤트를 큐에 저장하고, 하나씩 꺼내 컨트롤러 Reconciler 메서드를 실행해서 리소스를 사용자가 원하는 상태가 되도록 합니다. 즉, Kubebuilder는 컨트롤러 매니저가 구현되어 있기 때문에 커스텀 컨트롤러를 쉽게 구현할 수 있게 도와줍니다. 사용자는 커스텀 리소스 정의와 Reconcile 메서드 로직 구현에 집중할 수 있습니다. 이제 Kubebuilder 프레임워크를 이용해 Job 스케줄러를 구현해보겠습니다. 이 Job 스케줄러는 Operator 패턴을 기반으로 동작하며, 커스텀 리소스 JobScheduler가 생성되면 이를 관리하는 컨트롤러가 스케줄링을 수행합니다. 코드를 보기 전에 스케줄링의 전체 동작 방식을 간단히 정리하면 다음과 같습니다. 실행할 Job의 메타데이터에 그룹명과 실행 순서를 설정한 뒤, Job을 생성하고 일시 정지 상태로 둔다. 커스텀 리소스 JobScheduler에 Job 그룹명을 설정 후 생성하면, 컨트롤러는 그룹명에 해당하는 Job 목록을 조회해서 실행 순서에 맞게 일시 정지를 해제하는 방식으로 스케줄링한다. 그럼 먼저 커스텀 리소스를 구현하고 클러스터에 등록하겠습니다. kubebuilder create api --group <group> --version <version> --kind <Kind> Kubebuilder에서 위 명령어를 사용하면 커스텀 리소스 생성 템플릿이 생성됩니다. 생성된 템플릿에 필요한 코드를 추가해 스케줄러 리소스를 만들겠습니다. type JobScheduler struct { metav1.TypeMeta \`json:",inline"\` metav1.ObjectMeta \`json:"metadata,omitempty"\` Spec JobSchedulerSpec \`json:"spec,omitempty"\` Status JobSchedulerStatus \`json:"status,omitempty"\` } 생성된 템플릿에는 커스텀 리소스의 메타 정보와 Spec, Status를 정의할 수 있습니다. 일반적으로 Spec에는 리소스가 어떻게 동작해야 하는지 나타내는 값이나 리소스가 동작하기 위해 필요한 값을 설정하고, Status에는 리소스의 현재 상태를 설명하는 정보를 저장합니다. 여기에서는 컨트롤러가 클러스터를 모니터링하면서 기록하는 필드 값을 저장합니다. 이제 스케줄러 로직 수행에 필요한 값을 설정하겠습니다. type JobSchedulerSpec struct { JobGroupName string \`json:"jobGroupName"\` } type JobSchedulerStatus struct { ... CurrentActiveJobIndex int \`json:"currentJobIndex"\` JobOrderGroup \[\]JobInfo \`json:"jobOrderGroup"\` ... } Spec에는 스케줄러가 실행할 Job 그룹명을 JobGroupName 필드에 설정했습니다. Status에는 스케줄링 로직을 수행하는 데 필요한 값을 정의했습니다. 스케줄러가 실제 실행할 Job 그룹의 정보가 JobOrderGroup 필드에, 스케줄러가 현재 실행하고 있는 Job Index 값이 CurrentActiveJobIndex에 저장됩니다. 리소스를 정의했으니 리소스의 이벤트가 발생했을 때 실행되는, 실제 스케줄링을 담당할 컨트롤러 로직을 구현하겠습니다. Kubebuilder에서 다음 명령어로 컨트롤러 템플릿을 생성할 수 있습니다. kubebuilder create controller --group <group> --version <version> --kind <Kind> 생성된 컨트롤러를 보면, 리소스의 이벤트가 발생할 때마다 실행될 Reconcile 메서드를 볼 수 있습니다. func (r \*JobSchedulerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ... } Reconcile은 이벤트가 발생한 리소스가 원하는 상태가 될 때까지 조정하는 메서드입니다. 이를 이용해, 스케줄러 리소스가 생성되면 Spec에 설정해둔 Job 그룹이 스케줄링되도록 구현해보겠습니다. 스케줄링 로직을 크게 세 부분으로 나누면 다음과 같습니다. Spec에 설정된 그룹명을 확인해 해당하는 Job 그룹을 가져와 실행 순서에 맞게 정렬 후 Status에 저장한다. 실행 순서대로 Job의 일시 정지 상태를 해제해서 실행한다. 그 후 실행 중인 Job의 상태 값을 모니터링해서, 성공하면 다음 Job을 실행하고 실패하면 종료시키는 등의 스케줄링 작업을 진행한다. 그럼 먼저 1번 작업인 실행할 Job 그룹을 가져와 Status에 저장하는 부분을 보겠습니다. 리소스 Spec에 정의한 Job 그룹명에 해당되는 Job을 모두 가져와 실행 순서에 맞게 정렬 후 Status에 저장해두는 초기화 작업에 해당합니다. func (r \*JobSchedulerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ... var jobs kbatch.JobList if err := r.List(ctx, &jobs, client.InNamespace(req.Namespace)); err != nil { // Namespace에서 Job 리소스를 모두 가져온다. ... for \_, job := range jobs.Items { groupVal := job.ObjectMeta.Labels\["group"\] // Job 리소스 메타데이터에 설정해둔 group 이름과 실행 순서를 가져온다. orderVal := job.ObjectMeta.Labels\["order"\] if groupVal == jobScheduler.Spec.JobGroupName && orderVal != "" { // Spec에 설정한 JobGroupName과 group 이름이 동일한 리소스만 가져온다. jobInfo := v1.JobInfo{Name: job.Name, Namespace: job.Namespace, Order: orderVal} jobScheduler.Status.JobOrderGroup = append(jobScheduler.Status.JobOrderGroup, jobInfo) // 리소스 Status에 값 저장 } } sort.Slice(jobScheduler.Status.JobOrderGroup, func(i, j int) bool { // 실행 순서에 맞게 정렬한다. return jobScheduler.Status.JobOrderGroup\[i\].Order < jobScheduler.Status.JobOrderGroup\[j\].Order }) jobScheduler.Status.IsInitialized = true if err := r.Status().Update(ctx, &jobScheduler); err != nil { // 리소스 Status에 반영한다. logger.Error(err, "Fail update jobScheduler status jobOrderGroup") return ctrl.Result{}, err } } ... } 먼저 Namespace에서 실행 중인 모든 Job을 가져옵니다. Job 메타데이터에는 그룹명과 실행 순서가 적혀있는데, 그룹명이 Spec에 설정된 그룹명과 동일한 Job을 추출합니다. 그리고 추출한 Job을 Status JobOrderGroup 필드에 저장한 후 실행 순서에 맞게 정렬합니다. 스케줄러 리소스가 실행할 Job 목록을 저장해서 초기화를 완료했으므로, 이제 Job의 일시 정지를 풀어서 실행하겠습니다. if err := r.Get(ctx, client.ObjectKey{Name: currentActiveJob.Name, Namespace: currentActiveJob.Namespace}, &job); err != nil { ... suspend := false job.Spec.Suspend = &suspend // Job Spec에 일시 정지 옵션인 suspend 값을 변경해 실행한다. if err := r.Update(ctx, &job); err != nil { ... 실행 순서가 가장 낮은 Job 리소스를 가져와서 일시 정지 옵션인 Suspend 값을 false로 변경하면 해당 Job이 실행됩니다. 첫 번째 Job이 실행되었으니, 이제 상태를 확인해가며 다음 Job을 실행하는 스케줄링을 구현하겠습니다. ... if job.Status.Active > 0 { // Job의 Status에서 실행 상태 값을 확인한다. logger.Info("Active... : ", "job\_name", job.Name) return ctrl.Result{RequeueAfter: time.Minute}, nil // Job이 실행 중이면 Reconcile 메서드 1분 후 재시작 } if job.Status.Succeeded > 0 { ... jobScheduler.Status.CurrentActiveJobIndex = jobScheduler.Status.CurrentActiveJobIndex + 1 // Job 성공 시 다음 Job 실행 if err := r.Status().Update(ctx, &jobScheduler); err != nil { logger.Error(err, "Fail update JobScheduler currentActiveJobIndex", "current\_active\_job\_index", jobScheduler.Status.CurrentActiveJobIndex) return ctrl.Result{}, err } logger.Info("Job Success... ", "job\_name", job.Name, "current\_active\_job\_index", jobScheduler.Status.CurrentActiveJobIndex) return ctrl.Result{RequeueAfter: time.Minute}, nil } if job.Status.Failed > 0 { if job.Spec.BackoffLimit != nil && job.Status.Failed < \*job.Spec.BackoffLimit { // Job 실패 후 Retry 상태 logger.Info("Job is Failed retrying... : ", "job\_name", job.Name) return ctrl.Result{RequeueAfter: time.Minute}, nil // Reconcile 메서드 1분 후 재시작 } else { logger.Info("\[END\] Job Failed... End scheduler", "job\_name", job.Name) return ctrl.Result{}, nil // Retry까지 전부 실패했을 때 Reconcile 메서드 종료 } } ... Job 리소스는 Status에 현재 상태를 알려주는 Active, Failed, Succeeded 값이 있습니다. 이 값을 확인하면서 Job을 스케줄링하는 로직을 구현했습니다. Active: 아직 실행 중인 상태. 1분 간격으로 Reconcile 메서드가 재실행되도록 구현합니다. 이 동작은 Job이 완료될 때까지 반복됩니다. Succeeded: Job이 정상적으로 성공한 상태. 실행해야 할 CurrentActiveJobIndex 값을 올려서 다음 Job이 실행되도록 설정합니다. Failed: Job의 Retry까지 전부 실패한 경우에는 스케줄링을 종료합니다. 이제 정상적으로 스케줄링이 되는지 확인해보겠습니다. 로컬에서 클러스터를 생성해 테스트를 진행하겠습니다. Kubebuilder에서 제공되는 makefile 스크립트를 이용해 make 명령어로 빌드 배포를 포함한 다양한 작업을 수행할 수 있습니다. make install make run 커스텀 리소스를 클러스터에 등록하고 컨트롤러 매니저를 배포해야 합니다. install로 커스텀 리소스가 클러스터에 등록되고 make run으로 커스텀 컨트롤러 매니저가 deployment 됩니다. 이제 배포된 컨트롤러 매니저는 Kubernetes API와 통신해서 커스텀 리소스의 이벤트 발생을 감지하고 컨트롤러 로직을 실행합니다. 다만, 여기서 실행한 make run 스크립트는 로컬 환경에서 테스트할 때 사용되는 명령어입니다. 운영 클러스터에 배포할 때는 kustomization을 이용해 하나의 YAML 파일로 패키징해 배포할 수 있습니다. 컨트롤러 매니저가 배포되었으니, 이제 실행하고 싶은 Job을 실행해보겠습니다. 먼저 Job 메타데이터에 그룹 이름과 실행 순서를 설정하고 Job이 바로 실행되지 않도록 일시 정지 상태로 설정합니다. kind: Job metadata: ... labels: group: test-group order: "1" ... spec: suspend: true ... kind: Job metadata: ... labels: group: test-group order: "2" ... spec: suspend: true ... Job은 다음과 같이 일시 정지 상태입니다. 이제 리소스를 실행해서 Job 스케줄링을 시작해보겠습니다. ... kind: JobScheduler metadata: ... spec: jobGroupName: "test-group" test-group이라는 Job 그룹을 실행하도록 설정한 JobScheduler 커스텀 리소스 템플릿 파일을 생성했습니다. 이제 이 리소스를 실행하겠습니다. 리소스가 생성되면 앞에서 배포해둔 커스텀 컨트롤러 매니저가 커스텀 리소스의 이벤트 발생을 감지해서 해당 리소스를 큐에 넣어 Reconcile 메서드를 실행합니다. 스케줄러 로직이 실행된 후 다시 실행된 Job을 보면 스케줄링 로직에 따라 순서대로 실행된 것을 확인할 수 있습니다. Job 상태를 확인해 보면 모두 정상 성공한 것을 확인할 수 있습니다. 마치며 Job을 활용해 배치성 작업들을 독립적인 컨테이너에서 실행하도록 변경해서 동시에 여러 작업을 실행할 수 있게 했습니다. 이를 통해 병렬성을 높여 작업 효율성을 향상시키고, Kubernetes 커스텀 컨트롤러를 활용해 Job 스케줄링을 지원하는 실시간 워크플로를 구현해 작업 대기 시간을 줄이고 신뢰성을 높일 수 있었습니다. 다만 여기에서는 직접 워크플로를 구현했지만, 이미 잘 구현되어 있는 Argo Workflow나 Apache Airflow와 같은 오픈소스 워크플로가 있으므로 이를 활용해 작업을 관리하는 것이 효율적이고 신뢰성이 높으며 유지 보수에도 유리합니다. 오픈소스를 사용할 수 없는 환경이거나 특수한 요구 사항이 존재하는 경우에는 직접 구현해서 클러스터를 유연하게 확장해보는 것도 좋을 것입니다.

토스증권은 데이터센터 장애 상황에도 유저에게 정상적으로 서비스를 제공하기 위해 대부분의 시스템을 이중화했습니다. Kafka 이중화 구성에 대한 개요를 소개드려요.

안녕하세요, 네이버클라우드 팀입니다. 작년 7월, 네이버클라우드 테크 앰버서더 기술 컨퍼런스 NAVER Cloud Master Day가 성공적으로 개최되었습니다. (후기 1편 / 2편) 그 열기를 이어, 두 번째 컨퍼런스를 네이버의 두 번째 사옥 1784에서 열게 됐습니다! 클라우드와 AI 기술에 관심 있는 80여 명의 참석자분들과 함께 테크 앰버서더...
안녕하세요! 뱅크샐러드의 Server Engineer 조성민입니다. 이번 글에서는 제 팀인 금융쇼핑 PA…
By J Han, Pallavi PhadnisContextAt Netflix, we use Amazon Web Services (AWS) for our cloud infrastructure needs, such as compute, storage, and networking to build and run the streaming platform tha...
Part 1: Understanding The ChallengesBy: Varun KhaitanWith special thanks to my stunning colleagues: Mallika Rao, Esmir Mesic, Hugo MarquesIntroductionAt Netflix, we manage over a thousand global co...

안녕하세요 Dev Platform & Corporate IT팀 팀 박진규입니다.이번 포스팅에서는 Windows Container에 대한 내용을 공유드리려 합니다. 제가 담당하는 서비스들 중에서는 Windows OS에 종속적인 서비스들이 존재하는데,(ex. net framework)어...
![[네이버웍스] 경영지원 활용팁 웍스 경영지원 데모 계정에 초대합니다.](https://blogthumb.pstatic.net/MjAyNDEyMDlfMjI0/MDAxNzMzNzA2OTEwNjc5.jyAQNCmKVzb1N07O1ZMptJGVnSd0zN-HDzBVBOWRHtIg.qVGQh3Saso3_f8baDpwK_IGeuy-0WWQNCrWSBzjNbWEg.PNG/w_demo2.png?type=s3)
안녕하세요, 협업과 소통을 위한 필수 기능으로 글로벌 53만 기업의 든든한 협업툴 역할을 해온 네이버웍스(NAVER WORKS)입니다! “웍스 경영지원 도입을 검토하고 있는데, 상품 신청 전에 둘러볼 수 있는 방법이 없을까요?” “웍스 경영지원 상품을 가입했고 30일 무료 체험 중인데, 관리자와 사용자 역할을 왔다 갔다 하며 간편하게 테스트할 수 있는...
![[NEWS] 네이버클라우드, '2024년 가족친화인증기업'으로 선정](https://blogthumb.pstatic.net/MjAyNDEyMDlfMTE3/MDAxNzMzNzM1NjIyNjE5.QtXmi9Rvh-LXaySJWF7uId_qDIIPrKFERLzu2k_oB70g.0klV69JlbH3uU9RqW1_6jU-3-eU1BhpEd9MRJA_0xUEg.PNG/네이버클라우드_블로그_썸네일.001.png?type=s3)
안녕하세요, 누구나 쉽게 시작하는 클라우드 네이버클라우드 ncloud.com 입니다. 오늘은 기쁜 소식을 전해 드리러 왔습니다! 네이버클라우드가 '2024년 가족친화인증기업’으로 선정되었습니다! *가족친화인증이란? 가족친화제도를 모범적으로 운영하는 기업 및 공공기관에 대하여 심사를 통해 여성가족부가 인증을 부여하는 제도 네이버클라우드는 일과 가정생활을...

안녕하세요 올리브영의 POS서버를 맡고있는 개발자 Q평E평 입니다. 여러분은 운영 중인 시스템 배포를 어떻게 하고 있으신가요? 최근엔 다양한 CI/CD…
Spring Kafka 활용한 오프셋 이동 및 메시지 재처리 방법