도커 컨테이너를 실행할 때 리소스에 제한을 걸 수 있다.

 

예를 들면, redis 컨테이너의 메모리 사용을 제한해보도록 하자.

sudo docker run --name my-redis -d -p 6379:6379 --memory="1g" --memory-swap="1g" --restart=always redis

 

--memory : The maximum amount of memory the container can use. If you set this option, the minimum allowed value is 4m (4 megabyte).

-- memory-swap : The amount of memory this container is allowed to swap to disk

 

--memory-swap details

--memory-swap is a modifier flag that only has meaning if --memory is also set. Using swap allows the container to write excess memory requirements to disk when the container has exhausted all the RAM that is available to it. There is a performance penalty for applications that swap memory to disk often.

Its setting can have complicated effects:

  • If --memory-swap is set to a positive integer, then both --memory and --memory-swap must be set. --memory-swap represents the total amount of memory and swap that can be used, and --memory controls the amount used by non-swap memory. So if --memory="300m" and --memory-swap="1g", the container can use 300m of memory and 700m (1g - 300m) swap.
  • If --memory-swap is set to 0, the setting is ignored, and the value is treated as unset.
  • If --memory-swap is set to the same value as --memory, and --memory is set to a positive integer, the container does not have access to swap. See Prevent a container from using swap.
  • If --memory-swap is unset, and --memory is set, the container can use as much swap as the --memory setting, if the host container has swap memory configured. For instance, if --memory="300m" and --memory-swap is not set, the container can use 600m in total of memory and swap.
  • If --memory-swap is explicitly set to -1, the container is allowed to use unlimited swap, up to the amount available on the host system.
  • Inside the container, tools like free report the host’s available swap, not what’s available inside the container. Don’t rely on the output of free or similar tools to determine whether swap is present.

 

어떻게 컨테이너 마다 독립된 환경으로 리소스를 제한할 수 있는 것일까?

 

먼저 도커 구조를 살펴보고 이해하는데 도움을 얻자.

도커 구조

https://betterprogramming.pub/docker-for-front-end-developers-c758a44e622f

- 도커는 containerd를 활용하여 컨테이너를 생성, 관리하는 서비스로 docker.sock을 통해 통신한다.

- 도커 서비스는 docker cli를 docker.sock을 통해 받아서 containerd로 요청을 보내주는 역할을 한다.

 

아래 명령어를 통해 --containerd를 명시하고 docker.sock을 통해서 API를 리스닝 하는 것을 살펴볼 수 있다.

$ sudo service docker status
● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-07-26 07:43:57 UTC; 1 months 10 days ago
     Docs: https://docs.docker.com
 Main PID: 1314 (dockerd)
    Tasks: 89
   CGroup: /system.slice/docker.service
           ├─ 1314 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
$ journalctl -xu docker.service
-- Logs begin at Mon 2021-07-26 05:40:51 UTC, end at Sun 2021-09-05 06:52:46 UTC. --
Jul 26 06:32:36 xxx systemd[1]: Starting Docker Application Container Engine...
-- Subject: Unit docker.service has begun start-up
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
-- 
-- Unit docker.service has begun starting up.
...
Jul 26 06:32:37 xx dockerd[3815]: time="2021-07-26T06:32:37.322270147Z" level=info msg="API listen on /var/run/docker.sock"
Jul 26 06:32:37 xx systemd[1]: Started Docker Application Container Engine.

 

결론적으로 containerd는 docker로 부터 요청을 받아서 runc를 통해 컨테이너(프로세스)를 생성하는 구조가 된다.

$ sudo service containerd status
● containerd.service - containerd container runtime
   Loaded: loaded (/lib/systemd/system/containerd.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-07-26 07:43:55 UTC; 1 months 10 days ago
     Docs: https://containerd.io
 Main PID: 1153 (containerd)
    Tasks: 115
   CGroup: /system.slice/containerd.service

하지만 containerd가 여러가지 이유로 재시작될 수 있는데 직접 runc와 소통을 하게되면 runc 프로세스까지 모두 재시작 될 수 있기 때문에 shim을 중간 매개체로 넣어서 관리하게된다. 즉, shim은 컨테이너 runtime과 containerd 사이에서 소통을 담당하고, runtime이 독립적으로 돌 수 있도록 한다.

 

실습을 위해 메모리 제한을 주고 redis container 실행해보자

sudo docker run -it --name test-redis -d -p 6399:6399 --memory="1g" --memory-swap="1g" --restart=always redis /bin/bash

이미지는 redis를 사용하였고, init 커맨드로 /bin/bash를 사용했다. 즉, 해당 컨테이너 내부로 들어가면 /bin/bash가 init 프로세스가 되는 것이다. 

 

생성 프로세스(/bin/bash)의 부모 프로세스가 containerd-shim(25228)임을 확인할 수 있다.

$ ps -ef | grep container
root     25220  1314  0 06:40 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 6399 -container-ip 172.17.0.5 -container-port 6399
root     25228  1153  0 06:40 ?        00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424 -address /run/containerd/containerd.sock -c


$ ps -ef | grep /bin/bash
root     25254 25228  0 06:40 pts/0    00:00:00 /bin/bash
...

 


도커는 독립된 환경과 리소스 제한을 위해 namespace와 cgroup을 활용한다.

cgroup은 가용할 수 있는 자원의 제한을 설정하는 것이고, namespace는 볼 수 있는 자원의 범위를 의미한다.

 

Namespace

리눅스에서는 프로세스별로 독립적인 공간을 제공하고 서로가 충돌하지 않도록 하는 기능을 제공한다. 

mnt 홈스트 파일시스템에 구애받지 않고 독립적으로 파일시스템을 마운트하거나 언마운트 가능
pid 독립적인 프로세스 공간을 할당
net namespace간에 네트워크 충돌방지
ipc 프로세스간의 독립적인 통신통로 할당
uts 독립적인 hostname 할당
user 독립적인 사용자 할당

 

컨테이너는 네임스페이스가 분리되어 있는 독립적인 프로세스이고, 실제 아래 명령어로 확인가능하다.

$ sudo ls -al /proc/25254/ns
total 0
dr-x--x--x 2 root root 0 Sep  5 06:40 .
dr-xr-xr-x 9 root root 0 Sep  5 06:40 ..
lrwxrwxrwx 1 root root 0 Sep  5 07:17 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Sep  5 07:17 ipc -> 'ipc:[4026532883]'
lrwxrwxrwx 1 root root 0 Sep  5 07:17 mnt -> 'mnt:[4026532881]'
lrwxrwxrwx 1 root root 0 Sep  5 06:40 net -> 'net:[4026532886]'
lrwxrwxrwx 1 root root 0 Sep  5 07:17 pid -> 'pid:[4026532884]'
lrwxrwxrwx 1 root root 0 Sep  5 07:17 pid_for_children -> 'pid:[4026532884]'
lrwxrwxrwx 1 root root 0 Sep  5 07:17 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Sep  5 07:17 uts -> 'uts:[4026532882]'

 

Cgroup

cgroup은 자원에 대한 제어를 가능하게 해주는 리눅스 커널의 기능이다. 다음의 리소스를 제어할 수 있다.

- memory

- cpu

- I/O

- network

- device 노드

 

컨테이너를 띄우면 각각 고유한 컨테이너 ID를 부여받는다. runc는 이 ID를 기반으로 cgroup내의 디렉토리를 구성한다.

(/sys/fs/cgroup/memory/docker/container ID)

 

$ sudo cat /proc/25254/cgroup 
12:rdma:/
11:memory:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
10:hugetlb:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
9:perf_event:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
8:cpu,cpuacct:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
7:devices:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
6:pids:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
5:freezer:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
4:cpuset:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
3:net_cls,net_prio:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
2:blkio:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
1:name=systemd:/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
0::/system.slice/containerd.service

 

이제 redis에 메모리 제한을 주어 cgroup내에 디렉토리가 어떻게 구성되는지 살펴보자.

- memory: 1G

- memory-swap: 0

$ sudo docker run -it --name test-redis -d -p 6399:6399 --memory="1g" --memory-swappiness 0 --restart=always redis /bin/bash
bf5560cc4fc5975594016f028ca07ec435202cb276ca2f7f2f95f4f13346d5c1
$ cat /sys/fs/cgroup/memory/docker/bf5560cc4fc5975594016f028ca07ec435202cb276ca2f7f2f95f4f13346d5c1/memory.limit_in_bytes 
1073741824

$ cat /sys/fs/cgroup/memory/docker/bf5560cc4fc5975594016f028ca07ec435202cb276ca2f7f2f95f4f13346d5c1/memory.swappiness 
0

이번에는 

- memory : 100M

- memory-swap: 10

$ sudo docker run -it --name test-redis -d -p 6399:6399 --memory="200M" --memory-swappiness 10 --restart=always redis /bin/bash
2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424
$ cat /sys/fs/cgroup/memory/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424/memory.limit_in_bytes 
209715200
$ cat /sys/fs/cgroup/memory/docker/2035a84b0f9540774fa05c0e8c920c398fe9ddeefd1e54389dceb36fd9e90424/memory.swappiness 
10

 

docker -> (docker.sock) -> containerd -> (shim) -> runc을 통해 cgroup을 만들고 값을 넣어주어서 메모리 제한이 적용되는 것을 확인했다.

 

 

ps aux | grep docker 명령으로 확인해보면

- docker

- docker-containerd

- docker-containred-shim

- docker-runc

 

4개의 프로세스가 돌아가는 것을 확인할 수 있다.


참고

# 커널 swap 지원 불가

WARNING: Your kernel does not support swap limit capabilities, memory limited without swap.

# /etc/default/grub
...
...GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"
# update-grub 
# reboot

 

 

 


출처

- https://tech.ssut.me/what-even-is-a-container/

- https://betterprogramming.pub/docker-for-front-end-developers-c758a44e622f

- https://tech.kakao.com/2020/06/29/cgroup-driver/

- https://brunch.co.kr/@jehovah/35

- https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details

'클라우드 컴퓨팅 > 쿠버네티스' 카테고리의 다른 글

CKA(Certified Kubernetes Administrator) 시험 비중  (0) 2021.09.11
Kubernetes TroubleShooting  (0) 2021.09.11
docker 기본  (0) 2021.09.05
Kubernetes Pod Scheduling  (0) 2021.08.13
Kubernetes Workloads  (0) 2021.08.08

도커(docker)는 컨테이너를 실행하기 위한 실행 환경(컨테이너 런타임) 및 툴킷이다. 쿠버네티스는 도커 이외의 컨테이너 런타임도 지원하도록 개발되어있다. 

 

쿠버네티스 사용에 앞서 도커 이미지를 빌드하고, 이미지 보관/배포 서버인 도커 레지스트리로 푸시하는 방법을 알아 두어야 한다. 또한, 반드시 알아두면 좋은 지식을 간단히 알아볼 것이다.

 

도커 컨테이너

도커 컨테이너는 도커 이미지를 기반으로 실행되는 프로세스다. 도커 이미지만 있다면 환경의 영향을 받지 않고 다양한 환경에서 컨테이너를 기동시킬 수 있기 때문에 이식성이 높다. 

도커 컨테이너는 가상 머신에 비해 '가볍다', '시작과 중지가 빠르다' 등과 같은 장점이 있다.

그 이유는 가상 머신은 하이퍼바이저를 이용하여 게스트 OS를 동작시키지만, 도커 컨테이너는 호스트 머신의 커널을 이용하면서 네임스페이스 분리와 cgroups를 이용한 제어를 통해 독립적인 OS와 같은 환경을 만들 수 있기 때문이다. 

 

https://www.docker.com/blog/containers-replacing-virtual-machines/

 

도커 컨테이너 설계

도커 컨테이너를 생성할 때 주로 주의해야할 점은 아래와 같다.

- 1 컨테이너당 1 프로세스

여러 프로세스를 기동하도록 만들면 주변 에코시스템과 맞지 않거나 관리가 힘들어진다.

- 변경 불가능한 인프라 이미지로 생성

컨테이너 이미지안에 애플리케이션 실행 바이너리나 관련 리소스를 가능한 한 포함시켜 컨테이너 이미지를 변경불가능한 상태로 만든다.

- 경량의 도커 이미지로 생성

경량 배포판으로는 알파인 리눅스(Alpine Linux)나 Distroless가 있다.

- 실행 계정은 root 이외의 사용자로 한다.

 

도커 파일 작성법

# Alpine 3.11 버전 golang 1.14.1 이미지를 기반 이미지로 사용
From golang:1.14.1-alpine3.11

# 빌드할 머신에 있는 main.go 파일을 컨테이너에 복사
COPY ./main.go ./

# 빌드 시 컨테이너 내부에서 명령어 실행
RUN go build -o ./go-app ./main.go

# 실행 계정을 nobody로 지정
USER nobody

# 컨테이너가 기동할 때 실행할 명령어 정의
ENTRYPOINT ["./go-app"]

도커 파일에서 사용할 수 있는 명령

명령 요약
FROM 기반 이미지를 지정
MAINTAINER 컨테이너 이미지 관리자 정보를 기입(현재는 비추천, 아래 LABEL 명령을 사용함)
LABEL 컨테이너 이미지의 메타데이터를 키:밸류 형식으로 지정(예: LABEL maintainer="XX<xxx@xxx.com>")
USER 명령어 실행 계정 지정
WORKDIR 명령어를 실행할 작업 디렉터리 지정(디렉터리가 없을 경우 생성)
EXPOSE 컨테이너 실행 시 Listen할 포트 지정
COPY 로컬에 있는 파일을 컨테이너로 복사
ADD 로컬에 있는 tar.gz 파일의 압축을 풀고 파일을 컨테이너로 복사
RUN 컨테이너 안에서 명령을 실행
ENTRYPOINT 컨테이너 기동 시에 실행할 명령어
CMD 컨테이너 기동 시에 실행할 명령어 인수

 

ENTRYPOINT와 CMD의 관계

ENTRYPOINT CMD 실행되는 명령어
"/bin/ls" "-a" /bin/ls -a
지정안함 /bin/ls /bin/ls
"/bin/sh", "-c" "ls -a" /bin/sh -c "ls -a"

ex) docker run ubuntu-sleeper 10 -> 10초간 sleep

FROM ubuntu
ENTRYPOINT ["sleep"]


ex) docker run --entrypoint sleep ubuntu-sleeper 10

--entrypoint : it will override entrypoint

FROM ubuntu
ENTRYPOINT ["sleep"]
CMD["5"] #파라미터가 없을 때 default 값을 지정하고자 할 경우, 파라미터 전달시 overrided 됨.

도커 실행시  Environment 전달

docker run -e APP_COLOR=pink simple-webapp-color

 

도커 이미지 빌드 및 확인

docker image build -t sample-image:0.1 (-f DockerFile) .

docker images

 

멀티 스테이지 빌드

기동용 컨테이너에 불필요한 소프트웨어를 설치하지 않는 것은 보안 측면에서도 좋을 뿐만아니라 이미지 사이즈를 줄일 수 있다. 이를 위해 멀티 스테이즈 빌드를 활용하자.

# Stage 1 컨테이너(애플리케이션 컴파일)
FROM golang:1.14.1-alpine3.11 as builder
COPY ./main.go ./
RUN go build -o /go-app ./main.go

# Stage 2 컨테이너(컴파일한 바이너리를 포함한 실행용 컨테이너 생성)
FROM alpine:3.11
# Stage 1에서 컴파일한 결과물 복사
COPY --from=builder /go-app .
ENTRYPOINT ["./go-app"]
# Stage 1 컨테이너(애플리케이션 컴파일)
FROM golang:1.14.1-alpine3.11 as builder
COPY ./main.go ./
RUN CGO_ENABLED=0 go build -o /go-app ./main.go

# Stage 2 컨테이너(컴파일한 바이너리를 포함한 실행용 컨테이너 생성)
FROM scratch
# Stage 1에서 컴파일한 결과물 복사
COPY --from=builder /go-app .
ENTRYPOINT ["./go-app"]

예제는 2단계 구성이지만 3단계 이상의 구성도 가능하다!

 

alpine 이미지 대신 scratch 이미지를 기반으로 사용하면 이미지 사이즈를 더 줄일 수 있는 것을 확인할 수 있다.

(base) jds@jdsui-MacBookPro chapter01 % docker images

REPOSITORY                           TAG                                                     IMAGE ID       CREATED          SIZE

smaple-image                         0.3                                                     a96b26cc1aa5   16 seconds ago   7.41MB

smaple-image                         0.2                                                     72e55cd3def7   46 seconds ago   13.1MB

 

도커 허브에 이미지 푸시

docker login

option 1) docker image build -t DOCKERHUB_USER/sample-image:0.x .

option 2)docker image tag sample-image:0.1 DOCKERHUB_USER/sample-image:0.x

docker image push DOCKERHUB_USER/sample-image:0.x

docker logout

 

컨테이너 기동

docker container run -d -p 1234:8080 sample-image:0.1

(base) jds@jdsui-MacBookPro chapter01 % docker container run -d -p 1234:8080 smaple-image:0.2
7edb8b08e3c4f6385ec814b9157063bbd0fea3da8baed6b61e8c7ee85afee769

(base) jds@jdsui-MacBookPro chapter01 % curl http://localhost:1234
Hello, Kubernetes% 

 


Docker Storage

Docker Storage drivers

도커 엔진은 OS에 따라서 적절한 storage driver를 선택한다.

- AUFS, ZFS, BTRFS, Device Mapper, Overlay, Overlay2

 

 

Docker volume drivers

- local, Azure File Storage, Convoy, AWS EBS, ...

$ docker run -it \
      --name mysql
      --volume-driver rexray/ebs
      --mount src=ebs-vol,target=/var/lib/mysql
      mysql

 

 

도커 파일 시스템

/var/lib/docker# ls
builder   containers  network   plugins   swarm  trust
buildkit  image       overlay2  runtimes  tmp    volumes

 

Docker Layer

- Image Layers

$ docker build -t test/my-app:0.0.1

- Container Layers

$ docker run test/my-app

출처 : https://docs.docker.com/storage/storagedriver/

Docker Volumes & Usage

- volume mount : 볼륨을 생성하지 않은 상태에서 도커 run 명령을 사용해서 볼륨지정을 했다면 자동생성된다.

$ docker volume create data_volume
$ docker run -v data_volume:/var/lib/mysql mysql

- bind mount : any location on the docker host

$ docker run -v ./current-dir:/var/lib/mysql mysql

출처 : https://docs.docker.com/storage/tmpfs/

참고) Container Interface

- CRI(Container Runtime Interface)

- CNI(Container Network Interface)

- CSI(Container Storage Interface)

 

 

다음 포스팅에서 도커 namespace와 cgroup에 대해서 살펴보도록 하자.
https://espossible.tistory.com/19

 


출처

- https://www.docker.com/blog/containers-replacing-virtual-machines/

- [책]쿠버네티스완벽가이드 마사야 아오야마 지음, 박상욱 옮김(길벗)

'클라우드 컴퓨팅 > 쿠버네티스' 카테고리의 다른 글

Kubernetes TroubleShooting  (0) 2021.09.11
docker 리소스 제한  (0) 2021.09.05
Kubernetes Pod Scheduling  (0) 2021.08.13
Kubernetes Workloads  (0) 2021.08.08
Kubernetes JSONPATH 사용법  (0) 2021.08.08

프로젝트를 하며 지정된 시간 혹은 지정된 시간마다 반복적으로 특정 작업을 처리해야할 때가 있었다.

이를 위해 @Scheduled로 Spring Scheduled Task를 사용하면서 겪었던 문제와 알게된 사실들에 대해서 정리하고자한다.

 

API 서버에 여러 스케줄러가 운영되고 있고, 이 스케줄러들은 각기 다른 일들을 처리하며 1주일 이상 정상적으로 동작을 하였다. 하지만 갑자기 모든 스케줄러가 동작을 멈추었다...

원인은 스케줄러는 많은데 하나의 thread만 공유하여 사용하기 때문일 것이라고 생각했다. 그래서 우선 thread pool size를 증가시키고, 스케줄러 excution time을 계산하는 등 디버깅해보기로 결정했다. 아래는 이와 관련된 내용을 정리하며 알게된 내용들이다.

 

첫째, 기본적으로 scheduled task thread pool size는 1이다. 이 말은 여러 scheduler가 하나의 thread를 공유하며 실행된다는 것이다.

2046-05-22 14:04:32.801 [thread name] org.xxx.xxx.xxx: scheduled task is running

로그를 찍어봐도 알 수 있듯이 thread name이 동일하고, 각 스케줄러에 동일하게 출력될 것이다.

 

둘째, scheduled task thread pool size를 조정하기 위해 spring.task.scheduling.pool.size(spring boot 2.5.0)를 사용했지만 적용이 안된다. 대신에 bean을 등록하여 overriding 하자.

@Bean
public ThreadPoolTaskScheduler taskScheduler(){
	ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
	taskScheduler.setPoolSize(10);
	taskScheduler.setThreadNamePrefix("task-scheduler-");
	taskScheduler.initialize();
	return taskScheduler;
}

 

하지만 풀 사이즈를 조정을 하여도 같은 현상이 발생하였다. 스케줄러 내부 로직의 문제는 아닌 것으로 판단을 하였고, 스케줄러에서 동작하는 API의 실행시간을 디버깅해보기로 마음먹었다.

 

첫째, 각 스케줄러가 실행되는 시간을 확인하기 위해 AOP method를 활용하자.

thread와 스케줄러를 함께 모니터링하면 특정 스케줄러가 스레드를 독점적으로 사용하고 있는지 확인할 수 있다.

@Aspect
@Component
public class LoggingAspect {
	@Around("@annotation(org.springframework.scheduling.annotation.Scheduled)")
	public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    	long start = System.currentTimeMillis();
    	try {
      		// monitor threads which runs specific tasks with Thread.currentThread()
        	proceed = joinPoint.proceed();
    	} finally {
        	long executionTime = System.currentTimeMillis() - start;
        	log.info(executionTime)
     	}
    	return proceed;
	}
}

둘째, 스케줄러 내부에서 사용하는 내부 API execution time을 보고, 디버깅에 활용한다.

	@Bean
	public FilterRegistrationBean<OncePerRequestFilter> executionTimeLoggingFilter() {
		return new FilterRegistrationBean<OncePerRequestFilter>() {{
			//setUrlPatterns();
			setOrder(OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER);
			setFilter(new OncePerRequestFilter() {
				@Override
				protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
					StopWatch watch = new StopWatch();
					watch.start();
					try {
						chain.doFilter(req, res);
					}finally {
						watch.stop();
						long executionTime = watch.getTotalTimeMillis();
						// debugging with executionTime
					}
					
				}
			});
		}};
	}

 

 

디버깅 로그를 확인해보니 외부 API가 불규칙적으로 정상 동작하지 않았고, 응답 대기시간이 굉장히 긴 것을 확인하였다.

이러한 현상이 반복이 되자 스케줄러 풀이 정상 동작하지 않게됬을 것이고, 전체 스케줄러가 동자을 멈춘 것이었다.

 

따라서 해당 외부 API에 타임아웃을 주었고, 외부  업체에 장애에 대한 조치를 요청하였다.

 

resttemplate의 default timeout은 infinite 였다..timeout을 주도록하자.

HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
httpRequestFactory.setConnectionRequestTimeout(...);
httpRequestFactory.setConnectTimeout(...);
httpRequestFactory.setReadTimeout(...);

return new RestTemplate(httpRequestFactory);

 

그 후 2주동안 모니터링하였고, 시스템이 정상적으로 돌아가는 것을 확인하였다.

 

라이브러리에서 제공하는 기본 설정들을 주의깊게 살펴보는 습관을 가져야겠다는 생각이 들었다.

 

 


출처

- https://jsonobject.tistory.com/247

- https://howtodoinjava.com/spring-boot2/resttemplate/resttemplate-timeout-example/

- https://stackoverflow.com/questions/29796651/what-is-the-default-scheduler-pool-size-in-spring-boot

- https://mossgreen.github.io/Spring-Scheduled-Tasks-Stop-Working/

- https://stackoverflow.com/questions/29796651/what-is-the-default-scheduler-pool-size-in-spring-boot

- https://jasper-rabbit.github.io/posts/spring-security-architecture-review/

'프로그래밍 > Spring & Maven' 카테고리의 다른 글

Spring Transactional  (0) 2022.02.28
hibernateLazyInitializer  (0) 2022.01.14
프로그래밍/Spring & Maven/RedisCommandTimeout  (0) 2021.09.10

쿠버네티스에서 Pod 생성을 요청 했을 때, 적당한 node에 배치하는 것을 스케쥴링이라고 한다. 이때 쿠버네티스 스케줄러는 아래 내용을 살펴보고 Pod가 배포될 수 있는 Node를 선정하는 작업을 하게된다.

 

- 충분한 리소스가 있는가?

- 디스크 볼륨을 사용할 경우 node에서 접근이 가능한가?

- HA 구성을 위해 다른 node, 다른 rack, 다른 zone에 배포 가능한가?

- 기타..

 

Resource

충분한 리소스가 있는지 없는지 판단하기 위해서는 기준이 있어야한다. 컨테이너에 대해서 리소스 정의를 할 수 있고, 이 정의된 리소스를 기준으로 Node에 충분한 리소스가 있는지 판단할 수 있는 것이다.

# Requests, limits

If the node where a Pod is running has enough of a resource available, it's possible (and allowed) for a container to use more resource than its request for that resource specifies. However, a container is not allowed to use more than its resource limit.

For example, if you set a memory request of 256 MiB for a container, and that container is in a Pod scheduled to a Node with 8GiB of memory and no other Pods, then the container can try to use more RAM.

If you set a memory limit of 4GiB for that Container, the kubelet (and container runtime) enforce the limit. The runtime prevents the container from using more than the configured resource limit. For example: when a process in the container tries to consume more than the allowed amount of memory, the system kernel terminates the process that attempted the allocation, with an out of memory (OOM) error.

Limits can be implemented either reactively (the system intervenes once it sees a violation) or by enforcement (the system prevents the container from ever exceeding the limit). Different runtimes can have different ways to implement the same restrictions.

Note: If a Container specifies its own memory limit, but does not specify a memory request, Kubernetes automatically assigns a memory request that matches the limit. Similarly, if a Container specifies its own CPU limit, but does not specify a CPU request, Kubernetes automatically assigns a CPU request that matches the limit.
# Pod resource
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
    type: front-end
spec:
  containers:
    - name: nginx-container
      image: nginx
      ports:
        - containerPort: 8080
      resources:
        requests:
          memory: "1Gi"
          cpu: 1
        limits:
          memory: "2Gi"
          cpu: 2

# Resource Quota

A resource quota, defined by a ResourceQuota object, provides constraints that limit aggregate resource consumption per namespace. It can limit the quantity of objects that can be created in a namespace by type, as well as the total amount of compute resources that may be consumed by resources in that namespace.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    requests.memory: 5Gi
    limits.cpu: "10"
    limits.memory: 
  scopeSelector:
      matchExpressions:
      - operator : In
        scopeName: PriorityClass
        values: ["high"]
apiVersion: v1
kind: Pod
metadata:
  name: high-priority
spec:
  containers:
  - name: high-priority
    image: ubuntu
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo hello; sleep 10;done"]
    resources:
      requests:
        memory: "10Gi"
        cpu: "500m"
      limits:
        memory: "10Gi"
        cpu: "500m"
  priorityClassName: high
Resource Name Description
pods The total number of Pods in a non-terminal state that can exist in the namespace. A pod is in a terminal state if .status.phase in (Failed, Succeeded) is true.
limits.cpu Across all pods in a non-terminal state, the sum of CPU limits cannot exceed this value.
limits.memory Across all pods in a non-terminal state, the sum of memory limits cannot exceed this value.
requests.cpu Across all pods in a non-terminal state, the sum of CPU requests cannot exceed this value.
requests.memory Across all pods in a non-terminal state, the sum of memory requests cannot exceed this value.
cpu Same as requests.cpu
memory Same as requests.memory

- kubectl describe quota

Name:       pods-high
Namespace:  default
Resource    Used  Hard
--------    ----  ----
cpu         0     4
memory      0     5Gi
pods        0     10

 

# Limit Range

By default, containers run with unbounded compute resources on a Kubernetes cluster. With resource quotas, cluster administrators can restrict resource consumption and creation on a namespace basis. Within a namespace, a Pod or Container can consume as much CPU and memory as defined by the namespace's resource quota. There is a concern that one Pod or Container could monopolize all available resources. A LimitRange is a policy to constrain resource allocations (to Pods or Containers) in a namespace.

A LimitRange provides constraints that can:

  • Enforce minimum and maximum compute resources usage per Pod or Container in a namespace.
  • Enforce minimum and maximum storage request per PersistentVolumeClaim in a namespace.
  • Enforce a ratio between request and limit for a resource in a namespace.
  • Set default request/limit for compute resources in a namespace and automatically inject them to Containers at runtime.
apiVersion: v1
kind: LimitRange
metadata:
  name: mem-limit-range
spec:
  limits:
  - default:
      memory: 512Mi
    defaultRequest:
      memory: 256Mi
    type: Container
apiVersion: v1
kind: LimitRange
metadata:
  name: cpu-limit-range
spec:
  limits:
  - default:
      cpu: 1
    defaultRequest:
      cpu: 0.5
    type: Container

고급 스케줄링 정책

Pod를 배포할 때, 특정 Node를 선택할 수 있도록 정의할 수 있다. 예를 들어 Redis master와 slave들이 같은 Node에 배포되지 않도록 Pod의 스케줄링 정책을 인위적으로 조정할 수 있다. 즉, taint/toleration과 affinity with NodeSelector의 조합으로 특정 노드에 특정 포드가 스케줄링 되도록 할 수 있다.

# Taint/Toleration

출처 : https://livebook.manning.com/#!/book/kubernetes-in-action/chapter-16/section-16-3-1

Taints

Taint는 Node에 정의할 수 있고, Toleration은 Pod에 정의할 수 있다.

- Taints & Tolerations does not tell the part to go to a particular node.
- It tells the node to only accept parts with certain toleration

 

Taint는 특정 Node에 일반적인 pod가 배포되는 것을 막을 수 있다. 즉, 알맞은 toleration을 가지고 있는 Pod만 배포될 수 있다.

Taint Effect

Taint에 적용할 수 있는 effect는 아래와 같이 3가지가 있다.

  • NoSchedule : if there is at least one un-ignored taint with effect NoSchedule then Kubernetes will not schedule the pod onto that node(기존에 노드에 머물러 있던 Pod는 그대로 stay!)
  • PreferNoSchedule : if there is no un-ignored taint with effect NoSchedule but there is at least one un-ignored taint with effect PreferNoSchedule then Kubernetes will try to not schedule the pod onto the node. the system will try to avoid placing a pod that does not tolerate the taint on the node, but it is not required. 
  • NoExecute : if there is at least one un-ignored taint with effect NoExecute then the pod will be evicted from the node (if it is already running on the node), and will not be scheduled onto the node (if it is not yet running on the node). a toleration with NoExecute effect can specify an optional tolerationSeconds field that dictates how long the pod will stay bound to the node after the taint is added. pods that tolerate the taint without specifying tolerationSeconds in their toleration specification remain bound forever(돌고 있던 Pod도 evit하고, 다른 node로 옮긴다.)

NoExecute > NoSchedule > PreferNoSchedule 순으로 Pod에 영향을 많이 준다.

# Taint on Nodes
kubectl taint nodes node01 spray=mortein:NoSchedule
kubectl describe nodes node-1 | grep -i taint

# taint 제거
kubectl taint node master/controlplane node-role.kubernetes.io/master:NoSchedule-

kubectl explain pod --recursive | grep -A5 tolerations

# NoSchedule / PreferNoSchedule / NoExecute(killed)

Toleration

Taint 처리가 되어있는 Node에 Pod를 배포하기 위해서는 Toleration을 사용해야한다. 일종의 입장권이라고 생각하면 편할 것 같다. 다만 주의해야될 점은 배포되는 것이아니라 배포될 수 있다라는 점이다. 즉, 다른 조건에 맞는 Node가 있다면 해당 Node에 배포될 수도있다.

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
    type: front-end
spec:
  containers:
    - name: nginx-container
      image: nginx
      ports:
        - containerPort: 8080
  nodeSelector:
    size: Large # node labeling first!

  tolerations:
    - key: app
      operator: "Equal" / "Exists"
      value: "blue"
      effect: "NoSchedule"

 

 

# Affinity

Taint가 Pod가 배포되지 못하도록 하는 정책이라면, affinity는 Pod가 특정 노드로 배포되도록 하는 기능이다. affinity는 Node를 기준으로 하는 Node affinity와 다른 Pod가 배포된 노드를 기준으로 하는 Pod affinity가 있다.

Node Affinity

Node affinity는 Hard affinity와 soft affinity가 있다.

- Hard affinity는 조건이 딱 맞는 node에만 pod를 배포하는 기능을 하고(node selector와 같은 기능을 한다.) 예를들면, node selector

- Soft affinity는 Pod가 조건에 맞는 node에 되도록이면 배포되도록 하는 기능이다 

 

Node Affinity Types

Node Affinity에 적용할 수 있는 effect는 아래와 같이 2가지가 있다.

- requiredDuringSchedulingIgnoredDuringExecution(hard) : similar to nodeSelector but using a more expressive syntax. If labels on a node change at runtime such that the affinity rules on a pod are no longer met, the pod continues to run on the node.

- preferredDuringSchedulingIgnoredDuringExecution(soft), not guaranteed

 

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
    type: front-end
spec:
  containers:
    - name: nginx-container
      image: nginx
      ports:
        - containerPort: 8080
  # schedule a Pod using required node affinity
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: size
                operator: In #NotIn, Exists
                values:
                  - Large

  tolerations:
    - key: app
      operator: "Equal"
      value: "blue"
      effect: "NoSchedule"

Pod affinity

Pod Affinity Types

Pod Affinity에 적용할 수 있는 effect는 아래와 같이 2가지가 있다.

- requiredDuringSchedulingIgnoredDuringExecution(hard) : similar to nodeSelector but using a more expressive syntax. If labels on a node change at runtime such that the affinity rules on a pod are no longer met, the pod continues to run on the node.

- preferredDuringSchedulingIgnoredDuringExecution(soft), not guaranteed

 

예시는 아래와 같다.

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0

 

클러스터 구성을 할때 1개의 노드에 모든 파드가 배포되었다고 가정해보자. 노드가 갑자기 이상상황이 발생하여 정상 작동하지 않는다면 더이상 서비스를 제공할 수 없을 것이다. 이런 경우에 Pod anti-affinity를 활용할 수 있다.

 

예를 들어 Redis Cluster 구성한다고 가정해보자. 쿠버네티스 노드가 6개 이상이고, Redis cluster에 필요한 Pod도 6개라고 할때 노드마다 1개의 Redis Pod가 배치된다면 HA 구성을 할 수 있다.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-cluster
  labels:
    app: redis-cluster
spec:
  selector:
    matchLabels:
      app: redis-cluster
  serviceName: redis-cluster
  replicas: 6
  template:
    metadata:
      labels:
        app: redis-cluster
      annotations:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
                - key: "app"
                  operator: In
                  values:
                  - redis-cluster
            topologyKey: "kubernetes.io/hostname"
            
      ...

 


Multiple Scheduler

# Scheduler

static pod로 배포된다. 따라서 포드명은 kube-scheduler-{node_name} 다음과 같은 형식이다.

apiVersion: v1
kind: Pod
metadata:
   name: kube-scheduler
   namespace: kube-system
spec:
   containers:
   - command:
      - kube-scheduler
      - --address=127.0.0.1
      - --kubeconfig=/etc/kubernetes/scheduler.conf
      - --leader-elect=true
     image: k8s.gcr.io/kube-scheduler-amd64:v1.11.3
     name: kube-scheduler

# Custom Scheduler 

--learder-elect=false를 이용하여 기존 leader를 유지

--lock-object-name=my-custom-scheduler을 이용하여 새로운 스케줄러가 leader eletion 중에 default가 되는 것을 막음

apiVersion: v1
kind: Pod
metadata:
   name: my-custom-scheduler
   namespace: kube-system
spec:
   containers:
   - command:
      - kube-scheduler
      - --address=127.0.0.1
      - --kubeconfig=/etc/kubernetes/scheduler.conf
      - --leader-elect=false (for single master node)
      - --scheduler-name=my-custom-scheduler
      - --lock-object-name=my-custom-scheduler (for multiple master nodes)
     image: k8s.gcr.io/kube-scheduler-amd64:v1.11.3
     name: kube-scheduler
apiVersion: v1
kind: Pod
metadata:
   name: nginx
spec:
  containers:
  -  image: nginx
     name: nginx
  schedulerName: my-custom-scheduler

 

 

참고

- container resource default : 1 cpu, 512 Mi

1 cpu
- 1 AWS vCPU
- 1 GCP core
- 1 Azure core
- 1 Hyperthread

1 Gi(Gibibyte) like 1 G
1 Mi(Mebibyte) like 1 M
1 Ki(Kibibyte) like 1 K

 

- The status 'OOMKilled' indicates that the pod ran out of memory.

- debugging

kubectl get events

kubectl logs my-custom-scheduler -n=kube-system

kubectl pod kube-scheduler-master -n kube-system | grep Image

cd /etc/kubernetes/manifests
# 노드 테인트 정보가져오기
kubectl describe node kubemaster | grep -i taint

# 노드 라벨링(Node selector와 연동!)
kubectl label nodes node01 size=Large
kubectl label nodes node01 color=blue

# 노드 with label
kubectl get nodes node01 --show-labels
apiVersion: v1
kind: Binding
metadata:
  name: nginx
target:
  apiVersion: v1
  kind: Node
  name: node02

 


출처

- https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

- https://thenewstack.io/implementing-advanced-scheduling-techniques-with-kubernetes/

- https://bcho.tistory.com/1344?category=731548

- https://bcho.tistory.com/1346?category=731548

'클라우드 컴퓨팅 > 쿠버네티스' 카테고리의 다른 글

docker 리소스 제한  (0) 2021.09.05
docker 기본  (0) 2021.09.05
Kubernetes Workloads  (0) 2021.08.08
Kubernetes JSONPATH 사용법  (0) 2021.08.08
Kubernetes Network Model & Policy  (0) 2021.07.08

+ Recent posts