Xray API를 이용하면 현재 선택된 노드의 트래픽 통계와 연결 상태를 외부 스크립트에서 읽고, 정해진 기준에 따라 다른 노드로 전환할 수 있습니다. 이 글에서는 gRPC 기반 API 서비스 활성화, 노드 상태 판정, 지연 시간 측정, 장애 시 fallback, 주기적인 스크립트 실행까지 실제 운영에 필요한 흐름을 단계별로 정리합니다.
Xray JSON 설정을 직접 관리하거나 여러 출구 노드를 운영하는 사용자를 위한 안내입니다. API 포트 10085를 별도로 열고 StatsService와 외부 모니터링 스크립트를 연결한 뒤, 3회 연속 실패·지연 시간 800ms 초과·최근 오류 증가와 같은 구체적인 기준으로 노드를 교체하는 방법을 설명합니다.
Xray API는 노드 자체가 아니라 실행 중인 코어를 제어합니다
Xray API는 별도의 웹 관리 페이지가 아니라, 실행 중인 Xray 코어에 gRPC 요청을 보내 상태를 조회하거나 일부 런타임 설정을 조작하는 인터페이스입니다. 따라서 먼저 API 서비스를 제공하는 인바운드를 만들고, 해당 인바운드가 내부의 api 출구로 라우팅되도록 구성해야 합니다. API 포트가 열려 있다고 해서 외부에서 바로 노드 목록을 읽을 수 있는 것은 아닙니다. 코어 버전, 활성화한 API 서비스, 라우팅 태그가 모두 맞아야 요청이 정상적으로 처리됩니다.
자동 전환은 보통 네 가지 계층으로 나눕니다. 첫 번째는 노드 연결을 담당하는 Xray 코어이고, 두 번째는 통계와 상태를 반환하는 API입니다. 세 번째는 API 응답을 읽어 건강 상태를 계산하는 모니터링 스크립트이며, 네 번째는 계산 결과를 실제 라우팅 설정에 반영하는 전환 단계입니다. 이 계층을 나누면 “API 응답은 정상인데 웹 연결은 실패하는” 상황이나 “노드는 바뀌었지만 기존 연결은 그대로 유지되는” 상황을 구분하기 쉽습니다.
API는 반드시 로컬 주소에만 바인딩하는 것이 좋습니다. 127.0.0.1:10085에만 열면 같은 장치의 스크립트에서 접근할 수 있고, 인터넷에 API 포트를 직접 노출하지 않아도 됩니다. 원격 모니터링 서버가 필요하다면 방화벽 허용 목록, 전용 관리망 또는 별도의 암호화된 터널을 사용하세요. API 포트를 공인 주소에 열어 둔 채 인증이나 접근 제어 없이 운영하는 것은 노드 장애보다 더 큰 보안 문제가 될 수 있습니다.
API 서비스와 통계 인바운드를 JSON에 추가하는 방법
Xray는 API 요청을 일반 프록시 트래픽과 구분하기 위해 전용 dokodemo-door 인바운드를 사용할 수 있습니다. 이 인바운드의 포트는 10085, 주소는 127.0.0.1, 태그는 api로 지정합니다. settings.address는 gRPC API가 연결할 서버 주소를 의미하므로 보통 127.0.0.1로 둡니다. API 서비스에는 필요한 항목만 추가해야 하며, 통계가 필요하다면 StatsService를 활성화합니다.
{
"api": {
"tag": "api",
"services": [
"HandlerService",
"StatsService",
"LoggerService"
]
},
"inbounds": [
{
"tag": "api-in",
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"routing": {
"rules": [
{
"type": "field",
"inboundTag": [
"api-in"
],
"outboundTag": "api"
}
]
}
}
위 예시에서 실제 API 요청은 api-in으로 들어오고, 라우팅 규칙에 따라 api 태그가 붙은 내부 출구로 전달됩니다. 현재 사용하는 Xray 배포판이나 클라이언트가 설정 일부를 자동 생성한다면, 기존 api, inbounds, routing.rules 배열을 먼저 확인한 뒤 중복 태그를 피해야 합니다. 같은 태그를 두 번 선언하면 설정 검증 단계에서 실패하거나 모니터링 대상과 실제 트래픽의 출구를 잘못 연결할 수 있습니다.
API 인바운드
- 프로토콜
- dokodemo-door
- 주소
- 127.0.0.1
- 포트
- 10085
- 태그
- api-in
외부에 노출하지 않고 로컬 모니터링 스크립트만 접근하게 합니다.
통계 서비스
- 서비스
- StatsService
- 조회 방식
- QueryStats
- 대상 키
- outbound>>proxy>>traffic
- 주의점
- policy.stats 필요
통계 정책이 꺼져 있으면 API가 실행되어도 원하는 값이 누적되지 않습니다.
통계가 0으로 나올 때 확인할 항목
트래픽 통계는 API 서비스만 켠다고 자동으로 모두 기록되지 않습니다. Xray의 정책 설정에서 사용자 및 시스템 통계 항목이 활성화되어 있어야 하며, 측정하려는 출구의 태그도 정확해야 합니다. 예를 들어 모니터링 스크립트가 outbound>>node-a>>traffic>>uplink을 조회하는데 실제 태그가 node-a-vless라면 API는 정상적으로 응답하더라도 결과가 비어 있거나 0으로 나타날 수 있습니다.
{
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true,
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
"stats": {}
}
이 설정을 적용한 뒤에는 코어를 재시작하고 테스트 트래픽을 일부 발생시키세요. API 통계는 누적 카운터이므로, 모니터링 스크립트는 현재 값 자체보다 직전 조회와의 차이를 보는 편이 좋습니다. 64비트 카운터가 재시작으로 0으로 돌아가거나 값이 감소하면 장애로 오판하지 말고, 코어 재시작 시각을 기준으로 샘플을 초기화해야 합니다.
지연 시간과 트래픽 통계를 함께 사용해 상태를 판정합니다
자동 전환에서 가장 흔한 실수는 지연 시간 하나만 보고 노드를 바꾸는 것입니다. ICMP 응답이나 TCP 연결 시간이 40ms라고 해도 실제 TLS 핸드셰이크, 프록시 인증, 대상 도메인 요청이 정상이라는 보장은 없습니다. 반대로 테스트 대상 서버가 일시적으로 느려서 600ms가 측정되었지만 노드 자체는 정상일 수도 있습니다. 따라서 단일 측정값보다 연속 실패 횟수, 최근 지연 시간의 중앙값, 트래픽 증가 여부, API 연결 가능 여부를 함께 평가하는 것이 안전합니다.
지연 시간, 실패 횟수, 최근 트래픽, 마지막 성공 시각을 합산해 교체 여부를 판단합니다.
적합: 상시 운영, 여러 후보 노드
현재 측정값이 특정 한계를 넘으면 즉시 다음 노드로 전환합니다.
적합: 임시 테스트, 후보가 두 개뿐인 환경
스크립트는 경고만 기록하고 운영자가 실제 연결을 확인한 뒤 노드를 변경합니다.
적합: 설정 변경을 자동화하기 어려운 중요 업무
실무에서는 다음처럼 단계적인 기준을 둘 수 있습니다. 최근 3회 중 2회 이상 실제 연결에 실패하면 경고 상태로 만들고, 3회 연속 실패하거나 5분 동안 성공한 측정이 없으면 장애 상태로 분류합니다. 성공하더라도 중앙값이 800ms를 넘으면 품질 저하 상태로 보고, 현재 노드보다 새 후보의 중앙값이 30% 이상 낮을 때만 교체합니다. 이처럼 교체 조건과 복귀 조건을 다르게 두면 노드가 계속 왕복하는 플래핑을 줄일 수 있습니다.
| 상태 | 판정 예시 | 동작 |
|---|---|---|
| 정상 | 최근 3회 성공, 중앙값 300ms 이하 | 현재 노드 유지 |
| 주의 | 중앙값 300~800ms 또는 1회 실패 | 다음 주기에 재측정 |
| 저하 | 중앙값 800ms 초과가 2회 지속 | 후보 노드와 비교 |
| 장애 | 3회 연속 실패 또는 API 연결 실패 | fallback 노드로 전환 |
결론: 가장 낮은 지연 시간보다 안정적인 후보를 우선하세요
42ms와 95ms의 차이보다 95ms가 매번 성공하고 42ms가 5회 중 2회 실패하는 사실이 실제 사용성에 더 큰 영향을 줍니다. 자동 전환 점수에는 최저 지연 시간보다 실패율과 변동 폭에 더 큰 감점을 주는 편이 안전합니다.
gRPC 스크립트로 상태 조회와 전환 단계를 연결합니다
Python에서 Xray API를 호출하려면 Xray API proto 정의에 맞는 gRPC 클라이언트 코드를 준비해야 합니다. 배포판에 포함된 API proto를 기준으로 스텁을 생성하고, 생성된 모듈의 경로를 스크립트에서 불러옵니다. 환경마다 proto 파일의 위치와 패키지명이 다를 수 있으므로 아래 코드는 구조를 이해하기 위한 예시이며, 실제 설치 환경의 생성 모듈 이름에 맞게 바꿔야 합니다. 핵심은 127.0.0.1:10085에 연결한 뒤 StatsService의 QueryStats를 호출하는 것입니다.
API 설정 저장
api-in태그와 10085 포트를 추가하고, 로컬 주소 바인딩 및 API 라우팅 규칙을 확인합니다.통계 정책 활성화
statsOutboundUplink와statsOutboundDownlink을 켠 뒤 코어를 재시작하고 테스트 연결을 발생시킵니다.후보 태그 등록
proxy-a,proxy-b,proxy-c처럼 실제 outbounds의 태그를 목록으로 관리합니다.상태 샘플 수집
30초마다 API 통계와 실제 프록시 요청 결과를 기록하고, 최근 3~5회의 측정값을 보관합니다.
fallback 적용
장애 조건을 만족하면 라우팅의 기본 출구를 후보 노드로 변경하고, 변경 시각과 이유를 로그에 남깁니다.
import time
import grpc
# 실제 proto 생성 모듈 이름은 설치 환경에 맞게 수정합니다.
from xray.app.stats.command import command_pb2
from xray.app.stats.command import command_pb2_grpc
API_ADDR = "127.0.0.1:10085"
TARGETS = ["proxy-a", "proxy-b", "proxy-c"]
def query_stat(stub, name):
request = command_pb2.QueryStatsRequest(
pattern=name,
reset=False
)
response = stub.QueryStats(request)
if not response.stat:
return 0
return int(response.stat[0].value)
def read_outbound_bytes(stub, tag):
uplink = f"outbound>>{tag}>>traffic>>uplink"
downlink = f"outbound>>{tag}>>traffic>>downlink"
return {
"uplink": query_stat(stub, uplink),
"downlink": query_stat(stub, downlink)
}
with grpc.insecure_channel(API_ADDR) as channel:
stats_stub = command_pb2_grpc.StatsServiceStub(channel)
previous = {}
while True:
for tag in TARGETS:
current = read_outbound_bytes(stats_stub, tag)
old = previous.get(tag, current)
delta = max(0, current["uplink"] - old["uplink"])
delta += max(0, current["downlink"] - old["downlink"])
print(tag, "bytes in interval:", delta)
previous[tag] = current
time.sleep(30)
트래픽 통계는 해당 노드가 실제로 사용되었는지를 보여 주지만, 노드가 지금 연결 가능한지 직접 보장하지는 않습니다. 통계 증가량이 0인 이유는 장애일 수도 있고 단순히 사용자가 요청을 보내지 않았기 때문입니다. 따라서 별도의 합성 테스트가 필요합니다. 예를 들어 스크립트가 지정한 테스트 URL에 현재 후보 노드를 통해 HTTPS 요청을 보내고, 5초 안에 응답 헤더를 받는지 확인합니다. 테스트 요청은 너무 자주 보내지 말고, 응답 본문 전체를 다운로드하지 않는 방식으로 서버와 로컬 트래픽을 줄이세요.
실제 전환은 두 가지 방식으로 구현할 수 있습니다. 첫 번째는 외부 스크립트가 JSON 파일을 수정한 뒤 Xray를 재시작하는 방식입니다. 구현은 단순하지만 기존 연결이 끊기고, 설정 파일을 동시에 수정하면 손상될 위험이 있습니다. 두 번째는 Xray의 HandlerService가 제공하는 런타임 변경 기능이나 balancer 구성을 활용하는 방식입니다. 이 방식은 재시작을 줄일 수 있지만 사용 중인 Xray 버전이 필요한 API 메서드와 필드를 지원하는지 먼저 확인해야 합니다. 버전이 다른 코어에 동일한 API 요청을 무조건 보내면 전환 자체가 실패할 수 있습니다.
설정 파일을 다시 쓰는 경우에는 원본 백업, 임시 파일 저장, JSON 파싱 검증, 원자적 교체 순서를 지켜야 합니다. 전환 대상 태그가 실제 outbounds에 존재하는지 확인하고, 새 설정을 적용하기 전에 Xray의 설정 검사 명령을 실행하세요. 전환 성공 여부는 스크립트가 “변경 요청을 보냈다”는 사실이 아니라, 새 노드의 실제 테스트 요청과 코어 로그로 확인해야 합니다.
fallback과 반복 전환을 막는 운영 안전장치
fallback은 단순히 목록의 다음 노드를 선택하는 기능이 아닙니다. 후보 노드마다 최근 성공 시각, 연속 실패 횟수, 지연 시간 중앙값, 마지막 전환 시각을 저장해야 합니다. 현재 노드가 장애가 되면 후보 중 차단되지 않은 노드를 점수순으로 고르고, 후보도 실패하면 직접 연결로 조용히 돌아가는 대신 명확한 오류 상태를 남기는 것이 좋습니다. 직접 연결 fallback을 허용하면 프록시가 필요한 요청이 보호되지 않은 경로로 나갈 수 있으므로, 운영 목적에 따라 block 출구를 두는 선택도 검토해야 합니다.
전환 후에는 최소 유지 시간을 두세요. 예를 들어 한 번 바꾼 노드는 5분 동안 다시 평가하되, 새 후보가 현재 노드보다 30% 이상 빠르고 연속 3회 성공한 경우에만 복귀하도록 구성할 수 있습니다. 반대로 API 연결이 끊긴 것만으로 즉시 여러 노드를 차례로 변경하면 모든 후보가 정상이어도 모니터링 경로 때문에 불필요한 장애가 발생합니다. API 자체의 연결 실패, 특정 출구의 실패, 테스트 대상 서버의 실패를 서로 다른 오류로 기록해야 합니다.
권장 운영안: 조회와 변경을 분리하세요
모니터링 프로세스
- 30초마다 API 통계 조회
- 5초 제한의 실제 HTTPS 테스트
- 최근 5회 상태를 파일에 저장
- 장애 원인과 점수를 기록
전환 프로세스
- 변경 잠금으로 중복 실행 방지
- 후보 태그 존재 여부 검증
- 최소 유지 시간 적용
- 전환 후 새 노드 재검증
상태를 수집하는 작업과 설정을 바꾸는 작업을 분리하면 API 일시 오류가 전체 라우팅 변경으로 번지는 것을 줄일 수 있습니다.
스크립트는 로그 로테이션도 고려해야 합니다. 30초 간격의 전체 응답을 그대로 기록하면 며칠 만에 로그가 커질 수 있으므로 상태 변화, 실패 횟수, 중앙값, 선택된 태그, 전환 이유만 남기는 방식이 적절합니다. 프로세스가 중복 실행되지 않도록 PID 파일이나 운영체제 잠금을 사용하고, 스크립트가 예외로 종료되었을 때도 현재 노드를 즉시 비우지 말고 마지막으로 검증된 설정을 유지하세요.
Xray API 포트 10085에 연결할 수 없어요.
먼저 코어 설정에 api 서비스와 10085 포트의 dokodemo-door 인바운드가 모두 있는지 확인하세요. 그다음 API 인바운드의 태그가 라우팅 규칙의 inboundTag와 일치하는지, 실제 프로세스가 해당 포트를 열었는지 확인합니다.
통계 API 응답은 오는데 값이 항상 0입니다.
policy.system.statsOutboundUplink와 statsOutboundDownlink이 활성화되어 있는지 확인하고, 조회 문자열의 출구 태그가 실제 outbounds 태그와 같은지 비교하세요. 설정을 바꾼 뒤에는 코어 재시작과 테스트 트래픽이 필요합니다.
노드가 계속 A와 B 사이를 왕복합니다.
최근 한 번의 지연 시간만으로 전환하지 말고 3회 연속 실패, 중앙값 기준, 최소 유지 시간과 복귀 기준을 추가하세요. 전환 후 5분 동안은 새 후보가 30% 이상 개선된 경우에만 다시 바꾸는 방식이 안정적입니다.
API로 노드를 바꾸면 기존 연결도 즉시 이동하나요?
대부분의 경우 이미 수립된 TCP 연결은 기존 출구를 계속 사용할 수 있습니다. 새 요청부터 변경된 라우팅을 적용하는지 확인하고, 연결을 강제로 정리해야 한다면 재시작이나 해당 기능을 지원하는 HandlerService 메서드의 버전 호환성을 별도로 검증해야 합니다.