
안녕하세요! 에이플랫폼 Support Bulletin의 열여덟 번째 이야기입니다. 😊
온프레미스(Self-managed) 환경에서 분산 데이터베이스를 구축하다 보면 가장 먼저 마주하는 고민이 바로 '애플리케이션과 DB를 어떻게 연결할 것인가?'입니다.
단순히 포트가 열려있는지 확인하는 TCP 체크만으로는 DB 내부의 복잡한 상태(Role, Lock, Recovery 등)를 알 수가 없어서 곤란할 때가 많습니다.
클라우드 서비스라면 알아서 엔드포인트를 관리해 주겠지만 직접 운영하는 환경에서는 수십 개의 노드 중 "지금 누가 마스터(Master)인지", "누가 쿼리를 처리할 수 있는지"를 정확히 판단해서 트래픽을 보내줘야 합니다.
그래서 이번 시간에는 HAProxy를 이용한 L4 로드밸런싱 환경에서 간단한 Python 코드로 DB 상태를 똑똑하게 감지하는 '스마트한 로드밸런싱 구성 방법'을 소개해 드리려고 합니다.
DDL과 DML 트래픽을 용도에 맞게 분리하고, 장애 발생 시 자동으로 경로를 우회시켜 주는 이 아키텍처를 통해 운영의 부담을 한층 덜어내실 수 있을 것입니다.
시스템 아키텍처와 로직의 흐름
핵심은 Python으로 구현한 Health Check 에이전트입니다.
단순히 "살아있니?"라고 묻는 것을 넘어 요청받은 URL(/agg_check vs /ma_check)에 따라 정해진 정밀 검사를 수행하고 결과를 반환합니다.
HAProxy는 이 결과를 바탕으로 트래픽을 보낼지 말지를 결정하게 됩니다.
각 엔드포인트가 어떤 기준으로 판단을 내리는지 그 로직의 흐름을 살펴보겠습니다.
- DML Endpoint 진단 로직 (/agg_check)

일반적인 데이터 조회/수정(DML)은 클러스터의 마스터 노드(MA)의 생존 여부와 관계없이 실제 데이터를 가진 파티션만 살아있다면 처리 가능해야 합니다.
따라서 에이전트는 "데이터 서비스가 가능한가?"에 초점을 맞춰 검사를 수행합니다.
에이전트는 다음 4가지를 모두 통과했을 때만 200 OK를 반환합니다.
✅ 노드 확인: 해당 DB 호스트에 정상적으로 접속이 되는지 확인합니다.
✅ 클러스터 확인: 현재 노드가 속한 클러스터의 어그리게이터(AG) 노드가 Online 상태인지 확인합니다.
✅ 파티션 상태: 데이터 무결성을 위해 모든 마스터 파티션(Master Partition)이 정상(Online)인지 체크합니다.
✅ Consensus 상태: 만약 합의 기능이 켜져 있다면, 해당 노드가 강등(Demoted)된 상태는 아닌지 검증합니다.
판단 결과: 위 조건을 모두 만족한다면, 이 노드는 "DML 쿼리를 안전하게 처리할 수 있는 상태"로 판단합니다.
- (optional) DDL Endpoint (/ma_check)

테이블 생성(CREATE)이나 스키마 변경(ALTER)과 같은 DDL 작업은 클러스터 내에서 오직 마스터 어그리게이터(Master Aggregator)만 수행할 수 있습니다.
따라서 이 엔드포인트는 가장 엄격한 기준을 적용합니다.
에이전트는 다음 두 단계의 검증을 거쳐 200 OK를 반환합니다.
✅ 1. 기본 건전성 확인 (DML 조건 충족)
- 마스터 노드라 할지라도, DB 접속이 안 되거나 데이터 파티션에 접근할 수 없다면 DDL을 수행할 수 없습니다.
- 따라서 앞서 정의한 DML 진단 로직(접속, 역할, 파티션, Consensus 상태)을 먼저 모두 통과해야 합니다.
✅ 2. 신원 확인 (Identity Check)
- 현재 접속한 이 노드의 ID가 클러스터의 Master Aggregator ID와 일치하는지 대조합니다.
- (참고: DML 체크는 통과했으나 ID가 일치하지 않는다면, 이 노드는 건강한 Child Aggregator이므로 DDL 요청은 거부하고 503을 반환합니다.)
판단 결과: 노드 자체가 건강하며(DML 조건 충족), 동시에 마스터 권한을 가지고 있을 때만 "DDL 실행이 가능한 노드"로 판단합니다.
💡 잠깐! Cloud 환경을 사용 중이신가요? 만약 온프레미스가 아니라 Cloud 환경에서 운영 중이라면, 클라우드 네이티브 로드밸런서를 활용하여 더 간편하게 구성할 수 있습니다. 👉 [에이플랫폼 위키] Self managed - DDL/DML Endpoint 바로가기
Self managed - DDL/DML Endpoint | Notion
문서번호 : 11-3671425
a-platform.notion.site
인프라 환경: 테스트 VM 구성도
먼저 이번 시나리오를 검증하기 위해 구성한 테스트 환경을 살펴보겠습니다.
실제 운영 환경과 유사하게 구성하되 핵심 동작을 명확히 확인하기 위해 VM 3대를 사용했습니다.

위 그림은 구현할 전체 아키텍처입니다. 로드밸런서(LB)는 두 개의 Port를 사용했습니다.
- Port 3306 (DDL): 테이블 생성 등 관리 작업을 위한 전용 출입구
- Port 3307 (DML): 일반적인 데이터 조회/수정을 위한 출입구
Load Balancer 기능 구현
HAProxy 설치 및 설정
이제 로드밸런서 서버(10.0.1.6)에 HAProxy를 설치하고 설정 파일을 작성해 보겠습니다.
1) 패키지 설치 가장 먼저 HAProxy를 설치하고 서비스를 활성화합니다.
# HAProxy 설치
sudo dnf install haproxy -y
# 부팅 시 자동 실행 등록 및 서비스 시작
sudo systemctl enable haproxy
sudo systemctl start haproxy
sudo systemctl status haproxy
2) 설정(haproxy.cfg) 파일 구성
# /etc/haproxy/haproxy.cfg 파일을 수정
sudo vi /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Example configuration for a possible web application. See the
# full configuration options online.
#
# https://www.haproxy.org/download/1.8/doc/configuration.txt
#
#---------------------------------------------------------------------
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
# to have these messages end up in /var/log/haproxy.log you will
# need to:
#
# 1) configure syslog to accept network log events. This is done
# by adding the '-r' option to the SYSLOGD_OPTIONS in
# /etc/sysconfig/syslog
#
# 2) configure local2 events to go to the /var/log/haproxy.log
# file. A line like the following can be added to
# /etc/sysconfig/syslog
#
# local2.* /var/log/haproxy.log
#
log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
daemon
# turn on stats unix socket
stats socket /var/lib/haproxy/stats
# utilize system-wide crypto-policies
ssl-default-bind-ciphers PROFILE=SYSTEM
ssl-default-server-ciphers PROFILE=SYSTEM
#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
# Defaults (DB용 TCP 모드로 전면 수정)
defaults
mode tcp # <--- [중요] http에서 tcp로 변경
log global
option tcplog # <--- [중요] httplog에서 tcplog로 변경
option dontlognull
retries 3
# DB 연결 타임아웃 설정
timeout connect 5s
timeout client 1m
timeout server 1m
timeout check 10s
maxconn 3000
#---------------------------------------------------------------------
# 상태 모니터링 페이지 (웹 브라우저로 접속: http://LB_IP:9000/stats)
#---------------------------------------------------------------------
listen stats
bind *:9000
mode http # 모니터링 페이지는 웹이므로 http 모드
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:admin # <--- [접속 ID:PW]
#---------------------------------------------------------------------
# 1. DDL Endpoint (Master Aggregator 전용) -> 포트 3306
#---------------------------------------------------------------------
frontend ddl_front
bind *:3306
default_backend ddl_back
backend ddl_back
balance roundrobin
# [핵심] 파이썬 에이전트(3309포트)에게 '/ma_check'를 물어봄
# 200 OK가 떨어지는 서버(Master)로만 트래픽을 보냄
option httpchk GET /ma_check
http-check expect status 200
# 트래픽은 3306으로 보내고(check 앞), 상태확인은 3309로 함(check port 뒤)
# TODO: 아래 IP들을 실제 Aggregator 노드 IP로 수정하세요.
server agg-node1 10.0.1.4:3306 check port 3309
server agg-node2 10.0.1.5:3306 check port 3309
#---------------------------------------------------------------------
# 2. DML Endpoint (모든 Healthy Aggregator) -> 포트 3307
#---------------------------------------------------------------------
frontend dml_front
bind *:3307
default_backend dml_back
backend dml_back
balance leastconn # roundrobin / leastconn
# [핵심] 파이썬 에이전트(3309포트)에게 '/agg_check'를 물어봄
# 200 OK가 떨어지는 모든 정상 서버로 트래픽을 분산함
option httpchk GET /agg_check
http-check expect status 200
# TODO: 아래 IP들을 실제 Aggregator 노드 IP로 수정하세요.
server agg-node1 10.0.1.4:3306 check port 3309
server agg-node2 10.0.1.5:3306 check port 3309
💡 설정의 핵심 포인트
- mode tcp: DB 프로토콜을 해석하지 않고 패킷을 그대로 전달하여 오버헤드를 줄였습니다.
- frontend 분리: 사용 목적에 따라 3306(DDL)과 3307(DML)로 나눴습니다.
- option httpchk: 단순 TCP 연결 확인(check) 대신, 우리가 만든 Python 에이전트에게 HTTP 요청을 보내 논리적인 상태를 확인합니다.
- check port 3309: 실제 DB 서비스 포트(3306)와 헬스 체크 포트(3309)를 분리하여 지정했습니다.
설정이 완료되었다면 서비스를 재시작하여 변경 사항을 적용합니다.
sudo systemctl restart haproxy
Sidecar 구현: Python을 이용한 Custom Health Check
핵심 진단 로직 (Diagnosis Logic)
이 파이썬 에이전트의 가장 큰 특징은 단순 접속 체크(Ping)를 넘어선 '논리적 가용성' 판단입니다.
코드는 크게 두 가지 엔드포인트를 통해 서로 다른 기준을 적용합니다.
1) DML 가용성 판단 (/agg_check) 데이터 조회 및 변경 트래픽을 처리하는 엔드포인트입니다.
이를 위해 다음과 같은 4단계 검증을 수행합니다.
- DB Connection Check: DB 엔진이 쿼리에 응답하는가? (SELECT 1)
- Role Check: 현재 노드가 유효한 Aggregator인가?
- Partition Availability
- DISTRIBUTED_PARTITIONS 테이블을 조회하여 '오프라인 상태인 마스터 파티션'이 없는지 확인합니다. 데이터만 접근 가능하다면 서비스는 지속되어야 하기 때문입니다.
- DISTRIBUTED_PARTITIONS 테이블을 조회하여 '오프라인 상태인 마스터 파티션'이 없는지 확인합니다. 데이터만 접근 가능하다면 서비스는 지속되어야 하기 때문입니다.
- Split-Brain Check
- 네트워크 단절로 클러스터가 쪼개졌을 때를 대비합니다.
- lmv_consensus_nodes를 확인하여 자신이 과반수 그룹(Quorum)에 속해있는지 검증합니다.
2) DDL 가용성 판단 (/ma_check) DDL은 오직 MA에서만 가능합니다.
- 기본 건전성: 위 DML 체크를 모두 통과해야 합니다.
- 신원 확인: 현재 노드의 ID가 클러스터의 Master Aggregator ID와 일치하는지 확인합니다.
Python 코드 구현
GitHub - APlatform-SingleStoreKorea/SingleStore_agg-health-check: 안정적인 DDL/DML 로드밸런싱을 위해 SingleStoreDB
안정적인 DDL/DML 로드밸런싱을 위해 SingleStoreDB의 Master Aggregator를 식별하는 헬스 체크 에이전트. - APlatform-SingleStoreKorea/SingleStore_agg-health-check
github.com
실제 구현된 코드는 singlestoredb 드라이버를 사용했으며 쿼리 행 현상을 방지하기 위해 Multiprocessing 기반의 타임아웃 처리 로직이 포함되어 있습니다.
- DDL Enpoint (MA_CHECK_YN="Y”일 때만 동작)
- 조건을 모두 만족하는 경우 /ma_check 로 HTTP 200 을 반환.
- 조건을 하나라도 만족하지 못하는 경우 /ma_check 로 HTTP 503을 반환.
- DML Enpoint
- 조건을 모두 만족하는 경우 /agg_check 로 HTTP 200을 반환.
- 조건을 하나라도 만족하지 못하는 경우 /agg_check 로 HTTP 503을 반환.
환경 변수 설정
|
Variable Name
|
Desc.
|
|
MA_CHECK_YN
|
MA Check 실행 여부 (Y/N, default: Y)
|
|
HEALTH_CHECK_PORT
|
health check 결과를 반환할 HTTP port (default: 3309)
|
|
CHECK_INTERVAL
|
check 주기 (sec, default: 20)
|
|
QUERY_TIMEOUT
|
최대 쿼리 실행 시간 (sec, default: 10)
|
💡DB Connection 과 관련된 환경변수는 .env 파일을 사용하여 별도 관리
$ vi .env.sample
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASS=passwd
위 스크립트는 모든 DB 호스트에서 항상 실행되어야 하므로 리눅스 서비스로 등록하여 관리합니다.
$ sudo vi /etc/systemd/system/agg-health.service
[Unit]
Description=SingleStore Aggregator node Health Check HTTP Server
After=network.target
[Service]
ExecStart=/usr/bin/python3 /path/to/singlestore_work/agg_health_v1.0.4.py # python3 경로와 agg_health_v1.0.4.py 경로에 맞게 설정
WorkingDirectory=/path/to/singlestore_work
Restart=always
RestartSec=5
User=1001 # 서비스 실행할 O/S user id 로 변경
[Install]
WantedBy=multi-user.target
# 서비스 실행
$ sudo systemctl daemon-reload
$ sudo systemctl start agg-health.service
# 서비스 자동 재시작 설정
$ sudo systemctl enable agg-health.service
Created symlink '/etc/systemd/system/multi-user.target.wants/agg-health.service' → '/etc/systemd/system/agg-health.service'.
# 서비스 상태 체크
$ systemctl status agg-health.service
● agg-health.service - SingleStore Aggregator node Health Check HTTP Server
Loaded: loaded (/etc/systemd/system/agg-health.service; enabled; preset: disabled)
Active: active (running) since Thu 2025-12-11 18:25:00 KST; 4s ago
Invocation: be020b06d584433d8bcaced0b21e1f20
Main PID: 4366 (python3)
Tasks: 6 (limit: 10643)
Memory: 38.6M (peak: 41.7M)
CPU: 4.151s
CGroup: /system.slice/agg-health.service
├─4366 /usr/bin/python3 /path/to/work/agg_health_v1.0.4.py
├─4413 /usr/bin/python3 /path/to/work/agg_health_v1.0.4.py
└─4418 /usr/bin/python3 /path/to/work/agg_health_v1.0.4.py
시스템 동작 확인
먼저 HAProxy가 기본적으로 제공하는 웹 대시보드(Stats)에 접속하여, 우리가 설정한 헬스 체크 로직이 정상적으로 반영되었는지 확인합니다.
접속 주소: http://{LB_IP}:9000/stats

화면의 ddl_back과 dml_back 섹션을 살펴보면 다음과 같은 차이를 확인할 수 있습니다.
- ddl_back (상단) agg-node1만 초록색(UP)으로 활성화되어 있습니다. 이는 현재 agg-node1이 클러스터의 Master Aggregator(MA)임을 정확히 식별했음을 의미합니다. (나머지 노드는 DDL 트래픽을 받지 않습니다.)
- dml_back (하단) 모든 노드가 초록색(UP)으로 표시됩니다. 현재 클러스터 내 모든 Aggregator가 정상(Healthy) 상태이며, 쿼리를 분산 처리할 준비가 완료되었음을 보여줍니다.
결과 검증
구축한 HAProxy 아키텍처가 의도대로 동작하는지 확인해 보겠습니다. 검증은 터미널에서 Python를 통해 간단하게 수행할 수 있습니다.
테스트는 다음 두 가지 포인트에 집중합니다.
- DDL Endpoint (3306): 여러 번 요청해도 항상 Master Aggregator로만 연결되는가?
- DML Endpoint (3307): 요청할 때마다 여러 Aggregator로 부하 분산되는가?
테스트 스크립트 실행
아래 명령어를 복사하여 터미널에서 실행해 봅니다. (※ <LB_IP>와 <PASSWORD> 부분만 본인의 환경에 맞게 수정해 주세요)
- DDL Endpoint (Port 3306) 테스트
python3 -c "import singlestoredb as s2; p={'host':'<LB_IP>','user':'root','password':'<PASSWORD>'}; print('--- HAproxy DDL Test (3306) ---'); [print(f'Req {i+1}: ID={c.fetchone()[0]}') for i in range(5) for c in [s2.connect(port=3306, **p).cursor()] if c.execute('select aggregator_id()') is not None]"
→ 테스트 결과
--- HAproxy DDL Test (3306) ---
Req 1: ID=1
Req 2: ID=1
Req 3: ID=1
Req 4: ID=1
Req 5: ID=1
# DDL 포트(3306)로 보낸 요청은 /ma_check를 통과한 Master Node(ID=1)로만 고정적으로 연결됨을 확인할 수 있습니다.
- DML Endpoint (Port 3307) 테스트
python3 -c "import singlestoredb as s2; p={'host':'<LB_IP>','user':'root','password':'<PASSWORD>'}; print('--- HAproxy DML Test (3307) ---'); [print(f'Req {i+1}: ID={c.fetchone()[0]}') for i in range(5) for c in [s2.connect(port=3307, **p).cursor()] if c.execute('select aggregator_id()') is not None]"
→ 테스트 결과
--- HAproxy DML Test (3307) ---
Req 1: ID=4
Req 2: ID=1
Req 3: ID=4
Req 4: ID=1
Req 5: ID=4
# DML 포트(3307)로 보낸 요청은 /agg_check를 통과한 모든 Healthy Node(ID=1, 4)에 번갈아 가며 분산(Round-Robin) 처리되고 있습니다.

지금까지 HAProxy의 고성능 라우팅 기능과 Python Sidecar의 유연함을 결합하여, SingleStore를 위한 '지능형 로드밸런싱' 환경을 구축해 보았습니다.
단순히 "포트가 열려있는가?"를 넘어 "실제 쿼리를 처리할 수 있는 논리적 상태인가?"를 판단하는 이 아키텍처는 운영 환경에서 발생할 수 있는 수많은 예외 상황(Split-Brain, 파티션 오프라인 등)을 사용자 개입 없이 자동으로 방어해 줍니다.
특히 DDL과 DML 엔드포인트를 분리함으로써 개발자는 마스터 노드를 찾아 헤맬 필요가 없고 운영자는 트래픽 관리가 한결 수월해집니다.
오늘 소개한 이 패턴이 여러분의 온프레미스 데이터베이스 환경을 더욱 견고하게 만드는 데 도움이 되기를 바랍니다.





'SingleStoreDB > Support Bulletin' 카테고리의 다른 글
| SingleStore FTS2 안정화 버전 및 Broken Index 가이드 - [Support Bulletin17] (0) | 2025.11.28 |
|---|---|
| SingleStore vs PostgreSQL: 이스케이프 문자 처리 방식 차이 - [Support Bulletin 16] (0) | 2025.10.27 |
| MinIO 설치부터 SingleStore USD 연동까지 - [Support Bulletin 15] (0) | 2025.09.09 |
| HikariCP, SingleStore 연동 테스트 - [Support Bulletin 14] (0) | 2025.08.21 |
| SingleStore, 유휴 세션으로 인한 데이터베이스 성능 저하 해결하기 - [Support Bulletin 13] (0) | 2025.08.19 |