Kubernetes에서 대규모 Go 프로젝트 아키텍처 배움
Ethan Miller
Product Engineer · Leapcell

Go를 사용하여 확장성, 안정성 및 유지 관리성이 뛰어난 대규모 프로젝트를 구축하기 전에 Kubernetes의 프로젝트 구조를 먼저 살펴보고 컨테이너 오케스트레이션을 위한 일련의 기능 모듈을 어떻게 구성하는지 살펴보겠습니다.
Kubernetes 코드 레이아웃
다음은 Kubernetes의 주요 최상위 디렉토리와 해당 주요 기능 목록입니다. 다음으로 각 디렉토리의 목적을 하나씩 설명합니다.
- api: 인터페이스 프로토콜 저장
- build: 애플리케이션 빌드 관련 코드
- cmd: 각 애플리케이션의
main
진입점 - pkg: 각 구성 요소의 주요 구현
- staging: 구성 요소 간에 상호 의존적인 코드를 임시로 저장
api
JSON 및 프로토콜에 대한 정의를 포함하여 OpenAPI 및 Swagger 파일을 저장합니다.
build
여기에는 일시 중지 프로그램과 같이 필요한 이미지뿐만 아니라 K8s의 각 구성 요소를 구축하는 것을 포함하여 Kubernetes 프로젝트 구축을 위한 스크립트가 포함되어 있습니다.
cmd
cmd
디렉토리에는 실행 파일 빌드를 위한 기본 패키지의 소스 파일이 저장됩니다. 여러 실행 파일을 빌드해야 하는 경우 각 실행 파일을 자체 하위 디렉토리에 배치할 수 있습니다. Kubernetes cmd
디렉토리 아래의 특정 하위 디렉토리를 살펴보겠습니다.
- cmd: 각 애플리케이션의 `main` 메소드
- kube-proxy: 네트워크 관련 규칙 담당
- kube-apiserver: K8s API를 노출하고 요청을 처리하며 다양한 리소스(Pod, ReplicaSet, Service)에 대한 CURD 작업 제공
- kube-controller-manager
- kube-scheduler: 새로 생성된 Pod를 모니터링하고 실행할 노드 선택
- kubectl: 클러스터 액세스를 위한 명령줄 도구
보시다시피 kube-proxy 및 kube-apiserver와 같은 K8s의 친숙한 구성 요소를 여기에서 찾을 수 있습니다.
pkg
pkg
디렉토리에는 프로젝트 자체에 필요한 종속성 및 내보낸 패키지가 모두 포함되어 있습니다.
- pkg: 각 구성 요소의 주요 구현
- proxy: 네트워크 프록시 구현
- kubelet: 노드에서 Pod 유지 관리
- cm: cgroups와 같은 컨테이너 관리
- stats: `cAdvisor`에 의해 구현된 리소스 사용량
- scheduler: Pod 스케줄링 구현
- framework
- controlplane: 제어 평면
- apiserver
staging
스테이징 디렉터리의 패키지는 symbolic link를 통해 k8s.io에 연결됩니다. 먼저 Kubernetes 프로젝트는 규모가 크기 때문에 분산된 저장소로 인해 발생하는 개발 장애를 방지하는 데 도움이 되므로 모든 코드를 하나의 pull request로 제출하고 검토할 수 있습니다. 이러한 방식으로 모듈성이 보장됨과 동시에 주요 코드 저장소의 완전성이 유지됩니다.
동시에 go mod에서 replace
지시문을 사용하면 모든 종속성에 태그를 지정할 필요가 없어 버전 관리 및 릴리스 프로세스가 단순화됩니다.
이러한 방식으로 수행하지 않고 대신 모노레포 접근 방식을 사용하여 스테이징 아래의 모든 코드를 독립적인 저장소로 분할한 경우 이러한 하위 저장소의 코드가 변경될 때마다 먼저 하위 저장소에 제출하고 새 태그를 게시한 다음 추가 개발 전에 go mod에서 이전 태그를 교체해야 합니다. 이는 의심할 여지 없이 전체 개발 비용을 증가시킬 것입니다.
따라서 스테이징 디렉터리의 패키지를 symbolic link를 통해 주요 저장소에 연결하면 버전 관리 및 릴리스 프로세스가 효과적으로 간소화됩니다.
표준 Go 프로젝트 레이아웃과의 비교
internal
디렉토리는 외부용으로 내보낼 수 없는 패키지에 사용됩니다. Go에서 internal
의 기본 원칙은 프로젝트 자체 내에서 정상적으로 사용할 수 있는 동시에 외부 프로젝트에 표시되지 않도록 하는 것입니다.
그러나 Kubernetes에는 internal
디렉토리가 없습니다. 이는 Kubernetes 프로젝트가 2014년경에 시작되었고 internal
디렉토리 개념은 Go 1.4(2014년 말에 릴리스됨)에서만 도입되었기 때문입니다. Kubernetes 프로젝트의 초기 개발 동안 internal
을 사용하는 규칙은 아직 널리 확립되지 않았으며 이를 도입하기 위한 대규모 리팩터링은 나중에 없었습니다.
동시에 Kubernetes의 설계 목표 중 하나는 모듈성과 디커플링입니다. internal
패키지를 사용하여 패키지 액세스를 제한할 필요 없이 명시적인 패키지 구성 및 코드 구조를 통해 캡슐화를 달성합니다.
이 시점에서 우리는 프로젝트 구축을 위한 표준 최상위 디렉토리 구조를 이미 이해하고 있습니다.
Go에는 Java와 같은 표준 디렉토리 프레임워크가 없습니다. 결과적으로 다른 프로젝트를 시작할 때마다 각 프로젝트의 특정 코드 구조에 익숙해져야 합니다. 동일한 팀 내에서도 다른 구조가 존재할 수 있으며 이는 프로젝트를 이해하려는 신규 사용자에게 상당한 장애물이 될 수 있습니다.
이러한 장애물로 인해 협업이 어려울 수 있습니다. 통합된 최상위 디렉토리 구조를 사용하면 코드를 빠르게 찾고 프로젝트를 인수할 때 표준 진입점을 확보하여 개발 효율성을 높이고 협업 개발 중 코드 위치에 대한 혼동을 줄일 수 있습니다.
그러나 통합된 코드 디렉토리 구조만으로 완벽한 대규모 프로젝트를 만들 수 있을까요? 정답은 물론 아닙니다.
통합된 디렉토리 구조에만 의존해서는 코드가 점차 감소하고 혼란스러워지는 문제를 완전히 해결할 수 없습니다. 건전한 설계 원칙만이 프로젝트가 계속 확장됨에 따라 설계 컨텍스트를 명확하게 유지할 수 있습니다.
선언적 설계 철학
선언적 API는 Kubernetes의 전체 코드 설계를 통해 실행되므로 절차적 프로그래밍에 빠지지 않습니다.
예를 들어 리소스의 상태를 변경할 때 K8s에게 수행할 단계를 알리는 대신 원하는 상태를 K8s에게 알려야 합니다. 이것이 kubelet 롤링 업데이트가 단계적으로 폐지된 이유이기도 합니다. 그 설계는 Pod 업데이트의 전체 프로세스를 세밀하게 관리하기 때문입니다.
Kubernetes에 원하는 상태를 알림으로써 kubelet은 해당 상태에 따라 적절한 조치를 취할 수 있으며 외부로부터 과도한 개입이 필요하지 않습니다.
이 시점에서 다음과 같이 궁금할 수 있습니다. 선언적 API는 프로젝트가 확장될 때 모듈을 명확하게 유지하는 데 어떻게 도움이 될까요? 이것은 사용자가 Kubernetes를 사용할 때 인식하는 것 아닌가요? 내부 설계와 어떻게 관련이 있나요?
인터페이스를 설계할 때 전체 운영 프로세스를 사용자에게 노출하고 Pod가 업데이트되는 방식에 단계별로 간섭하도록 하면 우리가 설계하는 모듈은 필연적으로 절차적이 됩니다. 이러한 방식으로 코드 모듈은 많은 사용자 작업과 결합되어 명확하게 유지하기가 어렵습니다.
그러나 선언적 API를 사용하면 K8s에 원하는 상태를 알린 후 클러스터는 여러 내부 구성 요소 간에 조정하여 궁극적으로 원하는 상태를 달성할 수 있습니다. 사용자는 내부적으로 업데이트되는 방식을 알 필요가 없습니다. 또한 추가 협업 플러그인이 필요한 경우 사용자 작업을 위한 더 많은 API를 노출하지 않고도 새 모듈을 직접 추가할 수 있습니다.
cAdvisor는 K8s에서 배포한 리소스를 모니터링하고 컨테이너 리소스 메트릭을 수집합니다. 외부 구성 요소에 의존하지 않고 독립적으로 작동합니다. 그런 다음 컨트롤러는 이러한 메트릭을 사용자 정의된 대상과 비교하여 확장 또는 축소 조건이 충족되는지 여부를 확인합니다.
모듈이 독립적이기 때문에 cAdvisor는 이러한 메트릭이 관찰을 위한 것인지 자동 확장을 위한 것인지에 관계없이 이러한 메트릭이 사용되는 방법에 신경 쓰지 않고 모니터링 메트릭을 수집하고 반환하는 데만 집중하면 됩니다.
이는 다양한 작업 구성 요소를 설계할 때의 핵심 원칙이기도 합니다. 충족해야 할 요구 사항을 명확하게 정의합니다. 정보를 전달할 때 입력 및 출력에만 집중합니다. 내부 구현에 관해서는 외부 비즈니스 사용을 최대한 간단하게 만들면서 외부적으로 노출하지 않고 캡슐화할 수 있습니다.
과도한 엔지니어링 피하기
과도한 엔지니어링 설계는 종종 불충분한 설계보다 나쁩니다.
가장 초기의 Kubernetes 버전은 0.4였습니다. 네트워킹의 경우 공식 구현은 GCE가 salt 스크립트를 실행하여 브리지를 만드는 것이었고 다른 환경에 권장되는 솔루션은 Flannel 및 OVS였습니다.
Kubernetes가 개발됨에 따라 Flannel은 일부 상황에서 더 이상 충분하지 않았습니다. 2015년경에 Calico와 Weave가 커뮤니티에 등장했는데 기본적으로 네트워킹 문제를 해결했습니다. 따라서 Kubernetes는 더 이상 자체적으로 이러한 작업을 수행하는 데 노력을 기울일 필요가 없었으므로 CNI를 도입하여 네트워크 플러그인을 표준화했습니다.
Kubernetes가 처음부터 완벽하게 설계된 것은 분명히 아닙니다. 대신 새로운 문제가 발생함에 따라 다양한 환경의 변화에 적응하기 위해 새로운 설계가 도입되었습니다.
프로젝트를 시작할 때 종속성이 비교적 명확합니다. 따라서 엔지니어링 설계 초기에는 순환 종속성이 발생하지 않습니다. 그러나 프로젝트가 성장함에 따라 이러한 문제가 점차 나타납니다. 제품의 기능 요구 사항은 코드 설계에서 상호 참조를 유발합니다.
시작하기 전에 모든 비즈니스 배경과 해결해야 할 문제를 최대한 이해하려고 노력하더라도 제품 기능이 변경되고 프로그램이 반복됨에 따라 새로운 문제가 불가피하게 발생합니다. 우리가 할 수 있는 일은 모듈 설계 및 종속성 관리에 주의를 기울이고 기능을 최대한 응집력 있게 유지하며 나중에 추상화를 추가할 때 "리팩터링" 방식으로 이전 코드를 모두 점검해야 하는 것을 피하는 것입니다.
"확장성"을 위해 시스템을 과도하게 설계하거나 설계를 위해 설계하는 것은 향후 변경의 걸림돌이 될 수 있습니다.
전자 상거래 비즈니스 시나리오로 설계 진화를 설명해 보겠습니다.
처음에는 시스템에 두 개의 모듈이 있습니다.
- 주문 모듈: 주문 생성, 결제, 상태 업데이트 등을 처리합니다. 배송 주소, 연락처 정보 등과 같은 사용자 정보를 위해 사용자 모듈에 의존합니다.
- 사용자 모듈: 사용자 정보, 등록, 로그인 관리 및 사용자 데이터 저장을 담당합니다. 주문 모듈에 의존하지 않습니다.
이 초기 설계에서 종속성은 단방향입니다. 주문 모듈은 사용자 모듈에 의존합니다.
이 단계에서는 코드에서 과도하게 추상화할 필요가 없습니다. 많은 프로젝트가 성공할지 실패할지 예측할 수 없으므로 설계에 너무 많은 노력을 기울이는 것은 제품 릴리스 관점에서 실현 가능하지 않으며 제품 개념이 크게 변경되면 과도한 설계가 향후 수정의 장애물이 될 수 있습니다.
요구 사항이 진화함에 따라 새로운 요구 사항이 발생합니다. 플랫폼은 구매 내역(주문 기록)을 기반으로 사용자에게 개인화된 제품을 추천해야 합니다.
개인화된 추천을 달성하기 위해 사용자 모듈은 이제 사용자 주문 내역을 가져오기 위해 주문 모듈의 API를 호출해야 합니다.
이제 종속성은 다음과 같습니다.
- 주문 모듈은 사용자 정보를 위해 사용자 모듈에 의존합니다.
- 사용자 모듈은 주문 내역을 위해 주문 모듈에 의존합니다.
이 변경으로 인해 순환 종속성이 생성됩니다. 주문 모듈은 사용자 모듈에 의존하고 사용자 모듈도 주문 모듈에 의존합니다.
순환 종속성을 해결하기 위해 다음과 같은 여러 솔루션을 고려할 수 있습니다.
모듈 책임 분리: 개인화된 추천 논리 처리에 전념하는 추천 모듈과 같은 새 모듈을 도입합니다. 추천 모듈은 사용자 및 주문 모듈에서 별도로 데이터를 가져와서 모듈 간의 직접적인 종속성을 피할 수 있습니다.
모듈을 추출함으로써 사용자 및 주문 모듈 간의 결합을 해결합니다.
그러나 새로운 요구 사항이 발생합니다. 프로모션 이벤트 중에는 사용자가 이벤트별 제품을 구매합니다. 제품 관리자는 추천 모듈이 이러한 주문을 즉시 감지하고 관련 프로모션 제품에 대한 추천을 제공할 수 있기를 원합니다. 예를 들어 사용자가 할인된 스포츠 시계를 구매하고 할인된 Bluetooth 스포츠 이어폰도 추천하면 사용자의 재구매율이 더 높을 수 있습니다.
이 시나리오에서는 주문 모듈이 데이터를 전달하기 위해 추천 모듈을 직접 호출하는 것은 분명히 바람직하지 않습니다. 추천 모듈은 이미 사용자 구매 데이터를 위해 주문 모듈에 의존하고 있으며 단방향 종속성을 설정합니다. 주문 모듈이 추천 모듈을 호출하도록 하면 순환 종속성이 다시 생성됩니다.
그렇다면 추천 모듈은 어떻게 주문 변경 사항을 빠르게 감지할 수 있을까요? 이를 위해서는 이벤트 기반 아키텍처가 필요합니다.
이벤트 기반 접근 방식을 사용하면 사용자가 주문할 때 주문 모듈이 이벤트를 트리거하고 추천 모듈은 사용자 주문과 관련된 이벤트를 구독합니다. 이러한 방식으로 두 모듈은 서로의 API를 직접 호출할 필요가 없습니다. 대신 데이터는 이벤트를 통해 전달됩니다.
데이터를 받은 후 추천 모듈은 즉시 새로운 추천 모델을 재학습하고 관련 제품을 사용자에게 추천할 수 있습니다.
위의 예에서 엔터프라이즈 애플리케이션의 주요 과제인 비즈니스 도메인 모델링을 알 수 있습니다.
모델링에서는 요구 사항이 지속적으로 진화함에 따라 설계를 최적화하는 프로세스에 더 가깝습니다.
위에 설명된 사용자, 주문 및 추천 모듈은 대부분의 To-C(소비자 대면) 제품의 진화에서 일반적인 시나리오이기도 합니다.
진화 과정에서 모듈 설계 및 코드 구조를 지속적으로 최적화하고 반복 속도를 개선하는 방법은 우리가 탐구하고 생각해 봐야 할 문제입니다.
요약
이 문서의 내용을 검토해 보겠습니다.
- 대규모 프로젝트를 구축할 때 통합된 디렉토리 구조는 협업 효율성을 향상시킬 수 있지만 건전한 설계 원칙은 프로젝트가 성장함에 따라 명확성과 확장성을 유지하는 데 핵심입니다.
- Kubernetes의 선언적 API는 모듈이 독립적으로 유지되도록 돕고 절차적 프로그래밍의 함정을 피합니다.
- 프로젝트 설계는 실제 필요에 따라 단계별로 진화해야 하며 과도한 엔지니어링을 피해야 합니다.
- 모듈 책임과 종속성의 적절한 분리에 집중하고 이벤트 기반 접근 방식을 사용하여 모듈 간의 결합을 해결합니다.
Go 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발합니다.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불합니다. 요청도 없고 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불합니다.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전히 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없으므로 구축에 집중하십시오.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ