[Docker] 도커 실무 (레이어 관리, 캐싱을 활용한 빌드, Nignx 설정 커스터마이징, 3Tier 아키텍처 구성, 동적 서버 구성, 이중화 DB 구성, 컨테이너 애플리케이션 최적화)

yoonthegarden·2024년 6월 6일
0

Docker

목록 보기
9/9

Part8. 도커 실무

레이어 관리

이미지의 개수를 작게 유지하는 방법과 이미지의 크기를 작게 만드는 방법

도커 파일의 레이어 구조

  • 도커 이미지는 레이어로 구성되어 있다.
  • Dockerfile에 작성된 지시어 (대부분)하나 당 레이어가 한 개 추가된다.
  • 불필요한 레이어가 많아지면 이미지의 크기가 늘어나고 빌드 속도가 느려질 수 있다.

node 14 이미지에 총 5개의 지시어가 추가, CMD를 제외하고 4개의 이미지가 추가되었다. 기존의 14개 레이어에서 새로운 4개의 레이어를 추가해서 총 18개의 레이어로 빌드된다.

  • RUN 지시어는 컨테이너 레이어 안에서 특정 명령을 실행하는 지시어이다.
  • RUN 지시어는 &&을 활용해 최대한 하나로 처리한다.
  • 불필요한 명령어를 추가해서 레이어의 개수를 늘리지 않는다.

현업에서는 보통 이미지를 빌드하고 푸시하고 배포하는 과정을 모두 다른 기기에서 진행하는 경우가 많다. 새로운 버전으로 빌드한 이미지를 레지스트리에 푸시하고, 각각의 환경에서 이미지를 레지스트리로부터 다운받아 컨테이너를 업그레이드 한다.

이 과정에서 이미지는 네트워크를 통해 업로드되고, 다운로드 되기 때문에 이미지의 크기가 작은 것이 배포 속도와 네트워크 사용량에 유리하다 볼 수있다.

이미지 크기 줄이는 방법

  • 애플리케이션의 크기를 가능한 작게 관리한다. → 애플리케이션 소스의 불필요한 기능들을 줄이고 하나의 큰 모듈을 여러 모듈로 분리해서 애플리케이션 크기를 줄일 수 있다. → 애플리케이션 크기가 줄어들면 이미지의 크기도 줄어든다.
  • 베이스 이미지는 가능한 작은 이미지를 사용한다. alpine OS를 사용하는 것이 좋다. → 극단적으로 사이즈를 줄이려면 모든 이미지의 뿌리가 되는 스크래치 이미지를 활용할 수도 있다. 스크래치 이미지는 이미지를 빌드하기 위한 가장 최소한의 파일만 포함한 이미지이다.
  • .dockerignore 파일을 사용해서 불필요한 파일을 제거한다. → 불필요한 파일이 이미지로 들어가지 않도록 한다. (copy 지시어 사용 시)

실습

이미지 빌드

docker build -t helloworld .
docker run -d -p 8080:8080 --name go-helloworld helloworld

→ 이미지 크기를 작게 구성하는데 있어서 정적 바이너리 파일로 빌드할 수 있는 GO언어를 사용하는 것은 좋은 방법이다. 실제로 하나의 컨테이너의 크기를 줄이는 것이 중요한 미션 중의 하나인 MSA 아키텍처에서는 GO언어를 많이 사용한다.



캐싱을 활용한 빌드

캐싱은 빌드 속도를 크게 증가시킬 수 있는(빠르게 만들어주는) 기술이다. 시간이 걸리는 작업의 결과물을 미리 저장해두고, 동일한 작업이 발생했을 경우 다시 계산하지 않고 결과를 빠르게 제공해준다.

레이어를 어떻게 구성하는지에 따라서 캐싱을 활용하는 빈도가 달라질 수 있다.

도커 빌드 명령어를 실행하면 결과적으로 4개의 이미지 레이어를 추가한다. 이 과정에서 레이어들은 도커가 캐시로 저장해둔다.

그 다음빌드에서 동일한 지시어를 사용하면 이미지 레이어를 새롭게 만들지 않고, 캐시에 저장되어 있는 레이어를 그대로 사용한다. 완전히 동일한 소스코드로 다시 빌드할 경우에는 빠르게 이미지를 빌드할 수 있다.

도커는 지시어를 똑같이 작성한 경우에는 캐시에 저장되어 있는 레이어를 그대로 사용한다. (지시어만 같다고 X, 지시어로 처리하는 내용까지 같아야 한다.)

  • Dockerfile에 작성된 순서대로 결과 이미지의 레이어가 쌓입니다.
  • Docker는 각 단계의 결과 레이어를 캐시처리합니다. 지시어가 변경되지 않으면 다음 빌드에서 레이어를 재사용합니다.
  • COPY, ADD 명령의 경우 빌드 컨텍스트의 파일 내용이 변경되어도 캐시를 사용하지 않습니다.
  • 레이어가 변경되면 그 레이어와 이후의 모든 레이어는 캐시를 사용하지 않고 새로운 레이어가 만들어집니다.

  • 잘 변경되지 않는 파일들을 아래 레이어에 배치하면, 캐시를 활용하는 빈도를 높일 수 있습니다.
  • package.json, package-lock.json 파일은 소스 코드가 의존하는 외부 라이브러리 정보가 있습니다. 개발 시 자주 변경되지 않습니다.
  • package.json, package-lock.json 파일이 변경되지 않을 경우 npm ci 단계(의존 라이브러리 설치)까지 캐시를 활용할 수 있습니다.

라이브러리 설치와 같은 자주 변경되지 않는 부분을 이미지 빌드 초기 단계에 구성해놓으면 캐싱을 잘 활용할 수 있다.
기존 도커 파일은 빌드 컨텍스트에서 전체 파일을 복사 → 라이브러리 설치하는 부분을 별도로 분리한다.

실습

1. easydocker/leafy/leafy-frontend 디렉터리 이동 및 소스코드 상태 변경
git reset --hard HEAD && git clean -fd
git switch 01-dockerfile

2. 코드 변경 없이 2번 빌드 시도, 2번째 빌드에서 캐시 사용 확인
docker build -t leafy-front:2.0.0 . --no-cache
docker build -t leafy-front:2.0.1 .

3. 코드 일부분 수정 후 다시 빌드 시도, 시간 체크 (src/App.vue)
docker build -t leafy-front:2.0.2 .

4. 다음 페이지의 도커파일을 참고해 도커파일 수정 및 빌드
docker build -t leafy-front:2.0.3 . --no-cache

5. 코드 일부분 수정 후 다시 빌드 시도, 캐시 활용 한 시간 체크
docker build -t leafy-front:2.0.4 .
FROM node:14 AS build

WORKDIR /app

COPY package.json . // ---------------- 라이브러리 설치에 필요한 package.json 파일과 package-lock.json 파일만 복사
COPY package-lock.json .

RUN npm ci // --------------------------------- 의존 라이브러리 설치

COPY . /app //--------------------------------- 전체 소스코드 복사 RUN npm run build ----------------------------- 애플리케이션 빌드

FROM nginx:1.21.4-alpine

COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 80
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
1. easydocker/leafy/leafy-backend 이동 및 다음 파일의 도커파일 수정 후 빌드
cd ../leafy-backend
docker build -t leafy-backend:2.0.0 . --no-cache

2. 소스파일 변경 후 재빌드, (src/main/java/com/devwiki/leafy/controller/plant/PlantController.java) 캐시 활용하여 단축된 시간 확인
docker build -t leafy-backend:2.0.1 .
FROM gradle7.6.1-jdk11 AS build

WORKDIR /app

COPY build.gradle settings.gradle ./ // ---------------- 라이브러리 설치에 필요한 build.gradle파일과 settingsgradle 파일만 복사

RUN gradle dependencies --no-daemon //----------------- 의존 라이브러리 설치

COPY . /app //--------------------------------- 전체 소스코드 복사

RUN gradle clean build --no-daemon //-------------- 애플리케이션 빌드

FROM openjdk:11-jre-slim

WORKDIR /app
COPY --from=build /app/build/libs/*.jar /app/leafy.jar

EXPOSE 8080
ENTRYPOINT ["java"]
CMD ["-jar", "leafy.jar"]

→ 이미지 빌드 시 캐싱을 활용해서 할 수 있다. 캐시를 활용하기 위해서는 도커 파일을 작성할 때 자주 수정되지 않는 부분을 먼저 작성하는 것이 유리하다.




Nignx 설정 커스터마이징, 3Tier 아키텍처 구성

3Tier 아키텍처

엔터프라이즈 웹 애플리케이션 서버 구성

  • 프론트엔드 소스 제공하는 웹 서버
  • 비즈니스 로직을 수행하는 웹 애플리케이션
  • 데이터 저장하는 데이터베이스 서버

→ 이 세가지 종류의 서버가 유기적으로 상호작용하면서 하나의 애플리케이션으로 구성되는 것을 3Tier 아키텍처라고 한다. (Tier는 하나의 단계를 의미)

많은 엔터프라이즈 웹 애플리케이션이 3Tier 아키텍처를 기반으로 구성되어 있다.

여기서 가장 먼저 클라이언트의 첫번째 진입점 역할을 하는 서버가 웹 서버이다. 클라이언트가 주소창에 어떤 요청을 보냈을 때 요청한 경로에 해당하는 html, JS와 같은 정적 파일을 사용자에게 제공해준다. 정적 파일이라는 것은 어떤 사용자든 동일한 경로로 보낸 요청은 동일한 내용의 페이지를 제공한다는 의미이다.

사용자마다 다른 데이터를 제공하기 위해서는 프론트엔드에서 제공받은 파일을 활용해 클라이언트가 다시 백엔드 애플리케이션으로 요청을 보내야 한다. 백엔드 애플리케이션은 DB에 요청을 보내 영속성이 필요한 데이터를 조회하고, 저장할 수 있다.

Proxy

화면의 구조를 보면 클라이언트가 백엔드 서버에 요청을 직접한다. 하지만 일반적으로 백엔드 애플리케이션은 시스템과 실제 데이터에 밀접한 연관이 있기 때문에 보안에 주의해야 하므로 사용자가 직접 접근하지 않도록 설정하는 것이 좋다.

우리가 구성한 아키텍쳐는 웹 애플리케이션 서버가 클라이언트에 그대로 노출되기 때문에 보안상 뛰어나다고 볼 수 없다. 따라서 Nginx의 Proxy 기능을 활용해 백엔드 애플리케이션으로의 접근을 제한할 것이다.

Proxy 설정을 하기 위해서는 Nginx 이미지를 빌드할 때 Nginx 서버의 설정을 변경해야 하기 때문에 이미지를 빌드할 때 서버의 설정을 변경하는 방법을 실습한다.

개선된 3Tier 구조이다. 이 구조에서는 백엔드 애플리케이션에 대한 요청은 웹서버를 통해서만 접근이 가능하다. 웹 서버는 클라이언트와 백엔드 애플리케이션의 다리역할을 하기 위해서 Proxy라는 기술을 활용한다.

Proxy는 요청을 전달해준다는 의미로, Nginx의 Proxy 기술을 활용하면 특정 경로로 접근하는 요청은 원하는 곳으로 다시 전달해 줄 수 있다. 그래서 Proxy를 활용해서 Nginx 서버의 /api로 시작되는 요청들은 일반적인 요청처럼 정적파일을 제공해주는 것이 아니라 백엔드 애플리케이션으로 다시 전달해준다.

프록시 설정을 하기 위해서는 Nginx의 설정파일에 아래를 추가하면 Nginx 서버로 전달되는 요청 중에 /api로 시작되는 모든 요청은 이 proxy pass에 적혀있는 http 프로토콜의 leafy:8080으로 요청을 다시 전달할 수 있다.

Nginx 서버 설정 변경
location /api/ {
    proxy_pass http://leafy:8080;
}
  • Nginx의 프록시 기술을 활용해 보안에 뛰어난 3Tier 아키텍처를 구성할 수 있습니다.
  • Nginx는 특정 경로로 온 요청(/api로 시작하는 경로)를 지정한 서버로 전달합니다.
  • Nginx를 프록시 서버로 활용하여 보안 향상, 부하 관리 및 API 응답 캐싱을 활용할 수 있습니다.

http://localhost/index.html → 웹서버의 정적 파일을 응답

http://localhost/api/~ → 애플리케이션으로 요청 전달

Proxy는 특정 경로로 온 요청을 정해진 경로로 전달해준다.

이렇게 Nginx의 Proxy 기술을 활용하면 /api 이외의 경로로 접근하는 백엔드 애플리케이션으로의 접근을 물리적으로 차단할 수 있다.

추가적으로 api의 응답을 캐싱하거나 부하를 관리하고 접근 로그를 관리하는 것처럼 다양한 추가 기능을 Proxy를 동해 활용할 수 있다.

실습

import axios from 'axios'; 

const api = axios.create({
    ~~baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8080'~~
});

export default api;

→ 웹 브라우저에서 백엔드 애플리케이션에 대한 요청을 http://localhost:8080으로 보내는 설정, 해당 라인을 제거 후 저장한다.

easydocker/leafy/leafy-frontend/nginx.conf 파일 생성

server {
    listen       80;
    server_name  _;
    
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
		}
		# / 경로에 왔을 경우 usr/share/nignx/html에 있는 index.html을 제공해준다.
		
    location /api/ {
        proxy_pass http://leafy:8080;
		}
		# /api 경로에 왔을 경우 proxy 기능을 활용해 repeat 8080 포드로 요청을 전달한다. 
		
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

→ 웹 서버로 오는 요청 중 /api/로 시작하는 경로의 요청을 모두 http://leafy:8080으로 전달

새롭게 작성한 nignx 웹서버 설정파일인 nginx.conf 파일을 기존의 nginx 이미지에 덮어쓰기 하는 부분을 도커파일에 추가한다.

COPY nginx.conf /etc/nginx/conf.d/default.conf

→ 소스코드의 nginx.conf 파일을 이미지 빌드 시 nginx 설정으로 복사

1. 이미지 빌드
docker build -t leafy-front:3.0.0-proxy .

2. network 가 제대로 생성되어 있는지 확인
docker network create leafy-network

3. 애플리케이션 구동 및 테스트, 백엔드 애플리케이션을 포트포워딩 하지 않고 내부 통신만 사용하도록 설정
# postgres 컨테이너 실행
(MacOS) docker run -d --name leafy-postgres -v mydata:/var/lib/postgresql/data --network leafy-network devwikirepo/leafy-postgres:1.0.0 
(Windows) docker run -d --name leafy-postgres -v mydata://var/lib/postgresql/data --network leafy-network devwikirepo/leafy-postgres:1.0.0
# leafy 컨테이너 실행, DB_URL옵션으로 db서버 url을 db컨테이너 명으로 설정
# 기존엔 포트포워딩을 통해 8080 포트를 외부로 열었지만, Nginx의 Proxy를 통해 접근할 예정으로 백엔드 애플리케이션을 외부에 오픈할 필요가 없다. 
docker run -d -e DB_URL=leafy-postgres --name leafy --network leafy-network devwikirepo/leafy-backend:1.0.0
# 프론트엔드 컨테이너 실행
docker run -d -p 80:80 --name leafy-front --network leafy-network leafy-front:3.0.0-proxy

docker ps
# 3개 컨테이너 확인 가능
# 포트포워딩 되어있는 서버는 웹서버뿐이기 때문에 실습PC에서 접근할 수 있는 서버는 웹서버로 제한 

4. localhost:80 접속하여 정상 접속 확인, curl 명령으로 백엔드 접근 테스트(응답이 오지 않아야 정상)
curl http://localhost:8080/api/v1/users

5. 환경 정리
docker rm -f leafy-postgres leafy leafy-front
# Postgres에서 생성한 mydata 볼륨은 남아있기 때문에 볼륨도 삭제
docker volume rm mydata

웹 서버의 Proxy기능을 활용해 웹 애플리케이션으로 클라이언트가 직접 접근하는 것을 막았다. 클라이언트는 오로지 웹 서버만으로 요청을 수행하고, 웹 서버는 정적인 자료를 요청할 경우 바로 응답, api 요청이 전달될 경우에는 웹 애플리케이션으로 Proxy한다.

nginx 웹 서버 설정을 통해 /api 경로는 모두 전달되도록 설정했다.

백엔드 애플리케이션은 기존과 동일하게 요청을 받아서 데이터베이스 서버와 상호작용한 뒤 결과를 웹서버로 전달한다. 그러면 웹 서버는 결과를 클라에게 전달해준다.

→ proxy 기능을 활용해 클라이언트와 백엔드 서버가 연결되는 부분을 완전히 제거했다.
→ 네트워크 구성을 보면 기존과 차이점은 leafy container의 포트 포워딩이 삭제된 것이다.

기존엔 호스트의 8080주소를 활용해 백엔드 컨테이너와 상호작용 했다면, 프록시를 활용해 리피 컨테이너의 포트 포워딩을 제거하고, Nginx 웹서버에서만 리피컨테이너로 접근되도록 구성했다. 따라서 프론트엔드 컨테이너는 HostOS의 80포트로 접근하고, 백엔드 컨테이너는 HostOS의 80포트의 /api로 접근했을 때 Nginx 서버에서 프록시되어 접근할 수 있다. DB 컨테이너도 마찬가지로 기존과 동일하게 포트포워딩이 없기 때문에 여전히 외부에서 접근 불가능하고, 백엔드 애플리케이션을 통해서만 접근할 수 있는 구조이다.



환경 변수를 활용한 동적 서버 설정

  • Nginx 서버 설정에 백엔드 애플리케이션의 주소가 고정되어 있다.
  • 환경 별로 Nginx가 프록시 해야 하는 주소가 바뀔 수 있다. (leafy:8080 → leafy-backend:8080으로 변경될 경우)
  • 프록시 설정의 주소를 바꾸기 위해 설정 파일 수정 후, 이미지 빌드를 다시 해야 한다.

배포가 되는 환경 별로 달라지는 정보는 시스템 환경 변수로 처리하면 컨테이너 실행 시 결정할 수 있다.

SpringBoot의 경우, 이 프레임워크가 자체적으로 환경 변수를 찾아서 값을 결정해준다. 하지만 Nginx이미지 같은 경우는 이런 기능이 존재하지 않기 때문에 직접 nginx.conf 파일의 값을 수정해야 한다.

실습

1. easydocker/leafy/leafy-frontend 디렉터리 이동 및 소스코드 상태 변경
git reset --hard HEAD && git clean -fd 
git switch 03-proxy
# 또는 git switch 04-dynamicconfig
 		location /api/ {
        ~~proxy_pass http://leafy:8080;~~
        proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
		}

→ 기존 nginx.conf 파일 수정

FROM node:14 AS build
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.21.4-alpine
# 소스코드의 nginx.conf 파일을 template 파일로 복사 기본 환경 변수 지정
COPY nginx.conf /etc/nginx/conf.d/default.conf.template 
ENV BACKEND_HOST leafy
ENV BACKEND_PORT 8080

# 컨테이너 실행시 자동으로 실행될 스크립트 지정
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 80
ENTRYPOINT ["docker-entrypoint.sh"] # 스크립트 실행 처리
CMD ["nginx", "-g", "daemon off;"] # docker-entrypoint.sh의 옵션 값으로 제공됨 

/etc/nginx/conf.d/default.conf.template 에 복사된 파일은 바로 사용되진 않고, 환경 변수의 내용을 변경한 후 설정파일로 저장하기 위한 템플릿 역할을 하는 파일이다.

ENV BACKEND_HOST leafy
ENV BACKEND_PORT 8080

-e 옵션으로 지정할 수 있지만, -e를 주지 않았을 때 사용되는 기본 값을 설정해준다.

→ 실제 실행되는 명령어는 docker-entrypoint.sh nginx -g daemon off;

docker-entrypoint.sh 생성

#!/bin/sh
# 오류가 발생했을 때 스크립트를 중단하도록 설정 
set -e

# default.conf.template 파일에서 환경 변수를 대체하고 결과를 default.conf에 저장
# 템플릿 파일과 환경 변수를 읽은 뒤 nginx.conf 파일을 구성하여 nginx 설정으로 복사
envsubst '${BACKEND_HOST} ${BACKEND_PORT}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf

# 다음 명령어를 실행
# nginx -g daemon off 명령을 이 엔트리 포인트에 옵션으로 줬는데, "$@"가 옵션으로 제공받은 값 실행하는 부분이다. 
exec "$@"

docker-entrypoint.sh 파일에 nginx-demonoff 라는 값을 옵션으로 제공하면 먼저 envsubst 명령을 사용해 환경 변수를 읽어서 환경 설정파일을 만든 다음에 옵션으로 제공받은 명령어를 통해서 웹 서버를 실행시킨다.

1. 이미지 빌드
docker build -t leafy-front:4.0.0-env .

2. network 가 제대로 생성되어 있는지 확인
docker network create leafy-network

3. 애플리케이션 구동 및 테스트, 백엔드 애플리케이션의 접속 도메인 변경
localhost:80 접속하여 정상 접속 확인
(MacOS) docker run -d --name leafy-postgres -v mydata:/var/lib/postgresql/data --network leafy-network devwikirepo/leafy-postgres:1.0.0 
(Windows) docker run -d --name leafy-postgres -v mydata://var/lib/postgresql/data --network leafy-network devwikirepo/leafy-postgres:1.0.0 
docker run -d -e DB_URL=leafy-postgres --name leafy-backend --network leafy-network devwikirepo/leafy-backend:1.0.0
docker run -d -e BACKEND_HOST=leafy-backend -p 80:80 --name leafy-front --network leafy-network leafy-front:4.0.0-env

4. 설정 파일에 환경 변수가 적용되었는지 확인
docker exec leafy-front cat etc/nginx/conf.d/default.conf

5. 환경 정리
docker rm -f leafy-postgres leafy-backend leafy-front

→ 이전과 달라진 점은 컨테이너명이 leafy가 아니라 leafy-backend로 수정된 것이다. 환경변수 처리가 되지 않았다면 proxy 설정에는 leafy가 되어있기 때문에 소스코드에서 이 파일의 내용을 수정하고 새로운 이미지를 빌드해야한다. 하지만 환경변수 처리를 했기 때문에 환경변수 값에 백엔드 애플리케이션의 HOST를 옵션으로 지정해주면 된다.




PostgreSQL 이중화 DB 구성

서버 이중화(다중화)

  • 단일 서버 구성 시 단일 서버에 장애가 생기면 전체 서비스의 장애로 이어진다.
  • 같은 역할을 하는 서버가 두 대 이상 있다는 것을 의미한다.
  • 서버 이중화(Redundancy) 구성 시 하나의 서버가 실패해도 다른 서버가 동일한 역할을 수행하여 고가용성(가용성이 높다)을 보장한다.

웹서버나 WAS 서버 같은 경우에 서버의 상태가 없게 관리할 수 있기 때문에 이중화를 하지만 DB 서버는 데이터의 상태가 있기 때문에 이중화 서버를 구성하는 것이 까다롭다.

DB 이중화 구성

  • 동시에 같은 볼륨을 사용하거나, 각각의 컨테이너에 별도의 볼륨을 연결할 수 있다.
  • 동시에 하나의 볼륨을 사용하는 경우는 하나의 볼륨에 마운트해서 동시에 같은 스토리지를 바라보도록 구성한다.
  • 동시에 같은 볼륨을 사용하면 구성이 간단하지만 볼륨에 문제가 생길 경우 대처가 어렵다.
  • 동시에 같은 볼륨을 사용하면 볼륨의 성능에 부하가 생길 수 있다.
  • 각각의 컨테이너에 별도의 볼륨을 연결하면 안정적이지만 볼륨이 독립적으로 존재하기 때문에 데이터의 싱크를 맞추는 처리(동기화)를 별도로 해야 한다.

DB서버에서 각 볼륨을 가진 컨테이너가 데이터를 동기화하는 방법

  • 프라이머리-스탠바이의 경우 하나의 프라이머리 서버에 여러 개의 스탠바이 서버를 연결할 수 있다. 새로운 데이터를 입력하는 쓰기 작업은 프라이머리 서버에서만 가능하다. 쓰기 작업을 수행하면, 새롭게 생성한 데이터들은 즉시 스탠바이 서버의 볼륨으로 복제된다. 프라이머리 서버에 연결된 스탠바이 서버는 읽기 전용으로만 사용되며, 읽기 전용 스탠바이 서버를 여러 대 사용할 수 있다.
  • 프라이머리- 프라이머리 복제 구조의 경우 모든 서버에 읽기/쓰기 작업을 수행한다. 여러 서버에서 동시에 쓰기작업이 일어나기 때문에 동기화 구성 작업이 복잡하다.

실습

프라이머리-스탠바이로 실습한다.

1. easydocker/leafy/leafy-postgres 디렉터리 이동 및 소스코드 상태 변경
git reset --hard HEAD && git clean -fd
git switch 05-redundancy

2. 테스트용 네트워크 생성
docker network create postgres
3. 프라이머리 노드 실행
docker run -d \
  --name postgres-primary-0 \
	--network postgres \ # Windows는 //bitnami/postgresql
	-v postgres_primary_data:/bitnami/postgresql \
	-e POSTGRESQL_POSTGRES_PASSWORD=adminpassword \ ---------------- postgres 사용자(슈퍼유저)의 비밀번호 
	-e POSTGRESQL_USERNAME=myuser \ ------------------데이터베이스 사용자 이름
	-e POSTGRESQL_PASSWORD=mypassword \ ------------------ 데이터베이스 사용자 패스워드 
	-e POSTGRESQL_DATABASE=mydb \ ------------------데이터베이스 명
	# 프라이머리 스탠바이 구조를 구성하기 위한 환경변수 추가 지정
	-e REPMGR_PASSWORD=repmgrpassword \ --------- Repmgr 패스워드(레플리케이션 관리용) 
	-e REPMGR_PRIMARY_HOST=postgres-primary-0 \ ----------------- 프라이머리 노드의 호스트 명 
	-e REPMGR_PRIMARY_PORT=5432 \ ------------ 프라이머리 노드의 포트 
	-e REPMGR_PARTNER_NODES=postgres-primary-0,postgres-standby-1:5432 \ ---------- 통신할 노드 목록 
	-e REPMGR_NODE_NAME=postgres-primary-0 \ ---------------- 현재 노드의 이름 
	-e REPMGR_NODE_NETWORK_NAME=postgres-primary-0 \ ------- 현재 노드의 도메인명 
	-e REPMGR_PORT_NUMBER=5432 \ ------------------------- 현재 노드의 포트 
	bitnami/postgresql-repmgr:15 # 이미지는 bitnami에서 제공하는 postgresql 15버전 사용

→ 프라이머리의 호스트와 포트를 지정하고 파트너 노드 쪽에 자신의 컨테이너 명뿐만 아니라 PostgreSQL 스탠바이 컨테이너도 지정한다.

4. 스탠바이 노드(postgres-standby-1) 실행
docker run -d \
  --name postgres-standby-1 \
	--network postgres \  # Windows는 //bitnami/postgresql
	-v postgres_standby_data:/bitnami/postgresql \
	-e POSTGRESQL_POSTGRES_PASSWORD=adminpassword \
	-e POSTGRESQL_USERNAME=myuser \
	-e POSTGRESQL_PASSWORD=mypassword \
	-e POSTGRESQL_DATABASE=mydb \
	-e REPMGR_PASSWORD=repmgrpassword \
	-e REPMGR_PRIMARY_HOST=postgres-primary-0 \
	-e REPMGR_PRIMARY_PORT=5432 \
	-e REPMGR_PARTNER_NODES=postgres-primary-0,postgres-standby-1:5432 \
	-e REPMGR_NODE_NAME=postgres-standby-1 \
	-e REPMGR_NODE_NETWORK_NAME=postgres-standby-1 \
	-e REPMGR_PORT_NUMBER=5432 \
	bitnami/postgresql-repmgr:15 # 이미지는 bitnami에서 제공하는 postgresql 15버전 사용

→ 옵셥 값이나 사용하는 이미지는 이전의 프라이머리 컨테이너와 동일하지만 컨테이너의 이름, 볼륨 명이 다르다.

쓰기가 가능한 프라이머리 노드에서 테이블을 생성하고 데이터 삽입

5. SHELL1, SHELL2 각 컨테이너의 로그 확인
docker logs -f postgres-primary-0
docker logs -f postgres-standby-1

6. 프라이머리 노드에 테이블 생성 및 데이터 삽입
docker exec -it -e PGPASSWORD=mypassword postgres-primary-0 psql -U myuser -d mydb -c "CREATE TABLE sample (id SERIAL PRIMARY KEY, name
VARCHAR(255));"
docker exec -it -e PGPASSWORD=mypassword postgres-primary-0 psql -U myuser -d mydb -c "INSERT INTO sample (name) VALUES ('John'), ('Jane'),
('Alice');"

7. 스탠바이 노드에 데이터가 동기화되어 있는지 확인
docker exec -it -e PGPASSWORD=mypassword postgres-standby-1 psql -U myuser -d mydb -c "SELECT * FROM sample;"

8. 환경 정리
docker rm -f postgres-primary-0 postgres-standby-1
docker volume rm postgres_primary_data postgres_standby_data
docker network rm postgres




컨테이너 애플리케이션 최적화

모든 애플리케이션은 CPU와 메모리를 사용한다. 일반적인 프로세스는 사용량이 별도로 제한되어 있지 않기 때문에 프로세스를 실행시키면 하드웨어의 모든 리소스를 사용할 수 있다. 하지만 도커는 가상화 기술이고, 컨테이너를 격리된 공간에서 실행하기 때문에 프로세스가 사용 가능한 리소스를 제한할 수 있다.

컨테이너 리소스를 제한하는 방법

docker run

  • --cpus={CPUcore수} 컨테이너가 사용할 최대 CPU코어 수 정의 (호스트 머신의 CPU 성능에 상대적)
  • --memory={메모리용량} 컨테이너가 사용할 최대 메모리 정의 (b, k, m, g 단위로 지정 가능)

컨테이너의 리소스(CPU, 메모리, 네트워크 디스크) 사용량 조회

docker stats (컨테이너명/ID)

HOSTOS에서 발생하는 이벤트(생성, 종료 등) 로그 조회

docker events

실습

1.리소스 제약이 없는 상태로 컨테이너 실행
docker run --help
# 일반 컨테이너 실행
docker run -d --name no-limit nginx

2.컨테이너의 메타데이터 확인
# inspect 중 Memory Cpus 필터링 -> 0으로 나옴 (제한 없음 의미)
docker inspect no-limit | grep -e Memory -e Cpus

3.리소스 제약이 있는 상태로 컨테이너 실행 (0.5 Core / 256M Memory)
docker run -d --name with-limit --cpus=0.5 --memory=256M nginx

4.컨테이너의 메타데이터 확인
docker inspect no-limit | grep -e Memory -e Cpus

5.실습 컨테이너 삭제
docker rm -f no-limit with-limit

LIMIT에 지정한 CPU보다 사용량이 초과할 경우 CPU 스로틀링 발생

  • 컨테이너에 설정된 CPU LIMIT을 초과하는 CPU 사용이 감지되면, 시스템은 컨테이너의 CPU 사용을 제한
  • 애플리케이션의 성능 저하 발생

LIMIT에 지정한 MEMORY보다 사용량이 초과할 경우

  • OOM(Out Of Memory) Killer 프로세스가 실행되고 컨테이너가 강제로 종료

자바 가상 머신(JVM) 튜닝

  • JVM(Java Virtual Machine)은 자바를 실행할 수 있는 환경이다.
  • 자바 애플리케이션이 사용할 수 있는 메모리 영역인 힙(Heap) 메모리를 별도로 관리해야 한다.

자바 애플리케이션이 실행될 때 자바가 동적으로 할당되는 공간을 힙 메모리라한다. 객체가 많이 사용될수록 힙메모리 영역이 많이 사용된다. 이 힙메모리 사용량은 보통 애플리케이션이 실행될 때 지정하는데, 전체 서버 메모리의 50~80%를 설정하는 것이 일반적이다.

컨테이너 리미트 값이 16GB이고 JVM의 힙메모리 지정된 25%인 4GB면 애플리케이션은 자기가 실행되는 서버의 환경을 충분하게 활용하지 못하는 것이다. 하지만 컨테이너에 지정된 리미트 값은 환경마다 다를 수 있고, 사용 중에 늘릴 수도 있다. 따라서 자바의 힙 메모리를 지정할 때는 컨테이너의 리미트 값도 함께 고려해야 한다.

자바에서는 자바 애플리케이션을 실행할 때 -Xmx라는 옵션으로 힙메모리의 최대 값을 지정할 수 있다. 하지만 이렇게 고정할 경우, 컨테이너의 리미트 값이 변경될 경우에 자바의 실행 명령도 함께 수정해야 한다.

FROM openjdk:8-jre-alpine

# JVM 튜닝을 위한 환경 변수 추가
ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
JVM의 Heap 메모리를 컨테이너에 할당된 메모리에 맞추어 자동으로 조절합니다.
(Java 8u131 부터 지원, Java 10 이상에서는 기본 활성화)

WORKDIR /app 
COPY --from=build /app/build/libs/*.jar /app/leafy.jar

EXPOSE 8080
ENTRYPOINT ["java"]
CMD ["-jar", "leafy.jar"]
profile
https://garden-ying.tistory.com/

0개의 댓글