동시성 테스트를 하다 발견한 Docker의 CPU 계산법

2026. 4. 1. 17:44·우아한 테크 코스 8기

0. 개요

우아한 테크 코스 Level1 공통 교육 DB에 있는 문제를 생각해보다가 발생한 일을 나열해보려 한다.

1. 출석 시스템에서 동시에 100명이 등교 버튼을 누른다면 어떤 일이 일어날까?

1 - 1. 테이블 구조

교육을 들으면서 알게되는 테이블 실습 구조는 다음과 같다.

특정 크루의 출석 테이블이 쌓이게 된다.

이런 식으로

1 - 2. 생각해보기에서 다음과 같은 질문을 우리에게 던진다.

문제: 출석 시스템에서 동시에 100명이 등교 버튼을 누른다면 어떤 일이 일어날까?
출제 의도: 실습에서 직접 다룬 INSERT/UPDATE가 실제 운영 환경에서는 동시성 문제를 일으킬 수 있다. 원자성(Atomicity)과 격리성(Isolation)이 왜 필요한지 출석 시스템의 맥락에서 구체적으로 떠올려보자.

1 - 3. 나의 생각

문제와 출제 의도만 읽어보면 뭔가 동시성 문제가 발생할 것 같은 느낌을 받을 수 있다.

그러나, 크루 A가 등교 버튼을 누르면 Attendance 테이블에 자신의 crew_id를 기반으로한 새로운 attendance 데이터가 생성된다.

공유 자원을 사용할 일도 없고 다른 크루의 attendance를 사용하지 않는다.

즉, 나의 생각은 "동시성 문제가 발생하지 않는다." 이다.

 

단순히 이론적으로 생각해보기 보다는 직접 실험을 해보기로 했다.

 

2. 부하 테스트 환경

2 - 1. 환경

http 요청 테스트를 진행하지는 않았다.

Python으로 Insert 쿼리를 동시에 날리는 코드를 만들어 실행했다.

테스트 유저는 총 300명까지 crew_1부터 crew_300으로 채워두었다.

DB는 MySQL 8.0 Image를 가지는 Docker Container로 실행했다.

2 - 2. Python Code

시원하게 Gemini에게 만들어달라고 했다.

테스트를 할 때, 주의할 점은 윈도우상에서 메모리를 갑자기 100개를 만들어버리면 오류가 발생해버릴 수 있어서,

max_workers=100을 적당히 조절해가는 걸 추천한다. 

내 노트북에서는 100으로 설정하니까, 실행이 될 때도 있고 안될 때도 있었다.

import mysql.connector
import time
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

DB_CONFIG = {
    "host": "localhost",
    "port": 3307,
    "user": "root",
    "password": "1234",
    "database": "test_db",
    "ssl_disabled": True,
    "connection_timeout": 10
}

def check_in(crew_id):
    conn = None
    start = time.time()
    try:
        conn = mysql.connector.connect(**DB_CONFIG)
        cur = conn.cursor()
        cur.execute("""
            INSERT INTO attendance (crew_id, attendance_date, start_time)
            VALUES (%s, CURDATE(), %s)
        """, (crew_id, datetime.now().strftime("%H:%M:%S")))
        conn.commit()
        return {"crew_id": crew_id, "status": "success", "elapsed": (time.time() - start) * 1000}
    except mysql.connector.errors.IntegrityError:
        return {"crew_id": crew_id, "status": "duplicate", "elapsed": (time.time() - start) * 1000}
    except Exception as e:
        return {"crew_id": crew_id, "status": "fail", "elapsed": (time.time() - start) * 1000, "error": str(e)}
    finally:
        if conn is not None:
            conn.close()

print("=== 부하 테스트 시작 ===")
start = time.time()

results = []
with ThreadPoolExecutor(max_workers=100) as executor: # 이 부분을 조정
    futures = [executor.submit(check_in, crew_id) for crew_id in range(1, 301)]
    for future in as_completed(futures):
        results.append(future.result())

total = (time.time() - start) * 1000

# 집계
success  = [r for r in results if r["status"] == "success"]
fail     = [r for r in results if r["status"] == "fail"]
duplicate= [r for r in results if r["status"] == "duplicate"]
times    = sorted(r["elapsed"] for r in results)

print(f"총 소요:      {total:.0f}ms")
print(f"성공:         {len(success)}건")
print(f"중복:         {len(duplicate)}건")
print(f"실패:         {len(fail)}건")
print(f"평균 응답:    {sum(times)/len(times):.1f}ms")
print(f"p50:          {times[int(len(times)*0.50)]:.1f}ms")
print(f"p95:          {times[int(len(times)*0.95)]:.1f}ms")
print(f"p99:          {times[int(len(times)*0.99)]:.1f}ms")

# 실패 상세 출력
if fail:
    print("\n--- 실패 상세 ---")
    for r in fail:
        print(f"  crew_id {r['crew_id']}: {r.get('error')}")

 

3. 첫 테스트 결과

문제 없이 성공했다. 

즉, 동시성 문제는 발생하지 않았다.

=== 부하 테스트 시작 ===
총 소요:      1272ms
성공:         100건
중복:         0건
실패:         0건
평균 응답:    1047.0ms
p50:          1167.8ms
p95:          1249.2ms
p99:          1260.6ms

나의 예상을 뒷받침 하듯, 딱히 동시성 문제는 일어나지 않았다. 

4. 의심

docker mysql을 구동할 때 별다른 설정을 안했다.

Host Machine인 나의 갤북 5프로의 컴퓨터 자원 액세스에 별다른 제한이 없다. 

그래서 내 노트북이 성능이 괜찮아서 그런건가? 라는 생각이 들었다.

 

5. Cpu, Memory 설정 후 재시도

5 - 1. MySQL 컨테이너 초기화 후 재시작

docker 명령어에는 Container를 실행할 때, --cpus와 --memory 설정으로 사용 CPU와 메모리를 제한할 수 있다.

docker run -dit --name test-db \
  --memory="500m" \
  --memory-swap="500m" \
  --cpus="1.0" \
  -e MYSQL_ROOT_PASSWORD=1234 \
  -e MYSQL_DATABASE=test_db \
  -p 3307:3306 mysql:8.0

그래서 cpu와 memory 설정을 한 mysql container에서 다시 실행해보기로 했다.

5 - 2. 실행 결과

실행 결과 별다른 이상 없이 잘 수행되었다.

100명 정도는 CPU 1코어로 괜찮다는 뜻으로 이해!

6. CPU 사용률

6 - 1. docker stats

docker stats [컨테이너]를 사용하면 해당 컨테이너의 CPU 사용률을 알 수 있다.

나는 --cpus=1 설정을 해두었는데, 100명 정도의 요청이면 CPU를 얼마나 잡아먹는 지 궁금해서 관찰해봤다.

100명 동시 등교 요청

위와 같이 순간적으로 34.64%까지 올라갔다가, 다시 0.XX%로 돌아오는 걸 확인할 수 있었다.

6 - 2. 남자라면

100명으로 동시요청하면 34%까지 올라간다.

남자라면 한번쯤은 100%를 달성해, 터트려보는 상상을 하지 않는가?

그래서 해보기로 했다.

200명 300명 올려봤다.

6 - 3. 오잉?

특이한 순간을 관찰했다.

순간적으로 CPU 사용률이 104%까지 올라간 것이다.

300명 동시 요청

????

7. --cpus 옵션

7 - 1. 나의 오해

나는 지금까지 docker run에서 사용하는 --cpus 옵션이 Host Machine으로부터 사용하는 CPU코어수를 제한하는 옵션인 줄 알았다.

즉, 나는 --cpus=1을 해두었으니, 갤북5프로의 CPU중 1코어만 mysql docker이 사용할 수 있다로 이해하고 있었다.
그런데 갑자기 104%를 뚫어버렸다. 1코어를 1.04코어로 만드는 마술이 있는건가?

추가로 생각해보니 --cpus=1.5는 뭐지?라는 생각이 들었다. 0.5코어가 물리적으로 존재하나?

7 - 2. 알고보니

알고보니 --cpus 명령어는 코어 수를 제한(상한)하는 옵션이 아니었다.

--cpus=1은 1코어만 사용해라 라는 의미가 아니라, 1코어 분량만큼만 사용하라 라는 의미이다.

docker docs에 --cpus에 대한 설명이 적혀있다.

 

Resource constraints

Limit container memory and CPU usage with runtime configuration flags

docs.docker.com

--cpus는 period와 quota의 개념을 사용한 개념이다.

즉, 측정 주기동안 사용 가능한 CPU 사용 합산을 정하는 것이다.

따라서 100ms 주기동안 100ms의 시간만 CPU를 사용할 수 있다는 것이다.

이는 100ms/100ms가 되어 --cpus=1과 동일하게 된다.

7 - 3. 104%의 이유

Docker는 Linux의 CFS를 사용해 CPU 사용을 제한한다.

CFS는 코어를 고정하는 게 아니라 시간 할당량(quota) 을 제한하는 방식으로 어느 코어를 사용할지는 OS 스케줄러가 정한다.

여기에 추가로 리눅스에는 버스트 크레딧이라는 개념이 있다.

쉽게 말하면 복지 포인트 이월과 같다.

 

이번달에 복지 포인트를 못쓸 경우, 다음 달로 복지 포인트가 이월되는 경우를 생각하면 된다.

CPU 사용량도 모두 사용하지 않았다면 이 부분이 다음 주기로 넘어가게 된다. (모두 누적되어 넘어가지는 않는다.

따라서 위와 같은 상황이 발생하며 순간적으로 104%의 사용률을 확인할 수 있던 것이다.

(물론 코어를 2개만 사용했다는 건 가정이다. 그냥 100ms 주기동안 합쳐서 104정도를 사용했다는 걸로 받아들이면 된다.

8. 결론

검프의 공통 교육에 있는 문제 중, "동시에 100명이 요청이 보내면 어떻게 될까?"에 경우 동시성 문제가 발생하지 않는다.

docker run 명령어 중, --cpus 옵션은 사용하려는 cpu코어 개수를 '제한'하는 것이 아니다.

'우아한 테크 코스 8기' 카테고리의 다른 글

Scanner vs BufferedReader (+ 간단한 GC)  (0) 2026.03.23
불변 클래스 record  (0) 2026.03.20
Enum 캐싱은 정말 빠를까?  (0) 2026.03.19
우아한 테크 코스 8기 Level 1 - 1주차  (0) 2026.03.03
[우아한 테크 코스 8기] 최종 합격  (0) 2026.01.23
'우아한 테크 코스 8기' 카테고리의 다른 글
  • Scanner vs BufferedReader (+ 간단한 GC)
  • 불변 클래스 record
  • Enum 캐싱은 정말 빠를까?
  • 우아한 테크 코스 8기 Level 1 - 1주차
koreaioi
koreaioi
  • koreaioi
    koreaioi
    koreaioi
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (187)
      • JAVA (3)
      • 알고리즘 (90)
        • 백준 (11)
        • String(문자열) (12)
        • Array(1, 2차원 배열) (13)
        • Two pointers, Sliding windo.. (6)
        • HashMap, TreeSet(해쉬, 정렬지원 S.. (5)
        • Stack, Queue(자료구조) (8)
        • Sorting and Searching(정렬, 이.. (10)
        • Recursive, Tree, Graph(DFS,.. (14)
        • DFS, BFS 활용 (6)
        • 다시 시작! (2)
        • 기초 수학 (1)
      • 일상 (26)
      • Github (1)
      • MSA 공부 (4)
      • 경제, 금융, 디지털, 시사 (3)
      • 라즈베리파이 (10)
      • 프로젝트에서 일어난 일 (22)
      • FrontEnd 공부 (9)
        • React (8)
      • Spring (2)
      • 기술 세미나 (2)
      • DB (3)
      • 우아한 테크 코스 8기 (10)
      • 잡생각 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
koreaioi
동시성 테스트를 하다 발견한 Docker의 CPU 계산법
상단으로

티스토리툴바