# 06 — 관측 ## 이 단계가 끝나면 Prometheus 가 Keycloak 을 긁고, `vendor_cluster_size` 로 클러스터 상태를 밖에서 볼 수 있다. ## 전제 [05](../05-keycloak/) 가 끝나 Keycloak 두 노드가 떴다. ## 왜 필요한가 실험의 판정을 **밖에서만** 하면 놓친다. A-1 에서 7800 을 끊었는데 외부 응답이 전부 200 이었다 — 분단된 노드가 스스로 로드밸런서에서 빠졌기 때문이다. 클러스터 안을 보는 눈이 따로 있어야 한다. --- ## 1. 적용 **하기** ```bash kubectl apply -f deploy/lab/k8s/observability.yaml kubectl -n observability rollout status deploy/prometheus --timeout=180s ``` **확인** — 무엇이 몇 개 떴는가 ```bash kubectl -n observability get pods ``` **실측** ``` grafana-845b5678cf-b6gvc 1/1 Running node-exporter-9qk9w 1/1 Running node-exporter-c2mz4 1/1 Running prometheus-6774f94f7c-pzr2t 1/1 Running ``` **어디를 봐야 하는가** — **줄이 네 개인가**, 그리고 READY 칸이 전부 `1/1` 인가. 특히 `node-exporter` 로 시작하는 줄이 **둘**인지 센다. **이 결과가 의미하는 것** — node-exporter 가 둘인 것은 DaemonSet 이라 노드마다 하나씩 뜨기 때문이다. **하나뿐이면 노드 하나가 빠진 것**이고, 그러면 그 노드의 CPU·메모리·디스크 지표가 통째로 없는 채로 실험을 하게 된다 — 이때는 관측이 아니라 02 의 노드 상태부터 본다. 어느 노드에 붙었는지는 `-o wide` 로 확인한다. ```bash kubectl -n observability get pods -o wide ``` ## 2. 무엇을 긁고 있나 — 여기가 중요하다 **확인** — Prometheus 가 스스로 밝히는 대상 목록 ```bash kubectl -n observability exec deploy/prometheus -- \ wget -qO- localhost:9090/api/v1/targets | grep -o '"job":"[^"]*"' | sort -u ``` **실측** ``` "job":"keycloak" "job":"kubelet" "job":"node-exporter" "job":"prometheus" ``` **어디를 봐야 하는가** — **거기 있는 이름이 아니라 없는 이름**이다. 응답은 JSON 한 덩어리이고 그대로는 못 읽는다. **이 실험대에는 `jq` 가 없으므로** `grep -o` 로 필요한 필드만 뽑고 `sort -u` 로 중복을 없앤 것이다 — 여기까지가 사람이 손으로 치는 선이고, 그 이상 가공해야 한다면 파서를 짜지 말고 화면에 나온 JSON 을 그대로 읽는다. **이 결과가 의미하는 것** — **★ Redis · BFF · PostgreSQL 이 없다.** 이 실험대는 그것들을 긁지 않는다. 그래서 B층 실험 대부분에 Grafana 화면이 없는데, **안 찍은 것이 아니라 지표가 없는 것**이다. 어떤 실험에서 지표를 못 찾으면 「측정이 실패했다」로 적기 전에 **이 목록에 그 job 이 있었는지부터** 본다. > 이것을 「스크린샷 누락」이 아니라 **측정된 공백**으로 기록했다. > [`evidence/followup/04-observability-gap.txt`](../../evidence/followup/04-observability-gap.txt) 목록에 있는데도 값이 안 나온다면 그다음은 **상태**다. 같은 응답에서 `health` 만 훑는다. ```bash kubectl -n observability exec deploy/prometheus -- \ wget -qO- localhost:9090/api/v1/targets | tr ',' '\n' | grep -E '"(job|health|lastError)"' ``` **어디를 봐야 하는가** — `"health":"up"` 이 아닌 줄과, 그 **바로 뒤에 붙는 `lastError`**. `tr ',' '\n'` 으로 쉼표마다 줄을 나눴으므로 필드가 원래 순서대로 세로로 늘어선다 — job 줄 아래에 그 대상의 health 가 온다. **이 결과가 의미하는 것** — `down` 인 대상이 있으면 `lastError` 가 이유를 그대로 말해 준다(연결 거부·타임아웃·404). 3번에서 값이 한 노드만 나오는 증상의 원인이 대개 여기 있고, 그때 **클러스터가 아니라 스크레이프가 문제**다. ## 3. 클러스터 상태를 본다 **확인** — 두 노드가 각각 몇 명을 보고 있는가 ```bash kubectl -n observability exec deploy/prometheus -- \ wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' ``` **어디를 봐야 하는가** — 응답은 **줄바꿈 없는 JSON 한 줄**이다. `data.result` 배열의 원소가 **몇 개인가**(보고하는 노드 수), 각 원소에서 두 군데만 읽는다 — `"metric"` 안의 `node` 라벨과 `"value"` 배열의 **둘째 원소**(따옴표에 싸인 값). `jq` 가 없으므로 눈으로 읽는다. **실측** — 그렇게 읽어낸 값이다 ``` keycloak-1 → 2 keycloak-0 → 2 ``` **이 결과가 의미하는 것** — **두 노드가 각각 자기가 아는 멤버 수를 보고한다.** 둘 다 2 면 클러스터가 온전하다. 분단되면 한쪽은 2, 다른 쪽은 1 이 된다 — **한 노드만 보면 분단을 놓친다.** 원소가 하나뿐이면 분단이 아니라 스크레이프 실패일 수 있으므로 2번의 `health` 를 먼저 본다. 값이 아예 안 나오면 `"result":[]` 로 빈 배열이 오는데, 이는 「0 이다」가 아니라 **「그런 지표가 없다」**는 뜻이다. 자주 보는 지표들이다. | 지표 | 무엇 | |---|---| | `vendor_cluster_size` | 이 노드가 아는 멤버 수 | | `vendor_jgroups_*` | JGroups 프로토콜별 카운터 | | `vendor_statistics_approximate_entries_unique{cache="sessions"}` | 이 노드의 세션 캐시 엔트리 수 | | `agroal_*` | JDBC 커넥션 풀 | | `up` | 스크레이프 성공 여부 | ## 4. `up` 을 믿지 않는다 **A-2 에서 503 이 나는 동안에도 `up` 은 1 이었다.** 프로세스가 살아 있고 `/metrics` 가 응답하기만 하면 1 이므로 **「살아 있지만 쓸모없는」 상태를 보지 못한다.** ```bash kubectl -n observability exec deploy/prometheus -- \ wget -qO- 'localhost:9090/api/v1/query?query=up' ``` **어디를 봐야 하는가** — 원소마다 `job` 라벨과 값(`"1"`/`"0"`). 값이 1 이라는 것은 **마지막 스크레이프가 성공했다**는 사실 하나만 말한다. **이 결과가 의미하는 것** — `up=1` 은 「프로세스가 살아 있고 `/metrics` 가 응답했다」이지 「그 서비스가 쓸모 있다」가 아니다. 그래서 경보를 `up == 0` 하나로 걸면 **A-2 같은 「살아 있지만 503」 상태를 통째로 놓친다.** 기능 지표를 함께 본다 — 밖에서 실제 응답을 받아 보는 것이 가장 짧다. ```bash curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master ``` **어디를 봐야 하는가** — 코드 한 칸. 여기서 값만 뽑는 형태를 쓰는 것은 `up` 의 1/0 과 **나란히 놓고 비교하기 위해서**다. 처음 보는 오류를 파고들 때는 `curl -I` 나 `curl -v` 로 바꾼다(04 참조). **이 결과가 의미하는 것** — `up=1` 인데 이쪽이 200 이 아니면 그 조합이 곧 「살아 있지만 쓸모없는」 상태의 증거다. 그 두 값을 같이 기록해 두는 것이 A-2 의 판정 근거였다. ## 5. Grafana 를 볼 때 **하기** — 밖에 열지 않고 포트포워드로 본다 ```bash kubectl -n observability port-forward svc/grafana 3000:3000 ``` **어디를 봐야 하는가** — `Forwarding from 127.0.0.1:3000 -> 3000` 한 줄이 찍히고 **명령이 그대로 멈춰 있는가**. 이 명령은 끝나지 않는 것이 정상이라, 터미널 하나를 여기에 내준다. 브라우저를 열면 그 아래에 `Handling connection` 줄이 하나씩 붙는다 — 그것이 붙지 않으면 브라우저가 다른 곳을 보고 있는 것이다. **이 결과가 의미하는 것** — 이 터널은 **명령을 실행한 기계에서만** 열린다. 워크스테이션에서 쳤으면 워크스테이션 브라우저로 `http://localhost:3000`, lab host 에서 쳤으면 lab host 에서 봐야 한다. `bind: address already in use` 면 3000 을 이미 누가 쓰는 것이니 `3001:3000` 처럼 왼쪽만 바꾼다. Ctrl+C 로 끊으면 터널도 사라진다 — 밖에 포트를 여는 것이 아니라 **보는 동안만 뚫는 것**이라 실험대의 노출면이 늘지 않는다. > 실험 중에는 Grafana 보다 **Prometheus 쿼리 API** 가 편하다. 값을 그대로 > 뽑아 비교할 수 있고 스크린샷보다 근거로 남기기 좋다. --- ## 막히면 | 증상 | 원인 | 확인 | |---|---|---| | Keycloak 지표가 안 보임 | 9000 이 안 열렸거나 스크레이프 설정 누락 | 위 2번 targets | | 값이 한 노드만 나옴 | 다른 노드 스크레이프 실패 | targets 의 `health` 필드 | | 컨테이너 안에서 curl 실패 | **Keycloak 이미지에 curl 이 없다** | 밖에서 Prometheus 로 묻는다 | | Grafana 에 데이터 없음 | 데이터소스 주소 오류 | Prometheus 서비스 이름 확인 |