Selenium Grid 병렬 세션 수 정하는 법

3줄 요약
- Selenium Grid의 노드 기본 최대 세션 수는 옵션값이 아니라 그 머신의 사용 가능한 프로세서 수입니다. GitHub Actions private 저장소 러너는 2코어라 병렬 10을 걸어도 슬롯은 2개만 열립니다.
- 늘리는 방법은 세 갈래입니다. 한 노드의 세션 수를 올리거나(세로), 노드를 더 붙이거나(가로), 남의 그리드를 빌립니다. 코어당 세션 1개라는 기본값을 넘기려면
--override-max-sessions true가 필요하고, 공식 문서가 안정성 저하를 명시적으로 경고합니다. - 오늘
curl http://localhost:4444/status로maxSessions값을 먼저 확인하시고, 그 숫자보다 큰 병렬 수를 코드에 걸어두고 있었다면 그 차이만큼이 그동안 큐 대기 시간이었다고 보시면 됩니다.
지난달에 그리드를 CI에 붙이고 나서 회귀 스위트 시간이 기대만큼 줄지 않았습니다. 워커를 4에서 10으로 올렸는데 총 실행 시간은 거의 그대로였습니다. 테스트가 무거워서라고 생각하고 개별 케이스를 뜯어봤고, 셀렉터를 고치고 대기 로직을 줄였는데도 곡선이 움직이지 않았습니다.
원인은 테스트 쪽이 아니었습니다. 그리드에 슬롯이 두 개밖에 없었습니다. 열 개의 세션 요청이 들어오면 두 개만 잡히고 나머지 여덟은 큐에서 순서를 기다립니다. 실행 시간이 줄지 않은 게 아니라 병렬이 애초에 일어나지 않고 있었던 겁니다. 로그에는 실패가 없으니 대시보드만 보면 정상으로 보입니다. 이게 이 문제가 오래 살아남는 이유입니다.
이 글은 그 슬롯 수를 늘리는 세 갈래 선택지를 정리한 것입니다. 결론부터 적자면, QA 4~6명에 자동화 초기 단계인 팀이라면 한 노드의 세션 수를 무리해서 올리는 쪽(세로)보다 노드를 하나 더 붙이는 쪽(가로)이 대체로 낫습니다. 다만 노드를 붙일 머신이 없고 CI 러너 위에서만 돌리는 상황이라면 세로도 가로도 답이 아니고, 그때는 그리드를 자체 운영한다는 전제 자체를 다시 봐야 합니다.
1. 슬롯 수는 어디서 정해지나
기본값은 옵션이 아니라 하드웨어
Selenium 공식 문서의 Grid CLI 옵션 표를 보면 --max-sessions의 설명이 이렇게 적혀 있습니다. "Maximum number of concurrent sessions. Default value is the number of available processors." 값을 지정하지 않으면 그 머신이 인식한 프로세서 개수가 그대로 최대 세션 수가 됩니다.
여기서 착시가 생깁니다. 로컬 맥북에서 그리드를 띄워 시험하면 코어가 여덟 개, 열 개씩 있으니 병렬이 잘 돕니다. 그대로 CI로 옮기면 러너의 코어 수가 기본값이 되고, 그 숫자는 대부분 훨씬 작습니다. 코드도 설정도 그대로인데 병렬 폭만 조용히 줄어드는 상황입니다.
러너 사양이 곧 슬롯 수입니다
GitHub 문서의 러너 사양표를 보면 public 저장소와 private 저장소의 표준 러너 사양이 다릅니다. 스타트업 저장소는 대체로 private이므로 아래 두 번째 줄이 현실입니다.
| 실행 환경 | 프로세서 | 메모리 | 옵션 없을 때 슬롯 수 |
|---|---|---|---|
| GitHub Actions 표준 러너 (public 저장소, Linux) | 4 | 16 GB | 4 |
| GitHub Actions 표준 러너 (private 저장소, Linux) | 2 | 8 GB | 2 |
| 사내 미니PC 예시 (8코어) | 8 | 16 GB | 8 |
| 개발자 노트북 (10코어) | 10 | 16~32 GB | 10 |
병렬 10을 코드에 걸어둔 채 private 러너에서 돌리면 슬롯 2개에 요청 10개가 들어갑니다. 나머지 8개는 새 세션 큐에 쌓입니다.
지금 값을 확인하는 방법
추측하지 마시고 직접 물어보면 됩니다. Grid 4의 /status 엔드포인트는 노드마다 maxSessions와 slots를 돌려줍니다.
# 그리드에 실제로 몇 개의 슬롯이 열려 있는지 확인합니다
curl -s http://localhost:4444/status | jq '{
ready: .value.ready,
총슬롯: [.value.nodes[].slots[]] | length,
노드별: [.value.nodes[] | {uri: .uri, maxSessions: .maxSessions}]
}'
총슬롯이 테스트 러너의 워커 수보다 작으면, 그 차이만큼이 매 실행마다 큐 대기로 사라지고 있는 시간입니다. 저는 이 한 줄을 CI 파이프라인의 테스트 직전 단계에 넣어두고 있습니다. 실행마다 로그에 슬롯 수가 찍히니, 러너 사양이 바뀌거나 노드가 하나 죽었을 때 바로 눈에 들어옵니다.
2. 갈림길은 세 갈래입니다
슬롯을 늘리는 방법은 결국 세 가지입니다. 한 노드가 감당하는 세션 수를 올리거나, 노드 자체를 더 붙이거나, 남이 운영하는 그리드를 빌립니다.
| 선택지 | 하는 일 | 늘어나는 것 | 늘어나는 위험 |
|---|---|---|---|
| 세로 — 노드당 세션 상향 | --max-sessions를 코어 수 이상으로 올리고 --override-max-sessions true |
머신 추가 없이 슬롯 증가 | 메모리·공유메모리 고갈, 세션 불안정, 플레이키 증가 |
| 가로 — 노드 추가 | 노드 컨테이너나 머신을 늘려 허브에 등록 | 슬롯이 선형으로 증가, 장애 격리 | 관리 대상 증가, 이벤트 버스·네트워크 설정, 비용 |
| 위탁 — 클라우드 그리드 | 외부 그리드에 세션 요청 | 운영 부담 없음, 브라우저·OS 조합 확보 | 실행 건수 기반 과금, 사내망 접근 제약, 대기 시간 |
세 갈래 중 무엇을 고를지는 취향이 아니라 조건이 결정합니다. 지금 병목이 슬롯 수인지 아니면 한 세션의 실행 속도인지, 노드를 올릴 머신이 있는지, 테스트 대상이 사내망 뒤에 있는지. 이 세 가지만 확인하면 선택지는 대개 하나로 줄어듭니다.
순서를 하나 권하자면, 세 갈래를 놓고 고민하기 전에 병목이 정말 슬롯인지부터 확인하시는 게 낫습니다. 전체 실행 시간을 세션이 잡히기를 기다린 시간과 실제로 브라우저가 일한 시간으로 나눠보면 금방 갈립니다. 앞쪽이 크면 슬롯 문제이고, 뒤쪽이 크면 슬롯을 아무리 늘려도 총 시간은 그대로입니다. 후자인데 확장을 결정하면 머신을 늘리고 나서 "왜 그대로지"라는 질문으로 돌아오게 됩니다. 제 해석으로는 그리드 확장 논의가 길어지는 팀의 상당수가 이 구분을 건너뛴 상태에서 시작합니다.
늘리기 전에 봐야 할 옵션들
세 갈래 중 어디로 가든 아래 값들이 같이 움직입니다. 왼쪽 열은 CLI 이름이고, 컨테이너에서는 대체로 SE_ 접두어가 붙은 환경변수로 같은 값을 넘깁니다.
| 옵션 | 역할 | 늘릴 때 같이 봐야 하는 이유 |
|---|---|---|
--max-sessions |
노드가 동시에 받는 세션 수 | 기본값이 프로세서 수. 이걸 넘기려면 아래 옵션이 필요 |
--override-max-sessions |
코어당 1세션 권고를 무시할지 | 이걸 켜야 세로 확장이 실제로 적용됨 |
--session-request-timeout |
새 세션 큐에서 대기 허용 시간(초) | 슬롯이 모자라면 여기서 터짐. 예시값 300 |
--session-timeout |
노드가 유휴 세션을 죽이기까지의 시간(초) | 길면 죽은 세션이 슬롯을 붙잡고 있음 |
--drain-after-session-count |
지정 횟수 실행 후 노드를 비우고 종료 | 일회성 CI 노드에서 상태 누적을 끊는 데 유용 |
제 해석으로는 이 표에서 가장 자주 놓치는 건 --session-timeout입니다. 슬롯을 늘리는 데만 신경 쓰다 보면, 정작 죽은 세션이 슬롯을 붙잡고 있어서 실효 슬롯이 설정값보다 적은 상태를 오래 방치하게 됩니다.
3. 세로로 올릴 때 — 공식 문서가 먼저 경고합니다
첫 번째 함정: 값을 올려도 적용되지 않습니다
--max-sessions만 코어 수보다 크게 주면 조용히 무시됩니다. 공식 문서의 --override-max-sessions 설명이 이유를 그대로 적어두고 있습니다. "The # of available processors is the recommended max sessions value (1 browser session per processor). Setting this flag to true allows the recommended max value to be overwritten. Session stability and reliability might suffer as the host could run out of resources."
두 문장이 다 중요합니다. 앞 문장은 코어당 세션 1개가 권고값이라는 것이고, 뒷 문장은 그걸 넘기면 호스트 자원이 고갈되어 세션 안정성이 나빠질 수 있다는 경고입니다. 도구가 스스로 "여기서부터는 당신 책임"이라고 선을 그은 지점입니다.
# 세로 확장 예시 — override를 켜야 코어 수를 넘는 세션이 실제로 열립니다
docker run -d --name grid \
-p 4444:4444 -p 7900:7900 \
--shm-size="2g" \
-e SE_NODE_MAX_SESSIONS=4 \
-e SE_NODE_OVERRIDE_MAX_SESSIONS=true \
-e SE_NODE_SESSION_TIMEOUT=180 \
selenium/standalone-chrome:4.47.0-20260808
# 적용 여부는 로그가 아니라 /status로 확인합니다
curl -s http://localhost:4444/status | jq '.value.nodes[].maxSessions'
두 번째 함정: 공유 메모리가 먼저 바닥납니다
docker-selenium 저장소는 브라우저가 들어 있는 이미지를 실행할 때 --shm-size=2g를 쓰라고 README 첫 화면에서 안내합니다. 기본 공유 메모리는 작아서 크롬이 탭을 여러 개 띄우는 순간 부족해집니다. 세션 하나로 시험할 때는 잘 넘어가다가, 세션을 네 개로 올리는 순간 재현되기 시작합니다.
이게 세로 확장을 특히 다루기 어렵게 만듭니다. 실패가 세션 수에 비례해 늘어나는 게 아니라 어느 지점부터 갑자기 나타나고, 같은 커밋을 다시 돌리면 통과하기도 합니다. 플레이키로 분류되어 재시도 설정만 늘어나는 흐름이 여기서 시작됩니다.
가장 비싼 함정: 지표가 좋아 보입니다
세로 확장의 진짜 비용은 실패가 아니라 착시입니다. 슬롯을 늘리면 큐 대기가 줄어드니 전체 실행 시간은 실제로 짧아집니다. 대신 간헐적 실패가 늘고, 재시도가 붙고, 재시도로 통과한 케이스는 성공으로 기록됩니다. 리포트상의 통과율은 유지되는데 팀이 실패 원인을 파악하는 데 쓰는 시간은 늘어납니다.
| 세로 확장의 증상 | 지표에 보이는 모습 | 실제로 일어난 일 |
|---|---|---|
| 간헐적 세션 생성 실패 | 재시도 후 통과 → 성공 | 자원 부족으로 브라우저 기동 실패 |
| 특정 케이스만 느려짐 | 평균 실행 시간은 유지 | CPU 경합으로 렌더링 지연 |
| 야간 실행만 실패 | 낮에는 재현 안 됨 | 다른 잡과 러너 자원을 나눠 씀 |
| 재시도 설정 증가 | 통과율 유지 | 실패의 원인 파악을 포기한 상태 |
4. 가로로 늘릴 때 — 비용은 관리 대상 개수로 나옵니다
노드를 붙이면 슬롯은 선형으로 늘어납니다
허브에 노드를 하나 더 등록하면 슬롯이 그만큼 늘어납니다. 세로 확장과 달리 각 노드는 자기 코어 수만큼만 세션을 받으므로 권고값 안에 머무릅니다. 노드 하나가 죽어도 나머지는 계속 돌아서 장애가 격리되는 것도 장점입니다.
# docker-compose 예시 — 허브 1개 + 크롬 노드 2개
# 각 노드는 자기 몫의 공유 메모리를 따로 가져야 합니다
services:
selenium-hub:
image: selenium/hub:4.47.0-20260808
ports: ["4442:4442", "4443:4443", "4444:4444"]
chrome:
image: selenium/node-chrome:4.47.0-20260808
shm_size: 2gb # 노드마다 개별로 지정합니다
depends_on: [selenium-hub]
environment:
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: 4442
SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
SE_NODE_SESSION_TIMEOUT: 180 # 유휴 세션이 슬롯을 붙잡지 않도록
deploy:
replicas: 2 # 노드 수 = 가로 확장의 조절 손잡이
가로 확장이 실제로 비싼 지점
늘어나는 건 슬롯만이 아닙니다. 이벤트 버스 포트가 열려 있어야 하고, 노드가 허브에 도달할 수 있는 네트워크여야 하고, 이미지 태그를 노드마다 같은 값으로 맞춰야 합니다. 허브와 노드의 버전이 어긋나면 등록은 되는데 세션이 잡히지 않는 상태가 나오고, 이건 로그를 한참 봐야 알아차립니다.
그래서 이미지 태그를 latest로 두지 않는 게 중요합니다. docker-selenium의 최신 릴리스 태그는 4.47.0-20260808인데, 저장소 README의 빠른 시작 예제에는 아직 4.44.0-20260505가 적혀 있습니다. 문서를 그대로 복사해 노드만 새로 만들면 허브와 버전이 갈라집니다. 태그를 한 곳에 변수로 두고 모든 서비스가 그 변수를 참조하게 하는 편이 안전합니다.
노드 수를 정하는 기준
노드를 몇 개 둘지는 코어 수를 나눠 쓰는 문제로 환원됩니다. 8코어 머신 하나에 노드 컨테이너 네 개를 올리고 각각 2세션을 주면, 합계는 8세션으로 코어 수와 같아집니다. 겉보기에는 노드가 늘었지만 물리 자원은 그대로이므로 세로 확장과 다르지 않습니다. 가로 확장의 이점을 실제로 얻으려면 머신이 늘어나야 합니다.
저희 팀은 사내 미니PC 한 대를 노드 전용으로 쓰고 있습니다. 러너에서 그리드를 띄우던 때와 비교하면 슬롯 수가 고정되어 예측 가능해진 게 가장 큰 변화였습니다. 러너 사양은 우리가 통제할 수 없지만 미니PC의 코어 수는 바뀌지 않습니다.
대신 관리 대상이 하나 늘었다는 점은 정직하게 적어야겠습니다. 전원이 꺼지거나 네트워크가 끊기면 그날 회귀 실행이 통째로 멈춥니다. 그래서 노드가 허브에 등록돼 있는지 확인하는 점검을 파이프라인 앞단에 두었고, 등록이 안 됐으면 테스트를 돌리지 않고 바로 실패시킵니다. 슬롯이 없는데 테스트를 시작하면 큐 대기 끝에 타임아웃으로 무더기 실패가 나고, 그 실패는 원인이 보이지 않아서 분석에 시간이 훨씬 많이 듭니다. 실패는 빨리 나는 편이 쌉니다.
5. 도입 난이도 · 유지보수 비용 · 팀 학습곡선
| 선택지 | 도입 난이도 | 유지보수 비용 | 팀 학습곡선 |
|---|---|---|---|
| 세로 — 노드당 세션 상향 | 낮음. 환경변수 두 개 | 높음. 간헐 실패의 원인 추적이 상시 업무가 됨 | 낮음. 다만 실패 해석 능력이 없으면 재시도만 늘어남 |
| 가로 — 노드 추가 | 중간. compose·네트워크·버전 정합 | 중간. 노드 수만큼 점검 대상 증가, 값은 예측 가능 | 중간. 허브·노드·이벤트 버스 구조 이해 필요 |
| 위탁 — 클라우드 그리드 | 낮음. 접속 URL 교체 수준 | 낮음. 대신 과금과 사용량 관리가 새 업무 | 낮음. 벤더별 capability 규칙만 익히면 됨 |
QA 4~6명 규모에서 이 표를 읽는 방법을 하나 덧붙이겠습니다. 도입 난이도가 낮은 선택지가 항상 좋은 게 아닙니다. 세로 확장은 도입이 가장 쉬운데 유지보수 비용이 가장 높고, 그 비용이 "그리드 운영"이라는 이름이 아니라 "플레이키 테스트 대응"이라는 이름으로 청구됩니다. 항목 이름이 달라서 원인과 비용이 연결되지 않는 게 이 선택지의 위험한 점입니다.
6. 권할 만한 경우 / 권하지 않는 경우
| 상황 | 권하는 선택 | 이유 |
|---|---|---|
| 노드용 머신이 한 대라도 있다 | 가로 | 권고값 안에서 슬롯이 늘고 장애가 격리됨 |
| CI 러너 위에서만 그리드를 띄운다 | 위탁 또는 러너 상향 | 러너 코어 수가 상한이라 세로·가로 모두 한계 |
| 브라우저·OS 조합이 여러 개 필요하다 | 위탁 | 조합을 직접 유지하는 비용이 가장 큼 |
| 테스트 대상이 사내망 뒤에 있다 | 가로 | 외부 그리드에서 접근 경로를 뚫는 비용이 큼 |
| 병목이 슬롯이 아니라 케이스 실행 시간이다 | 아무것도 안 함 | 슬롯을 늘려도 총 시간이 줄지 않음 |
| 실행 건수가 적고 인력이 부족하다 | 위탁 | 운영 인력이 슬롯보다 먼저 부족해짐 |
권하지 않는 경우를 하나만 더 강조하겠습니다. 슬롯 수를 확인하지 않은 채로 확장을 결정하는 것입니다. 앞의 /status 한 줄을 돌리기 전에는 지금 병목이 슬롯인지 아닌지조차 알 수 없습니다. 저도 워커 수를 올리는 데 두 스프린트를 쓰고 나서야 슬롯이 두 개였다는 걸 알았습니다.
오늘 돌려볼 점검 스크립트
#!/usr/bin/env bash
# 그리드 슬롯 점검 — CI의 테스트 실행 직전 단계에 넣어두면 됩니다
set -euo pipefail
GRID_URL="${GRID_URL:-http://localhost:4444}"
WORKERS="${WORKERS:-4}" # 테스트 러너에 건 병렬 수
STATUS="$(curl -sf "${GRID_URL}/status")"
SLOTS="$(echo "$STATUS" | jq '[.value.nodes[].slots[]] | length')"
READY="$(echo "$STATUS" | jq -r '.value.ready')"
echo "그리드 준비 상태: ${READY} / 전체 슬롯: ${SLOTS} / 요청 병렬: ${WORKERS}"
if [ "$READY" != "true" ]; then
echo "노드가 아직 등록되지 않았습니다. 실행을 중단합니다."; exit 1
fi
if [ "$SLOTS" -lt "$WORKERS" ]; then
echo "경고: 슬롯(${SLOTS})이 요청 병렬(${WORKERS})보다 적습니다."
echo " 초과분 $((WORKERS - SLOTS))개는 새 세션 큐에서 대기합니다."
fi
SLOTS가 WORKERS보다 작다는 경고가 뜬다면, 그 차이가 그동안 병렬이라고 믿고 있던 부분입니다. 거기서부터 세로로 갈지 가로로 갈지 정하시면 됩니다.
댓글
댓글 쓰기