통제와 한계
게이트웨이가 존재하는 이유는 «열기» 가 아니라 «열되 다치지 않게» 입니다. 이 페이지는 무엇이 막히고, 무엇이 제한되고, 무엇이 기록되는지를 값과 함께 적습니다.
스키마를 바꾸는 문장은 막습니다
첫 토큰이 아래 여섯 중 하나면 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"}
첫 토큰만 검사하는 방식은 한 요청에 문장이 하나일 때만 성립합니다. SELECT 1; DROP TABLE x 를 허용했다면 첫 토큰은 SELECT 이고 검사를 그대로 통과합니다. 그래서 세미콜론이 보이면 내용을 보기 전에 거부합니다.
결과 행 상한
| 설정 | 기본값 |
|---|---|
data.gateway.query.max.rows | 100,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.limit | 1000 | 클라이언트별 초당 요청 수 |
data.gateway.rate.limit.global | 0 (무제한) | 모든 클라이언트 합산 상한 |
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.bytes | 1,048,576 (1 MB) |
data.gateway.max.params.bytes | 65,536 (64 KB) |
초과하면 413 으로 조기 거부합니다 — 읽어 들인 뒤가 아니라 읽기 전에 끊습니다.
타임아웃과 슬로우 로그
| 설정 | 기본값 | 무엇 |
|---|---|---|
data.gateway.async.timeout.ms | 60,000 | 비동기 요청 타임아웃 |
data.gateway.slow.query.ms | 10,000 | 이보다 느린 읽기는 경고 |
data.gateway.slow.update.ms | 2,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)은 둘 다 채워져야 열립니다. 하나라도 비면 화면이 「설정되지 않았다」고 말하고 503LOGIN_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 키 바꾸기입니다.