헬스 체크를 지원하는 도커 이미지 빌드하기

도커는 컨테이너를 시작할 때마다 애플리케이션의 기본적인 상태를 확인한다.

  • 컨테이너를 실행하면 내부에서 실행되는 프로세스(앱 실행파일 | 닷넷 런타임 | 셸 스크립트 같은 특정한 프로세스)가 있는데 도커가 확인하는 것은 이 프로세스의 실행 사태다.
  • 만약 이 프로세스가 종료되면 컨테이너도 종료상태가 된다.

만약 웹 애플케이션이  '500 Internal Server Error' 응답 받고 중지되어도 컨테이너는 정상이라고 판단한다.

 

웹 앱이 중지되어도 컨테이너가 정상적으로 작동하는 예시

500 상태코드를 받은 웹애플리케이션 예시
컨테이너가 정상적으로 작동하고 있는 예시

컨테이너의 진입점 프로세스가 실행 중 상태이므로 도커는 애플리케이션도 정상상이라고 판단하여 STATUS 가 Up 인것을 확인할 수 있다.

 

Dokcerfile 에서 HEALTHCHECK 인스트럭션을 이용하면 인스트럭션에 정의된 정보를 이용해 동작 중인 애플리케이션의 상태가 정상인지 확인할 수 있다.

  • HEALTHCHECK 동작방식
    • 인스트럭션에 도커 컨테이너안에서 실행하는 명령을 지정
    • 이 명령이 반환하는 상태코드를 보고 애플리케이션의 상태를 파악
    • 도커는 일정간격으로 이 명령을 실행
      • 상태 코드가 연속으로 일정 횟수 이상 실패로 나오면 해당 컨테이너를 이상 상태로 간주

 

Dockerfile 스크립트의 HEALTHCHECK 인스트럭션 예시

FROM diamol/dotnet-aspnet

ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"]
HEALTHCHECK CMD curl --fail http://localhost/health

WORKDIR /app
COPY --from=builder /out/ .
  • ENTRYPOINT 에서 "dotnet" 명령을 실행 → 앱 상태를 확인하기 위해 모니터링하는 프로세스도 "dotnet"
  • HEALTHCHECK CMD curl
    • --fail  = 정상이면 0 이외에는 다른 숫자 반환하도록 하는 옵션
    • http://localhost/health = 버그가 발동했는지 확인하기 위한 또 다른 API 엔트포인트 (500:버그, 200:정상)

< HEALTHCHECK가 적용된 도커파일 사용해보기>

diamol/ch08/exercises/numbers 폴더로 가서 이미지 빌드

docker image build -t diamol/ch08-numbers-api:v2 -f ./numbers-api/Dockerfile.v2 .

 

실행시킨 앱의 건강상태 확인

docker run -d -p 8081:80 diamol/ch08-numbers-api:v2

docker container ls

STATUS : healthy

버그 생성

curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng

 

컨테이너 건강상태 체크 (90초 정도 시간이 지나고 나서 해야 상태가 변함, 3번 건강상태를 체크해야 하기 때문)

docker container ls

에러가 발생하여 건강상태가 unhealthy 로 변함

  • 3 번 연속 건상상태를 확인한 결과 실패하였기 때문에 컨테이너 상태가 이상(unhealthy)로 출력
  • 컨테이너 상태가 이상이여도 종료되지는 않음

최근 컨테이너의 상태를 출력하여 더 자세히 확인해 보자

docker container inspect $(docker container ls --last 1 --format '{{.ID}}')

건강상태는 unhealthy 인데 앱은 runnging 상태인 것을 알 수 있다.

 

왜 이상 상태에 있는 컨테이너를 재시작하거나 다른 컨테이너로 교체하지 않은 것일까?

→ 도커가 컨테이너 재시작/교체 작업을 안전하게 처리할 수 없기 때문

 

클러스터는 항상  컨테이너를 추가로 실행할 여력이 있기 때문에 이상상태를 보이는 컨테이너는 두고, 대체컨테이너를 실행해 앱을 중단 시간없이 상태를 회복가능

디펜던시 체크가 적용된 켄테이너 실행하기

여러 컨테이너에 나뉘어 실행되는 분산 애플리케이션은 이상이 생긴 컨테이너를 교체할 때 처음 앱을 실행할 때처럼 컨테이너 간 의존관계를 고려하지 않기때문에 문제를 격는다.

 

도커가 동작하는 서버가 한 대뿐이라면 웹컨테이너를 실행하기전에 API 컨테이너가 실행되도록 가능하다.

하지만, 클러스터 환경의 컨테이너 플랫폼이라면 컨테이너의 순서까지 통제할수가 없어서 API 가 사용 가능한 상태가 되기 전에 웹 앱이 실행되는 일이 있을 수도 있다.

 

(예시) 컨테이너 상태는 정상인데 핵심 의존 관계를 만족하지 않아 앱이 정상적으로 동작하니 않는 상황

방식 : 실행 중인 모든 컨테이너를 제거해 동작 중인API 컨테이너가 없게한 후 앱 컨테이너를 실행한 다음 웹 브라우저에서 앱에 접근한다. 

docker container rm -f $(docker container ls -aq)
docker container run -d -p 8082:80 diamol/ch08-numbers-web
docker container ls

 

컨테이너 실행 상태 : 컨테이너 실행 중, 앱도 이상이 없음
서버 프로세스가 실행 중이고 컨테이너도 정상이지만 API를 사용할 수 없는 상태여서 웹 앱도 제대로 동작하지 않음

 

이런 경우를 방지하기 위해

의존 관계를 만족하는지 점검하는 디펜던시 체크 기능도 도커 이미지에 추가할 수 있다.

 

디펜던시 체크

  • 앱 실행전에 필요한 요구사항을 체크
  • 명시된 모든 요구사항이  확인되면 디펜던시 체크가 성공하고 앱이 실행
  • 인스트럭션으로 구현 된것은 아니고 앱 실행 명령에 로직을 추가하는 방법으로 구현

디펜던시 체크가 구현된 예시 (CMD 인스트럭션에 구현)

FROM diamol/dotnet-aspnet

ENV RngApi:Url=http://numbers-api/rng

CMD curl --fail http://numbers-api/rng && \
    dotnet Numbers.Web.dll

   먼저 API가 사용가능한 상태라면 curl 명령이 성공하고 이어지는 닷넷 코어 앱 실행 명령을 실행

 

디펜던시 체크가 설정된 컨테이너 실행

docker container run -d -p 8083:80 diamol/ch08-numbers-web:v2

 

도커 컨테이너 상태 확인 ▶ 실행 중인 API 가 없어 v2 는 종료된 것을 확인 가능

 

애플리케이션 체크를 위한 커스텀 유틸리티 만들기

curl은 웹 애플리케이션이나 API 를 테스트하는데 매우 유용한 도구다.

  • 실무에서 개발하는 앱을 테스트하는 목적으로는 사용하지 않는다.
  • 이유 = 사용하지 않을 도구를 추가하면 
    • 이미지 크기증가
    • 외부 공격에 노출될 여지 증가

▶  앱 체크에는 앱과 같은 언어로 구현된 별도의 커스텀 유틸리티를 사용

 

앱과 같은 언어로 구현된 커스텀 유틸리티의 장점

  • 이미지에 추가적인 소프트웨어를 포함 시킬 필요가 없다.
  • 재시도 횟수나 분기등 셸 스크립트로는 표현하기 까다로운 복잡한 체크 로직을 적용할 수 있다. 특히 리눅스와 윈도 양쪽에서 사용할 크로스 플랫폼 이미지라면 더욱 유용
  • 앱과 같은 설정을 사용해 대상 URL을 여러 곳에 반복 정의하거나 수정에서 누락시키는 일을 방지할 수 있다.
  •  컨테이너 실행 전에 확인이 필요한 모든 사항을 검증할 수 있다. (앱과 같은 라이브러리 환경에서 데이터베이스 접속이나 인증서 파일의 존재 유무등)
  • 다양한 상황에서 동작이 가능하다

 

닷넷 코어로 구현한 간단한 HTTP 테스트 유틸리티를 사용해 API 이미지의 헬스 체크와 웹 이미지의 디펜던시 체크 앱 빌드 과정

 마지막 단계 코드

FROM diamol/dotnet-aspnet
ENTRYPOINT ["dotnet", "Numbers.Api.dll"]
HEALTHCHECK CMD ["dotnet", "Utilities.HttpCheck.dll", "-u", "http://localhost/health"]

WOKDIR /app
COPY --from=http-check-builder /out/ .
COPY --from=builder /out/ .

HEALTHCHECK 에서 curl 대신 닷넷 코어로 구현된 테스트 유틸리티를 사용

 

<실습> 모든 컨테이너 삭제후 무작위 숫자 API를 v3 버전의 컨테이너로 실행한다. 이번에는 헬스 체크 간격을 조금 줄인다. 컨테이너의 상태가 정상인지 확인하고 API를 몇번 호출해 상태가 이상으로 바뀌는지 확인하라.

# 기존 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
      -a  = 모든 컨테이너(중지된 컨테이너 포함)를 조회합니다.
      -q = 컨테이너 ID만 출력
      -aq = 모든 컨테이너의 ID를 출력 
      $(...) = 명령어 치환으로  ( )에서 얻은 결과를 가져온다. 

# API를 v3 버전의 이미지로 실행한다.
docker container run -d -p 8080:80 --health-interval 5s diamol/ch08-numbers-api:v3

# 5초 정도 기다린 후 컨테이너 목록을 확인한다.
docker container ls

# API 를 네번 호출 - 세번은 성공, 마지막은 실패
curl http://localhost:8080/rng

# 앱에 버그가 발생했다. 15초 기다린 후 상태가 이상으로 바뀌는지 확인한다.
docker container ls

 

커맨드 실행 출력 내용

4번 째 요청 부터 버그가 발생 → HTTP 테스트 유틸리리티가 세 번 연속 실패를 반환 → unhealthy 상태로 변환

 

<실습> 웹 앱 버전 v3를 실행하라. API 가 없으므로 컨테이너가 바로 종료된다.

docker container run -d -p 8081:80 diamol/ch08-numbers-web:v3
docker container ls --all

 

diamol/ch08-numbers-web:v3 : 웹 앱 컨테이너의 디펜던스 체크가 실패해 컨테이너가 종료

diamol/ch08-numbers-api:v3   : api 컨테이너가 실행 중이지만 컨테이너 이름이 "number-api" 로 지정되지 않아서 웹 앱이 API                                                                컨테이너를 발견하지 못한다.

 

도커 컴포즈에 헬스 체크와 디펜던시 체크 정의하기

디펜던시 체크에 실패했을 때 실행하던 컨테이너를 종료해야 하는 이유

▶  단일 서버에서 앱을 실행 중이라면 이상이 생긴 컨테이너를 새 컨테이너로 교체하면 더 심각한 장애를 일으킬 수 있기 때문

 하지만 종료된 컨테이너를 재시작하거나 이미지에 정의 되지 않은 헬스 체크를 추가할 수는 있다.

 

도커 컴포즈 파일에서 헬스 체크 옵션 설정하기

number-api:
	image: diamol/ch08-numbers-api:v3
    ports:
    	- "8087:80"
    healthCheck:
    	interval: 5s
        timeout: 1s
        retries: 2
        start_period: 5s
    networks:
    	- app-net

 

이미지에 헬스 체크가 정의 되어 있지 않은 경우 컴포즈 파일에서 정의하는 법

numbers-web:
    image: diamol/ch08-numbers-web:v3
    restart: on-failure
    environment:
      - RngApi__Url=http://numbers-api/rng
    ports:
      - "8088:80"
    healthcheck:
      test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "150"]
      interval: 5s
      timeout: 1s
      retries: 2
      start_period: 10s
    networks:
      - app-net

 

<실습> 지금 있는 컨테이너를 모두 삭제하고 도커 컴포즈를 이용해 무작위 숫자 애플리케이션을 실행하라. 애플리케이션이 제대로 실행됐는지 알아보기 위해 실행 후 컨테이너 목록을 확인하라

 

# 컴포즈 파일이 있는 디렉터리로 이동
 
# 현재 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
 
# 애플리케이션 실행
docker-compose ls
 
# 5초를 기다린 다음 컨테이너 목록을 확인
docker container ls
 
# 웹 애플리케이션 로그도 확인
docker container logs numbers-numbers-web-1

 

도커 컴포즈 파일에 depends_on 설정을 사용해 직접 디펜던시 체크를 하도록 하지 않는 이유

▶ 도커 컴포즈가 디펜던시 체크를 할 수 있는 범위가 단일 서버로 제한되기 떄문

헬스 체크와 디펜던시 체크로 복원력있는 애플리케이션을 만들 수 있는 이유

물리 서버가 한 대뿐인 환경이라면 도커 컴포즈에 웹 컨테이너보다 API 컨테이너를 먼저 실행시키라고 지시할 수 있다.

 

10 대의 물리 서버와 20개의 API 컨테이너와 50 여개 웹앱을 실행해야 한다면 어떻게 이 애플리케이션의 시작 절차를 설계해야할까?

---> API 컨테이너를 먼저 실행시킨 후 웹앱을 실행하도록 설계를 한 경우, 마지막 1 개의 API 가 실행이 늦어져 5분이나 걸리면 웹앱은 그 동안 하나도  실행되지 않아서 애플리케이션이 동작 중이라고 할 수가 없다.

  ▶ 여기서 문제는 API 컨테이너가 부족하더라도 웹 앱 컨테이너를 실행하는데는 문제가 없다.

 

!! 디펜던시 체크와 헬스 체크를 도입하면 보이는 효과

  • 플랫폼이 실행 순서를 보장하게 할 필요가 없다.
  • 가능한 빨리 컨테이너를 실행하면 된다.
  • 일부 컨테이너가 의존관계를 만족하지 못한 상태라면 재실행되거나 다른 컨테이너로 교체된다.

운영 환경의 클러스터에서 이러나느 컨테이너의 생애주기

*애플리케이션의 자기수복 :  일시적인 오류를 플래폼이 해소해 주는 것 

      앱에 메모리 누수를 일으키는 까다로운 버그가 있더라도 플랫폼에서 해당 컨테이너를 메모리를 잃지 않은 새 컨테이너로 대체 - 버그를 수정하지 않았지만 애플리케이션은 계속 동작할 수 있다. 

 

헬스 체크와 디펜던시 체크에 주의 할 점

  • 헬스 체크는
    • 주기적으로 실행되므로 시스템에 부하를 주는 내용이어서는 안 된다.
    • 자원을 너무 많이 소모하지 않으면서 앱이 실질적으로 동작 중인지 검증할 수 있는 핵심적인 부분을 테스트해야 한다.
  • 디펜던시 체크는
    • 앱 시작 시에 한 번만 실행되므로 테스트 대상이 빠짐없이 정확하도록 주의해야 한다.
    • 누락된 의존 관계가 있으면 이 문제를 플랫폼이 해결하지 못하면 앱에도 문제가 발생한다.

연습문제 : 메모리 누수 대처

조건

  • 애플리케이션 시작 시 충분한 메모리가 있는지 확인하고, 메모리가 부족한 경우 컨테이너를 종료한다
  • 애플리케이션 실행 중 4 초 간격으로 최대치를 초과해 메모리를 사용하는지 확인한다. 최대치를 초과했다면 해당 컨테이너의 상태를 이상으로 판정해야 한다.
  • 테스트 로직은 memory-check.js 스크립트에 이미 작성돼 있다. Dockerfile 스크립트에서 테스트 스크립트를 그대로 사용하면 된다.
  • 테스트 스크립트와 Dockerfile 슼크립트는 ch08/lab 디렉터리에 있다.

Dokerfile 스크립트

FROM diamol/node

ENV MAX_ALLOCATION_MB=4096 \
    LOOP_ALLOCATION_MB=512 \
    LOOP_INTERVAL_MS=2000

CMD ["node", "memory-hog.js"]

WORKDIR /app
COPY src/ .

 

 

Dockerfile solution

더보기

FROM diamol/node

ENV MAX_ALLOCATION_MB=4096 \
    LOOP_ALLOCATION_MB=512 \
    LOOP_INTERVAL_MS=2000

CMD node memory-hog.js && \

          node memory-check.js

 

HEALTHCHECK --interval=5S \

  CMD node memory-check.js

WORKDIR /app
COPY src/ .

 

문제 풀면서 틀린 부분

 

CMD ["node", "memory-hog.js", "memory-check.js"] 

  • 형태: JSON 배열 방식
  • 실행: node memory-hog.js memory-check.js 로 실행
  • 동작: node 명령어에 두 파일이 인자로 전달
  • 결과: node는 첫 번째 파일(memory-hog.js)만 실행하고, 두 번째 인자(memory-check.js)는 실행되지 않고 단순히 인자로 전달
  • 의도: 만약 memory-hog.js가 다른 스크립트를 받아서 실행하도록 설계되었다면 정상 동작하지만, 그렇지 않다면 memory-check.js는 무시됨.

 

 

실행 계획(EXPLAIN)

옵티마이저가 SQL문을 어떤 방식으로 어떻게 처리할 지를 계획한 걸 의미

 이 실행 계획을 보고 비효율적으로 처리하는 방식이 있는 지 점검하고, 비효율적인 부분이 있다면 더 효율적인 방법으로 SQL문을 실행하게끔 튜닝을 하는 게 목표

 

사용하는 방법

# 실행 계획 조회하기
EXPLAIN [SQL문]

# 실행 계획에 대한 자세한 정보 조회하기
EXPLAIN ANALYZE [SQL문]

 

사용예시

   가정 :  콜롬이 name, age 로 구성된 users 테이블에 7명의 데이터가 저장되어 있다.


EXPLAIN [SQL문]

EXPLAIN SELECT * FROM users
WHERE age = 23;

 

결과

  • id : 실행 순서
  • select_type : (처음에는 몰라도 됨)
  • table : 조회한 테이블 명
  • partitions : (처음에는 몰라도 됨)
  • type : 테이블의 데이터를 어떤 방식으로 조회하는 지 
  • possible keys : 사용할 수 있는 인덱스 목록을 출력 
  • key : 데이터 조회할 때 실제로 사용한 인덱스 값 
  • key_len : (처음에는 몰라도 됨)
  • ref : 테이블 조인 상황에서 어떤 값을 기준으로 데이터를 조회했는 지
  • rows : SQL문 수행을 위해 접근하는 데이터의 모든 행의 수 (= 데이터 액세스 수)  이 값을 줄이는 게 SQL 튜닝의 핵심
  • filtered : 필터 조건에 따라 어느 정도의 비율로 데이터를 제거했는 지 의미
    → filtered 비율이 낮을 수록 쓸데없는 데이터를 많이 불러온 것.
    → filtered의 값이 30이라면 100개의 데이터를 불러온 뒤 30개의 데이터만 실제로 응답하는데 사용했음을 의미한다.
  • Extra : 부가적인 정보를 제공
    → ex. Using where, Using index

(주의점) rows, filtered의 값은 정확한 수치가 아닌 추정값이기 때문에 오차가 있을 수 있다.

 

실행 계획에서 type 의미 분석하기

All : 풀 테이블 스캔

인덱스를 활용하지 않고 테이블을 처음부터 끝까지 전부다 뒤져서 데이터를 찾는 방식 → 비효율적이므로 최적화 필요

DROP TABLE IF EXISTS users; # 기존 테이블 삭제

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    age INT
);

INSERT INTO users (name, age) VALUES 
('Alice', 30),
('Bob', 23),
('Charlie', 35);

# 실행 계획 조회하기
EXPLAIN SELECT * FROM users WHERE age = 23;

 

 

type 에 ALL 이 나온 이유

  • users 테이블의 데이터는 age를 기준으로 정렬되어 있지 않고 id를 기준으로 정렬되어 있다.
  • age = 23의 값을 가진 데이터를 찾으려면 테이블의 처음부터 끝까지 다 뒤져봐야 한다.
    • 그래서 실행 계획의 type이 ALL로 나온 것 

index : 풀 인덱스 스캔

인덱스 테이블을 처음부터 끝까지 다 뒤져서 데이터를 찾는 방식  인덱스의 테이블은 실제 테이블보다 크기가 작기 때문에, 풀 테이블 스캔(Full Table Scan)보다 효율적

 

# 높은 재귀(반복) 횟수를 허용하도록 설정
# 생성할 더미 데이터의 개수와 맞춰서 작성
SET SESSION cte_max_recursion_depth = 1000000; 

# 더미 데이터 1000000 삽입 쿼리
INSERT INTO users (name, age)
WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 1000000 
)
SELECT 
    CONCAT('User', LPAD(n, 7, '0')),   -- 'User' 다음에 7자리 숫자로 구성된 이름 생성
    FLOOR(1 + RAND() * 1000) AS age    -- 1부터 1000 사이의 난수로 나이 생성
FROM cte;


# 인덱스 생성
CREATE INDEX idx_name ON users (name);

# 실행 계획 조회
EXPLAIN SELECT * FROM users 
ORDER BY name 
LIMIT 10;
  1. name을 기준으로 정렬해서 데이터를 가져와야 하기 때문에, name을 기준으로 정렬되어 있는 인덱스를 조회
    • 덩치가 큰 users 테이블의 데이터를 하나씩 찾아보면서 정리를 하는 것보다, 이미 name을 기준으로 정렬되어 있는 인덱스를 참고하는 게 효율적이라고 판단
  2. 모든 인덱스의 값을 다 불러온 뒤에 최상단 10개의 인덱스만 추출
  3. 10개의 인덱스에 해당하는 데이터를 users 테이블에서 조회

 

const  : 1건의 데이터를 바로 찾을 수 있는 경우

고유 인덱스 또는 기본 키를 사용해서 1건의 데이터만 조회한 경우에 type 에 const가 출력된다.

 

예시

DROP TABLE IF EXISTS users; # 기존 테이블 삭제

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    account VARCHAR(100) UNIQUE
);

INSERT INTO users (account) VALUES 
('user1@example.com'),
('user2@example.com'),
('user3@example.com');


EXPLAIN SELECT * FROM users WHERE id = 3;
EXPLAIN SELECT * FROM users WHERE account = 'user3@example.com';

UNIQUE 로 설정한 콜롬은 인덱스가 자동으로 생성이 되고 고유한 값이므로 한번의 검색으로 해결이 된다.

 

range : 인덱스 레인지 스캔(Index Range Scan)

인덱스를 활용해 범위 형태의 데이터를 조회한 경우를 의미

  • 범위 형태란 BETWEEN, 부등호(<, >, ≤, ≥), IN, LIKE를 활용한 데이터 조회
  • 데이터를 조회하는 범위를 신경써야 한다.

예시

DROP TABLE IF EXISTS users;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    age INT
);

-- 높은 반복 횟수를 허용하도록 설정
-- (아래에서 생성할 더미 데이터의 개수와 맞춰서 작성)
SET SESSION cte_max_recursion_depth = 1000000;  

-- 더미 데이터 삽입 쿼리
INSERT INTO users (age)
WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n < 1000000 -- 생성하고 싶은 더미 데이터의 개수
)
SELECT 
    FLOOR(1 + RAND() * 1000) AS age    -- 1부터 1000 사이의 난수로 나이 생성
FROM cte;

-- 인덱스 생성
CREATE INDEX idx_age ON users(age);

 

실행계획 조회

EXPLAIN SELECT * FROM users
WHERE age BETWEEN 10 and 20;

EXPLAIN SELECT * FROM users
WHERE age IN (10, 20, 30);

EXPLAIN SELECT * FROM users
WHERE age < 20;

 

ref : 풀 인덱스 스캔

UNIQUE가 아닌 컬럼의 인덱스를 사용한 경우 type에 ref가 출력

 

예시

DROP TABLE IF EXISTS users; # 기존 테이블 삭제

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);

INSERT INTO users (name) VALUES 
('박재성'),
('홍길동'),
('임정우');

CREATE INDEX idx_name ON users(name);

 

EXPLAIN SELECT * FROM users WHERE name = '임정우';

 

 

참고 강의

(인프런) 비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전

 

Index

MySQL은 UNIQUE 제약 조건을 추가하면 자동으로 Index가 생성된다.

- UNIQUE 옵션을 사용하면 Index가 같이 생성되기 때문에 조회 성능이 향상된다.

Index 를 사용하면 데이터를 조회할 때의 성능이 향상된다.

 

그러면 Index 를 무조건적으로 많이 추가하는 게 좋을까?

  ⊕ Index 를 추가하면 조회 성능은 올라간다.

  ⊖ 쓰기 작업(삽입, 수정, 삭제)의 성능은 저하된다. 

 

<중요>

- 최소한의 인덱스만 사용하려고 하자.

- 인덱스를 추가하면 조회 속도는 빨라지나, 쓰기(삽입, 수정, 삭제) 속도는 느려짐을 항상 기억하자.

 

Multiple-Column Index

2개 이상의 컬럼을 묶어서 인덱스를 생성하는 방법

 

user 라는 테이블에 (이름, 부서, 나이) 컬럼이 있다고 가정해보자.

 

멀티 컬럼 인덱스 생성 예시

CREATE INDEX idx_부서_이름 ON users (부서, 이름);
  • 앞에 있는 부서를 기준으로 데이터를 정렬한 후 그 다음 같은 부서 내에서 이름을 기준으로 정렬

데이터 조회할 때 인덱스를 활용

SELECT * FROM users
WHERE 부서 = '인사' 
ORDER BY 이름;
  • 이미 (부서, 이름)으로 인덱스를 만들어놔서 WHERE 부서 = ‘인사’를 만족하는 데이터들은 금방 찾을 수 있다.
  • ORDER BY 이름; 을 사용했지만 불러오면 이미 정렬이 되어 있다.

주의 할 점

1. 이미 정렬된 것을 또 정렬할 필요 없다.

  •  멀티 컬럼 인덱스(부서, 이름)를 부서 컬럼의 인덱스처럼 활용할 수 있다.
SELECT * FROM users
WHERE 부서 = '인사' 
ORDER BY 이름;

 에서 "ORDER BY 이름" 을 사용할 필요가 없다.

  • 멀티 컬럼 인덱스에서 이미 부서를 기준으로 정렬이 되어 있으므로 부서 인덱스를 따로 생성할 필요가 없다. 이 멀티 컬럼 인덱스를 바로 사용하면 된다.

2. 멀티 컬럼 인덱스(부서, 이름)를 이름 컬럼의 인덱스 처럼 활용할 수 없다.

  • 부서를 기준으로만 정렬이 되어 있지 이름을 기준으로 정렬이 되어 있지는 않다.
  • 멀티 컬럼 인덱스에서 일반 인덱스처럼 활용할 수 있는 건 처음에 배치된 컬럼들뿐

3. 순서에 주의해야 한다.

  • 소분류 → 중분류 → 대분류 컬럼 순으로 정렬하기
    • 소분류를 탐색한 다음 대분류를 탐색하는 방식이 더 빠르기 때문
      • 예)  대기업에서 "회계"부서에서 "홍길동"을 찾는다고 해보자. 회계 부서에 있는 인원보다 홍길동 이름을 가진 인원이 적기 때문에 홍길동을 찾은뒤 회계 부서 사람인지 확인하는 것이 더 빠르다.
      • 데이터 중복도가 낮은 컬럼이 앞쪽으로 오는 것이 좋다.

요약

- 멀티 컬럼 인덱스 컬럼의 순서는 매우 중요하다.

- 멀티 컬럼 인덱스에서 처음에 배치된 컬럼들은 일반 인덱스처럼 활용할 수 있다.

- 멀티 컬럼 인덱스를 구성할 때 데이터 중복도가 낮은 컬럼이 앞쪽으로 오는 게 좋다. 

 

커버링 인덱스(Covering Index)

SQL문을 실행시킬 때 필요한 모든 컬럼을 갖고 있는 인덱스

 

예시 유저 테이블과 인덱스

위의 데이터를 조회하기 위해 실행하는 SQL 문 예시

SELECT id, created_at FROM users;
SELECT id, name FROM users;

첫번째 SQL = 두 컬럼(id, created_at)만 조회하는데 실제 데이터 테이블에 접근을 해야한다.

두 번째 SQL = idname 컬럼은 실제 테이블에 접근하지 않고 인덱스에만 접근해서 알아낼 수 있는 정보들

SQL문을 실행시킬 때 필요한 모든 컬럼을 갖고 있는 인덱스를 커버링 인덱스 라고 한다.

 

 

참고 강의

(인프런) 비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전

 

도커 컴포즈 파일의 구조

도커 컴포즈 파일?

  • 모든 컴포넌트가 실행 중일 때 애플리케이션이 어떤 상태여야 하는지를 기술하는 파일
  • docker container run 명령으로 컨테이너를 실행할때 지정하는 모든 옵션을 한데 모아 놓은 단순한 형식의 파일

* 컨테이너 : 실제로 실행되고 있는 도커 인스턴스. 예를 들어, 웹 서버 (예: Nginx)가 실행되고 있는 컨테이너, 애플리케이션 서버(“Node.js 앱)이 실행되고 있는 컨테이너. 즉 웹 서버, 애플리케이션 서버, 데이터베이스 각각이 하나의 컨테이너로 실행되고 있는 것. 

 

* 서비스 : 각 컨테이너는 하나의 역할을 담당하지만, 도커 컴포즈에서는 같은 역할을 하는 컨테이너들을 "서비스"라는 논리적인 단위로 묶어서 관리

 

도커 컴포즈 파일의 전체 스크립트.( to-do 애플리케이션을 실행하는 내용)

version: '3.7'

services:
  
  todo-web:
    image: diamol/ch06-todo-list
    ports:
      - "8020:80"
    networks:
      - app-net

networks:
  app-net:
    external:
      name: nat

 - YAML 문법으로 기술 , YAML 문법은 들여쓰기를 통해 구조를 정의하므로 들여쓰기가 중요

 

도커 컴포즈 파일의 구조

  • version
    • 파일에 사용된 도커 컴포즈 파일 형식의 버전을 나타낸다 : 버전마다 문법과 표현 가능 요소에 많은 변화가 있었으므로 버전 지정 필수
      • 버전을 포함하여 실행하면 warnning messge 가 발생하지만 무시해도 된다.
        • the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
  • networks
    • 서비스 컨테이너가 연결될 모든 도커 네트워크를 열거하는 부분
  • services
    • 애플리케이션을 구성하느 모든 컴포넌트를 열거하는 부분
    • 도커 컴포즈에서는 실제 컨테이너 대신 서비스 개념을 단위로 삼는다. 하나의 서비스를 같은 이미지로 여러 컨테이너에서 실행할 수 있기 때문이다.
      • 컨테이너 대신 서비스 개념을 단위로 삼는 이유
        • 도커 컴포즈는 단순히 하나의 컨테이너만 다루는 게 아니라, 여러 컨테이너를 조합해서 큰 애플리케이션을 구성하도록 설계되어 있기 때문
        • 하나의 서비스에 같은 역할(웹서버 | 데이터베이스 | 애플리케이션 서버)을 하는 여러 컨테이너를 하나의 서비스로 묶어서 관리가 가능하기 때문
      • 같은 이미지를 여러 컨테이너에서 실행한다는 뜻
        • 서비스의 설정에서 replicas(복제본) 옵션을 사용하면 같은 이미지를 사용하는 컨테이너 여러 개를 실행 가능
          • 예시
services:
  web:
    image: my-web-app:latest
    deploy:
      replicas: 3

 

왜 여러 컨테이너를 묶는 걸까?  → 확장성(Scalability) 때문

 

트래픽이 많은 웹 서버를 운영해야 한다고 가정

  • 하나의 컨테이너만 실행하면 그 컨테이너가 모든 요청을 처리해야 하므로 과부하가 생길 수 있다.
  • 이를 해결하기 위해 같은 이미지를 사용하는 여러 컨테이너를 실행해서 트래픽을 분산 처리해야 한다.

▶ 이때, 도커 컴포즈에서 같은 역할을 하는 여러 컨테이너를 하나의 "서비스"로 묶어서 관리하면 쉽고 효율적

 

 

도커 컴포즈 파일의 전체 스크립트 중 아래의 부분이 어떻게 구성이 되는지를  보여주는 그림

services:
  
  todo-web:
    image: diamol/ch06-todo-list
    ports:
      - "8020:80"
    networks:
      - app-net

애플리케이션이 어떤 리소스로 어떻게 구성되는지 나타낸 그림

① todo-web 이라는 서비스는  diamol/ch06-todo-list 이미지로부터 단일 커테이너로 실행

② 이 컨테이너는 호스트 컴퓨터의 80번 포트로 자신의 8020번 포트를 공개

③ app-net 이라는 이름의 도커 네트워크에 연결

 

최종적인 결과는 아래의 명령을 실행한 것과 같은 상태가 된다.

docker container run -p 8020:80 --name todo-web --network nat diamol/ch06-todo-list

 

네트워크 부분 내용 설명

networks:
  app-net:
    external:
      name: nat

    nat 이라는 이름의 외부 네트워크로 연결

    external =  nat 네트워크가 이미 존재하므로 새로 생성하지 말라는 뜻

 

 

도커 컴포즈 파일 실행 방법

docker network create nat

cd (ch07/exercises/todo-list 가 있는 경로)

docker-compose up

Container todo-list-todo-web-1  Cre...    ← 현재 있는 리소스와 애플리케이션을 구성하는 리소스를 비교해 더 필요한 요소를 생성

Attaching to todo-web-1 아래는 ←  to-do 애플리케이션 시동 로그

 

도커 컴포즈를 사용해 여러 컨테이너로 구성된 애플리케이션 실행하기

4장에서 오늘의 천문 사진을 보여주는 애플리케이션은 여러가지 언어를 이용하여 구현된 분산 애플리케이션이다.

  • 웹 프런트엔드 = 자바 (image-gallery)
  • 로그 수집 모듈 = Node.js (accesslog)
  • REST API  =  GO (iotd)

 

4 장에서는 분산 애플리케이션을 실행한 과정

1. 차례로 이들 컨테이너를 실행 시켜서 애플리케이션을 가동

2. 모든 컨테이너를 동일한 도커 가상 네트워크에 미리 약속된 이름으로 접속시켜 애플리케이션의 구성 요소가 서로 통신할 수 있도록 구성

 

4 장에서 실행한 분산 애플리케이션을 실행하기 위한 도커 컴포즈 스크립트

accesslog:
	image: diamol/ch04-access-log

iotd:
	image: diamol/ch04-image-of-the-day
    ports:
    	- "80"

image-gallery:
	image: diamol/ch04-image-gallery
    ports:
    	- "8010:80"
    depends_on:
    	- accesslog
        - iotd
  • accesslog = 설정값과 포트가 필요없으므로 이미지 이름만 기술
  • iotd = REST API 이므로 포트 번호설정
  • image-gallery = depends_on 을 실행하여 의존성을 선언.
    • image-gallery 서비스를 실행하기 전에, 이 의존성을 만족하기 위해 iotd, accesslog 를 먼저 실행하여 시도함.

 

위 애플리케이션의 아키텍쳐를 나타낸 그림

* 참고로 이 그림은 도커 컴포즈 스크립트를 다이어그램으로 변환해 주는 도구를 사용해 만든것 

( https://github.com/pmsipilot/docker-compose-viz )

 

 

<분리 모드 (detachede mode)로 애플리케이션을 실행하기>

(docker-compse 파일이 있는 폴더로 이동한 후)

docker-compose up --detach

 - 컨테이너는 백그라운드에서 실행

- 로그를 확인하려면 명령어를 별도로 실행      docker-compose logs <service_name>

 

실행 한 결과

http://localhost:8010/ 에 접속하면 4장에서 실행한 애플리케이션과 동일하게 작동

 

< 도커 컴포즈를 사용해 iotd 서비스의 컨테이너 수를 늘려보기>

이점 : 컴포즈 파일을 이용해여 여러 개의 컨테이너로 구성된 애플리케이션을 마치 한 덩어리 처럼 다룰 수 있음.

특히 API 서비스는 상태가 없으므로 컨테이너를 늘리는 방법으로 스케일 아웃할 수 있다. 즉, 웹 컨테이너가 API에 데이터를 요청하면 도커가 여러 개의 API 컨테이너에 이 요청을 고르게 분배

 

docker-compose up -d --scale iotd=3

명령 실행 후 출력된 결과

이제 http://localhost:8010/ 페이지를 여러 번 새로고침을 해보자

docker-compose logs --tail=1 iotd

   --tail=1 : 각 iotd 컨테이너의 제일 마지막 로그만 출력

출력 된 내용을 보면 새로고침 요청을 늘어난 3개의 컨테이너가 고르게 나눠 처리한 것을 볼수 있다.

 

docker-compose ls

 

도커 컴포즈를 이용하여 재시작

// 애플리케이션 중지, 컴포즈 리소스와 컨테이너 삭제
docker-compose down 

// 컴포즈 파일에 정의된 대로 리소소를 다시 생성, 애플리케이션 재시작
docker-compose up -d

docker-compose ls

 

앞에서 iot를 3개로 확장했던 것이 사라진 것을 running(3) 에서 알수 있다.

 

< 참고 > 도커 컴포즈는 YAML 파일에 정의된 애플리케이션 정의에 의존하는 클라이언트 측 도구임을 잊어서는 안된다.

 

도커 컨테이너간의 통신

애플리케이션 생애주기동안에 컨테이너가 교체되면 IP 주소도 변경된다.

IP 주소가 변경돼도 문제가 없도록 도커에서 DNS 를 이용해 서비스 디스커버리 기능을 제공한다.

 

<iot를 3개로 확장한 후 DNS 조회 : nslookup>

docker-compose up -d scale --iotd=3

docker container exec -it image-of-the-day-image-gallery-1 sh
/web # nslookup aceeslog

  accesslog 서비스를 DNS에 조회한 결과 해당 컨테이너의 IP 주소가 조회된 것을 볼 수 있다.

 

도커 네트워크에 연결된 모든 컨테이너는 이 네트워크의 범위에 포함되는 IP 주소를 부여 받는다. 

그리고 이 네트워크를 통해 컨테이너 간 통신이 가능하다.

▶ DNS 조회를 사용하면 컨테이너가 교체돼 IP 주소가 변경되더라도 항상 새로 만들어진 컨테이너에 접근할 수 있다.

 

< accesslog 컨테이너 삭제 후 애플리케이션 재실행할때 컴포즈의 행동 확인 > 

만약 도커 명령행에서 accesslog 컨테이너를 직접 삭제하고 도커 컴포즈로 애플리케이션을 재실행할 때

1. accesslog 컨테이너가 없으므로 새로운 accesslog 컨테이너를 생성

2. 애플리케이션 재실행

▶ DNS 조회하면 새로운 IP 주소가 할당된것을 알 수 있다.

 

accesslog 컨테이너 삭제

docker container rm -f image-of-the-day-accesslog-1

삭제 후 DNS 확인한 결과

docker-compose up -d --scale iotd=3

을 실행한 후 결과

accesslog 컨테이너가 없으므로 새롭게 accesslog 컨테이너를 생성하여 대응

 

 DNS 확인한 결과

DNS 조회 결과를 보면 accesslog 서비스에 새로 배정된 컨테이너가 기존 컨테이너의 IP를 그래도 유지했다.

이유) 기존 컨테이너가 삭제되면서 부여됐던 IP 주소도 재사용이 가능해졌기 때문

 

도커 컴포즈로 애플리케이션 설정값 지정하기

소규모 프로젝트라면 어떤 데이터 베이스를 사용하더라도 문제가 없다.

일정 규모 이상의 애플리케이션이라면 별도의 데이터베이스를 사용하는 것이 낫다.

이번에는 SQLite 데이터베이스를 사용하는 to-do 앱을  원격 컨테이너에서 동작하는 PostgreSQL 데이터베이스를 이용해보자.

 

SQLite → PostgreSQL

PostgreSQL을 사용하는 to-do 앱의 서비스를 정의한 컴포즈 파일

services:
  todo-db:
    image: diamol/postgres:11.5
    ports:
      - "5433:5432"
    networks:
      - app-net

  todo-web:
    image: diamol/ch06-todo-list
    ports:
      - "8030:80"
    environment:
      - Database:Provider=Postgres
    depends_on:
      - todo-db
    networks:
      - app-net
    secrets:
      - source: postgres-connection
        target: /app/config/secrets.json

networks:
  app-net:

secrets:
  postgres-connection:
    file: ./config/secrets.json
  • todo-db 에서 사용할 데이터벵스 PostgresSQL 설정
  • todo-wb
    • environment 에서 사용할 데이터베이스 지정
    • depends_on 에서 todo-db 연결 (의존성 연결)
    • secrets:
      • postgres-connection 에 설정된 비밀값을  secrets.json 에 기록
  • secrets
    • postgres-connection 이름으로 로컬에서 비밀값을 읽어오는 부분

위의 도커컴포즈 파일이 있는 폴더에서 실행했을 때 출력된 내용

docker-compose up -d

 

Tip. docker-compose 로 실행한 애플리케이션 만 보는 명령어

docker-compose ps

 

도커 컴포즈도 만능은 아니다.

docker-compose 파일을 실행하면 내가 설정한 상태로 애플리케이션을 실행할 수 있다.

하지만, 애플리케이션이 지속적으로 정의된 상태를 유지하도록 하는 기능이 없다.

→ 일부 컨테이너가 오류를 일으키거나 강제로 종료되면 docker-compose up 명령을 다시 실행시켜야 애플리케이션의 상태를 원대로 되돌아 간다.

 

도커 컴포즈를 사용하기 적합한 시기를 나타내는 그림

  • 자신의 컴퓨터에서 앱을 실행하고 e2e 테스트를 수행하는데 사용
  • 지속적 통합 프로세스 중 빌드 및 자동화된 테스트에 컴포즈를 사용해 앱을 실행
  • 단일 서버에서 컴포즈를 실행해 테스트를 진행하며 테스트 환경을 최소한으로 유지
  • 운영환경에는 컴포즈보다는 도커 스윔이나 쿠버네티스를 사용, 하지만 애플리케이션 정의에는 컴포즈 파일을 포맷을 사용

연습문제: 도커 컴포즈를 이용하여 애플리케이션을 좀 더 신뢰성 있게 실행하기

 

조건

  • 호스트 컴퓨터가 재부팅되거나 도커 엔진이 재시작되면 애플리케이션 컨테이너도 재시작되도록 하라.
  • 데이터베이스 컨테이너는 바인드 마운트에 파일을 저장해 애플리케이션을 재시작하더라도 데이터를 유지할 수 있도록 하라.
  • 테스트를 위해 웹 애플리케이션은 80 번 포트를 주시하도록 하라.

참고

https://docs.docker.com/reference/compose-file/

 

Compose file reference

Find the latest recommended version of the Docker Compose file format for defining multi-container applications.

docs.docker.com

 

정답?

더보기

version: "3.7"

services:
  todo-db:
    image: diamol/postgres:11.5
    restart: unless-stopped
    environment:
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - type: bind
        source: /data/postgres
        target: /var/lib/postgresql/data
    networks:
      - app-net

  todo-web:
    image: diamol/ch06-todo-list
    restart: unless-stopped
    ports:
      - "8050:80"
    environment:
      - Database:Provider=Postgres
    depends_on:
      - todo-db
    secrets:
      - source: postgres-connection
        target: /app/config/secrets.json

secrets:
  postgres-connection:
    file: postgres-connection.json

networks:
  app-net:
    external:
      name: nat

설명

더보기

restart: unless-stopped

위 설정의 동작

* 컨테이너가 비정상적으로 종료될 경우 자동 재시작

* 사용자가 컨테이너를 수동으로 중지한 경우에는 재시작하지 않음

* 서버 재부팅 시 다시 시작

-> 비정상 종료되거나 서버 재부팅 시 다시 시작하지만, 수동으로 중지된 경우는 재시작하지 않음. 

 

 

 

   volumes:
      - type: bind
        source: /data/postgres
        target: /var/lib/postgresql/data

 

-> bind 를 이용하여 데이터를 보존

source : 호스트 컴퓨터에서 데이터를 저장하는 경로

target : 컨테이너 내부 경로, 

 

컨테이너의 애플리케이션은 target 디렉토리에 데이터를 저장하지만, 실제로 그 데이터는 호스트의 source 경로에 저장

Infrastructure Layer

외부 시스템과의 통신을 담당하며, 애플리케이션의 비즈니스 로직과 외부 시스템 간의 추상화 계층 역할을 합니다.

주요 역할

  • 데이터베이스, 파일 시스템, 네트워크 서비스 등의 외부 리소스와의 상호작용
  • 구현 기술에 대한 세부사항 처리: 레포지토리 구현, 인프라 서비스 포함

인프라스트럭처 서비스 예시

  1. 외부 API 통합
    • RESTful API 및 GraphQL API 통합
    • 소셜 미디어 API (예: Facebook, Twitter)
    • 지불 게이트웨이 (예: Stripe, PayPal)
    • HTTP 클라이언트 사용: Axios, Fetch 등
  2. 메시징 서비스
    • 이메일 발송 : AWS SES, SendGrid
    • SMS 발송 : Twilio, Nexmo
    • 메시지 큐 : RabbitMQ, Kafka, AWS SQS
  3. 데이터베이스 접근
    • ORM 설정: TypeORM, Sequelize, Prisma
    • 데이터베이스 연결 관리 및 트랜잭션 처리

이점

코드 작성

책임 분리 (Separation of Concerns)

  • 비즈니스 로직과 기술적 구현을 분리하여, 각 레이어의 역할이 명확
  • 예) 도메인 레이어는 데이터 처리 규칙만 다루고, 인프라스트럭처 레이어는 데이터베이스 접근만 담당

유지보수 용이성

  • 외부 시스템(데이터베이스, API 등)이 변경되더라도, 인프라스트럭처 레이어에서만 수정하면 된다.
  • 구현 세부 사항이 도메인 로직에 노출되지 않아, 코드 수정 범위를 최소화 가능

재사용성

  • 공통적으로 사용하는 서비스(예: 이메일 발송, API 호출 등)를 모듈화하여 재사용 가능
  • 여러 도메인에서 동일한 인프라스트럭처 서비스를 활용

확장성

  • 새로운 데이터베이스나 외부 API를 추가하거나 변경할 때도 기존 구조를 크게 변경하지 않고 확장가능

협업

역할 집중

  • 외부 시스템과의 통신을 인프라스트럭처 레이어가 담당하여, 도메인 팀은 비즈니스 로직에만 집중 가능

변경 시 영향 최소화

  • 외부 API나 데이터베이스 변경 시, 인프라스트럭처 레이어에서만 작업하면 되므로 다른 팀원 작업에 영향이 적음.

표준화된 인터페이스

  • 인터페이스(예: 레포지토리 패턴)를 통해 도메인 로직과 인프라스트럭처 간의 통신 방식을 표준화하여 충돌 방지
  • 일관된 방식으로 외부 시스템과 상호작용하게 되어 협업 효율이 향상

테스트 코드

테스트 용이성

  • Mock 객체 또는 Stub을 사용하여 외부 시스템 의존성을 제거
  • 외부 시스템과 독립적으로 도메인 로직이나 비즈니스 규칙에 대한 단위 테스트 수행 가능

테스트 환경

  • 운영 환경과 유사한 조건에서 전체적인 통합 동작 검증 가능
  • 외부 시스템과의 통합 동작을 실제 환경이나 시뮬레이션된 환경에서 검증
  • 실제 데이터베이스 연결이나 API 호출이 제대로 동작하는지 확인가능

문제진단 용이

  • 외부 시스템 관련 문제는 외부 연동과 관련된 코드를 우선적으로 살펴보고 빠르게 진단하고 해결가능

 

 

  •  

+ Recent posts