이후 Pod의 상태를 확인하면서 CrashLoopBackOff, Error인 것들을 재시작 수행.
이 과정에 갑자기 Node가 NotReady가 되면서 그 node의 pod 전체가 다 Terminating 됨.
- 이때부터 pod 상태가 엉망이 됨.
노드의 상태가 NotReady였다가 일부 pod가 종료 처리되고 잠시 Ready 되었었다.
처음에는 pod에 짧은 시간에 많은 노드가 기동해서 그런가 싶었다.
문제파악
우선은 k8s의 노드의 kubelet 상태를 확인하였다.
kubelet은 k8s의 모든 노드에서 실행되는 Agent로써 이 서비스가 내려가면 노드가 동작하지 않는다.
간단하게 현재상태의 일부는 systemctl로도 조회 가능하다.
$ systemctl status kubelet
...
kubelet[3694]: I0220 14:23:09.120100 3694 kubelet.go:1775] skipping pod synchronization - [PLEG is not healthy: pleg was last seen active 4h19m47.369998188s ago; threshold is 3m0s]
노드는 NotReady로 상태가 변경되면서 모든 pod가 다 Terminating 되었다.
PLEG (Pod Lifecycle Event Generator)
Kubelet과 통신하여 노드에 떠있는 컨테이너 상태를 Pod 상태와 주기적으로 동기화.
Kubelet은 Control Plane에서 만든 spec에 맞춰 상태를 유지시키기 위해 각 pod의 상태 정보를 가지고 있어야 한다. 이런 상태 정보를 유지하기 위해 kubelet이 pod을 polling 하는 작업을 한다. 주기적으로 polling하는 작업은 pod의 수 가증가함에따라무시할수없는오버헤드가 발생한다. Pod를 주기적으로 polling하는작업의 오버헤드를 줄이기 위해 PLEG (Pod Lifecycle EventGenerator)를사용한다 PLEG를 활용하는 것은 kubelet이 polling해서 정보를 가져오는 작업과 유사하지만 PLEG를 수행하는 싱글 쓰레드를통해컨테이너의상태를확인하여kubelet이polling하는 작업보다 오버헤드를 줄이는 효과를 볼 수 있다
PLEG는 컨테이너 이벤트를 검색하기 위해 relist를 한다. relist 하는 주기가 길다는 것은 kubelet이 컨테이너 변경 사항을 감지하고 파드 상태를 업데이트하는 데 시간이 더 오래 걸린다는 것을 의미한다. 반면에 주기가 짧으면 재등록(예: 컨테이너 런타임 작업)이 더 자주 발생하여 CPU 사용량이 증가하게 된다. 주기를 1초로 설정하더라도 컨테이너 런타임이 느리게 응답하거나 한 주기에 많은 컨테이너 변경 사항이 있는 경우 relisting 자체가 완료되는 데 1초 이상 걸릴 수 있다는 점에 유의한다.
수시로 변경될 수 있는 컨테이너를 생성 (Create ephemeral containers)
컨테이너는 멈추고 파기될 수 있고 언제든 바뀔수 있기에 최소한의 셋업과 설정으로 구성해야 하므로
도커 이미지를 정의하는 Dockerfile은 이를 염두해두고 생성해야한다.
build context에 대한 이해 (Understand build context)
`docker build` 명령을 실행하는 현재 디렉터리를 build context라고 하고 이 디렉터리안에 Dockerfile이 있습니다.
이 명령을 실행할 때 옵션으로 -f --file string(Default is 'PATH/Dockerfile')을 사용하면 다른 디렉터리에 있는 Dockerfile을 지정할 수 있다.
Dockerfile이 저장된 디렉터리의 위치는 상관이 없고,
그 디렉터리 안에 있는 모든 파일과 디렉터리들이 build context로서 도커 데몬에 보내지게 된다.
# Create a directory for the build context and cd into it.
# Write “hello” into a text file named hello and
# create a Dockerfile that runs cat on it.
# Build the image from within the build context (.):
mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .
# Move Dockerfile and hello into separate directories
# and build a second version of the image
# (without relying on cache from the last build).
# Use -f to point to the Dockerfile and specify the directory of the build context:
mkdir -p dockerfiles context
mv Dockerfile dockerfiles && mv hello context
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
이미지를 만드는데 필요없는 파일을 포함시키면 build context가 커지게되고
도커 데몬에 전달할 데이터가 커지게 된다.
이로 인해 이미지의 size가 커지게 되면서 build에 소요되는 시간, push와 pull에 소요되는 시간, run time등
모든작업의 실행에 소요되는 시간에 영향을 준다.
그렇기 때문에 Dockerfile을 build할 때 결과 메시지에서 이 크기를 가급적 줄이도록한다.
Sending build context to Docker daemon 187.8MB
.dockerignore로 제외한다 (Exclude with .dockerignore)
.dockerignoer는 .gitignore와 유사하게 빌드에서 제외할 파일 패턴을 제외할 수 있도록 지원한다.
이로 인해 빌드이미지와 실행이미지를 분리할 수 있어서 간편하게 이미지의 크기를 줄일 수 있다.
734.28MB -> 6.91MB로 감소
##############################################
FROM golang:1.19.3 AS builder
# Set Environment Variables
ENV HOME /app
ENV CGO_ENABLED 0
ENV GOOS linux
# go package install
WORKDIR /app
RUN go mod init mux
RUN go mod tidy
RUN go mod download
RUN go get github.com/gorilla/mux
COPY . .
RUN go build -a -o main .
##############################################
FROM scratch
WORKDIR /root/
# 이전stage에서 build했던 바이너리 파일을 복사
COPY --from=builder /app/main .
EXPOSE 8080
CMD [ "./main" ]
불필요한 패키지 설치하지 않는다. (Don’t install unnecessary packages)
복잡도, 의존성, 파일크기, 빌드 시간등을 줄이기 위해서 불필요한 패키지들은 뺀다.
반드시 필요하지 않는 패키지를 의미하는데 예를 들면 Database 이미지에 텍스트 에디터 패키지는 필요가 없다.
어플리케이션 디커플링 (Decouple applications)
각각의 컨테이너는 단일한 처리를 목적으로 한다.
어플리케이션을 디커플링해서 쪼개면 수평 확장과, 컨테이너 재사용이 쉬워진다.
예를 들어, 웹 어플리케이션을 스택을 3개의 컨테이너로 구성한다고 했을때
웹, 데이터 베이스, 인메모리 캐시로 분리하여 관리할 수 있다.
각 컨테이너를 디커플링하여 하나의 프로세스로 제한하는것은 경험상 좋은 규칙이지만 모두 그렇지는 않습니다.
Celery의 multiple worker process들과 Apache는 request마다 프로세스를 생서할 수도 있습니다.
각 명령을 검토할 때 도커는 새로운 (중복) 이미지를 만들지 않고 캐시에서 재사용할 수 있는 이미지를 찾는다.
캐시를 사용하지 않으려면 docker build 할때 --no-cache=true 옵션을 사용할 수 있다.
도커가 캐시를 활용할 수 있도록 하기 위해서는, 일치하는 이미지를 찾을 수 있는 경우와 없는 경우에 대해 이해하는 것이 중요하다.
도커가 따르는 기본적인 규칙은 아래와 같다.
이미 캐시에 있는 부모 이미지를 시작으로, 다음 명령어를 해당 기본 이미지에서 파생된 모든 하위 이미지와 비교하여 동일한 명령어를 사용하여 빌드되었는지 확인한다. 그렇지 않으면 캐시가 무효화 된다.
대부분의 경우Dockerfile의 명령어를 하위 이미지 들과 비교하는 것으로 충분하다. 하지만, 어떤 명령어는 더 많은 검토가 필요하다.
ADDCOPY의 경우 이미지 파일 내용을 검사하고 각 파일에 대한 체크섬을 추가로 계산한다. 여기에서 파일의 마지막 수정 시간이나 엑세스 시간은 고려하지 않는다. 캐시 조회 중에 체크섬을 기존 이미지의 체크섬과 비교한다. 파일에서 내용이나 메타데이터의 변경이 있으면 캐시가 무효화 된다.
ADDCOPY명령어 외에도 캐시 일치 여부를 확인하기 위해 컨테이너의 파일을 확인하지 않는다. 일례로RUN apt-get -y update를 처리할때 컨테이너에서 업데이트된 파일을 검사하여 캐시와 치하는 경우가 존재하는지 확인하지 않는다. 이 경우는, 명령 문자열 자체만 일치하는지만 검토한다.
일단 캐시가 무효화되면 이후의 모든 Dockerfile 명령은 새로운 이미지를 생성하며, 캐시는 사용되지 않는다.
Pipe를 이용한 Standard Input사용(Pipe Dockerfile through `stdin`)
docker build를 수행할 때 pipe를 이용하여 Dockerfile을 disk에 만들지 않을 수도 있다.
일회성 빌드나 테스트 같이 가볍게 사용할때 유용하다.
# For example, the following commands are equivalent:
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF
# You can substitute the examples with your preferred approach,
# or the approach that best fits your use-case.
build context 전송없이 stdin으로 이미지 생성 (Build an image using a Dockerfile from stdin, without sending build context)
docker build 할때 `-` 하이픈 옵션을 사용하면 Dockerfile을 생성하지 않고 build context 전송하지도 않아서
build 속도를 향상 시킬 수 있다.
.dockerignore 를 이용해서 docker build 할때 포함하지 않도록 제외할 수 있다.
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
주의할점은 COPY 혹은 ADD를 사용하는 경우라면 실패
# create a directory to work in
mkdir example
cd example
# create an example file
touch somefile.txt
docker build -t myimage:latest -<<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF
# observe that the build fails
...
Step 2/3 : COPY somefile.txt ./
COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eno1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state DOWN mode DEFAULT group default qlen 1000
link/ether f4:a3:91:c6:73:c0 brd ff:ff:ff:ff:ff:ff
3: eno2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state DOWN mode DEFAULT group default qlen 1000
link/ether f4:a3:91:c6:73:c0 brd ff:ff:ff:ff:ff:ff
5: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
link/ether f4:a3:91:c6:73:c0 brd ff:ff:ff:ff:ff:ff
작업의 시작은 운용한지 3년이 다되가는 서버들을 점검하는 차원에서 Reboot 겸 update를 하려했다.
Dell 서버는 iDRAC(Integrated Dell Remote Access Controller)이란 웹 콘솔을 제공해주어서 직접 Data Center에 가지 않아도 이런작업은 웹 브라우저에서 실행할 수 있다. 웹 브라우저로 접속은 할 수 있지만 물리서버의 Management 전용 IP로 붙어야 한다. 예를 들면 https://172.21.239.47 과 같은 형태의 주소로 접속하고 ID/PW를 입력하여 로그인 하면 서버의 상태 확인 및 remote로 직접 제어할 수 있다.
서버의 전원을 켤 때는 높은 전압과 과부하가 생기기때문에 3년이 다되어가는 서버를 껐다가 다시 부팅을 시도하는 경우
다양한 원인으로 서버가 켜지지 않을 수 있다. 그래서 Warm Boot 방식으로 진행했다.
사용한지 3년이 다 되어가는(실제로는 2년 10개월) 서버들중 1대에서 Reboot을 완료하고
서버 상태를 확인하는데 네트워크 인터페이스가 Down 상태로 내려가 있어서 통신이 되지 않았다.
서버가 이상한건지 이 앞단에 연결된 스위치가 문제인지 알 수 없었다.
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eno1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state DOWN mode DEFAULT group default qlen 1000
link/ether f4:a3:91:c6:73:c0 brd ff:ff:ff:ff:ff:ff
3: eno2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state DOWN mode DEFAULT group default qlen 1000
link/ether f4:a3:91:c6:73:c0 brd ff:ff:ff:ff:ff:ff
5: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
link/ether f4:a3:91:c6:73:c0 brd ff:ff:ff:ff:ff:ff
네트워크 엔지니어의 확인 결과 LACP(Link Aggregation Control Protocol) PDU(Protocol Data Units)를 받지 못하고 있는 상황이었다.
확인 시에는 스위치장비에 ssh로 접근해서 show interface 를 이용하여 확인한 결과중에 suspended due to no lacp pdus 메시지를 보고 판단하였다.
물론 HTTP Header에 Cache-Control: max-age=xx,Cache-Control: s-maxage=xx 같은 값이 없으면 default TTL인 것은 알겠으나, minimum TTL, maximum TTL 관련해서는 이 메뉴얼을 읽어보았지만 이해를 못했었다.
default TTL (Cache-Control, Expires header가 없는 경우)