이 글은 '코어 쿠버네티스'의 5장 내용을 실습한 내용입니다.

 

kind에서 Calico CNI 를 테스트 해본다.

kind 클러스터에 아래 config를 제공해 기본 CNI를 비활성화 하고 클러스터를 생성한다.

# cat kind-Calico-conf.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
        disableDefaultCNI: true
        podSubnet: 192.168.0.0/16
nodes:
- role: control-plane
- role: worker

kind 설정에 대해서는 아래 문서를 참고할 수 있다.

https://kind.sigs.k8s.io/docs/user/configuration/

책이 출간된 이후 apiVersion이 변경되었으므로 kind.sigs.k8s.io가 아닌 kind.x-k8s.io를 사용해야 한다.

# kind create cluster --name=calico --config=./kind-Calico-conf.yaml
ERROR: failed to create cluster: unknown apiVersion: kind.sigs.k8s.io/v1alpha4

정상적으로 완료되면 아래와 같이 확인 가능하다. defaultCNI가 disable 되었으므로 아래와 같은 결과가 정상이다.

# kind create cluster --name=calico --config=./kind-Calico-conf.yaml
Creating cluster "calico" ...
 ✓ Ensuring node image (kindest/node:v1.27.3) 🖼
 ✓ Preparing nodes 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-calico"
You can now use your cluster with:

kubectl cluster-info --context kind-calico

Have a nice day! 👋
# kubectl get no
NAME                   STATUS     ROLES           AGE   VERSION
calico-control-plane   NotReady   control-plane   40s   v1.27.3
calico-worker          NotReady   <none>          19s   v1.27.3
# kubectl get po -n kube-system
NAME                                           READY   STATUS    RESTARTS   AGE
coredns-5d78c9869d-5zkr4                       0/1     Pending   0          44s
coredns-5d78c9869d-t2c9x                       0/1     Pending   0          44s
etcd-calico-control-plane                      1/1     Running   0          57s
kube-apiserver-calico-control-plane            1/1     Running   0          57s
kube-controller-manager-calico-control-plane   1/1     Running   0          59s
kube-proxy-kqsfb                               1/1     Running   0          39s
kube-proxy-qqmw4                               1/1     Running   0          44s
kube-scheduler-calico-control-plane            1/1     Running   0          57s

 

 

아래와 같이 Calico CNI를 설치한다.

먼저 calico operator와 CRD를 생성한다.

# kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.3/manifests/tigera-operator.yaml
namespace/tigera-operator created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgpfilters.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/apiservers.operator.tigera.io created
customresourcedefinition.apiextensions.k8s.io/imagesets.operator.tigera.io created
customresourcedefinition.apiextensions.k8s.io/installations.operator.tigera.io created
customresourcedefinition.apiextensions.k8s.io/tigerastatuses.operator.tigera.io created
serviceaccount/tigera-operator created
clusterrole.rbac.authorization.k8s.io/tigera-operator created
clusterrolebinding.rbac.authorization.k8s.io/tigera-operator created
deployment.apps/tigera-operator created

위 단계에서는 operator ns와 operator가 생성된다.

# kubectl get po -A -w
NAMESPACE            NAME                                           READY   STATUS    RESTARTS   AGE
kube-system          coredns-5d78c9869d-5zkr4                       0/1     Pending   0          8m42s
kube-system          coredns-5d78c9869d-t2c9x                       0/1     Pending   0   ..
tigera-operator      tigera-operator-f6bb878c4-x4whq                1/1     Running   0          56s

 

실제로 calico를 설치한다.

# kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.3/manifests/custom-resources.yaml
installation.operator.tigera.io/default created
apiserver.operator.tigera.io/default created

 

아래와 같이 노드도 Ready 상태가 되고, calico 파드와 기타 네트워크 연결이 안되서 Pending이던 파드들이 보드 Running 상태가 되었다.

# kubectl get po -A
NAMESPACE            NAME                                           READY   STATUS    RESTARTS   AGE
calico-apiserver     calico-apiserver-76cc85cf7f-b2j7p              1/1     Running   0          5m24s
calico-apiserver     calico-apiserver-76cc85cf7f-lgnjs              1/1     Running   0          5m24s
calico-system        calico-kube-controllers-5f6db5bc7b-ntvhc       1/1     Running   0          9m56s
calico-system        calico-node-45w75                              1/1     Running   0          9m57s
calico-system        calico-node-s8nzv                              1/1     Running   0          9m57s
calico-system        calico-typha-5bbf9665bd-5mf7t                  1/1     Running   0          9m57s
calico-system        csi-node-driver-f4958                          2/2     Running   0          9m56s
calico-system        csi-node-driver-v54cj                          2/2     Running   0          9m56s
kube-system          coredns-5d78c9869d-5zkr4                       1/1     Running   0          21m
kube-system          coredns-5d78c9869d-t2c9x                       1/1     Running   0          21m
kube-system          etcd-calico-control-plane                      1/1     Running   0          21m
kube-system          kube-apiserver-calico-control-plane            1/1     Running   0          21m
kube-system          kube-controller-manager-calico-control-plane   1/1     Running   0          21m
kube-system          kube-proxy-kqsfb                               1/1     Running   0          21m
kube-system          kube-proxy-qqmw4                               1/1     Running   0          21m
kube-system          kube-scheduler-calico-control-plane            1/1     Running   0          21m
local-path-storage   local-path-provisioner-6bc4bddd6b-jhj7s        1/1     Running   0          21m
tigera-operator      tigera-operator-f6bb878c4-x4whq                1/1     Running   0          13m
# kubectl get no
NAME                   STATUS   ROLES           AGE   VERSION
calico-control-plane   Ready    control-plane   22m   v1.27.3
calico-worker          Ready    <none>          21m   v1.27.3

책이 출간된 이후로 변경되어 아래 링크를 참고했다. 현재는 operator 방식을 사용하는 것으로 보인다.

https://docs.tigera.io/calico/latest/getting-started/kubernetes/kind

 

그리고 daemonset로 calico-node 만 있었던 것에서 calico-typha 라는 파드가 추가로 생기는 아키텍처 변화가 있었던 것으로 보인다.

node에서 노드에 필요한 BGP와 IP 경로를 설정하고, Typha에서 API server를 watch 하면서 k8s 리소스와 caclio custom resource의 변화를 바탕으로 node를 업데이트 한다.

 

https://docs.tigera.io/calico/latest/getting-started/kubernetes/hardway/install-typha

Typha sits between the Kubernetes API server and per-node daemons like Felix and confd (running in `calico/node`). It watches the Kubernetes resources and Calico custom resources used by these daemons, and whenever a resource changes it fans out the update to the daemons.

calico에 대한 추가 테스트는 다시 작성한다.

 

테스트를 마치면 아래의 명령으로 클러스터를 삭제할 수 있다.

kind delete cluster --name calico

 

WSL(Windows Subsystem for Linux) 환경에서 kind를 실행하기 위해 필요한 패키지들을 설치한다.

docker 설치

참고로 Microsoft 공식 문서의 가이드에서는 docker desktop 에 대한 설치 가이드만 있다.

https://learn.microsoft.com/ko-kr/windows/wsl/tutorials/wsl-containers

단순히 docker 인스턴스를 설치하기 위해 아래를 참고한다.

https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

 

apt 리포지터리 설정한다.

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add the repository to Apt sources:
echo \
  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

 

docker 설치한다.

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

 

다만 이렇게 해도 docker 명령을 수행해보면 아래와 같이 에러가 발생하는 것을 알 수 있다.

# docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
# systemctl status docker
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down

 

기본적으로 systemd에서 docker를 실행해 줄 것으로 기대하지만, wsl에서는 기본적으로 systemd 가 실행되지 않기 때문에 아래와 같이 /etc/wsl.conf 를 작성한다.

https://learn.microsoft.com/ko-kr/windows/wsl/systemd

[boot]
systemd=true

참고로, 위 방법이 제대로 동작하지 않는다면 사전에 wsl --update를 수행해야 할 수 있다. (WSL 버전이 0.67.6 이상)

> wsl --update
설치 중: LinuxWindows 하위 시스템
[==========================59.0%===                        ]
LinuxWindows 하위 시스템이(가) 설치되었습니다.

 

이제 사용 가능하다.

# systemctl status docker
● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2023-11-05 12:52:49 KST; 23s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 397 (dockerd)
      Tasks: 14
     Memory: 102.7M
     CGroup: /system.slice/docker.service
             └─397 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.436289714+09:00" level=info msg="Loading contai>
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.537940657+09:00" level=info msg="Loading contai>
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.574554915+09:00" level=warning msg="WARNING: No>
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.574608315+09:00" level=warning msg="WARNING: No>
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.574617075+09:00" level=warning msg="WARNING: No>
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.574621785+09:00" level=warning msg="WARNING: No>
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.574642268+09:00" level=info msg="Docker daemon">
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.574820321+09:00" level=info msg="Daemon has com>
Nov 05 12:52:49 DESKTOP-UN5DI0K systemd[1]: Started Docker Application Container Engine.
Nov 05 12:52:49 DESKTOP-UN5DI0K dockerd[397]: time="2023-11-05T12:52:49.609024013+09:00" level=info msg="API listen on >
# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

 

설치 확인

sudo docker run hello-world

 

아래와 같은 결과를 얻는다면 정상이다.

# sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25f9d34f58ce9a0df68429c5af48e8d
Status: Downloaded newer image for hello-world:latest

Hello from Docker!

 

kind 설치

kind는 docker에 컨테이너로 노드를 실행 시켜서 kuberentes를 실행하므로써 로컬 환경에서 kubernetes 테스트를 가능케하는 도구이다. 

 

아래를 참고한다.

https://kind.sigs.k8s.io/docs/user/quick-start/#installation

# For AMD64 / x86_64
[ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
# For ARM64
[ $(uname -m) = aarch64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-arm64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

 

실제로 kind는 바이너리에 불과함을 알 수 있다.

kind로 클러스터를 생성하면, 아래와 같이 컨테이너 이미지가 각 노드 처럼 실행되는 것을 알 수 있다.

CONTAINER ID   IMAGE                  COMMAND                  CREATED       STATUS       PORTS                       NAMES
e9d61cb5b472   kindest/node:v1.27.3   "/usr/local/bin/entr…"   8 hours ago   Up 8 hours   127.0.0.1:44423->6443/tcp   calico-control-plane
1d481afc16a6   kindest/node:v1.27.3   "/usr/local/bin/entr…"   8 hours ago   Up 8 hours                               calico-worker

 

 

kind로 로컬 환경에서 kubernetes 를 테스트 해보기 위해 docker와 kind를 설치했다. 

kind 실제 사용은 앞으로 작성될 블로그를 참고할 수 있다.

wsl에서 root 패스워드를 분실 했을 때, 아래 명령을 실행하면 root로 바로 진입할 수 있다.

> ubuntu config --default-user root

 

wsl로 들어가보면 root 로 바로 들어간 것을 알 수 있다.

root 사용자에서 passwd 로 변경하면 된다.

 

root@com:/# passwd
New password:
Retype new password:
passwd: password updated successfully

이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 23~26장 써머리입니다.

Go에서 에러를 처리하는 방법과 동시성 프로그래밍에 대한 주제를 다루고 있습니다.

23. 에러 핸들링

에러 핸들링(error handling)은 프로그램의 에러를 처리하는 방법을 말한다. 특정 에러가 발생했을 때 프로그램이 강제 종료 되는 것보다는 적절한 메시지를 출력하고, 에러를 다른 방식으로 처리해서 사용자 경험을 향상 시킬 수 있다.

package main

import (
	"os"
)

const filename string = "data.txt"

func main() {
	file, _ := os.Open(filename)

	defer file.Close()
}

이렇게 하면 Program exited.로 종료한다.

package main

import (
	"fmt"
	"os"
)

const filename string = "data.txt"

func main() {
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()
}

이렇게하면 open data.txt: no such file or directory으로 종료한다.

fmt 패키지의 Errorf() 함수로 err을 반환하면 원하는 에러 메시지를 만들어 전달할 수 있다. 또는 errors 패키지의 New()함수를 이용해서 error를 생성할 수도 있다.

import "errors"

errors.New("에러 메시지")

error 타입은 인터페이스로 문자열을 반환하는 Error() 메서드로 구성되어 있다. 즉 어떤 타입이든 문자열을 반환하는 Error()메서드를 포함하고 있다면 에러로 사용할 수 있다.

type error interface {
	Error() sting
}

회원 가입에서 패스워드 길이를 체크하는 예제를 살펴본다.

package main

import (
	"fmt"
)

type PasswordError struct { // 에러 구조체 선언
	Len        int
	RequireLen int
}

func (err PasswordError) Error() string {
	return "암호 길이가 짧습니다."
}

func RegisterAccount(name, password string) error {
	if len(password) < 8 {
		return PasswordError{len(password), 8} // 에러 반환, 암호 길이가 짧을 때 PasswordError 구조체 정보 반환
	}
	return nil
}

func main() {
	err := RegisterAccount("myaccnt", "mypw")
	if err != nil { // 에러 확인
		if errInfo, ok := err.(PasswordError); ok { // 인터페이스 변환, 인터페이스 변환 성공 여부 검사(ok)->다양한 에러 타입에 대응
			fmt.Printf("%v Len:%d RequireLen:%d\\n",
								 errInfo, errInfo.Len, errInfo.RequireLen)
		}
	} else {
		fmt.Println("회원 가입 했습니다.")
	}

}

한편 패닉(panic)은 프로그램을 정상 진행시키기 어려운 상황을 만났을 때 프로그램 흐름을 중지시키는 기능이다.

지금까지 error 인터페이스를 사용해 에러를 처리해 사용자에게 에러의 이유를 알려주는 것이지만(사용자 관점), 이와는 다르게 panic()은 문제 발생 시점 빠르게 프로그램을 종료시켜서 빠르게 문제 발생 시점을 알게하는 방식이다(개발자 관점).

panic() 내장 함수를 호출하고 인수로 에러 메시지를 입려갛면 프로그램을 즉시 종료하고 에러 메시지를 추력하고, 함수 호출 순서를 나타내는 콜 스택(call stack)을 표시한다.

package main

import (
	"os"
)

const filename string = "data.txt"

func main() {
	file, err := os.Open(filename)
	if err != nil {
		panic("파일을 읽을 수 없습니다")
	}
	defer file.Close()
}

앞선 예제를 panic()으로 처리하면 아래와 같이 call stack이 떨어진다.

panic: 파일을 읽을 수 없습니다

goroutine 1 [running]:
main.main()
	/tmp/sandbox3015253680/prog.go:12 +0x85

Program exited.

콜 스택이란 panic 이 발생한 마지막 함수 위치부터 역순으로 호출 순서를 표시한다.

프로그램을 개발하는 시점에는 문제점을 파악하고 수정하는 것이 중요하지만, 사용자에게 인도가 된 이후에는 문제가 발생하더라도 프로그램이 종료되는 대신 에러 메시지를 표시하고 복구를 시도하는 것이 나을 수 있다.

panic은 호출 순서를 거슬러 올라가며 전파되는데, main() → f() → g() → h() 순서로 호출되었을 때, h()에서 패닉이 발생하면 호출 순서를 거꾸로 올라가면서 g() → f() → main() 으로 전달된다. 이때, recover()를 만나면 패닉을 복구할 수 있다.

다만 recover() 또한 제한적으로 사용하는 것이 좋다. 복구가 되더라도 프로그램 상태가 불안정한 상태(데이터가 일부가 쓰여지거나 하면서 데이터가 비정상 적으로 저장될 수 있음)로 남는 것을 주의해야 한다.

 

24. 고루틴과 동시성 프로그래밍

고루틴(goroutine)은 경량 스레드로 함수나 명령을 동시에 실행할 때 사용한다. 프로그램 시작점인 mian() 또한 고루틴에 의해 실행된다.

보통 단일 Core에서 멀티 스레드 프로세스를 지원하기 위해 시분할로 다수의 스레드를 처리하지만 이 과정에서 컨텍스트 스위치(context switch: 현재의 상태_context를 보관하고 새로운 상태를 복원) 비용이 발생한다.

Go에서는 CPU 코어 마다 OS 스레드를 하나만 할당해서 사용하기 때문에 컨텍스트 스위칭 비용이 발생하지 않는다. 실제로 하나의 OS 스레드에 하나의 고루틴이 실행된다.

컨텍스트 스위칭은 CPU 코어가 스레드를 변경할 때 발생하는데, 고루틴을 이용하면 코어와 스레드는 변경되지 않고 오직 고루틴만 옮겨 다니기 때문에(코어가 스레드를 변경하지 않음) 컨텍스트 스위칭 비용이 발생하지 않는다.

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup // wiatGroup 객체

func SumAtoB(I, a, b int) {
	sum := 0
	for i := a; i <= b; i++ {
		sum += i
	}
	fmt.Printf("%d번째: %d ~ %d 합계는 %d 이다\\n", I, a, b, sum)
	wg.Done() // 작업 1개 완료를 의미
}

func main() {
	wg.Add(10) // 총 작업 수 설정
	for i := 0; i < 10; i++ {
		go SumAtoB(i, 1, 100000)
	}
	wg.Wait() // 모든 작업의 완료 대기
}

순서를 보장하지 않는다.

9번째: 1 ~ 100000 합계는 5000050000 이다
5번째: 1 ~ 100000 합계는 5000050000 이다
0번째: 1 ~ 100000 합계는 5000050000 이다
1번째: 1 ~ 100000 합계는 5000050000 이다
2번째: 1 ~ 100000 합계는 5000050000 이다
3번째: 1 ~ 100000 합계는 5000050000 이다
4번째: 1 ~ 100000 합계는 5000050000 이다
8번째: 1 ~ 100000 합계는 5000050000 이다
6번째: 1 ~ 100000 합계는 5000050000 이다
7번째: 1 ~ 100000 합계는 5000050000 이다

동시성 프로그래밍의 문제점은 동일한 메모리 자원에 여러 고루틴이 접근할 때 발생한다. 이 문제를 해결하기 위해서 한 고루틴에서 값을 변경할 때 다른 고루틴이 건들지 못하게 해당 자원에 대해서 뮤텍스(mutex)를 이용할 수 있다.

뮤텍스의 Lock() 메서드를 호출해 뮤텍스를 획득하면, 다른 고루틴에서는 획득한 뮤텍스가 반납될 때까지 대기하게 된다. 앞서 뮤텍스를 획득했으면 Unlock() 메서드를 호출해 반납해야 한다.

아래는 뮤텍스 예제이다. 특정 리소스를 대상으로 뮤텍스를 획득한다기 보다는, 동시성 문제를 일으킬 수 있는 동작 자체를 뮤텍스 처리한다.

package main

import (
	"fmt"
	"sync"
	"time"
)

var mutex sync.Mutex // 패키지 전역 변수 뮤텍스

type Account struct {
	Balance int
}

func DepositAndWithdraw(account *Account) {
	mutex.Lock()         // 뮤텍스 획득
	defer mutex.Unlock() // defer로 Unlock() 지연 호출

	if account.Balance < 0 {
		panic(fmt.Sprintf("Blaance should not be negative"))
	}
	// 1000원을 입금하고 1000원을 출금한다.
	account.Balance += 1000
	time.Sleep(time.Millisecond)
	account.Balance -= 1000
}

func main() {
	var wg sync.WaitGroup

	account := &Account{0}
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			DepositAndWithdraw(account)
			wg.Done()
		}()
	}
	wg.Wait()
}

다만 뮤텍스를 사용하면 동시성 프로그래밍 문제를 해결할 수 있지만, 성능 향상을 저해할 수 있고 또한 데드락(deadlock, 서로 뮤텍스에 들어간 자원을 얻으려고 대기하는 상황)이 발생할 수 있는 문제가 있다.

 

25. 채널과 컨텍스트

채널(channel)과 컨텍스트(context)는 Go에서 동시성 프로그래밍을 도와주는 기능이다.

25.1 채널

채널은 고루틴 간 메시지를 전달하는 메시지 큐이다. 메시지가 들어온 순서대로 쌓이고, 차례대로 읽게 된다.

아래의 예시와 같이 사용할 수 있다.

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	ch := make(chan int) // 채널 생성
	wg.Add(1)

	go square(&wg, ch) // 고루틴 생성, 채널 전달
	ch <- 9            // 채널에 데이터 넣음
	wg.Wait()          // 작업이 완료될때까지 기다림
}

func square(wg *sync.WaitGroup, ch chan int) {
	n := <-ch // 채널에서 데이터 빼옴

	time.Sleep(time.Second)
	fmt.Printf("Square: %d\\n", n*n)
	wg.Done()
}

채널을 아래와 같이 생성하면 버퍼(내부에 데이터를 보관할 수 있는 메모리 영역)를 가진 채널을 만들 수 있다.

var chan string messages = make(chan string, 2)

채널을 활용하면 생산자 소비자 패턴(Producer Consumer Pattern, 한쪽에서 데이터를 생성해서 넣어주면 다른 쪽에서 생성된 데이터를 빼서 사용하는 방식)을 구현할 수 있다.

25.2 컨텍스트

컨텍스트는 고루틴에 작업을 요청할 때 작업 취소나 작업 시간 등을 설정할 수 있는 작업 명세서 역할을 한다.

작업 취소가 가능한 컨텍스트를 아래와 같이 만들 수 있다.

ctx, cancel := context.WithCancel(context.Background())

작업 시간을 설정한 컨텍스트를 아래와 같이 만들 수 있다. 아래는 3초 후 종료한다.

ctx, cancle := context.WithTimeout(context.Background(), 3*time.Second)

별도 지시사항을 추가한 컨텍스틀 아래와 같이 만들 수 있다.

ctx := context.WithValue(context.Background(), "number", 9)

 

26. 단어 검색 프로그램 만들기

파일에서 단어 검색하는 프로그램에 대한 예제이다.

여기서는 앞서 다루지 않은 프로그램 그램 실행 시점 실행 인수를 전달하는 방법과 파일에서 한 줄 씩 읽어서 처리하는 방식을 추가로 다루고 있다. 한편 검색해야 하는 파일이 여러 개일 때, 파일을 하나 씩 검색하는 것보다 빠르게 실행하기 위해서, 각 파일에 대한 검색을 고루틴으로 적용한다.

먼저 Go에서는 실행 인수를 os.Args 변수를 이용해서 가져올 수 있다.

os.Args // 실행 인수 개수
os.Args[1] // 실행 인수 가져오기

그리고 파일에서 단어를 찾기 위해서 파일을 열고, bufio 패키지의 NewScanner()함수를 이용해 스캐너를 만들고 파일 내용을 한 라인씩 읽어 올 수 있다.

scanner := bufio.NewScanner(file) // 스캐너를 생성해서 한 줄씩 읽기
for scanner.Scan() {
	fmt.Println(scanner.Text()) 
}

실제 scanner.Text()에서 파일이 있는지 여부는 한 라인에 대해서 strings.Contains(라인, 찾는단어)로 확인한다.

다만 실행 인수에 단어를 찾을 다수의 파일이 들어온다면, 다수 파일에서 단어 검색을 처리하기 위해서 고루틴을 활용해 시간을 단축할 수 있다.

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 7주차(31장)  (0) 2023.11.09
Go스터디: 6주차(27~30장)  (0) 2023.11.05
Go스터디: 4주차(18~22장)  (1) 2023.10.22
Go스터디: 3주차(12~17장)  (1) 2023.10.15
Go스터디: 2주차(3~11장)  (1) 2023.10.08

이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 18~22장 써머리입니다.

여기서 부터 주제가 조금씩 어려워지고 생각이 필요한 부분이 많습니다.

18. 슬라이스

일반적인 배열은 고정 길이를 가진다. 아래의 배열은 10개까지 값을 저장할 수 있다.

var array [10]int

슬라이스는 배열과 비슷하지만 []안에 개수를 지정하지 않고 선언하는 동적 배열이다.

다만 슬라이스를 초기화 하지 않으면 길이가 0인 슬라이스가 만들어 지는 것이기 때문에 임의로 인덱스를 접근하면 패닉이 발생한다.

package main

func main() {
	var slice []int

	slice[1] = 10
}

// 에러
panic: runtime error: index out of range [1] with length 0

goroutine 1 [running]:
main.main()
	/tmp/sandbox4095475310/prog.go:6 +0x14

슬라이스의 초기화와 요소 추가, 삭제

var slice1 []int{1,2,3} // 대괄호 안에 길이가 없다, {} 안에 요소값을 넣어서 초기화 할수 있다.
var array [...]int{1,2,3} // 고정길이 3인 배열이다. 슬라이스와 다르다.
var slice = make([]int,3) // 슬라이스는 make()를 통해서 초기화 할 수 있다.

// 슬라이스는 요소 추가를 위해 append()를 사용할 수 있다.
slice1 = append(slice1, 4)

// 슬라이스 요소 삭제를 위해 append()를 활용할 수 있다.
slice1 = append(slice[:idx], slice[idx+1:]...)

// 슬라이스의 중간에 요소 추가하기 위해 append()를 활용할 수 있다.
slice1 = append(slice[:idx], append([]int{100}, slice[idx:]...)...)

// 슬라이스 정렬
sort.Ints(slice1) // float64는 sort.Float64s() 사용

… 사용이 복잡하다 → 예시 확인

append()함수는 append(slice, 3,4,5)와 같이 첫 번째 인자로 주어진 slice에 여러 값을 추가할 수 있다. 아래의 예시에서도 append(slice, 요소,요소)와 같이 사용하기 위해, 여러 요소라는 의미로 slice…(슬라이스 전체)를 사용한 것이라고 이해하자.

package main

import "fmt"

func main() {
	var slice1 = []int{1, 2, 3}
	fmt.Println(slice1) //[1 2 3]

	//슬라이스 요소 추가
	slice1 = append(slice1, 4)
	fmt.Println(slice1) //[1 2 3 4]

	//슬라이스 요소 삭제
	idx := 1
	//slice1 = append(slice1[:idx], slice1[idx+1:])  //[에러] ./prog.go:13:38: cannot use slice1[idx + 1:] (value of type []int) as int value in argument to append
	slice1 = append(slice1[:idx], slice1[idx+1:]...) // 배열이나 슬라이스 뒤에 ...를 하면 모든 요소값을 넣어준 것과 같게 됨
	fmt.Println(slice1)                              //[1 2 3 4]

	//슬라이스의 중간에 요소 추가
	slice1 = append(slice1[:idx], append([]int{100}, slice1[idx:]...)...)
	fmt.Println(slice1) // [1 100 3 4]

}

예시에서는 슬라이싱(배열의 일부를 집어내는 기능)을 사용했다. 슬라이싱의 결과는 슬라이스이다.

array[startIdx:endIndx]

슬라이스는 배열의 일부를 나타내는 타입으로 포인터, len, cap 필드로 구성되어 있다. 포인터로 배열의 중간을 가리키고, len으로 포인터부터 일정 개수를, 그리고 cap은 포인터가 가리키는 배열이 할당된 크기(안전하게 사용할 수 있는 남은 배열 개수)를 나타낸다.

array := [5]int{1,2,3,4,5}
slice := array[1:2] // 이때 포인터는 array[1], len은 1, cap은 4가 된다.

slice1 := []int{1,2,3,4,5}
slice2 := slice1[:3] // 처음부터 슬라이싱
slice3 := slice1[2:] // 끝까지 슬라이싱

결과

package main

import "fmt"

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := slice1[:3] // 처음부터 슬라이싱
	slice3 := slice1[2:] // 끝까지 슬라이싱

	fmt.Println(slice1)
	fmt.Println(slice2) // [1 2 3], 시작 인덱스 부터 슬라이싱
	fmt.Println(slice3) // [3 4 5], 끝 인덱스-1 까지 슬라이싱
}

슬라이스 동작 원리

슬라이스의 내부 정의는 아래와 같다. 슬라이스가 실제 배열을 가리키는 포인터를 가지고 있어서, 쉽게 크기가 다른 배열을 가리키도록 변경할 수 있고, 슬라이스 변수 대입 시 배열에 비해서 사용되는 메모리나 속도에 이점이 있다.

type SliceHeader struct {
		Data uintptr // 실제 배열을 가리키는 포인터
	Len int        // 요소 개수
	Cap int        // 실제 배열의 길이
}

슬라이스와 배열 동작은 아래와 같은 차이가 있다.

package main

import "fmt"

// 배열은 모든 값이 복사되기 때문에, 함수내의 배열은 다른 배열이다.
func changeArray(array2 [5]int) {
	array2[2] = 500
}

// slice값이 복사되면 구조체의 각 필드값이 복사되어서 포인터의 메모리 주소값도 복사되고, 실제로 slice2는 복사되어도 같은 배열 데이터를 가리키게 된다.
func changeSlice(slice2 []int) {
	slice2[2] = 500
}

func main() {
	array := [5]int{1, 2, 3, 4, 5}
	slice := []int{1, 2, 3, 4, 5}

	changeArray(array)
	changeSlice(slice)

	fmt.Println(array) //[1 2 3 4 5]
	fmt.Println(slice) //[1 2 500 4 5]
}

 

19. 메서드

메서드(method)는 함수의 일종으로, 구조체 밖에서 메서드를 지정할 수 있다. 이때 특정 구조체의 메소드라는 것을 명시하기 위 리시버(reciver)를 func 키워드와 함수 이름 사이에 중괄호로 명시한다.

func (r Rabbit) info() int {
	return r.width * r.height
}

리시버로 모든 로컬 타입(해당 패키지 안에서 type 키워드로 선언된 타입)이 사용 가능하다. 기본 내장 타입도 사용자 정의 타입으로 별칭 타입으로 변환하여 메서드를 선언할 수 있다.

메서드는 왜 필요한가?

메서드는 리서버에 속한다. 메서드를 사용해서 데이터와 기능을 묶을 수 있게 된다.

아래의 예시와 같이 함수에 포인터 변수를 전달해서 동일한 목적을 수행할 수도 있는데, 왜 메서드를 이용할까?

package main

import "fmt"

type account struct {
	balance int
}

func withdrawFunc(a *account, amount int) { // 일반 함수 표현
	a.balance -= amount
}

func (a *account) withdrawMethod(amount int) { // 메서드 표현
	a.balance -= amount
}

func main() {
	a := &account{100} // balance가 100인 account 포인터 변수 생성

	withdrawFunc(a, 30) // 함수 형태 호출
	fmt.Println(a)  // 70 

	a.withdrawMethod(30) // 메서드 형태 호출
	fmt.Println(a)  // 40
}

예를 들어, ‘성적 입력 프로그램’을 만들 때, Student라는 구조체가 있다고 하면, 이 구조체의 필드로 이름, 반, 번호, 성적 등의 데이터가 있다. 메서드는 성적 입력, 반 배정 등의 Student 구조체의 기능을 나타낸다.

좋은 프로그래밍이라면 결합도(coupling, 객체간의 의존 관계)를 낮추고, 응집도(cohesion, 모듈 내 요소들의 상호 관련성)을 높여햐 한다. 메서드는 데이터와 관련된 기능을 묶기 때문에 코드 응집도를 높이는 중요한 역할을 한다. 응집도가 낮으면 새로운 기능을 추가할 때 흩어진 모든 부분을 검토하고 고쳐야 하는 문제가 발생한다. 응집도가 높으면 필요한 코드만 수정하면 된다.

현대의 프로그래밍에서는 함수 호출 순서보다 객체를 만들고, 다른 객체와 상호 관계를 맺는 것이 더 중요해 졌다. 이때 객체 간 상호 관계는 메서드로 표현된다.

리시버를 값 타입 vs. 포인터 타입 메서드

포인터 타입 메서드를 호출하면 포인터가 가리키고 있는 메모리 주소 값이 복사된다(서로 같은 주소값을 가지게 됨). 반면 값 타임 메서드를 호출하면 리시버 타입의 모든 값이 복사된다(서로 다른 주소값을 가지게 됨).

포인터 타입 메서드는 메서드 내부에서 리시버의 값을 변경시킬 수 있다. → 인스턴스 중심

값 타입 메서드는 호출하는 쪽과 메서드 내부의 값은 별도 인스턴스로 독립되기 때문에 메서드 내부에서 리시버의 값을 변경시킬 수 없다. → 값 중심

 

20. 인터페이스

인터페이스(interface)란 구현하지 않은 메서드 집합이다. 이를 이용하면 메서드 구현을 포함한 구체화된 객체(concrete object)가 아닌 추상화된 객체로 상호작용을 할 수 있다.

→ 구체화된 타입이 아닌, 인터페이스만 가지고 메서드를 호출할 수 있어, 추후 프로그램 요구사항 변경 시 유연하게 대체할 수 있다.

→ Go에서는 인터페이스 구현 여부를 그 타입이 인터페이스에 해당하는 메서드를 가지고 있는지로 판단한다(덕 타이핑: 타입 선언 시 인터페이스 구현 여부를 명시적으로 나타낼 필요 없이 인터페이스에 정의한 메서드 포함 여부만으로 결정한다).

Keyword: 인터페이스를 구현하면 인터페이스로 메서드를 호출할 수 있다.
프로그램의 요구사항 변경에 유연하게 대처 할 수 있다.

인터페이스 선언은 아래와 같다.

type DuckInterface interface {
	// 메서드 집합
	Fly()
	Walk(distance int) int
}

인터페이스 예시

package main

import "fmt"

type Stringer interface { // String 메서드를 가지면 뒤에 ~er 로 인터페이스명을 만든다.
	String() string
}

type Student struct {
	Name string
	Age  int
}

func (s Student) String() string { // Student의 String() 메서드가 Stringer 인터페이스를 구현함, Student 타입은 Stringer 인터페이스로 사용될 수 있다.
	return fmt.Sprintf("안녕! 나는 %d살 %s라고 해", s.Age, s.Name)
}

func main() {
	student := Student{"후후", 12}
	var stringer Stringer

	stringer = student // stringer 값으로 Student  타입 변수 student를 대입힌다. stringer는 Stringer 인터페이스이고 Stduent 타입은 String() 메서드를 포함하고 있기 때문에 stinger 값으로 stduent를 대입할 수 있다.

	fmt.Printf("%s\\n", stringer.String()) // stringer 인터페이스가 가지고 있는 메서드 String()을 호출한다. stringer 값으로 Stduent 타입 ㄴtudent를 가지고 있기 때문에 student의 메서드 String()이 호출되어 반환된다.
}

→ 인터페이스 타입 변수에 인터페이스를 구현한 타입 변수를 대입할 수 있다.

→ 인터페이스 타입 변수로 메서드를 호출하면, 이를 구현한 타입의 메서드를 호출 한다.

추상화

내부 동작을 감춰서 서비스를 제공하는 쪽과 사용하는 족 모두에게 자유를 주는 방식을 추상화라고 한다. 인터페이스는 추상화를 제공하는 추상화 계증(abstration layer)이다.

이렇게 추상화 계층을 이용해 서로 결합을 귾는 것을 디커플링(decoupling)이라 말한다. 결함도는 낮출 수록 좋다.

추상화 계층을 거치면, 내부구현을 알 수 없고 오직 인터페이스(메서드의 집합)만 알 수 있다. 이것을 객체 간 관계라고 정의할 수 있다. 택배회사와 택배 이용자는 택배 전송 관계로, 은행과 은행 이용자는 입금과 인출 관계로 상호작용 한다.

→ 구체화된 타입(내부 구현이 모드 있는 타입)으로 상호작용 하는 것이 아니라 관계로 상호작용 한다.

인터페이스의 특별한 기능

  • 포함된 인터페이스: 인터페이스가 다른 인터페이스를 포함하는 경우가 있다.
  • 빈 인터페이스 interface{}를 인수로 받기: 어떤 값이든 받을 수 있는 함수, 메서드, 변수값을 만들 때 사용한다.
  • 인터페이스 변수의 기본값은 유효하지 않은 메모리 주소를 나타내는 nil이다.
  • 인터페이스를 구체화된 다른 타입으로 타입 변환하려면 a.(ConcreteType)와 같이 구체화된 타입이나 다른 인터페이스로 변환 할 수 있다.

 

21. 함수 고급편

21.1 가변 인수 함수

fmt.Println() 과 같이 함수의 인수가 정해져 있지 않은 경우, 즉 함수 인수 개수가 고정적이지 않은 함수를 가변 인수 함수(variadic function)이라고 한다.

이런 함수는 … 키워드를 사용해서 가변 인수를 처리하면 된다.

package main

import "fmt"

func sum(nums ...int) int { // 가변 인수 함수는 ... 키워드로 가변 인수를 받는다.
	sum := 0

	fmt.Printf("nums의 타입: %T\\n", nums) // nums 의 타입은 []int (슬라이스) 이다.
	for _, v := range nums {
		sum += v
	}
	return sum
}
func main() {
	fmt.Println(sum(1, 2, 3)) 
	fmt.Println(sum(10, 20))
}

만약 다양한 인수를 섞어서 사용하고 싶다면, …interface{} 로 받을 수 있다. (모든 타입이 빈 인터페이스를 포함하고 있기 때문에 가능함)

package main

import "fmt"

func Print(args ...interface{}) {
	for _, arg := range args {
		switch arg.(type) {
		case bool:
			val := arg.(bool) // 인터페이스 변환
			fmt.Printf("type: %T\\n", val)
		case float64:
			val := arg.(float64) // 인터페이스 변환
			fmt.Printf("type: %T\\n", val)
		case int:
			val := arg.(int) // 인터페이스 변환
			fmt.Printf("type: %T\\n", val)
		}
	}
}
func main() {
	Print(2, true, 3.14)
}

21.2 defer 지연 실행

함수가 종료하기 직전에 실행해야 하는 코드가 있을 수 있다. 혹은 리소스를 사용하고 반납을 해야 하는 것과 같이 반드시 실행해야 하는 코드가 있다면, 리소스를 생성할 때 바로 defer를 사용해서 필요한 명령을 전달할 수 있다(흔히 발생하는 닫지 않은 리소스로 인한 leak을 방지한다).

defer 예시에서 주의할 점은 defer는 역순으로 호출된다. (아래는 3, 2, 1 순서로 출력된다)

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Create("test.txt") // 파일 생성 -> 닫아줘야 한다
	if err != nil {
		fmt.Println("err")
		return
	}

	defer fmt.Println("1")
	defer f.Close() // defer로 처리해 놓으면 함수가 종료하기 전에 f를 닫아준다
	defer fmt.Println("2")
	fmt.Fprintln(f, "hello")
	defer fmt.Println("3")
}

21.3 함수 타입 변수

함수 타입 변수란, 함수를 값으로 갖는 변수를 의미한다.

함수는 시작 번지를 가지고, 함수가 시작되면 순차적으로 시작한다고 가정할 때, CPU에서는 프로그램 카운터(program counter)가 증가하며 다음 실행 라인을 나타낸다. 이때 main() 함수에서 f() 함수를 실행하면, 프로그램 카운터가 f()함수의 시작 지점을 가리키게 된다.

즉 함수 시작 지점이 함수를 가리키는 값이고, 마치 포인터 처럼 함수를 가리킨다고 해서 함수 포인터(function pointer)라고 부른다.

func add(a,b int) int {
	return a+b
}

함수 add()를 카리키는 함수 포인터는 아래와 같이 표현한다.

func add(int, int) int

함수 타입 변수를 활용한 예제를 살펴본다.

package main

import (
	"fmt"
)

func add(a, b int) int {
	return a + b
}

func mul(a, b int) int {
	return a * b
}

func getOperator(op string) func(int, int) int {
	if op == "+" {
		return add
	} else if op == "*" {
		return mul
	} else {
		return nil
	}
}

func main() {
	var operator func(int, int) int // int 타입 인수 2개를 받아서 int 타입을 반환하는 함수 타입 변수 operator 선언
	operator = getOperator("+")
	result := operator(1, 2)

	fmt.Println(result)
}

실제 활용 사례가 궁금하다.

 

21.4 함수 리터럴

함수 리터럴(function literal)은 이름 없는 함수로 함수명을 적지 않고 함수 타입 변수값으로 대입되는 함수값을 의미한다. (다른 언어의 익명함수 혹은 람다_Lambda와 동일하다)

예제를 살펴보면, 함수 타입 변수가 없이, 그냥 함수 자체를 정의한 것을 반환한다.

package main

import (
	"fmt"
)

type opFunc func(a, b int) int

func getOperator(op string) opFunc {
	if op == "+" {
		// 함수 리터럴을 사용해서 더하기 함수 자체를(함수 이름이 없어짐) 정의하고 반환
		return func(a, b int) int {
			return a + b
		}
	} else if op == "*" {
		return func(a, b int) int {
			return a * b
		}
	} else {
		return nil
	}
}

func main() {
	operator := getOperator("*")

	result := operator(1, 2)

	fmt.Println(result)
}

함수 리터럴은 필요한 변수를 내부 상태로 가질 수 있다. 함수 범위 내에서 유효할 것 같지만, 외부 변수를 실제로 변경할 수 있다.

함수 리터럴을 이용해서 원하는 함수를 그때그때 정의해서 함수 타입 변수값으로 사용할 수 있다. 한편 아래 예제에서 writeHello() 함수 입장에서는 인수로 Writer 함수 타입을 받는다. 실제로 어떤 동작을 할지는 호출했을 때 알 수 있게되는데, 이렇게 외부에서 로직을 주입하는 것을 의존성 주입(dependency injection)이라고 한다. 뭔가 아리송 하다.

package main

import (
	"fmt"
	"os"
)

type Writer func(string) // 함수 타입

func WriteHello(writer Writer) {
	writer("Hello World")
}

func main() {
	f, err := os.Create("test.txt")
	if err != nil {
		fmt.Println("failed")
		return
	}

	defer f.Close()

	WriteHello(func(msg string) {
		fmt.Fprintln(f, msg)
	})
}

 

22. 자료 구조

22.1 리스트

리스트(list)는 container 패키지에서 제공하는 자료 구조이다. 여러 데이터를 보관하는 배열과 비슷하다고 생각되지만, 배열이 연속된 메모리에 데이터를 저장하는 구조인 반면, 리스트는 연속되지 않는 메모리 공간에 데이터를 저장한다.

이러한 구조로 배열과 리스트는 데이터의 지역성(data locality)에 차이가 있다. 컴퓨터는 연산을 할 때 메모리에서 데이터를 가져와 캐시라는 임시 저장소에 보관하는데, 실제로 필요한 데이터만 가져오지 않고, 그 주변의 데이터를 가져온다. 보통은 높은 확률로 연산이 주변 데이터를 참조하기 때문에 효과적이다. 필요한 데이터가 인접해 있을 수록 데이터 처리 속도가 빨라지는데 데이터 지역성이 좋다고 한다. 배열은 연속된 메모리 이기 때문에 지역성이 리스트에 비해 좋다. 단, 요소 수가 적으면 데이터 지역성 때문에 배열이 효율적이지만, 삽입/삭제가 빈번한 연산이라면 리스트가 더 효율적이다.

리스트의 구조체 구조를 보면 요소들이 포인터로 연결된 링크드 리스트(Linked list) 형태인 것을 알 수 있다.

type Element struct {
	value interface{}
	Next *Element
	Prev *Element
}

다음 예제에서 List의 기본적인 사용법과 순회 방법을 살펴본다.

package main

import (
	"container/list"
	"fmt"
)

func main() {
	v := list.New()
	e4 := v.PushBack(4)
	e1 := v.PushFront(1)
	v.InsertBefore(3, e4)
	v.InsertAfter(2, e1)

	for e := v.Front(); e != nil; e = e.Next() { // Next() 메서드는 현재 요소의 다음 요소를 반환한다. 다음 요소가 없으면 nil 반환
		// (초기문)e는 v.Front() 부터; (조건문)e가 nil 값일 때까지; (후처리)e의 다음 요소로 넘어감
		fmt.Print(e.Value, " ")  // 1 2 3 4
	}

}

 

22.2 링

링(ring)은 container 패키지에서 제공하는 자료 구조이다. 리스트와 유사한 구조인데, 맨 뒤의 요소와 맨 앞의 요소가 서로 연결된 자료 구조이다.

기본 예제를 살펴본다.

package main

import (
	"container/ring"
	"fmt"
)

func main() {
	r := ring.New(5) // 요소가 5개인 링 생성

	//n := r.Len()

	// 순회하면서 모든 요소에 값 대입
	for i := 0; i < r.Len(); i++ {
		//r.Value = i // ring의 요소는 타입이 없나?, int를 넣으면 int가 저장됨.
		//r.Value = 'A' + i
		//이렇게 해도된다. 요소에 타입이 없는 것 같다.
		if i%2 == 0 {
			r.Value = 1
		} else {
			r.Value = '아'
		}
		r = r.Next()
	}

	for i := 0; i < r.Len(); i++ {
		fmt.Printf("%c ", r.Value)
		r = r.Next()
	}

}

이러한 원형 구조이기 때문에, 개수가 고정되고 오래된 요소는 지워도 되는 경우에 적합한 자료 구조이다. 예를 들어, 문서 편집기의 실행 취소 기능(일정 개수의 명령을 저장하고, 실행 취소 할 수 있음, 너무 오래된 명령은 지워짐)에서 사용할 수 있다.

 

22.3 맵

맵(map)은 키와 값(key/value) 형태로 데이터를 저장하는 자료 구조이다. 언어에 따라서 딕셔너리(dictionary), 해시테이블(hash table), 해시맵(hashmap) 등으로 부른다.

맵은 키와 값의 쌍으로 데이터를 저장하고, 키를 사용해 접근하여 값을 저장하거나 변경할 수 있다.

맵의 기본 예제를 살펴본다.

package main

import (
	"fmt"
)

type Product struct {
	Name   string
	Prince int
}

func main() {
	m := make(map[int]Product) // 맵 생성, map[key타입]value타입
	m[1001] = Product{"볼펜", 500}
	m[1002] = Product{"지우개", 500}
	m[1003] = Product{"연필", 500}
	m[1004] = Product{"샤프", 500}

	delete(m, 1002)
	delete(m, 1005) // 없는 값에 접근해도 에러가 발생하지는 않는다. v, ok := m[3] 으로 존재여부를 체크할 수 있다.

	for k, v := range m {
		fmt.Println(k, v)
	}
}

맵은 hash() 함수로 만들어진다. 해시 함수는 결과값이 항상 일정 범위(개수)를 가진다. 같은 입력에 대해서는 같은 결과를 보장하고, 일정 범위에서 반복된다.

그래서 입력값(key)을 hash()에 넣어 결과값을 인덱스로 사용해 배열에 값(value)를 넣는 방식으로 사용하면 맵과 같은 형태를 만들 수 있다. 물론 이런 단순한 구현에서는 다른 입력값으로 같은 결과가 나올 수 있는 함정이 있기 때문에 리스트를 활용할 수 있다. 그래도 hash() 정도의 연산으로 고정된 시간에서 데이터를 저장, 사용할 수 있는 장점이 있는 것 같다.

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 7주차(31장)  (0) 2023.11.09
Go스터디: 6주차(27~30장)  (0) 2023.11.05
Go스터디: 5주차(23~26장)  (0) 2023.10.29
Go스터디: 3주차(12~17장)  (1) 2023.10.15
Go스터디: 2주차(3~11장)  (1) 2023.10.08

이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 12~17장 써머리입니다.

12. 배열

배열(array)은 같은 타입의 데이터들로 이루어진 타입이다. 배열의 각 값은 요소(element)라고 하고, 이를 가리키는 위치값을 인덱스(index)라고 한다.

// var 변수명 [요소개수]타입
var t [5]float64
days := [3]string{"monday","tuesday","wednesday"}
x := [...]int{10,20,30} // 요소 개수 생략
var b = [2][5]int{ // 다중 배열
	{1,2,3,4,5},
	{6,7,8,9,10}, // 초기화 시 닫는 중괄호 } 가 마지막 요소와 같은 줄에 있지 않은 경우 마지막 항목 뒤에 쉼표, 를 찍어줘야 함!
} // 추후 항목이 늘어날 경우 쉼표를 찍지 않아서 생길 수 있는 오류를 방지하기 위해 존재하는 규칙

배열 선언 시 개수는 항상 상수여야 한다. 아니면 에러 발생!

package main

func main() {
	x := 5
	b := [x]int{1, 2, 3, 4, 5} // invalid array length x
}

 

배열 순회

package main

import "fmt"

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	for _, v := range a {
		fmt.Println(v)
	}

	var b = [2][5]int{ // 다중 배열
		{1, 2, 3, 4, 5},
		{6, 7, 8, 9, 10},
	}

	for _, b1 := range b {
		for _, b2 := range b1 {
			fmt.Print(b2, " ")
		}
		fmt.Println()
	}
}

배열의 핵심

  1. 배열은 연속된 메모리다.
  2. 컴퓨터는 인덱스와 타입 크기를 사용해서 메모리 주소를 찾는다.

 

중요 공식

요소 위치 = 배열 시작 주소 + (인덱스 X 타입크기)
→ a 배열 시작 주소가 100번지 라면 a[3] 주소는 100 + (3*4) = 112번지

배열 크기 = 타입크기 X 항목개수
→ [5]int = 8*5 = 40bytes

왜 두 공식의 int 타입크기를 다르게 한거지? 오타?

 

13. 구조체

여러 필드(field)를 묶어서 하나의 구조체(structure)를 만든다. 구조체는 다른 타입의 값들을 변수 하나로 묶어준다.

프로그래밍의 역사는 객체 간 결합도(객체 간 의존관계)는 낮추고, 연관있는 데이터 간 응집도를 올리는 방향으로 흘러왔다. 함수와 구조체 모드 응집도를 증가시키는 역할을 한다.

→ 구조체의 등작으로, 프로그래머는 개별 데이터의 조작/연산보다는 구조체 간의 관계와 상호작용 중심으로 변화하게 되었다.

type 타입명 struct {
	필드명 타입
}

학생(Student) 구조체를 만들고, 이름, 반과 같은 정보를 넣는다. 구조체의 각 필드는 .을 통해서 접근할 수 있다.

package main

import "fmt"

func main() {
	type Student struct {
		name  string
		class int
	}
	var std1 Student

	std1.name = "홍"
	std1.class = 1

	fmt.Println(std1.name, std1.class)

	// 초기화를 아래와 같이 해줄 수도 있다.
	std2 := Student{"김", 2}

	fmt.Println(std2.name, std2.class)
}

구조체를 포함하는 구조체를 만들 때,

내장 타입처럼 포함할 때는 instance.StructName.fieldName 으로 접근한다.

포함된 필드 방식으로 사용할 수 있는데, 이때는 instance.fieldName으로 포함된 구조체 필드를 바로 접근할 수도 있다.

 

 

필드 배치 순서에 따른 구조체 크기 변화

구조체의 인스턴스가 생성되면 구조체 필드의 크기를 더한 만큼의 메모리 공간을 차지하게 된다.

다만 필드 배치 순서에 따라 구조체 크기가 달라 질 수 있는데, 메모리 정렬(Memory Alignment)를 고려해서 8의 배수인 메모리 주소에 데이터를 할당하도록 동작하기 때문에, 만약 적은 사이즈의 필드가 있는 경우는 메모리 패딩(memory Padding)을 넣고, 다음 8배수 공간에 데이터를 할당하기 때문이다.

메모리 정렬이란 컴퓨터가 데이터에 효과적으로 접근하고자 메모리를 일정 크기 간격으로 정렬하는 것을 말한다.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	type User struct {
		Age   int32
		Score float64
	}

	user := User{23, 77.2}

	fmt.Println(unsafe.Sizeof(user.Age), unsafe.Sizeof(user.Score)) // 4, 8
	fmt.Println(unsafe.Sizeof(user)) // 16
}

이러한 이유로 구조체에서 메모리 패딩을 고려한 필드 배치방법을 사용해야 한다.

→ 8 bytes 보다 작은 필든느 8 bytes 크기(단위)를 고려해서 몰아서 배치하자.

 

14. 포인터

포인터는 메모리 주소를 값으로 갖는 타입이다. 포인터 변수를 초기화 하지 않으면 기본 값은 nil이다.

package main

import (
	"fmt"
)

func main() {
	var a int
	var p1 *int
	p1 = &a // a의 메모리 주소를 포인터 변수 p에 대입

	var p2 *int = &a

	fmt.Println(p1 == p2) // == 연산을 사용해 포인터가 같은 메모리 공가늘 가리키는 지 확인
}

포인터는 왜 쓸까?

→ 변수 대입이나 함수 인수 전달은 항상 값 복사를 하기 때문에, 메모리 공간을 사용하는 문제와 큰 메모리 공간을 복사할 때 발생하는 성능 문제가 있다.

→ 또한 다른 공간으로 복사되기 때문에 실제 변수의 값에 변경 사항이 적용되지 않는다.

구조체를 생성해 포인터 변수 초기화 하기

구조체 변수를 별도로 생성하지 않고, 곧바로 포인터 변수에 구조체를 생성해 주소를 초기값으로 대입하는 방법

방식1) Data타입 구조체 변수 data를 선언하고, data변수의 주소를 반환하여 대입

var data Data
var p *Data = &data

방식2) *Data 타입 구조체 변수 p를 선언하고, Data 구조체를 만들어서 주소를 반환

var p *Data = &Data{}

→ 이렇게 하면 포인터 변수 p만 가지고도 구조체의 필드값에 접근하고 변경할 수 있다. (실제 Data 구조체에 대한 변수를 생성하지 않음)

방식3) new() 내장 함수: 포인터값을 별도의 변수를 선언하지 않고 초기화 할 수도 있는데, new 내장 함수를 이용하면 더 간단히 표현할 수 있다.

p1 := &Data{} // &를 사용하는 초기화
var p2 = new(Data) // new()를 사용하는 초기화

 

인스턴스

인스턴스란 메모리에 할당된 데이터의 실체이다. 포인터를 이용해서 인스턴스에 접근할 수 있다.

구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 것과 동일하다.

Go 언어는 가비지 컬렉터(Garbage Collector)라는 메모리 정리 기능을 제공하는데, 카비지 컬렉터가 일정 간격으로 메모리에 쓸모 없어진 데이터를 정리한다.

쓸모 없어진 데이터: 아무도 찾지 않는 데이터는 쓸모 없는 데이터다. 예를 들어, 함수가 종료되면 함수에서 사용한 인스턴스는 더 이상 쓸모가 없게 된다.

 

 

스택 메모리와 힙 메모리

대부분의 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용한다. 이론상 스택 메모리 영역이 효율적이지만, 스택 메모리는 함수 내부에서만 사용 가능한 영역이다. 그래서 함수 외부로 공개되는 메모리 공간은 힙 메모리 공간을 할당한다.

자바는 클래스 타입을 힙에, 기본 타입을 스택에 할당한다. Go에서는 탈출 검사(escape anlaysis)를 해서 어느 메모리에 할당할지 결정한다. 즉 Go 언어는 어떤 타입이나 메모리 할당 공간이 함수 외부로 공개되는지 여부를 자동으로 검사해서 스택 메모리에 할당할지 힙 메모리에 할당할지 결정한다.

package main

import (
	"fmt"
)

type User struct {
	Name string
	Age  int
}

func NewUser(name string, age int) *User {
	var u = User{name, age}
	return &u // 보통 함수가 종료하면, 함수 내 선언된 변수는 사라진다. 탈출 분석을 통해 u 메로리가 사라지지 않음
}

func main() {
	userPointer := NewUser("AAA", 22)
	fmt.Println(userPointer)
}

Go의 스택 메모리는 계속 증가되는 동적 메모리 풀로, 일정한 크기를 같는 C/C++과 비교해 메모리 효율성 높고, 스택 고갈 문제도 발생하지 않는다.

 

15. 문자열

문자열은 문자 집합을 나타내는 타입으로 string이다. 문자열은 큰 따옴표나 백쿼트로 묶어서 표시한다.

Go는 UTF-8 문자코드를 표준 문자 코드로 사용한다. UTF-8은 자주 사용되는 영문자, 숫자 일부 특수문자를 1바이트로 표현하고, 그 외 다른 문자들은 2~3바이트로 표현한다. (한글 사용 가능)

 

rune 타입

문자 하나를 표현하기 위해 rune 타입을 사용한다. UTF-8은 한 글자가 1~3바이트 크기이기 때문에, UTF-8 문자값을 가지려면 3바이트가 필요하다. Go에서는 기본 타입에서 3 바이트 정수 타입은 제공되지 않기 때문에 rune 타입은 4 바이트 정수 타입인 int32 타입의 별칭 타입이다.

package main

import (
	"fmt"
)

func main() {
	str := "Hello 월드"
	runes := []rune(str)

	fmt.Printf("len(str) = %d\\n", len(str))     // string 타입 길이,12 
	fmt.Printf("len(runes) = %d\\n", len(runes)) // []rune 타입 길이, 8
}

string에서 영문은 1바이트, 한글은 3바이트이므로, 총 12가 된다.

string 타입을 []rune으로 변환하면, 각 글자들로 이뤄진 배열로 변환된다. 그래서 각각이 1바이트로 8이 된다.

 

[]byte 타입

string 타입과 []byte 타입은 상호 타입 변환이 가능하다. []byte는 byte 즉 1바이트 부호 없는 정수 타입의 가변 길이 배열이다. 문자열은 메모리에 있는 데이터고, 메모리는 1바이트 단위로 저장되기 때문에 모든 문자열은 1바이트 배열로 변환 가능하다.

파일을 쓰거나, 네트워크로 데이터를 전송하는 경우, io.Writer 인터페이스를 사용하고, io.Writer 인터페이스는 []byte 타입을 인수로 받기 때문에 []byte 타입으로 변환해야 한다. 문자열을 쉽게 전송하고자 string에서 []byte 타입으로 변환을 지원한다.

 

문자열 구조

string은 필드가 1개인 구조체이다. 첫번째 필드 Data는 uintptr 타입으로 문자열의 데이터가 있는 메모리 주소를 나타내는 일정의 포인터이고, 두번째 필드 Len은 문자열의 길이를 나타낸다.

type StringHeader struct {
	Data uintptr
	Len int
}

→ str2 변수에 str1 변수를 대입하면, str1의 Data와 Len값만 str2에 복사한다. 문자열 자체가 복사되지는 않는다.

문자열과 immutable

string 타입이 가리키는 문자열의 일부만 변경 할 수 없다. 변경하려면 슬라이스로 타입 변환하고, 변경하는 방식을 취할 수 있다.

package main

func main() {
	str := "Hello world"
	str = "How are you"
	str[2] = 'a' // cannot assign to str[2] (neither addressable nor a map index expression)
}

→ 문자열의 합산을 하면 기존 문자열 메모리 공간을 건드리지 않고, 새로운 메모리 공간을 만들어서 두 문자열을 합치기 때문에 주소값이 변경된다. (문자열 불변 원칙이 준수 된다.)

이로 인해 메모리 낭비가 있을 수 있는데, strings 패키지의 Builder를 이용해서 메모리 낭비를 줄일 수 있다.

 

16. 패키지

16.01. 패키지

패지키(package)란 Go에서 코드를 묶는 가장 큰 단위 이다.

함수로 코드 블록을, 구조체로 데이터를, 패키지로 함수와 구조체와 그 외 코드를 묶는다. main 패키지는 특별한 패키지로 프로그램 시작점을 포함한 패키지이다. main() 함수와 다른 함수, 구조체 등을 가진다. 외부 패키지는 main() 을 가지지 않는다.

한 프로그램은 main 패키지 외에 다수의 다른 패키지를 포함할 수 있다.

이러한 패키지를 import해 사용한다.

패키지를 가져오면 해당 패키지명을 쓰고 . 연산자를 사용해 패키지에서 제공하는 함수, 구조체 등에 접근할 수 있다.

import (
	"fmt"
	"math/rand"
	"text/template"
	htemplate "html/template" // 동일한 패키지 명에는 별칭 htemplate
	_ "github.com/mattn/go-sqllite3" // 패키지를 import하면 무조건 사용해야한다. 
  // 패키지를 직접 사용하지 않지만 부가효과를 얻는 경우 _ 을 패키지명 앞에 붙여준다.
  // 부과효과: 패키지가 초기화 되면서 실행되는 코드에 따른 효과
)

fmt.Println("Hello World") // fmt 패키지명 . Println() 함수명
fmt.Println(rand.Int()) // 경로가 있느 패키지인 math/rand 패키지의 경우는 마지막 폴더명인 rand만 사용한다.

패키지를 import 하면 컴파일러는 패키지 내 전역 변수를 초기화 한다. 그런 다음 패키지에 init() 함수가 있다면 호출해 패키지를 초기화 한다. init() 함수는 반드시 입력 매개변수가 없고, 반환값도 없는 함수여야 한다.

이때 패키지의 초기화 함수인 init() 함수 기능만 사용하기 원할 경우 밑줄 _을 이용해서 import 한다.

16.02. 모듈

모듈은 패키지를 모아 놓은 Go의 프로젝트 단위로 Go 1.16부터 기본이 됐다.

이전에는 Go 모듈을 만들지 않는Go 코드는 모두 GOPATH/src 아래 폴더 아래에 있어야 했지만, 모듈이 기본이 되면서 모든 Go 코드는 Go 모듈 아래에 있어야 한다.

go build를 사용하려면 반드시 Go 모듈 루트 폴더에 go.mod 파일이 있어야 한다. go build를 통해 실행 파일을 만들 때, go.mod와 외부 저장소 패키지 버전 정보를 담고 있는 go.sum 파일을 통해 외부 패키지와 모듈 내 패키지를 합쳐서 실행 파일을 만든다.

go mod init 패키지명

go.mod : Go 버전과 외부 패키지 등이 명시된 파일
go.sum: 외부 저장소 패키지 버전 정보를 담고 있는 파일, 패키지 위조 여부 검사를 위한 checksum 결과가 있다.

 

모듈 예제

1. goproject/usepkg 폴더를 만든다.

2. go 모듈 생성

go mod init goproject\usepkg

3. goproject/usepkg/custompkg/custompkg

package custompkg

import "fmt"

func PrintCustom() {
	fmt.Println("This is custom pkg.")
}

4. goproject/usepkg/usepkg.go

package main

import (
	"fmt" // go가 설치되면 같이 설치되는 표준 패키지
	"goprojects/usepkg/custompkg" // 현재 모듈에 속한 패키지

	"github.com/guptarohit/asciigraph" // 외부 저장소 패키지
	"github.com/tuckersGo/musthaveGo/ch16/expkg"
)

func main() {
	custompkg.PrintCustom()
	expkg.PrintSample()

	data := []float64{3, 4, 5, 5, 5, 2, 13, 5, 8, 6, 4}
	graph := asciigraph.Plot(data)
	fmt.Println(graph)
}

5. go mod tidy 로 모듈에 필요한 패키지를 찾아서 다운로드 해주고, 필요한 패키지 정보를 go.mod 파일과 go.sum 파일에 적어주게 된다. 다운 받은 외부 패키지인 asciigraph 패키지와 expkg 패키지는 GOPATH/pkg/mod 폴더에 버전별로 저장되어 있다.

6. 파일 내용: go.mod와 go.sum에 필요한 패키지의 버전 정보가 기입되어서 항상 같은 버전의 패키지가 사용되므로, 버전 업데이트에 따른 문제가 발생하지 않는다.

// go.mod
module goprojects/usepkg

go 1.20

require (
	github.com/guptarohit/asciigraph v0.5.6
	github.com/tuckersGo/musthaveGo/ch16/expkg v0.0.0-20230126175348-6f7945b85bda
)

// go.sum
github.com/guptarohit/asciigraph v0.5.6 h1:0tra3HEhfdj1sP/9IedrCpfSiXYTtHdCgBhBL09Yx6E=
github.com/guptarohit/asciigraph v0.5.6/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
github.com/tuckersGo/musthaveGo/ch16/expkg v0.0.0-20230126175348-6f7945b85bda h1:F21GWOayUeFkA47sc6oB2zb6ly9emQUx2A1wHTETta0=
github.com/tuckersGo/musthaveGo/ch16/expkg v0.0.0-20230126175348-6f7945b85bda/go.mod h1:o12FpIqEJes/Y7CWE9BJemI9VUTQBsH7t3wYlDCw3Fw=

 

17. 숫자 맞추기 게임 만들기

숫자 맞추기 게임은 책에서 소개하는 첫번째 프로젝트로 간단히 랜던값을 생성하고, 표준 입력으로 숫자를 받고, 두 값을 비교해서 결과를 출력하는 프로젝트이다.

사용자 입력과 비교를 반복적으로 처리하기 위해 main()함수는 for문으로 구성되어 있다.

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 7주차(31장)  (0) 2023.11.09
Go스터디: 6주차(27~30장)  (0) 2023.11.05
Go스터디: 5주차(23~26장)  (0) 2023.10.29
Go스터디: 4주차(18~22장)  (1) 2023.10.22
Go스터디: 2주차(3~11장)  (1) 2023.10.08

이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 3~11장 써머리입니다.

이 책은 다른 언어에 익숙한 분들은 go의 특성을 이해할 수 있고, 다른 언어에 대한 이해가 없어도 프로그래밍에 대한 기본을 이해하기 쉽게 쓰여진 장점이 있습니다.

03 Hello Go World

03.01. Go에서 코드 실행 단계

코드가 프로그램이 되어 실행되기 까지 5가지 단계를 거쳐야 한다.

  1. 폴더 생성
    1. 모든 코드는 패키지 단위로 작성된다. 같은 폴더에 위치한 .go 파일은 모두 같은 패키지에 포함되고, 패키지 명으로 폴더명을 사용한다.
    2. 예를 들어, goproject/hello/extra
      1. 여기서 hello 폴더에 든 .go 파일은 hello 패키지가 된다.
      2. extra 폴더에 든 .go 패키지는 extra 패키지가 된다.
  2. .go 파일 생성 및 작성
  3. Go 모듈 생성 (→ 16장)
    1. go 1.16 버전 이후로 go 모듈이 기본으로 적용된다. 그러므로, go 코드는 빌드하기 전에 모듈을 생성해야 한다.
    2. 모듈 생성: go mod init <모듈 이름>
    3. go mod init goproject/hello
    4. go 모듈을 생성하면 go.mod 파일이 생성되고, 여기에 모듈명, go버전, 필요한 패키지 목록 정보가 저장된다.
  4. 빌드
    1. go build 명령으로 go 코드를 기계어로 변환해 실행 파일을 만든다.
    2. GOOS, GOARCH 환경변수로, 다른 환경에 실행되는 실행 파일 만들 수 있다.
    3. GOOS=linux GOARCH=amd64 go build
    4. go tool disk list 로 설정 가능한 값을 확인
  5. 실행
    1. 실행 파일을 명령어로 실행

03.02. 샘플 코드 설명

package main // 패키지 선언: 이 코드가 어떤 패키지에 속하는지 알려줌
						 // main 패키지는 프로그램 시작점(entry point)을 포함하는 특별한 패키지

import "fmt" // 패키지 가져온다. fmt는 표준 입출력을 다루는 내장 패키지 -> 5장

func main() { // main 함수 시작, main() 함수는 프로그램 진입점 함수
	// Hello Go World 출력 _ // 는 주석 예약어
  // go 규약: 다른 프로그램에서 쓰이는 함수 앞에 함수명으로 시작하는 주석을 달아 함수를 설명
  // 한줄 주석 /* */ 여러줄 주석
	fmt.Println("Hello Go World") // 표준 출력(터미널 화면)으로 문자열을 출력 함수
} // 코드 블록 종료, 여기서는 main 함수 블록 종료

 

04 변수

04.01. 변수 기본

변수(Variable)는 ‘값을 저장하는 메모리 공간을 가리키는 이름’이다.

컴퓨터 입장에서 프로그램은 ‘메모리에 있는 데이터를 언제 어떻게 변경할지 나타낸 문서’이다

그래서 데이터를 어떻게 저장하고, 조작(선언, 사용, 출력)하는지가 핵심이 된다.

go에서 변수는 아래와 같이 선언한다. (메모리 할당이라고 부른다)

var a int = 10 // 변수선언 키워드, 변수명, 타입 = 초기값

변수는 4가지의 속성을 가진다.

  • 이름: 프로그래머는 이름을 통해 메모리 공간을 접근, 이름은 camel case 를 사용하는 것이 권장, 변수가 함수 내부가 아닌 외부에 선언되어 있고, 변수명이 대문자로 시작하면 이 변수는 패키지 외부로 공개된다
  • 값: 메모리 공간에 저장된 값
  • 주소: 메모리 공간의 시작 주소
  • 타입: 변수 값의 형태(정수, 실수, 문자열, ..), 실제로 메모리 시작 주소에서 타입 크기에 해당하는 공간을 할당한다

변수 선언은 선언 대입문을 사용할 수 있다 _ var 키워드와 타입을 생략해 변수를 선언

var b = 3.1415
c := 365
s := "hello"

근데 같은 변수에 대해서 :=를 또 사용하면 아래와 같은 에러가 발생한다. NO NEW VAR!!

.\\5.1.go:18:9: no new variables on left side of :=

go는 강 타입 언어이기 때문에 연산이나 대입에서 타입이 다르면 에러가 발생한다.

a := 3
b := 3.5

var c int = b // cannot use b (variable of type float64) as int value in variable declaration
d := a * b // invalid operation: a * b (mismatched types int and float64)

타입을 변환하려면 원하는 타입명을 적고 ()로 변환할 수 있는데, 이럴 때 값이 변경될 수 있다

a := 3
b := 3.5

d := a * int(b)

fmt.Println(d)  // 9

전역 변수(global variable)는 같은 패키지 내에서 언제나 접근할 수 있지만, 지역 변수(local variable)의 범위는 {} 안에서만 유효하다.

04.02. 숫자의 표현

정수의 표현에서 부호를 표현하기 위해 부호 비트를 이용한다. 단 값도 2의 보수(비트를 반전하고 +1)를 사용해 표현한다.

실수의 표현을 위해 부호비트(1비트), 지수부(8비트), 소수부(23비트)를 사용한다. 실수 타입은 유효자리 수가 정해져 있으므로 데이터 처리에 유의해야 한다.

 

05 fmt 패키지를 이용한 텍스트 입출력

05.01 입출력 기본

프로그램과 사용자는 입력과 출력을 통해서 상호작용을 하는데, 기본적으로는 입력을 위해 키보드, 출력을 위해 화면을 이용한다. 모든 입력과 출력을 프로그램에서 구현하기는 복잡하기 때문에, 운영체제가 제공하는 표준 입출력 스트림(standard input/output stream)을 사용하면, 프로그램 내부에서 입출력을 간편하게 처리할 수 있다.

go에서는 fmt 패키지를 이용한다.

fmt의 표준 출력 함수

Print() 함수 입력값을 출력한다

Println() 함수 입력값을 출력+개행한다
Printf() 함수 입력값을 서식 문자를 이용한 출력한다

fmt의 표준 입력 함수

Scan() 표준 입력에서 값을 입력 받는다

Scanf() 표준 입력에서 서식 문자로 입력 받는다
Scanln() 표준 입력에서 한 줄을 읽어서 값을 받는다

Scan()

변수들의 메모리 주소를 인수로 받는다. _ 여러 값을 입력받을 때 입력 값은 공백이나 줄바꿈으로 구분한다.

반환값은 성공적으로 입력한 개수와 입력 실패 시 에러를 반환한다.

func Scan(a ...interface{}) (n int, err error)

반환값의 의미를 아래 예시로 살펴본다.

go run .\\5.2.go
1 hello  // 입력
1 expected integer  // 1개 받고, err

go run .\\5.2.go
hello 1  // 입력
0 expected integer  // 0개 받고 err

Scanf() 함수 원형

func Scanf(format string, a ...interface{}) (n int, err error)

05.02 키보드 입력과 Scan()의 동작 원리

사용자가 표준 입력 장치로 입력하면 입력 데이터는 컴퓨터 내부에 표준 입출력 스트림(standard input stream)이라는 메모리 공간에 미시 저장되는데, Scan()함수들은 그 표준 입력 스트림에서 값을 읽어서 입력값을 처리한다.

Stream 이란 흐름의 의미이다. 즉, 입력 데이터가 연속된 데이터 흐름 형태를 가지고 있다는 뜻이다. 한편 한번 읽은 데이터를 다시 읽을 수 없다는 의미도 포함한다.

이때 먼저 입력한 데이터 부터 읽어오기 때문에 데이터는 거꾸로 저장된다. (FIFO 구조)

다만 이러한 구조 때문에 var int a 에 대해서 Scan(&a)으로 Hello를 읽으면 H 먼저 읽게 되는데, 이러한 구조 때문에 첫번째 캐릭터에서 에러가 발생하면 표준 입력 스트림에 불필요한 입력이 남게된다(여기서는 ello)

그래서 여러 번 Scan() 함수를 호출할 때는 이러한 문제가 있으므로, 표준 입력 스트림을 비워줄 필요가 있다.

package main

import (
	"bufio" // io를 담당하는 패키지
	"fmt"
	"os" // 표준 입출력 등을 가지고 있는 패키지
)

func main() {
	stdin := bufio.NewReader(os.Stdin)  // 표준 입력을 읽는 객체

	var a int
	var b int

	n, err := fmt.Scan(&a, &b)
	if err != nil {
		fmt.Println(n, err)
		stdin.ReadString('\\n') // 줄바꿈 문자가 나올때까지 읽는다 (표준 입력 스트림 비워짐)
	} else {
		fmt.Println(n, a, b)
	}

	n, err = fmt.Scan(&a, &b)
	if err != nil {
		fmt.Println(n, err)
	} else {
		fmt.Println(n, a, b)
	}
}

결과 확인

// stdin.ReadString('\\n') 없을 때
PS C:\\Users\\montauk\\Desktop\\projects\\goprojects\\go05> go run .\\5.1.go
1 Hello
1 expected integer
0 expected integer

// stdin.ReadString('\\n') 있을 때
PS C:\\Users\\montauk\\Desktop\\projects\\goprojects\\go05> go run .\\5.1.go
1 Hello
1 expected integer
2 3
2 2 3

bufio는 입력 스트림으로 부터 한 줄을 읽는 Reader 객체를 제공한다.

func NewReader(rd io.Reder) *Reader

 

06 연산자

산술연산자(사칙연산자, 비트 연산자, 시프트 연산자), 비교연산자(==, != , <, >), 논리 연산자(&&, ||, !)

대입 연산자(=)는 우변값을 좌변(메모리 공간)에 복사한다.

  • 좌변에는 반드시 저장할 공간이 있는 변수가 와야 한다.
  • 대입연산자는 값을 반환하지 않는다.
  • 복수 대입연산자 사용 가능(a, b = 3, 4) 하다.

기타 연산자

[] 배열의 요소에 접근

. 구조체나 패키지 요소에 접근
& 변수의 메모리 주소값을 반환
* 포인터 변수가 가리키는 메모리 주소에 접근
슬라이스 요소에 접근하거나, 가변 인수를 만들 때 사용
: 배열의 일부분을 집어올 때 사용
채널에서 값을 빼거나 넣을 때 사용

 

07 함수

함수는 함수 키워드, 함수명, 매개변수, 반환타입, 함수 코드 블록으로 구성된다.

func Add(a int, b int) int {
	// code block
}

함수를 호출할 때 입력하는 변수를 argument(아규먼트, 인수)라고 한다.

함수가 외부로 입력 받는 변수를 parameter(매개변수, 파라미터)라고 한다.

c := Add(3, 4) // 함수 호출 부에서 3, 4는 인수

func Add(a int, b int) int { // 함수 정의 부의 a, b는 매개변수
	return a+b 
}

함수를 호출하며 입력한 값은 실제 함수에 어떻게 전달되는가?

인수는 매개변수로 복사된다. 매개변수와 함수 내 선언된 변수는 함수가 종료되면 변수 범위를 벗어나서 접근하지 못한다.

 

보낸 값을 그대로 사용하지 않고, 값을 복사해서 사용한다. 이 것은 함수 내에서 선언한 a, b 변수에 초기값을 대입하는 것과 깉다.

위 예시에서, 3,4는 복사되어 매개변수에 전달되고, return 값은 복사되어 반환된다. 호출한 함수가 종료되면 함수에서 사용한 지역 변수에 접근할 수 없다.

return으로 함수 결과가 반환 되면서 함수가 즉시 종료되고, 함수를 호출했던 호출 위치로 명령 포인터(instruction pointer)가 되돌아가 수행을 이어간다.

 

08 상수

상수는 변하지 않는 값을 말한다. 기본 타입이(primitive) 아닌 타입(complex)은 상수를 사용할 수 없다.

const ContVal int = 10

상수를 변경하려고 할 때 발생하는 에러

cannot assign to c (neither addressable nor a map index expression)

상수를 메모리 접근할 때 발생하는 에러

invalid operation: cannot take address of c (constant 10 of type int)

상수는 초기화된 값이 변하지 않는다. 좌변에 올 수 없다. 상수는 메모리 주소값을 접근할 수 없다.

왜? 상수는 컴파일 할 때 숫자 자체로 치환되나?

→ 184P. 상수의 메모리 주솟갑셍 접근할 수 없는 이유 역시 컴파일 타임에 리터럴로 전환되어서 실행 파일에 값 형태로 쓰이기 때문이다. (동적 메모리 할당 영역 사용하지 않음)

프로그램이 로드될 때, 실행 파일이 올라간 영역을 코드 영역이라 하고, 프로그램 실행을 위해서 실행 중 할당해서 사용되는 영역을 동적 할당 메모리 영역이라고 한다. 상수는 리터럴로 코드 영역에 포함되기 때문에 동적 할당 영역을 사용 안함.

상수는 언제 사용하나?

  • 변하면 안되는 값
  • 코드 값: 어떤 숫자에 의미를 부여하는 것 (ex. 404: NOT FOUND)
  • iota 로 간편하게 열거값 사용하기 (iota 키워드를 사용하면 1로 증가하는 코드값 생성 한다)

리터럴(literal)이란 고정된 값, 값 자체로 쓰인 문구라고 볼 수 있다. go에서 상수는 리터럴과 같이 취급한다. 그래서 컴파일 될 때 상수는 리터럴로 변환되어 실행 파일에 쓰인다.

 

09~11 if, switch, for

09. If

기본형

if 조건문 {
	문장
} else if 조건문 {
	문장
} else {
	문장
}

쇼트 서킷(short circuit)? &&와 ||의 특성으로, 우변을 체크하지 않을 수 있음

  • && 연산은 좌변이 false이면 우변을 검사하지 않고 false 처리한다.
  • || 연산은 좌변이 true 이면 우변은 검사하지 않고 true 처리한다.

if 초기문 사용 예시 (단, 초기문의 변수의 범위는 if 문에서만 사용된다)

if filename, success := UploadFile(); success {
	fmt.Println("success:", filename)
} else {
	fmt.Println("fail")
}

10. switch

기본형

switch 비교값 {
case1:
	문장
case2:
	문장
default:
	문장
}

go의 switch 문의 각 case 종료 시에 break 가 없어도 빠져 나간다. 다음 case 문까지 체크하고 싶다면 fallthrough 키워드를 사용할 수 있다. (단, 혼동을 일으킬 수 있으므로 fallthrough를 권장하지 않음)

11. for

기본형

for 초기문; 조건문; 후처리 {
	코드블록
}

생략형

for ; 조건문; 후처리 {}
for 초기문; 후처리 {}
for ; 조건문; {}
for 조건문 {}

중첩 for문에서 flag 변수로 break를 처리하기 → 더 복잡한 경우는 label을 사용할 수 있다.

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 7주차(31장)  (0) 2023.11.09
Go스터디: 6주차(27~30장)  (0) 2023.11.05
Go스터디: 5주차(23~26장)  (0) 2023.10.29
Go스터디: 4주차(18~22장)  (1) 2023.10.22
Go스터디: 3주차(12~17장)  (1) 2023.10.15

curl 로 웹 서비스를 테스트할 때 옵션이 없으면 무한 대기할 수 있다. 이럴 때는 ctrl+c 로 끊어줘야 한다.

 

$ curl 192.168.0.1
^C

 

이때 -m <sec> 옵션을 주면 해당 시간(초) 만큼 요청하고, time out 한다.

 

$ curl -m 3 192.168.0.1
curl: (28) Connection timed out after 3002 milliseconds

 

테스트를 할 때 -m 옵션을 주면 간단하게 성공 여부를 체크할 수 있다.

containerd 를 사용하는 윈도우 노드 추가

지난 '쿠버네티스 윈도우 워커 노드 추가(with Calico CNI)' 에서 Docker EE 를 사용하여 Windows 워커 노드를 추가했습니다. Docker EE 가 deprecated 됨에 따라 containerd 를 활용할 필요가 있어서 추가로 containerd 를 이용해 Windows 워커노드를 추가하는 절차를 기록했습니다.

 

아래 'containerd 시작하기' 문서를 참고하여 containerd 를 설치합니다.

https://github.com/containerd/containerd/blob/main/docs/getting-started.md#installing-containerd-on-windows

PS C:\Users\Administrator> $Version="1.6.4"
PS C:\Users\Administrator> curl.exe -L https://github.com/containerd/containerd/releases/download/v$Version/containerd-$Version-windows-amd64.tar.gz -o containerd-windows-amd64.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 30.0M  100 30.0M    0     0  1393k      0  0:00:22  0:00:22 --:--:-- 1630k
PS C:\Users\Administrator> tar.exe xvf .\containerd-windows-amd64.tar.gz
x bin/
x bin/containerd.exe
x bin/containerd-shim-runhcs-v1.exe
x bin/containerd-stress.exe
x bin/ctr.exe
PS C:\Users\Administrator> Copy-Item -Path ".\bin\" -Destination "$Env:ProgramFiles\containerd" -Recurse -Force
PS C:\Users\Administrator> cd $Env:ProgramFiles\containerd\
PS C:\Program Files\containerd> .\containerd.exe config default | Out-File config.toml -Encoding ascii
PS C:\Program Files\containerd> Get-Content config.toml
disabled_plugins = []
imports = []
oom_score = 0
plugin_dir = ""
required_plugins = []
root = "C:\\ProgramData\\containerd\\root"
state = "C:\\ProgramData\\containerd\\state"
temp = ""
version = 2

[cgroup]
  path = ""

[debug]
  address = ""
  format = ""
  gid = 0
  level = ""
  uid = 0

[grpc]
  address = "\\\\.\\pipe\\containerd-containerd"
  gid = 0
  max_recv_message_size = 16777216
  max_send_message_size = 16777216
  tcp_address = ""
  tcp_tls_ca = ""
  tcp_tls_cert = ""
  tcp_tls_key = ""
  uid = 0

[metrics]
  address = ""
  grpc_histogram = false

[plugins]

  [plugins."io.containerd.gc.v1.scheduler"]
    deletion_threshold = 0
    mutation_threshold = 100
    pause_threshold = 0.02
    schedule_delay = "0s"
    startup_delay = "100ms"

  [plugins."io.containerd.grpc.v1.cri"]
    device_ownership_from_security_context = false
    disable_apparmor = false
    disable_cgroup = false
    disable_hugetlb_controller = false
    disable_proc_mount = false
    disable_tcp_service = true
    enable_selinux = false
    enable_tls_streaming = false
    enable_unprivileged_icmp = false
    enable_unprivileged_ports = false
    ignore_image_defined_volumes = false
    max_concurrent_downloads = 3
    max_container_log_line_size = 16384
    netns_mounts_under_state_dir = false
    restrict_oom_score_adj = false
    sandbox_image = "k8s.gcr.io/pause:3.6"
    selinux_category_range = 0
    stats_collect_period = 10
    stream_idle_timeout = "4h0m0s"
    stream_server_address = "127.0.0.1"
    stream_server_port = "0"
    systemd_cgroup = false
    tolerate_missing_hugetlb_controller = false
    unset_seccomp_profile = ""

    [plugins."io.containerd.grpc.v1.cri".cni]
      bin_dir = "C:\\Program Files\\containerd\\cni\\bin"
      conf_dir = "C:\\Program Files\\containerd\\cni\\conf"
      conf_template = ""
      ip_pref = ""
      max_conf_num = 1

    [plugins."io.containerd.grpc.v1.cri".containerd]
      default_runtime_name = "runhcs-wcow-process"
      disable_snapshot_annotations = false
      discard_unpacked_layers = false
      ignore_rdt_not_enabled_errors = false
      no_pivot = false
      snapshotter = "windows"

      [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
        base_runtime_spec = ""
        cni_conf_dir = ""
        cni_max_conf_num = 0
        container_annotations = []
        pod_annotations = []
        privileged_without_host_devices = false
        runtime_engine = ""
        runtime_path = ""
        runtime_root = ""
        runtime_type = ""

        [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime.options]

      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes]

        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runhcs-wcow-process]
          base_runtime_spec = ""
          cni_conf_dir = ""
          cni_max_conf_num = 0
          container_annotations = []
          pod_annotations = []
          privileged_without_host_devices = false
          runtime_engine = ""
          runtime_path = ""
          runtime_root = ""
          runtime_type = "io.containerd.runhcs.v1"

          [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runhcs-wcow-process.options]

      [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime]
        base_runtime_spec = ""
        cni_conf_dir = ""
        cni_max_conf_num = 0
        container_annotations = []
        pod_annotations = []
        privileged_without_host_devices = false
        runtime_engine = ""
        runtime_path = ""
        runtime_root = ""
        runtime_type = ""

        [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime.options]

    [plugins."io.containerd.grpc.v1.cri".image_decryption]
      key_model = "node"

    [plugins."io.containerd.grpc.v1.cri".registry]
      config_path = ""

      [plugins."io.containerd.grpc.v1.cri".registry.auths]

      [plugins."io.containerd.grpc.v1.cri".registry.configs]

      [plugins."io.containerd.grpc.v1.cri".registry.headers]

      [plugins."io.containerd.grpc.v1.cri".registry.mirrors]

    [plugins."io.containerd.grpc.v1.cri".x509_key_pair_streaming]
      tls_cert_file = ""
      tls_key_file = ""

  [plugins."io.containerd.internal.v1.opt"]
    path = "C:\\ProgramData\\containerd\\root\\opt"

  [plugins."io.containerd.internal.v1.restart"]
    interval = "10s"

  [plugins."io.containerd.internal.v1.tracing"]
    sampling_ratio = 1.0
    service_name = "containerd"

  [plugins."io.containerd.metadata.v1.bolt"]
    content_sharing_policy = "shared"

  [plugins."io.containerd.runtime.v2.task"]
    platforms = ["windows/amd64", "linux/amd64"]
    sched_core = false

  [plugins."io.containerd.service.v1.diff-service"]
    default = ["windows", "windows-lcow"]

  [plugins."io.containerd.service.v1.tasks-service"]
    rdt_config_file = ""

  [plugins."io.containerd.tracing.processor.v1.otlp"]
    endpoint = ""
    insecure = false
    protocol = ""

[proxy_plugins]

[stream_processors]

  [stream_processors."io.containerd.ocicrypt.decoder.v1.tar"]
    accepts = ["application/vnd.oci.image.layer.v1.tar+encrypted"]
    args = ["--decryption-keys-path", "C:\\Program Files\\containerd\\ocicrypt\\keys"]
    env = ["OCICRYPT_KEYPROVIDER_CONFIG=C:\\Program Files\\containerd\\ocicrypt\\ocicrypt_keyprovider.conf"]
    path = "ctd-decoder"
    returns = "application/vnd.oci.image.layer.v1.tar"

  [stream_processors."io.containerd.ocicrypt.decoder.v1.tar.gzip"]
    accepts = ["application/vnd.oci.image.layer.v1.tar+gzip+encrypted"]
    args = ["--decryption-keys-path", "C:\\Program Files\\containerd\\ocicrypt\\keys"]
    env = ["OCICRYPT_KEYPROVIDER_CONFIG=C:\\Program Files\\containerd\\ocicrypt\\ocicrypt_keyprovider.conf"]
    path = "ctd-decoder"
    returns = "application/vnd.oci.image.layer.v1.tar+gzip"

[timeouts]
  "io.containerd.timeout.bolt.open" = "0s"
  "io.containerd.timeout.shim.cleanup" = "5s"
  "io.containerd.timeout.shim.load" = "5s"
  "io.containerd.timeout.shim.shutdown" = "3s"
  "io.containerd.timeout.task.state" = "2s"

[ttrpc]
  address = ""
  gid = 0
  uid = 0
PS C:\Program Files\containerd> .\containerd.exe --register-service
PS C:\Program Files\containerd> Start-Service containerd
PS C:\Program Files\containerd> Get-Service containerd

Status   Name               DisplayName
------   ----               -----------
Running  containerd         containerd

containerd 가 윈도우의 서비스 형태로 실행됩니다.

docker 가 설치되지 않기 때문에 별도의 CLI를 설치합니다. crictl 명령을 수행해보면 각각의 컨테이너 런타임의 endpont를 찾기 때문에, 아래를 참고하여 user profile 쪽에 crictl.config을 생성했습니다.

https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md

PS C:\Users\Administrator> $VERSION="v1.24.2"
PS C:\Users\Administrator> curl.exe -L https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-$VERSION-windows-amd64.tar.gz -o crictl-windows-amd64.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 14.1M  100 14.1M    0     0  1275k      0  0:00:11  0:00:11 --:--:-- 1705k
PS C:\Users\Administrator> tar.exe xvf .\crictl-windows-amd64.tar.gz -C $ENV:WINDIR\system32
x crictl.exe
PS C:\Users\Administrator> crictl ps
time="2022-07-12T14:35:37+09:00" level=warning msg="runtime connect using default endpoints: [npipe:////./pipe/dockershim npipe:////./pipe/containerd-containerd npipe:////./pipe/cri-dockerd]. As the default settings are now deprecated, you should set the endpoint instead."
time="2022-07-12T14:35:37+09:00" level=error msg="unable to determine runtime API version: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing open //./pipe/dockershim: The system cannot find the file specified.\""
time="2022-07-12T14:35:37+09:00" level=warning msg="image connect using default endpoints: [npipe:////./pipe/dockershim npipe:////./pipe/containerd-containerd npipe:////./pipe/cri-dockerd]. As the default settings are now deprecated, you should set the endpoint instead."
time="2022-07-12T14:35:37+09:00" level=error msg="unable to determine image API version: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing open //./pipe/dockershim: The system cannot find the file specified.\""
CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD

PS C:\Users\Administrator> mkdir $Env:UserProfile\.crictl


    Directory: C:\Users\Administrator


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----     2022-07-12   오후 2:40                .crictl


PS C:\Users\Administrator> notepad $Env:UserProfile\.crictl\crictl.yaml
PS C:\Users\Administrator> type $Env:UserProfile\.crictl\crictl.yaml
runtime-endpoint: npipe:\\\\.\\pipe\\containerd-containerd
image-endpoint: npipe:\\\\.\\pipe\\containerd-containerd
timeout: 2
debug: true
pull-image-on-create: false
PS C:\Users\Administrator> crictl ps
time="2022-07-12T14:43:19+09:00" level=debug msg="get runtime connection"
time="2022-07-12T14:43:19+09:00" level=debug msg="get image connection"
time="2022-07-12T14:43:19+09:00" level=debug msg="ListContainerResponse: []"
CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD

Calico 를 설치하려고 진행하니 아래와 같이 에러가 발생합니다.

PS C:\Users\Administrator> mkdir c:\k


    Directory: C:\


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----     2022-07-12   오후 2:51                k


PS C:\Users\Administrator> scp root@172.16.3.170:~/.kube/config c:\k\
root@172.16.3.170's password:
config                                                                                                                                                                                                          100% 5640     5.5KB/s   00:00
PS C:\Users\Administrator> Invoke-WebRequest https://projectcalico.docs.tigera.io/scripts/install-calico-windows.ps1 -OutFile c:\install-calico-windows.ps1
PS C:\Users\Administrator> c:\install-calico-windows.ps1 -KubeVersion 1.22.6 -ServiceCidr 10.96.0.0/12 -DNSServerIPs 10.96.0.10
WARNING: The names of some imported commands from the module 'helper' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose
parameter. For a list of approved verbs, type Get-Verb.
c:\calico-windows.zip not found, downloading Calico for Windows release...
Downloaded [https://github.com/projectcalico/calico/releases/download/v3.23.2//calico-windows-v3.23.2.zip] => [c:\calico-windows.zip]
C:\install-calico-windows.ps1 : The term 'Get-HnsNetwork' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and t
ry again.
At line:1 char:1
+ c:\install-calico-windows.ps1 -KubeVersion 1.22.6 -ServiceCidr 10.96. ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Get-HnsNetwork:String) [install-calico-windows.ps1], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException,install-calico-windows.ps1

Docker EE를 사용할 때와 다르게, containerd 를 설치하는 것 자체만으로 Windows 의 컨테이너 feature를 설치하지 않기 때문에 HNS와 같은 서비스 및 기타 cmdlet이 설치되어 있지 않습니다.

The term 'Get-HnsNetwork' is not recognized as the name of a cmdlet, function, script file, or operable program.

 

윈도우 서버에서 containers fature를 먼서 설치합니다. (재시작으로 수행결과가 없지만 아래와 같이 진행하면 됩니다)

PS C:\Users\Administrator> Install-WindowsFeature -Name containers
PS C:\Users\Administrator> Restart-Computer -Force

재시작후 다시 확인해보면 Host Network Service 가 조회됩니다.

PS C:\Users\Administrator> Get-Service hns

Status   Name               DisplayName
------   ----               -----------
Stopped  hns                Host Network Service

containerd 로 변경한 이후 한 가지 에러가 더 발생합니다.

PS C:\Users\Administrator> c:\install-calico-windows.ps1 -KubeVersion 1.22.6 -ServiceCidr 10.96.0.0/12 -DNSServerIPs 10.96.0.10
WARNING: The names of some imported commands from the module 'helper' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose
parameter. For a list of approved verbs, type Get-Verb.
Unzip Calico for Windows release...
Creating CNI directory
<생략>
Validating configuration...
CNI binary directory C:\Program Files\containerd\cni\bin doesn't exist.  Please create it and ensure kubelet is configured with matching --cni-bin-dir.
At C:\CalicoWindows\libs\calico\calico.psm1:35 char:13
+             throw "CNI binary directory $env:CNI_BIN_DIR doesn't exis ...
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (CNI binary dire... --cni-bin-dir.:String) [], RuntimeException
    + FullyQualifiedErrorId : CNI binary directory C:\Program Files\containerd\cni\bin doesn't exist.  Please create it and ensure kubelet is configured with matching --cni-bin-dir.

containerd로 변경되면서 cni\bin 위치를 제대로 생성하지 않고, 참조하는 것 같습니다만.. 이 부분은 설치 스크립트를 더 분석해야 확인가능 할 것 같습니다.

일단 해당 디렉터리를 수동으로 생성해주고 시작합니다.

PS C:\Program Files\containerd\cni\conf> mkdir "C:\Program Files\containerd\cni\bin"


    Directory: C:\Program Files\containerd\cni


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----     2022-07-12   오후 3:51                bin


PS C:\Program Files\containerd\cni> c:\install-calico-windows.ps1 -KubeVersion 1.22.6 -ServiceCidr 10.96.0.0/12 -DNSServerIPs 10.96.0.10
WARNING: The names of some imported commands from the module 'helper' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose
parameter. For a list of approved verbs, type Get-Verb.
Unzip Calico for Windows release...
Creating CNI directory
Downloading Windows Kubernetes scripts
[DownloadFile] File c:\k\hns.psm1 already exists.
WARNING: The names of some imported commands from the module 'hns' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose
parameter. For a list of approved verbs, type Get-Verb.
Downloaded [https://dl.k8s.io/v1.22.6/kubernetes-node-windows-amd64.tar.gz] => [C:\Users\Administrator\AppData\Local\Temp\2\tmp3B38.tar.gz]
Setup Calico for Windows...
Error from server (NotFound): namespaces "calico-system" not found
Calico running in kube-system namespace
Backend networking is vxlan

Start Calico for Windows install...

Setting environment variables if not set...
Environment variable KUBE_NETWORK is already set: Calico.*
Environment variable CALICO_NETWORKING_BACKEND is already set: vxlan
Environment variable K8S_SERVICE_CIDR is already set: 10.96.0.0/12
Environment variable DNS_NAME_SERVERS is already set: 10.96.0.10
Environment variable DNS_SEARCH is already set: svc.cluster.local
Environment variable CALICO_DATASTORE_TYPE is already set: kubernetes
Environment variable KUBECONFIG is already set: c:\k\config
Environment variable ETCD_ENDPOINTS is not set. Setting it to the default value:
Environment variable ETCD_KEY_FILE is not set. Setting it to the default value:
Environment variable ETCD_CERT_FILE is not set. Setting it to the default value:
Environment variable ETCD_CA_CERT_FILE is not set. Setting it to the default value:
Environment variable CNI_BIN_DIR is already set: C:\Program Files\containerd\cni\bin
Environment variable CNI_CONF_DIR is already set: C:\Program Files\containerd\cni\conf
Environment variable CNI_CONF_FILENAME is already set: 10-calico.conf
Environment variable CNI_IPAM_TYPE is already set: calico-ipam
Environment variable VXLAN_VNI is already set: 4096
Environment variable VXLAN_MAC_PREFIX is already set: 0E-2A
Environment variable VXLAN_ADAPTER is not set. Setting it to the default value:
Environment variable NODENAME is already set: k8s-ww2
Environment variable CALICO_K8S_NODE_REF is already set: k8s-ww2
Environment variable STARTUP_VALID_IP_TIMEOUT is already set: 90
Environment variable IP is already set: autodetect
Environment variable CALICO_LOG_DIR is already set: C:\CalicoWindows\logs
Environment variable FELIX_LOGSEVERITYFILE is already set: none
Environment variable FELIX_LOGSEVERITYSYS is already set: none
Validating configuration...
Installing node startup service...


    Hive: HKEY_LOCAL_MACHINE\Software


Name                           Property
----                           --------
Tigera


    Hive: HKEY_LOCAL_MACHINE\Software\Tigera


Name                           Property
----                           --------
Calico
Service "CalicoNode" installed successfully!
Set parameter "AppParameters" for service "CalicoNode".
Set parameter "AppDirectory" for service "CalicoNode".
Set parameter "DisplayName" for service "CalicoNode".
Set parameter "Description" for service "CalicoNode".
Set parameter "Start" for service "CalicoNode".
Reset parameter "ObjectName" for service "CalicoNode" to its default.
Set parameter "Type" for service "CalicoNode".
Reset parameter "AppThrottle" for service "CalicoNode" to its default.
Creating log directory.

PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\CalicoWindows\logs
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\CalicoWindows
PSChildName       : logs
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : True
Name              : logs
FullName          : C:\CalicoWindows\logs
Parent            : CalicoWindows
Exists            : True
Root              : C:\
Extension         :
CreationTime      : 2022-07-12 오후 3:53:55
CreationTimeUtc   : 2022-07-12 오전 6:53:55
LastAccessTime    : 2022-07-12 오후 3:53:55
LastAccessTimeUtc : 2022-07-12 오전 6:53:55
LastWriteTime     : 2022-07-12 오후 3:53:55
LastWriteTimeUtc  : 2022-07-12 오전 6:53:55
Attributes        : Directory
Mode              : d-----
BaseName          : logs
Target            : {}
LinkType          :

Set parameter "AppStdout" for service "CalicoNode".
Set parameter "AppStderr" for service "CalicoNode".
Set parameter "AppRotateFiles" for service "CalicoNode".
Set parameter "AppRotateOnline" for service "CalicoNode".
Set parameter "AppRotateSeconds" for service "CalicoNode".
Set parameter "AppRotateBytes" for service "CalicoNode".
Done installing startup service.
Installing Felix service...
Service "CalicoFelix" installed successfully!
Set parameter "AppParameters" for service "CalicoFelix".
Set parameter "AppDirectory" for service "CalicoFelix".
Set parameter "DependOnService" for service "CalicoFelix".
Set parameter "DisplayName" for service "CalicoFelix".
Set parameter "Description" for service "CalicoFelix".
Set parameter "Start" for service "CalicoFelix".
Reset parameter "ObjectName" for service "CalicoFelix" to its default.
Set parameter "Type" for service "CalicoFelix".
Reset parameter "AppThrottle" for service "CalicoFelix" to its default.
Set parameter "AppStdout" for service "CalicoFelix".
Set parameter "AppStderr" for service "CalicoFelix".
Set parameter "AppRotateFiles" for service "CalicoFelix".
Set parameter "AppRotateOnline" for service "CalicoFelix".
Set parameter "AppRotateSeconds" for service "CalicoFelix".
Set parameter "AppRotateBytes" for service "CalicoFelix".
Done installing Felix service.
Copying CNI binaries to C:\Program Files\containerd\cni\bin
Writing CNI configuration to C:\Program Files\containerd\cni\conf\10-calico.conf.
Wrote CNI configuration.

Calico for Windows installed

Starting Calico...
This may take several seconds if the vSwitch needs to be created.
Waiting for Calico initialisation to finish...
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Waiting for Calico initialisation to finish...StoredLastBootTime , CurrentLastBootTime 2022-07-12 오후 3:09:43
Calico initialisation finished.
Done, the Calico services are running:

Status      : Running
Name        : CalicoFelix
DisplayName : Calico Windows Agent


Status      : Running
Name        : CalicoNode
DisplayName : Calico Windows Startup


Caption                 :
Description             : Enable kubectl exec and log
ElementName             : kubectl exec 10250
InstanceID              : KubectlExec10250
CommonName              :
PolicyKeywords          :
Enabled                 : True
PolicyDecisionStrategy  : 2
PolicyRoles             :
ConditionListType       : 3
CreationClassName       : MSFT|FW|FirewallRule|KubectlExec10250
ExecutionStrategy       : 2
Mandatory               :
PolicyRuleName          :
Priority                :
RuleUsage               :
SequencedActions        : 3
SystemCreationClassName :
SystemName              :
Action                  : Allow
Direction               : Inbound
DisplayGroup            :
DisplayName             : kubectl exec 10250
EdgeTraversalPolicy     : Block
EnforcementStatus       : NotApplicable
LocalOnlyMapping        : False
LooseSourceMapping      : False
Owner                   :
Platforms               : {}
PolicyStoreSource       : PersistentStore
PolicyStoreSourceType   : Local
PrimaryStatus           : OK
Profiles                : 0
RuleGroup               :
Status                  : The rule was parsed successfully from the store. (65536)
StatusCode              : 65536
PSComputerName          :
Name                    : KubectlExec10250
ID                      : KubectlExec10250
Group                   :
Profile                 : Any
Platform                : {}
LSM                     : False

Calico Node와 Calico Felix 가 정상 실행되었습니다.

PS C:\Program Files\containerd\cni\bin> Get-Service -Name CalicoNode

Status   Name               DisplayName
------   ----               -----------
Running  CalicoNode         Calico Windows Startup


PS C:\Program Files\containerd\cni\bin> Get-Service -Name CalicoFelix

Status   Name               DisplayName
------   ----               -----------
Running  CalicoFelix        Calico Windows Agent

추가 스크립트를 수행하여 kubelet과 kube-proxy 를 설치하고 서비스를 실행합니다.

PS C:\Program Files\containerd\cni\bin> C:\CalicoWindows\kubernetes\install-kube-services.ps1
Installing kubelet service...
Service "kubelet" installed successfully!
Set parameter "AppParameters" for service "kubelet".
Set parameter "AppDirectory" for service "kubelet".
Set parameter "DisplayName" for service "kubelet".
Set parameter "Description" for service "kubelet".
Set parameter "Start" for service "kubelet".
Reset parameter "ObjectName" for service "kubelet" to its default.
Set parameter "Type" for service "kubelet".
Reset parameter "AppThrottle" for service "kubelet" to its default.
Set parameter "AppStdout" for service "kubelet".
Set parameter "AppStderr" for service "kubelet".
Set parameter "AppRotateFiles" for service "kubelet".
Set parameter "AppRotateOnline" for service "kubelet".
Set parameter "AppRotateSeconds" for service "kubelet".
Set parameter "AppRotateBytes" for service "kubelet".
Done installing kubelet service.
Installing kube-proxy service...
Service "kube-proxy" installed successfully!
Set parameter "AppParameters" for service "kube-proxy".
Set parameter "AppDirectory" for service "kube-proxy".
Set parameter "DisplayName" for service "kube-proxy".
Set parameter "Description" for service "kube-proxy".
Set parameter "Start" for service "kube-proxy".
Reset parameter "ObjectName" for service "kube-proxy" to its default.
Set parameter "Type" for service "kube-proxy".
Reset parameter "AppThrottle" for service "kube-proxy" to its default.
Set parameter "AppStdout" for service "kube-proxy".
Set parameter "AppStderr" for service "kube-proxy".
Set parameter "AppRotateFiles" for service "kube-proxy".
Set parameter "AppRotateOnline" for service "kube-proxy".
Set parameter "AppRotateSeconds" for service "kube-proxy".
Set parameter "AppRotateBytes" for service "kube-proxy".
Done installing kube-proxy service.
PS C:\Program Files\containerd\cni\bin> Get-Service kubelet

Status   Name               DisplayName
------   ----               -----------
Stopped  kubelet            kubelet service


PS C:\Program Files\containerd\cni\bin> Get-Service kube-proxy

Status   Name               DisplayName
------   ----               -----------
Stopped  kube-proxy         kube-proxy service

PS C:\Program Files\containerd\cni\bin> Start-Service kubelet
PS C:\Program Files\containerd\cni\bin> start-Service kube-proxy
PS C:\Program Files\containerd\cni\bin> Get-Service kubelet

Status   Name               DisplayName
------   ----               -----------
Running  kubelet            kubelet service


PS C:\Program Files\containerd\cni\bin> Get-Service kube-proxy

Status   Name               DisplayName
------   ----               -----------
Running  kube-proxy         kube-proxy service

kubelet과 kube-proxy가 실행되면 워커 노드가 Join 된 것으로 확인됩니다. 기존에 조인되었던 k8s-ww 서버가 다운된 상태라, 해당 노드에 스케줄되었던 pod가 신규로 조인된 서버로 스케줄링이 되었습니다.

root@k8s-m:~# kubectl get no
NAME     STATUS     ROLES                  AGE    VERSION
k8s-lw   Ready      <none>                 120d   v1.22.6
k8s-m    Ready      control-plane,master   120d   v1.22.6
k8s-ww   NotReady   <none>                 120d   v1.22.6
root@k8s-m:~# kubectl get no
NAME      STATUS     ROLES                  AGE    VERSION
k8s-lw    Ready      <none>                 120d   v1.22.6
k8s-m     Ready      control-plane,master   120d   v1.22.6
k8s-ww    NotReady   <none>                 120d   v1.22.6
k8s-ww2   Ready      <none>                 49s    v1.22.6
root@k8s-m:~# kubectl get po -owide
NAME                     READY   STATUS              RESTARTS         AGE     IP              NODE      NOMINATED NODE   READINESS GATES
iis-7dfbf869dd-4472b     0/1     ContainerCreating   0                5h46m   <none>          k8s-ww2   <none>           <none>
iis-7dfbf869dd-brqrc     1/1     Terminating         0                120d    192.168.208.5   k8s-ww    <none>           <none>
netshoot                 1/1     Running             11 (6h51m ago)   120d    192.168.114.5   k8s-lw    <none>           <none>
nginx-868547d6bf-kv858   1/1     Running             1 (6h51m ago)    120d    192.168.114.4   k8s-lw    <none>           <none>
root@k8s-m:~# kubectl get no
NAME      STATUS     ROLES                  AGE    VERSION
k8s-lw    Ready      <none>                 120d   v1.22.6
k8s-m     Ready      control-plane,master   120d   v1.22.6
k8s-ww    NotReady   <none>                 120d   v1.22.6
k8s-ww2   Ready      <none>                 85m    v1.22.6
root@k8s-m:~# kubectl get po -owide
NAME                     READY   STATUS        RESTARTS      AGE     IP              NODE      NOMINATED NODE   READINESS GATES
iis-7dfbf869dd-4472b     1/1     Running       0             7h11m   192.168.112.4   k8s-ww2   <none>           <none>
iis-7dfbf869dd-brqrc     1/1     Terminating   0             120d    192.168.208.5   k8s-ww    <none>           <none>
netshoot                 1/1     Running       11 (8h ago)   120d    192.168.114.5   k8s-lw    <none>           <none>
nginx-868547d6bf-kv858   1/1     Running       1 (8h ago)    120d    192.168.114.4   k8s-lw    <none>           <none>

윈도우 워커노드에서도 crictl 로 컨테이너를 확인할 수 있습니다.

C:\Users\Administrator>crictl ps
time="2022-07-12T22:45:46+09:00" level=debug msg="get runtime connection"
time="2022-07-12T22:45:46+09:00" level=debug msg="get image connection"
time="2022-07-12T22:45:46+09:00" level=debug msg="ListContainerResponse: [&Container{Id:077ab4df6b9972af7806c017a7e85e3b437475de5f3a8c9316c983240af73af7,PodSandboxId:f994462e02b028c6cf9e0f38eab984bf0158734db64aaf4379c326224fe53c87,Metadata:&ContainerMetadata{Name:iis,Attempt:0,},Image:&ImageSpec{Image:sha256:cf88a43a7460e1a84be0a24ee22042f73069346710907e702dadb1a7e8a39eaf,Annotations:map[string]string{},},ImageRef:sha256:cf88a43a7460e1a84be0a24ee22042f73069346710907e702dadb1a7e8a39eaf,State:CONTAINER_RUNNING,CreatedAt:1657614998696454400,Labels:map[string]string{io.kubernetes.container.name: iis,io.kubernetes.pod.name: iis-7dfbf869dd-4472b,io.kubernetes.pod.namespace: default,io.kubernetes.pod.uid: 8aecf61b-2e61-4f55-80a0-551b146b4cc6,},Annotations:map[string]string{io.kubernetes.container.hash: 42b06ae0,io.kubernetes.container.restartCount: 0,io.kubernetes.container.terminationMessagePath: /dev/termination-log,io.kubernetes.container.terminationMessagePolicy: File,io.kubernetes.pod.terminationGracePeriod: 30,},}]"
CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD
077ab4df6b997       cf88a43a7460e       5 hours ago         Running             iis                 0                   f994462e02b02       iis-7dfbf869dd-4472b

calico 에서 제공하는 스크립트를 통해 워커노드 조인이 진행되어 과정이 많이 생략되어 있습니다. 상세한 과정을 이해하기위해서는 스크립트를 전반적으로 살펴봐야 할 것 같습다.

현재 calico 의 QuickStart 가이드에는 'Install Calico for Windows using HostProcess containers' 라는 방식을 추가로 제공하고 있습니다. 2022/7/12일 현재 GA가 안된 상태이긴 한데, 이후 이과정도 한번 살펴보겠습니다.

로그 분석을 위해 로그를 확인했는데 로그의 한줄 한줄이 너무 길다.

곰곰히 살펴보면 개행 문자(\n)까지 엄청 들어가서 줄바꿈을 해서 보고 싶다.

 

notepadd++ 을 사용한다.

바꾸기(ctrl+h)를 한다. 

내용을 작성하고, 확장 (\n, \r, \t ..)를 체크하고 모두 바꾸기(A) 한다.

+ Recent posts