본문으로 건너뛰기

통제와 한계

게이트웨이가 존재하는 이유는 «열기» 가 아니라 «열되 다치지 않게» 입니다. 이 페이지는 무엇이 막히고, 무엇이 제한되고, 무엇이 기록되는지를 값과 함께 적습니다.

스키마를 바꾸는 문장은 막습니다​

첫 토큰이 아래 여섯 중 하나면 400 QUERY_SYNTAX 입니다.

DROP · TRUNCATE · ALTER · CREATE · GRANT · REVOKE
{"result":"ERROR","error_code":"QUERY_SYNTAX",
"message":"DDL statements are not allowed: DROP"}

주석으로 우회할 수 없습니다. 블록 주석과 줄 주석을 먼저 걷어낸 뒤 첫 토큰을 봅니다.

/* 안녕 */ DROP TABLE pp.mm_tag -- 막힙니다
-- 한 줄
DROP TABLE pp.mm_tag -- 막힙니다

주석을 걷어냈더니 아무것도 남지 않으면 그것도 400 입니다.

세미콜론이 있으면 거부합니다​

{"result":"ERROR","error_code":"QUERY_SYNTAX",
"message":"Multiple statements are not allowed"}
문장 «하나» 만 받는 것이 DDL 차단의 전제입니다

첫 토큰만 검사하는 방식은 한 요청에 문장이 하나일 때만 성립합니다. SELECT 1; DROP TABLE x 를 허용했다면 첫 토큰은 SELECT 이고 검사를 그대로 통과합니다. 그래서 세미콜론이 보이면 내용을 보기 전에 거부합니다.

결과 행 상한​

설정기본값
data.gateway.query.max.rows100,000 (0 = 무제한)

상한에 도달하면 게이트웨이는 거기서 읽기를 끊습니다 — 전체를 끝까지 읽은 뒤 자르는 것이 아닙니다. 그래서 거대한 결과에도 무한 스트림이 생기지 않습니다.

  • POST /api/v1/query → 응답의 truncated 필드
  • POST /api/v1/query/stream → end 줄의 truncated 필드

서버 로그에도 남습니다.

Query result truncated to max_rows=[100000], lang=[PQL]
잘린 결과는 «성공» 으로 돌아옵니다

HTTP 는 200 이고 행도 정상입니다. truncated 를 보지 않으면 잘린 결과를 전체로 착각한 채 집계하게 됩니다. 그것이 이 플래그를 항상 싣는 이유입니다.

레이트리밋​

설정기본값무엇
data.gateway.rate.limit1000클라이언트별 초당 요청 수
data.gateway.rate.limit.global0 (무제한)모든 클라이언트 합산 상한
data.gateway.trusted.proxies비어 있음X-Forwarded-For 를 믿을 직속 peer 목록

버킷 키는 신뢰 가능한 클라이언트 IP 입니다.

X-Forwarded-For 는 기본적으로 무시됩니다

직속 peer 가 trusted.proxies 에 있을 때만 XFF 의 첫 hop 을 클라이언트 IP 로 신뢰합니다. 비워 두면 언제나 실제 접속 주소를 씁니다 — 그렇게 하지 않으면 헤더 하나를 지어내 한도를 우회할 수 있습니다.

리버스 프록시 뒤에 두었다면 그 프록시의 주소를 반드시 이 목록에 넣으십시오. 넣지 않으면 모든 요청이 프록시 하나의 버킷에 몰립니다.

전역 상한은 «버킷 폭증» 에 대한 방어입니다 — 서로 다른 IP 를 아무리 많이 만들어도 전체 처리량이 이 값을 넘지 못합니다.

/admin/* 도 같은 한도를 «공유» 합니다

/api/* 와 /admin/* 이 같은 카운터를 씁니다. 따로 두면 전역 상한이 실질적으로 두 배가 되고, 부하를 두 진입점으로 나눠 우회할 수 있습니다.

요청 크기​

설정기본값
data.gateway.max.body.bytes1,048,576 (1 MB)
data.gateway.max.params.bytes65,536 (64 KB)

초과하면 413 으로 조기 거부합니다 — 읽어 들인 뒤가 아니라 읽기 전에 끊습니다.

타임아웃과 슬로우 로그​

설정기본값무엇
data.gateway.async.timeout.ms60,000비동기 요청 타임아웃
data.gateway.slow.query.ms10,000이보다 느린 읽기는 경고
data.gateway.slow.update.ms2,000이보다 느린 쓰기는 경고

요청은 가상 스레드로 처리합니다. 동시 실행 상한은 data.gateway.async.concurrency.limit 이고 기본값은 256 입니다(0 이하면 무제한). 상한에 닿으면 제출 쪽이 자리가 날 때까지 기다립니다 — 버리지 않고 백프레셔를 겁니다. 기동 로그가 실제 값을 찍습니다.

Async executor: virtual threads, concurrency limit=[256]
가상 스레드로 바꾼 이유

예전 고정 풀은 큐가 500건 차기 전까지 스레드를 20개 넘게 늘리지 않아 실질 동시 질의가 20 이었고, 큐에서 기다리던 요청이 60초 타임아웃에 걸려 503 이 났습니다. 실제 병목은 DB 커넥션 풀이므로, 지금은 풀 크기를 튜닝하지 않고 동시성 상한 하나만 둡니다.

질의 로그 — 모든 요청이 남습니다​

항목값
테이블tm_data_gateway_log (Aster TSDB, timeuuid PK)
남는 것언어 · 질의문 · 행 수 · 실행 시간 · 실패 여부 · 오류 메시지
쓰기큐에 넣고 배치로 flush — 질의 경로를 막지 않습니다

큐의 상태는 GET /admin/metrics 의 gateway.querylog.* 게이지가 냅니다.

/api/health 는 liveness 가 아니라 readiness 입니다

컨텍스트가 살아 있다는 것만으로 UP 을 보고하는 것은 무의미합니다 — 질의를 받을 준비가 됐을 때만 UP 입니다. 체크 셋(database · inmemory · query_log)이 모두 참이어야 하고, 아니면 STARTING 또는 DEGRADED 입니다. 질의 로그가 초기화되지 않은 게이트웨이는 UP 이 아닙니다 — 감사 기록이 빠진 채 «정상» 으로 보이면, 나중에 「누가 무엇을 조회했나」에 답할 수 없습니다.

인증 · 콘솔 계정​

  • 인증은 Spring Security 체인 하나가 정합니다. 목록에 없는 경로는 401 이지 익명이 아닙니다.
  • 세션도 쿠키도 만들지 않습니다(STATELESS). 자격증명은 요청마다 헤더로 옵니다.
  • 콘솔 로그인 계정(data.gateway.web.user · data.gateway.web.password)은 둘 다 채워져야 열립니다. 하나라도 비면 화면이 「설정되지 않았다」고 말하고 503 LOGIN_NOT_CONFIGURED 로 전부 거부합니다.
기본 계정을 심지 않습니다 — 그것이 의도된 기본값입니다

기본 계정을 넣으면 모든 고객 설치에 같은 자격증명이 실려 나가고 아무도 바꾸지 않습니다. 값은 렌더되는 설정(plantpulse-datalake-cli 의 템플릿)에 넣으십시오.

아이디와 비밀번호는 둘 다 항상 비교하고(한쪽이 틀려도 나머지를 건너뛰지 않습니다), 실패 응답은 어느 쪽이 틀렸는지 말하지 않습니다.

값을 바꾸려면​

정본은 plantpulse-datalake-cli 의 config/templates/ 입니다. 컨테이너 안에서 고친 것은 재기동에 사라집니다.

docker exec plantpulse-datalake pd config diff --templates
docker exec plantpulse-datalake pd restart data-gateway

절차는 설정 바꾸는 법, 키 회전은 비밀번호 · API 키 바꾸기입니다.

관련 문서​