From 2a98ef1090bbcb2438d96294197b917f7c3a056f Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Fri, 4 Sep 2026 11:52:49 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20A-1=20=E2=80=94=20sessions=20survive=20?= =?UTF-8?q?a=20JGroups=20partition=20but=20logout=20invalidation=20does=20?= =?UTF-8?q?not?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cutting TCP 7800 leaves cross-node refresh working (200), confirming sessions travel through PostgreSQL rather than the cluster transport. Logout is the opposite: the database row is deleted but the other node answers from its stale local cache, so the A-0 conclusion that invalidation rides the database is corrected here. Two things the plan did not anticipate: a NetworkPolicy cannot sever an established connection because conntrack accepts it before policy evaluation, and Keycloak reports the partition through its readiness probe so the split node removes itself from the Service. Co-Authored-By: Claude Opus 5 --- .../console-2026-09-04T02-50-02-217Z.log | 21 + .../page-2026-09-04T02-50-03-122Z.yml | 151 ++++++ .../lab/k8s/a1-block-jgroups-transport.yaml | 46 ++ .../01-baseline-cluster.txt | 13 + .../02-control-before-block.txt | 17 + .../03-block-applied.txt | 8 + .../04-after-block-state.txt | 26 + .../05-conntrack-problem.txt | 16 + .../06-partition-observed.txt | 13 + .../07-cluster-size.txt | 16 + .../08-restart-forced-partition.txt | 9 + .../09-cross-node-under-partition.txt | 16 + .../10-logout-not-propagated.txt | 33 ++ .../11-service-impact.txt | 25 + .../12-recovery.txt | 24 + .../a1-jgroups-transport-block/README.md | 26 + .../a1-cluster-size-partition-recovery.png | Bin 0 -> 67415 bytes docs/experiment-a1-jgroups-transport-block.md | 506 ++++++++++++++++++ 18 files changed, 966 insertions(+) create mode 100644 .playwright-mcp/console-2026-09-04T02-50-02-217Z.log create mode 100644 .playwright-mcp/page-2026-09-04T02-50-03-122Z.yml create mode 100644 deploy/lab/k8s/a1-block-jgroups-transport.yaml create mode 100644 docs/evidence/a1-jgroups-transport-block/01-baseline-cluster.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/02-control-before-block.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/03-block-applied.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/04-after-block-state.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/05-conntrack-problem.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/06-partition-observed.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/07-cluster-size.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/08-restart-forced-partition.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/09-cross-node-under-partition.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/10-logout-not-propagated.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/11-service-impact.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/12-recovery.txt create mode 100644 docs/evidence/a1-jgroups-transport-block/README.md create mode 100644 docs/evidence/a1-jgroups-transport-block/a1-cluster-size-partition-recovery.png create mode 100644 docs/experiment-a1-jgroups-transport-block.md diff --git a/.playwright-mcp/console-2026-09-04T02-50-02-217Z.log b/.playwright-mcp/console-2026-09-04T02-50-02-217Z.log new file mode 100644 index 0000000..f0f542f --- /dev/null +++ b/.playwright-mcp/console-2026-09-04T02-50-02-217Z.log @@ -0,0 +1,21 @@ +[ 344ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 869ms] [WARNING] is deprecated. Please include @ https://app2.hyeonworks.com/explore?schemaVersion=1&panes=%7B%22cf1%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22%7B%7Bpod%7D%7D+on+%7B%7Bnode%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%22now-45m%22%2C%22to%22%3A%22now%22%7D%7D%7D&orgId=1:0 +[ 1014ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 2039ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 4085ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 7289ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 15556ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 26818ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 32659ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 35930ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 53849ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 61722ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 79450ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 84571ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 87233ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 92665ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 113244ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 119181ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 137390ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 157071ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 +[ 166091ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362 diff --git a/.playwright-mcp/page-2026-09-04T02-50-03-122Z.yml b/.playwright-mcp/page-2026-09-04T02-50-03-122Z.yml new file mode 100644 index 0000000..d875764 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T02-50-03-122Z.yml @@ -0,0 +1,151 @@ +- generic [active] [ref=e1]: + - generic [ref=e4]: + - link "Skip to main content" [ref=e5] [cursor=pointer]: + - /url: "#pageContent" + - banner [ref=e7]: + - generic [ref=e8]: + - link [ref=e10] [cursor=pointer]: + - /url: / + - img "Grafana" [ref=e11] + - generic [ref=e14]: + - button "Search or jump to..." [ref=e18] [cursor=pointer] + - generic [ref=e19]: ctrl+k + - generic [ref=e23]: + - button "New" [ref=e24] [cursor=pointer] + - button "Help" [ref=e30] [cursor=pointer] + - button "News" [ref=e33] [cursor=pointer] + - button "Profile" [ref=e36] [cursor=pointer]: + - img "User avatar" [ref=e37] + - generic [ref=e38]: + - button "Open menu" [ref=e40] [cursor=pointer] + - navigation "Breadcrumbs" [ref=e43]: + - list [ref=e44]: + - listitem [ref=e45]: + - link "Home" [ref=e46] [cursor=pointer]: + - /url: / + - listitem [ref=e50]: + - link "Explore" [ref=e51] [cursor=pointer]: + - /url: /explore + - listitem [ref=e55]: + - generic "Prometheus" [ref=e56] + - generic [ref=e57]: + - generic [ref=e60]: + - button "Copy shortened URL" [ref=e61] [cursor=pointer] + - button "Open copy link options" [ref=e64] [cursor=pointer] + - button "Toggle top search bar" [ref=e68] [cursor=pointer] + - main [ref=e74]: + - generic [ref=e76]: + - heading "Explore" [level=1] [ref=e77] + - generic [ref=e82]: + - navigation "Explore toolbar" [ref=e84]: + - navigation "Search links" [ref=e86]: + - generic [ref=e87]: + - button "Content outline" [expanded] [ref=e89] [cursor=pointer]: + - generic [ref=e92]: Outline + - generic [ref=e97] [cursor=pointer]: + - img "Prometheus logo" [ref=e99] + - textbox "Select a data source" [ref=e100]: + - /placeholder: "" + - generic [ref=e104]: + - button "Split the pane" [ref=e106] [cursor=pointer]: + - generic [ref=e109]: Split + - button "Add" [ref=e111] [cursor=pointer] + - generic [ref=e116]: + - 'button "Time range selected: Last 45 minutes" [ref=e117] [cursor=pointer]' + - button "Zoom out time range" [ref=e122] [cursor=pointer] + - generic [ref=e126]: + - button "Run query" [ref=e127] [cursor=pointer] + - button "Auto refresh turned off. Choose refresh time interval" [ref=e131] [cursor=pointer] + - generic [ref=e135]: + - generic [ref=e139]: + - button "Collapse outline" [expanded] [ref=e141] [cursor=pointer]: + - img "arrow-from-right" [ref=e142] + - button "Queries" [ref=e145] [cursor=pointer]: + - img "arrow" [ref=e146] + - button "Graph" [ref=e150] [cursor=pointer]: + - img "graph-bar" [ref=e151] + - generic [ref=e158]: + - generic [ref=e160]: + - generic "Query editor row" [ref=e163]: + - generic [ref=e164]: + - generic [ref=e166]: + - generic [ref=e167]: + - button "Collapse query row" [expanded] [ref=e168] [cursor=pointer] + - generic [ref=e171]: + - button "Query editor row title A" [ref=e172] [cursor=pointer]: + - generic [ref=e173]: A + - emphasis [ref=e174]: (Prometheus) + - generic [ref=e175]: + - button "Show data source help" [ref=e177] [cursor=pointer] + - button "Duplicate query" [ref=e181] [cursor=pointer] + - button "Hide response" [ref=e185] [cursor=pointer] + - button "Remove query" [ref=e189] [cursor=pointer] + - button "Drag and drop to reorder" [ref=e192]: + - img "Drag and drop to reorder" [ref=e193] + - generic [ref=e196]: + - generic [ref=e197]: + - button "Kick start your query" [ref=e198] [cursor=pointer] + - generic [ref=e201]: + - generic [ref=e202] [cursor=pointer]: Explain + - generic [ref=e203]: + - checkbox "Explain Toggle switch" [ref=e204] + - generic "Toggle switch" [ref=e205] [cursor=pointer] + - radiogroup [ref=e210]: + - generic [ref=e211]: + - radio "Builder" [ref=e212] [cursor=pointer] + - generic [ref=e213] [cursor=pointer]: Builder + - generic [ref=e214]: + - radio "Code" [checked] [ref=e215] [cursor=pointer] + - generic [ref=e216] [cursor=pointer]: Code + - generic [ref=e218]: + - generic [ref=e220]: + - button "Metrics browser" [ref=e221] [cursor=pointer] + - code [ref=e228]: + - generic [ref=e229]: + - generic [ref=e234]: vendor_cluster_size + - textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=e239]: vendor_cluster_size + - 'button "Options Legend: {{pod}} on {{node}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=e245] [cursor=pointer]': + - generic [ref=e249]: + - heading "Options" [level=6] [ref=e250] + - generic [ref=e251]: + - generic [ref=e252]: "Legend: {{pod}} on {{node}}" + - generic [ref=e253]: "Format: Time series" + - generic [ref=e254]: "Step: auto" + - generic [ref=e255]: "Type: Range" + - generic [ref=e256]: "Exemplars: false" + - generic [ref=e257]: + - button "Add query" [ref=e258] [cursor=pointer] + - button "Query history" [ref=e262] [cursor=pointer] + - button "Query inspector" [ref=e266] [cursor=pointer] + - main [ref=e270]: + - region [ref=e272]: + - generic [ref=e273]: + - heading "Graph" [level=2] [ref=e275] + - radiogroup [ref=e278]: + - generic [ref=e279]: + - radio "Lines" [checked] [ref=e280] [cursor=pointer] + - generic [ref=e281] [cursor=pointer]: Lines + - generic [ref=e282]: + - radio "Bars" [ref=e283] [cursor=pointer] + - generic [ref=e284] [cursor=pointer]: Bars + - generic [ref=e285]: + - radio "Points" [ref=e286] [cursor=pointer] + - generic [ref=e287] [cursor=pointer]: Points + - generic [ref=e288]: + - radio "Stacked lines" [ref=e289] [cursor=pointer] + - generic [ref=e290] [cursor=pointer]: Stacked lines + - generic [ref=e291]: + - radio "Stacked bars" [ref=e292] [cursor=pointer] + - generic [ref=e293] [cursor=pointer]: Stacked bars + - list [ref=e302]: + - listitem [ref=e303]: + - button "keycloak-0 on kc-lab-2" [ref=e307] [cursor=pointer] + - listitem [ref=e308]: + - button "keycloak-0 on kc-lab-2" [ref=e312] [cursor=pointer] + - listitem [ref=e313]: + - button "keycloak-1 on kc-lab-1" [ref=e317] [cursor=pointer] + - generic [ref=e322]: + - alert + - alert + - complementary + - complementary \ No newline at end of file diff --git a/deploy/lab/k8s/a1-block-jgroups-transport.yaml b/deploy/lab/k8s/a1-block-jgroups-transport.yaml new file mode 100644 index 0000000..f25528f --- /dev/null +++ b/deploy/lab/k8s/a1-block-jgroups-transport.yaml @@ -0,0 +1,46 @@ +# Experiment A-1 — cut the JGroups transport (TCP 7800) while leaving discovery alone. +# +# The point is to separate two things that are easy to conflate: +# +# discovery how the nodes FIND each other -> PostgreSQL JGROUPS_PING table +# transport how they actually TALK -> TCP 7800 +# +# Blocking only the transport produces a state that cannot happen on a single +# node: both members stay registered in the database, so each believes the other +# exists, yet no message gets through. +# +# kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml +# kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport +# +# NetworkPolicy is an ALLOWLIST, not a firewall with deny rules. There is no way +# to write "deny 7800". The moment a pod is selected by a policy carrying +# policyTypes: [Ingress], every inbound port is denied unless a rule permits it. +# So 7800 is blocked by *omission*: 8080 and 9000 are listed, 7800 is not. +# +# That makes the two allow rules load-bearing — get them wrong and the experiment +# measures a dead Keycloak instead of a partitioned cluster: +# +# 8080 the HTTP endpoint. Traefik, the other pod's REST calls, and the probe +# traffic all arrive here. +# 9000 the management port: /health/started, /health/ready, /health/live and +# /metrics. Losing it means the kubelet fails the readiness probe and +# kills the pod — the cluster would break for the wrong reason. +# +# Both rules deliberately omit `from:`, which allows those ports from any source. +# Narrowing the source is not the subject here; the 2-hop experiment already +# established how to do that by label when it matters. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: a1-block-jgroups-transport + namespace: keycloak-lab +spec: + podSelector: + matchLabels: + app: keycloak + policyTypes: [Ingress] + ingress: + - ports: + - { port: 8080, protocol: TCP } # HTTP — must stay open + - { port: 9000, protocol: TCP } # health + metrics — must stay open + # 7800 is absent on purpose. That is the whole experiment. diff --git a/docs/evidence/a1-jgroups-transport-block/01-baseline-cluster.txt b/docs/evidence/a1-jgroups-transport-block/01-baseline-cluster.txt new file mode 100644 index 0000000..2c281db --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/01-baseline-cluster.txt @@ -0,0 +1,13 @@ +=== [기준선 1] 클러스터 뷰 — 양쪽 파드의 마지막 ISPN000094 === + keycloak-0: [keycloak-1-48749(v=16.0.12)|5] (2) [keycloak-1-48749(v=16.0.12), keycloak-0-30843(v=16.0.12)] + keycloak-1: [keycloak-1-48749(v=16.0.12)|5] (2) [keycloak-1-48749(v=16.0.12), keycloak-0-30843(v=16.0.12)] + +=== [기준선 2] JGROUPS_PING — 디스커버리 등록 === + name | ip | coord | coordinated_by +------------------+-----------------+-------+--------------------------------------------- + keycloak-0-30843 | 10.42.1.43:7800 | f | uuid://00000000-0000-0000-0000-000000000007 + keycloak-1-48749 | 10.42.0.35:7800 | t | uuid://00000000-0000-0000-0000-000000000007 +(2 rows) + +=== [기준선 3] 기존 NetworkPolicy === +No resources found in keycloak-lab namespace. diff --git a/docs/evidence/a1-jgroups-transport-block/02-control-before-block.txt b/docs/evidence/a1-jgroups-transport-block/02-control-before-block.txt new file mode 100644 index 0000000..b48d899 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/02-control-before-block.txt @@ -0,0 +1,17 @@ +=== [대조군] 차단 전 — keycloak-0 로그인 → keycloak-1 에서 refresh === + sid tAWs2gCPr6SOcD4jDR9-_CzB + keycloak-1 에서 refresh: 200 + +=== [기준선 4] JGroups 지표 — 양쪽 노드 === +--- K0 (10.42.1.43) --- +vendor_jgroups_stats_bytes_sent_total 31476.0 +vendor_jgroups_merge3_get_num_merge_events 0.0 +vendor_jgroups_merge3_get_views 0.0 +vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 +vendor_jgroups_nakack2_get_xmit_table_missing_messages 0.0 +--- K1 (10.42.0.35) --- +vendor_jgroups_merge3_get_views 0.0 +vendor_jgroups_stats_bytes_sent_total 126765.0 +vendor_jgroups_nakack2_get_xmit_table_missing_messages 0.0 +vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 +vendor_jgroups_merge3_get_num_merge_events 0.0 diff --git a/docs/evidence/a1-jgroups-transport-block/03-block-applied.txt b/docs/evidence/a1-jgroups-transport-block/03-block-applied.txt new file mode 100644 index 0000000..54384f1 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/03-block-applied.txt @@ -0,0 +1,8 @@ +=== 차단 적용 === +networkpolicy.networking.k8s.io/a1-block-jgroups-transport created +a1-block-jgroups-transport map[app:keycloak] + +적용 시각: 11:38:08 +=== FD_SOCK2 가 상대를 의심하기까지 기다린다 (15초 간격, 최대 3분) === + +15초 suspected(k0 k1) = + → 변화 감지 diff --git a/docs/evidence/a1-jgroups-transport-block/04-after-block-state.txt b/docs/evidence/a1-jgroups-transport-block/04-after-block-state.txt new file mode 100644 index 0000000..da17cd2 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/04-after-block-state.txt @@ -0,0 +1,26 @@ +차단 경과: 11:38:51 (적용 11:38:08) + +=== [차단 후 1] JGroups 지표 === +--- keycloak-0 --- +vendor_jgroups_stats_bytes_sent_total 32857.0 +vendor_jgroups_merge3_get_num_merge_events 0.0 +vendor_jgroups_merge3_get_views 0.0 +vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 +vendor_jgroups_nakack2_get_xmit_table_missing_messages 0.0 +--- keycloak-1 --- +vendor_jgroups_merge3_get_views 0.0 +vendor_jgroups_stats_bytes_sent_total 129103.0 +vendor_jgroups_nakack2_get_xmit_table_missing_messages 0.0 +vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 +vendor_jgroups_merge3_get_num_merge_events 0.0 + +=== [차단 후 2] 클러스터 뷰 — 갈라졌는가 === + keycloak-0: + keycloak-1: +=== [차단 후 3] JGROUPS_PING — 디스커버리는 살아 있는가 === + name | ip | coord +------------------+-----------------+------- + keycloak-0-30843 | 10.42.1.43:7800 | f + keycloak-1-48749 | 10.42.0.35:7800 | t +(2 rows) + diff --git a/docs/evidence/a1-jgroups-transport-block/05-conntrack-problem.txt b/docs/evidence/a1-jgroups-transport-block/05-conntrack-problem.txt new file mode 100644 index 0000000..7c86074 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/05-conntrack-problem.txt @@ -0,0 +1,16 @@ +=== [문제 확정] NetworkPolicy 적용 후에도 기존 연결이 conntrack 에 살아 있다 === +--- kc-lab-1 --- + tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 src=10.42.1.43 dst=10.42.0.35 sport=7800 dport=40023 [ASSURED] mark=0 use=1 + tcp 6 79982 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=50477 dport=57800 src=10.42.1.43 dst=10.42.0.35 sport=57800 dport=50477 [ASSURED] mark=0 use=1 +--- kc-lab-2 --- + tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 src=10.42.1.43 dst=10.42.0.35 sport=7800 dport=40023 [ASSURED] mark=0 use=1 + tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED] src=10.42.0.35 dst=10.42.1.58 sport=7800 dport=34824 mark=0 use=1 + tcp 6 79982 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=50477 dport=57800 src=10.42.1.43 dst=10.42.0.35 sport=57800 dport=50477 [ASSURED] mark=0 use=1 + +=== [조치] 7800 흐름의 conntrack 항목을 지운다 → 다음 패킷이 정책을 다시 탄다 === + kc-lab-1: tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 src=10.42.1.43 dst=10.42.0.35 sport=7800 dport=40023 [ASSURED] mark=0 use=1 conntrack v1.4.7 (conntrack-tools): 0 flow entries have been deleted. + kc-lab-2: tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED] src=10.42.0.35 dst=10.42.1.58 sport=7800 dport=34824 mark=0 use=1 conntrack v1.4.7 (conntrack-tools): 0 flow entries have been deleted. + +=== 삭제 후 7800 conntrack === + kc-lab-1: 2 건 + kc-lab-2: 2 건 diff --git a/docs/evidence/a1-jgroups-transport-block/06-partition-observed.txt b/docs/evidence/a1-jgroups-transport-block/06-partition-observed.txt new file mode 100644 index 0000000..c960c6c --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/06-partition-observed.txt @@ -0,0 +1,13 @@ +관찰 시작: 11:42:03 + +20초 suspected(k0 k1) = [] + +40초 suspected(k0 k1) = [] + +60초 suspected(k0 k1) = [0.0 0.0 0.0 0.0 ] + +80초 suspected(k0 k1) = [] + +100초 suspected(k0 k1) = [] + +120초 suspected(k0 k1) = [] + +140초 suspected(k0 k1) = [0.0 ] + +160초 suspected(k0 k1) = [0.0 0.0 0.0 0.0 ] + +=== 클러스터 뷰 변화 (최근 8분) === +--- keycloak-0 --- +--- keycloak-1 --- diff --git a/docs/evidence/a1-jgroups-transport-block/07-cluster-size.txt b/docs/evidence/a1-jgroups-transport-block/07-cluster-size.txt new file mode 100644 index 0000000..04b12b4 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/07-cluster-size.txt @@ -0,0 +1,16 @@ +=== vendor_cluster_size — 지난 25분 (차단 11:38:08, conntrack 삭제 11:41) === + keycloak-0: + 11:20=2 11:21=2 11:22=2 11:23=2 11:24=2 11:25=2 11:26=2 11:27=2 11:28=2 11:29=2 11:30=2 11:31=2 11:32=2 11:33=2 11:34=2 11:35=2 11:36=2 11:37=2 11:38=2 11:39=2 11:40=2 11:41=2 11:42=2 11:43=2 11:44=2 11:45=2 + keycloak-1: + 11:20=2 11:21=2 11:22=2 11:23=2 11:24=2 11:25=2 11:26=2 11:27=2 11:28=2 11:29=2 11:30=2 11:31=2 11:32=2 11:33=2 11:34=2 11:35=2 11:36=2 11:37=2 11:38=2 11:39=2 11:40=2 11:41=2 11:42=2 11:43=2 11:44=2 11:45=2 + +=== 현재 값 === + keycloak-1 = 2 멤버 + keycloak-0 = 2 멤버 + +=== 7800 소켓 상태 (파드 내부) === + keycloak-0 2 + keycloak-1 2 +=== conntrack === + kc-lab-1 1 건 + kc-lab-2 1 건 diff --git a/docs/evidence/a1-jgroups-transport-block/08-restart-forced-partition.txt b/docs/evidence/a1-jgroups-transport-block/08-restart-forced-partition.txt new file mode 100644 index 0000000..c4032a2 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/08-restart-forced-partition.txt @@ -0,0 +1,9 @@ +=== 정책이 걸린 상태에서 keycloak-0 을 재시작한다 → 재연결이 막힌다 === +재시작 시각: 11:46:07 +pod "keycloak-0" deleted from keycloak-lab namespace +keycloak-0 false 10.42.1.67 2026-09-04T02:44:23Z + +=== cluster_size 추이 === + keycloak-0: 11:45:27=1 11:45:57=1 11:46:27=1 11:46:57=1 11:47:27=1 + keycloak-0: 11:40:57=2 11:41:27=2 11:41:57=2 11:42:27=2 11:42:57=2 11:43:27=2 11:43:57=2 + keycloak-1: 11:40:57=2 11:41:27=2 11:41:57=2 11:42:27=2 11:42:57=2 11:43:27=2 11:43:57=2 11:44:27=1 11:44:57=1 11:45:27=1 11:45:57=1 11:46:27=1 11:46:57=1 11:47:27=1 diff --git a/docs/evidence/a1-jgroups-transport-block/09-cross-node-under-partition.txt b/docs/evidence/a1-jgroups-transport-block/09-cross-node-under-partition.txt new file mode 100644 index 0000000..33afaf5 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/09-cross-node-under-partition.txt @@ -0,0 +1,16 @@ +=== keycloak-0 헬스 상태 === + keycloak-0 = 10.42.1.67 keycloak-1 = 10.42.0.35 + PodReadyToStartContainers=True + Initialized=True + Ready=False ContainersNotReady + ContainersReady=False ContainersNotReady + PodScheduled=True + +=== ★ 본 시험 — 분단 상태에서 교차 노드 세션이 되는가 === + [1] keycloak-0 로그인 sid=nShl5TaBrZnKStDqaspjgmJB + [2] keycloak-1 에서 refresh HTTP 200 + [3] keycloak-1 에서 로그아웃 HTTP 204 + [4] keycloak-0 에서 재갱신 시도 HTTP 200 + (400 이면 무효화가 전파된 것) + +=== DB 세션 수 === diff --git a/docs/evidence/a1-jgroups-transport-block/10-logout-not-propagated.txt b/docs/evidence/a1-jgroups-transport-block/10-logout-not-propagated.txt new file mode 100644 index 0000000..88eec93 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/10-logout-not-propagated.txt @@ -0,0 +1,33 @@ +=== 그 sid 가 DB 에 남아 있는가 === + user_session_id | offline_flag | last_session_refresh +-----------------+--------------+---------------------- +(0 rows) + +=== 전체 온라인 세션 수 === + 1 + +=== 노드별 세션 캐시 엔트리 (Prometheus) === + keycloak-1 kc-lab-1 = 0 + keycloak-0 kc-lab-2 = 1 + +=== keycloak-0 이 Ready 가 아닌 이유 — 헬스 응답 === +{ + "status": "DOWN", + "checks": [ + { + "name": "Graceful Shutdown", + "status": "UP" + }, + { + "name": "Keycloak cluster health check", + "status": "DOWN", + "data": { + "Failing since": "2026-09-04 02:45:14,251" + } + }, + { + "name": "Keycloak database connections async health check", + "status": "UP" + }, + { + "name": "Keycloak Initialized", diff --git a/docs/evidence/a1-jgroups-transport-block/11-service-impact.txt b/docs/evidence/a1-jgroups-transport-block/11-service-impact.txt new file mode 100644 index 0000000..be5cdcf --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/11-service-impact.txt @@ -0,0 +1,25 @@ +=== 양쪽 노드의 readiness — 둘 다 DOWN 이면 전면 장애다 === +Traceback (most recent call last): + File "", line 3, in + d=json.load(sys.stdin) + File "/usr/lib/python3.14/json/__init__.py", line 298, in load + return loads(fp.read(), + cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw) + File "/usr/lib/python3.14/json/__init__.py", line 352, in loads + return _default_decoder.decode(s) + ~~~~~~~~~~~~~~~~~~~~~~~^^^ + File "/usr/lib/python3.14/json/decoder.py", line 348, in decode + raise JSONDecodeError("Extra data", s, end) +json.decoder.JSONDecodeError: Extra data: line 21 column 2 (char 446) + +=== 파드 Ready 상태 === +keycloak-0 false 0 +keycloak-1 true 0 + +=== ★ Service 엔드포인트 — 트래픽을 받는 파드가 남아 있는가 === + ready 주소: [10.42.0.35] notReady : [10.42.1.67] +=== ★ 외부 진입점으로 실제 로그인이 되는가 (nginx→Traefik→Service) === + https://auth.hyeonworks.com/realms/master HTTP 200 + 토큰 발급 HTTP 200 diff --git a/docs/evidence/a1-jgroups-transport-block/12-recovery.txt b/docs/evidence/a1-jgroups-transport-block/12-recovery.txt new file mode 100644 index 0000000..33347e3 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/12-recovery.txt @@ -0,0 +1,24 @@ +=== 차단 해제 === +해제 시각: 11:49:58 +networkpolicy.networking.k8s.io "a1-block-jgroups-transport" deleted from keycloak-lab namespace + +=== 자동으로 다시 붙는가 (30초 간격, 최대 4분) === + +30초 keycloak-0=1 keycloak-1=1 | Ready 파드 2 개 + +60초 keycloak-0=1 keycloak-1=1 | Ready 파드 2 개 + +90초 keycloak-0=2 keycloak-1=2 | Ready 파드 3 개 + → 클러스터 재형성 + +=== 복구 로그 === + keycloak-0: [keycloak-0-26403(v=16.0.12)|0] (1) [keycloak-0-26403(v=16.0.12)] + keycloak-1: [keycloak-1-48749(v=16.0.12)|6] (1) [keycloak-1-48749(v=16.0.12)] + +=== MERGE3 가 합쳤는가 === + merge_events keycloak-1 = 1 + merge_events keycloak-0 = 1 +=== JGROUPS_PING — 코디네이터가 하나로 돌아왔는가 === + name | ip | coord +------------------+-----------------+------- + keycloak-0-26403 | 10.42.1.67:7800 | t + keycloak-1-48749 | 10.42.0.35:7800 | f +(2 rows) + diff --git a/docs/evidence/a1-jgroups-transport-block/README.md b/docs/evidence/a1-jgroups-transport-block/README.md new file mode 100644 index 0000000..251a7b6 --- /dev/null +++ b/docs/evidence/a1-jgroups-transport-block/README.md @@ -0,0 +1,26 @@ +# A-1 — JGroups 트랜스포트(7800) 차단 증거 + +2026-09-04 11:38–11:52 KST · Keycloak 26.7.0 / Infinispan 16.0.12 +해설: [`docs/experiment-a1-jgroups-transport-block.md`](../../experiment-a1-jgroups-transport-block.md) + +| 파일 | 무엇을 보여주는가 | +|---|---| +| `01-baseline-cluster.txt` | 차단 전 — 양쪽이 뷰 ID 5·멤버 2로 일치, `JGROUPS_PING` 코디네이터 1명 | +| `02-control-before-block.txt` | **대조군** — 차단 전 교차 노드 refresh `200`, JGroups 지표 전부 0 | +| `03-block-applied.txt` | NetworkPolicy 적용. **빈 측정값을 "변화 감지"로 오판한 기록** | +| `04-after-block-state.txt` | 차단 43초 후 — 지표 무변화, `JGROUPS_PING` 그대로 | +| `05-conntrack-problem.txt` | **핵심 문제** — `ESTABLISHED [ASSURED]` 로 기존 연결이 살아 있음. FD_SOCK2 의 **57800** 포트도 함께 드러남 | +| `06-partition-observed.txt` | 임시 curl 파드 폴링의 실패 — 빈 값·개수 불일치 | +| `07-cluster-size.txt` | **`vendor_cluster_size` 가 25분 내내 2** — 분단이 일어나지 않았다는 결정적 증거 | +| `08-restart-forced-partition.txt` | 재연결 강제 후 `2 → 1` | +| `09-cross-node-under-partition.txt` | **본 시험** — 교차 refresh `200`(예측 적중), **로그아웃 후 재갱신 `200`(예측 빗나감)** | +| `10-logout-not-propagated.txt` | 기제 확정 — **DB 행 0건인데 keycloak-0 캐시에 1건**, 헬스체크 `cluster health: DOWN` | +| `11-service-impact.txt` | **분단 노드가 Service 에서 빠짐.** `ready=[10.42.0.35] notReady=[10.42.1.67]`, 외부 로그인 `200` | +| `12-recovery.txt` | 90초 만에 자동 재형성, `merge3_get_num_merge_events = 1`, 코디네이터 재선출 | +| `a1-cluster-size-partition-recovery.png` | Grafana — `vendor_cluster_size` 가 `2 → 1 → 2` 로 움직이는 전 구간 | + +## 핵심 세 줄 + +1. **NetworkPolicy 만으로는 이미 붙어 있는 클러스터를 못 끊는다.** conntrack 의 ESTABLISHED 가 먼저 통과시킨다. +2. **세션 공유는 분단을 견딘다(200).** 통념이 틀렸고 A-0 모델이 맞다. +3. **로그아웃 무효화는 7800 을 탄다.** DB 행이 지워져도 반대편은 낡은 캐시로 200 을 준다 — A-0 의 인과 해석을 정정한다. diff --git a/docs/evidence/a1-jgroups-transport-block/a1-cluster-size-partition-recovery.png b/docs/evidence/a1-jgroups-transport-block/a1-cluster-size-partition-recovery.png new file mode 100644 index 0000000000000000000000000000000000000000..6449a36f6a41c8bc24cb5eb3611905a18e196d20 GIT binary patch literal 67415 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 literal 0 HcmV?d00001 diff --git a/docs/experiment-a1-jgroups-transport-block.md b/docs/experiment-a1-jgroups-transport-block.md new file mode 100644 index 0000000..1ccc33a --- /dev/null +++ b/docs/experiment-a1-jgroups-transport-block.md @@ -0,0 +1,506 @@ +# A-1 — 노드 간 통신(TCP 7800)을 끊으면 무엇이 깨지는가 + +브랜치 `feature/keycloak-a1-jgroups-transport-block` · +증거 [`docs/evidence/a1-jgroups-transport-block/`](evidence/a1-jgroups-transport-block/) · +2026-09-04 11:38–11:52 KST · Keycloak 26.7.0 / Infinispan 16.0.12 + +맥락은 [`session-lab-prerequisites.md`](session-lab-prerequisites.md), +기준선은 [`experiment-00-session-replication.md`](experiment-00-session-replication.md). + +--- + +## 0. 결론부터 + +| 예측 | 결과 | +|---|---| +| 세션 공유는 **안 깨진다** | **맞다.** 교차 노드 refresh 가 `200` | +| 로그아웃 전파는 **안 깨진다** | **틀렸다.** `400` 이어야 할 것이 `200` | +| — | **NetworkPolicy 만으로는 분단이 일어나지 않는다** (예상 못 함) | +| — | **분단된 노드가 스스로 로드밸런서에서 빠진다** (예상 못 함) | + +**예측 하나가 빗나갔고, 예상하지 못한 것이 둘 나왔다.** 그중 하나는 +실험 방법 자체를 무효화할 뻔했다. + +--- + +## 1. 왜 이 실험인가 + +A-0 에서 **세션은 Infinispan 복제가 아니라 PostgreSQL 로 공유된다**는 것을 +측정했다. 그렇다면 통념과 정면으로 어긋난다. + +| | | +|---|---| +| **통념** (Keycloak 24 이전 자료) | 세션은 7800 으로 복제된다 → **막으면 세션 공유가 깨진다** | +| **A-0 측정** | 세션은 DB 로 공유된다 → **막아도 안 깨진다** | + +둘 중 하나는 틀렸고, 이 실험이 판정한다. + +--- + +## 2. 기준선 + +``` +=== [기준선 1] 클러스터 뷰 === + keycloak-0: [keycloak-1-48749|5] (2) [keycloak-1-48749, keycloak-0-30843] + keycloak-1: [keycloak-1-48749|5] (2) [keycloak-1-48749, keycloak-0-30843] + +=== [기준선 2] JGROUPS_PING === + keycloak-0-30843 | 10.42.1.43:7800 | f + keycloak-1-48749 | 10.42.0.35:7800 | t ← 코디네이터는 하나 + +=== [기준선 4] JGroups 지표 (양쪽 동일) === + fd_sock2_get_num_suspected_members 0.0 + merge3_get_num_merge_events 0.0 + nakack2_get_xmit_table_missing 0.0 +``` + +**대조군** — 차단 전에 같은 절차를 그대로 한 번 돌린다. + +``` +=== [대조군] keycloak-0 로그인 → keycloak-1 에서 refresh === + sid tAWs2gCPr6SOcD4jDR9-_CzB + keycloak-1 에서 refresh: 200 +``` + +A-0 에서 배운 규칙이다 — **시험군만 재는 측정은 측정이 아니다.** + +--- + +## 3. 주입 — NetworkPolicy 로 7800 만 막는다 + +```bash +kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml +``` + +```yaml +spec: + podSelector: { matchLabels: { app: keycloak } } + policyTypes: [Ingress] + ingress: + - ports: + - { port: 8080, protocol: TCP } # HTTP — 열어둔다 + - { port: 9000, protocol: TCP } # health+metrics — 열어둔다 + # 7800 은 일부러 없다 +``` + +### 개념 — NetworkPolicy 는 방화벽이 아니라 **허용 목록**이다 + +**"7800 을 거부"라고 쓸 수 없다.** 파드가 `policyTypes: [Ingress]` 를 가진 +정책에 선택되는 순간 **모든 인바운드가 거부**되고, 규칙에 적힌 것만 통과한다. +그래서 7800 은 **빠뜨림으로써** 막힌다. + +이 구조가 두 허용 규칙을 **결정적으로 만든다.** 잘못 쓰면 분단된 클러스터가 +아니라 **죽은 Keycloak 을 측정하게 된다.** + +| 포트 | 빼면 | +|---|---| +| 8080 | Traefik·상대 노드의 REST 호출이 전부 끊긴다 | +| **9000** | **readiness 프로브가 실패해 kubelet 이 파드를 죽인다** — 엉뚱한 이유로 클러스터가 깨진다 | + +적용 직후 확인했다. + +``` +파드 상태: keycloak-0 ready=true restarts=0 + keycloak-1 ready=true restarts=0 +9000 도달: 10.42.1.43:9000 health=200 / 10.42.0.35:9000 health=200 +8080 도달: 10.42.1.43:8080 root=200 / 10.42.0.35:8080 root=200 +``` + +**주입이 의도한 것만 건드렸음을 먼저 확인한 뒤에 결과를 해석한다.** + +--- + +## 4. 문제 ① — **NetworkPolicy 만으로는 분단이 안 된다** + +가장 중요한 발견이며, 하마터면 **실험 전체를 무효로 만들 뻔했다.** + +차단 후 지표가 꿈쩍도 하지 않았다. 신규 연결은 분명히 막히는데. + +``` +=== 7800 신규 연결 === + 10.42.1.43:7800 curl exit=7 (연결 실패) + 10.42.1.43:9000 curl exit=28 (연결됨, telnet 이라 대기 → 타임아웃) +``` + +그런데 파드 내부 소켓을 보니 + +``` +=== /proc/net/tcp6 · 7800 = 0x1E78 === +keycloak-0: ...2B012A0A:1E78 ...23002A0A:9C57 01 ← 01 = ESTABLISHED +keycloak-1: ...23002A0A:9C57 ...2B012A0A:1E78 01 + (10.42.0.35:40023 → 10.42.1.43:7800) +``` + +**기존 연결이 멀쩡히 살아 있다.** + +### 왜 그런가 — conntrack + +``` + 패킷 도착 + │ + ├─▶ [ conntrack: ESTABLISHED/RELATED 이면 ACCEPT ] ← 여기서 통과해버린다 + │ + └─▶ [ NetworkPolicy 규칙 평가 ] ← 여기까지 오지 않는다 +``` + +리눅스 방화벽은 성능을 위해 **이미 성립한 연결을 먼저 통과**시킨다. +NetworkPolicy 는 그 뒤에 있으므로 **신규 연결(SYN)만** 걸러낸다. + +``` +=== conntrack 확인 === +tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 ... [ASSURED] +tcp 6 79982 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=50477 dport=57800 ... [ASSURED] +tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED] + ───────────────────────────────────────────────────────────── + 신규 연결은 응답을 못 받는다 = 정책이 동작하고는 있다 +``` + +> **운영적 함의 — NetworkPolicy 는 이미 붙어 있는 것을 떼어내지 못한다.** +> 보안 사고 대응으로 "지금 당장 이 통신을 끊어라"에 NetworkPolicy 를 적용하면, +> **새 연결만 막히고 진행 중인 연결은 계속된다.** 끊으려면 conntrack 을 지우거나 +> 파드를 재시작해야 한다. + +### 덤 — **57800 포트도 있다** + +`sport=50477 dport=57800` — FD_SOCK2 는 **`bind_port + 50000`** 을 쓴다. +7800 만 막고 57800 을 열어두면 장애 감지 채널이 남는다. +이 실험의 허용 목록 방식은 **둘 다 자동으로 막았다** — 8080·9000 외 전부 거부이므로. + +### 조치 + +```bash +# 정확한 튜플로 지정해야 지워진다. --dport 만으로는 0건이었다 +sudo conntrack -D -p tcp -s 10.42.0.35 -d 10.42.1.43 --sport 40023 --dport 7800 +sudo conntrack -D -p tcp -s 10.42.1.43 -d 10.42.0.35 --sport 7800 --dport 40023 # 역방향 +``` + +**양쪽 노드에서, 양쪽 방향으로** 지워야 한다. 서버 쪽 노드에는 튜플이 뒤집혀 +기록되어 있다. + +그리고 **즉시 끊기지 않는다.** + +``` +11:41 conntrack 삭제 +11:44 cluster_size 2 → 1 ← 약 3분 뒤 +``` + +TCP 는 상대가 사라졌음을 **재전송 타임아웃**으로 알아낸다. 소켓은 한동안 +`ESTABLISHED` 로 남아 있다. + +--- + +## 5. 문제 ② — 계측 도구가 잘못됐다 + +임시 curl 파드로 20초마다 지표를 긁었더니 이런 결과가 나왔다. + +``` + +20초 suspected(k0 k1) = [] + +60초 suspected(k0 k1) = [0.0 0.0 0.0 0.0 ] + +140초 suspected(k0 k1) = [0.0 ] +``` + +**빈 값, 개수가 맞지 않는 값이 섞인다.** `kubectl run --rm` 은 매번 파드를 +만들고 지우므로 느리고 경합이 있다. + +게다가 첫 시도의 판정 조건이 + +```sh +[ "$R" != "0.0 0.0 " ] && echo "→ 변화 감지" && break +``` + +여서 **빈 문자열을 "변화"로 읽고 즉시 빠져나왔다.** A-0 에서 똑같은 실수를 +했는데 또 했다. + +> **임시 파드는 계측 도구가 아니다.** 15초마다 이미 긁고 있는 Prometheus 가 +> 그러라고 있는 것이다. + +```bash +kubectl -n observability port-forward svc/prometheus 19090:9090 & +curl -s "http://localhost:19090/api/v1/query_range?query=vendor_cluster_size&start=$START&end=$END&step=60" +``` + +그리고 이 과정에서 **`vendor_cluster_size`** 를 발견했다 — 멤버 수를 직접 +알려주는 지표다. 처음부터 이걸 봤어야 했다. + +```bash +curl -s "http://localhost:19090/api/v1/label/__name__/values" | grep -E "cluster|member|view" +``` + +--- + +## 6. 진짜 분단이 일어난 순간 + +``` +=== vendor_cluster_size === +keycloak-1: 11:43:57=2 11:44:27=1 ... 11:51:28=2 +keycloak-0: 11:43:57=2 (파드 교체) 11:45:27=1 ... 11:51:28=2 +``` + +![cluster_size 추이](evidence/a1-jgroups-transport-block/a1-cluster-size-partition-recovery.png) + +정책이 걸린 채 `keycloak-0` 이 재시작되자, 로그가 정확히 말해준다. + +``` +GMS: JOIN(keycloak-0-26403) sent to keycloak-1-48749 timed out ← 10회 +GMS: too many JOIN attempts (10): becoming singleton ← 포기 +ISPN000094: new cluster view [keycloak-0-26403|0] (1) [keycloak-0-26403] +``` + +`keycloak-1` 쪽도 혼자가 되었다. + +``` +ISPN000094: [keycloak-1-48749|6] (1) [keycloak-1-48749] +``` + +### **DB 에는 둘 다 있는데 클러스터는 안 붙는다** — 예측한 그 상태 + +``` +=== JGROUPS_PING === + name | ip | coord +------------------+-----------------+------- + keycloak-0-26403 | 10.42.1.67:7800 | t ← 코디네이터 + keycloak-1-48749 | 10.42.0.35:7800 | t ← 코디네이터 +``` + +**`coord = t` 가 둘.** 교과서적인 split brain 이며, **데이터베이스 한 줄로 +확인된다.** 디스커버리(DB)는 살아 있고 트랜스포트(7800)만 죽은 상태다. + +**단일 노드에서는 만들 수 없는 고장**이며, 이 실험대를 2 VM 으로 만든 이유다. + +--- + +## 7. 본 시험 — 분단 상태에서 세션은 어떻게 되는가 + +``` + [1] keycloak-0 로그인 sid=nShl5TaBrZnKStDqaspjgmJB + [2] keycloak-1 에서 refresh HTTP 200 ← 예측대로 + [3] keycloak-1 에서 로그아웃 HTTP 204 + [4] keycloak-0 에서 재갱신 시도 HTTP 200 ← 400 이어야 했다 +``` + +### [2] 세션 공유 — **예측이 맞았다** + +클러스터가 갈라졌는데도 **한쪽에서 만든 세션을 반대쪽이 갱신했다.** +A-0 의 모델이 맞고, **통념이 틀렸다.** 세션은 7800 으로 다니지 않는다. + +### [4] 로그아웃 전파 — **예측이 틀렸다** + +A-0 에서는 같은 절차가 `400 invalid_grant / Session not active` 였다. +분단 상태에서는 `200` 이다. **로그아웃한 세션이 반대편에서 살아 있다.** + +기제를 확정했다. + +``` +=== 그 sid 가 DB 에 남아 있는가 === + user_session_id | offline_flag | last_session_refresh +-----------------+--------------+---------------------- +(0 rows) ← DB 행은 삭제되었다 + +=== 노드별 세션 캐시 엔트리 === + keycloak-1 kc-lab-1 = 0 + keycloak-0 kc-lab-2 = 1 ← 캐시에는 남아 있다 +``` + +``` + keycloak-1 로그아웃 + │ + ├──▶ PostgreSQL 행 삭제 ✔ 되었다 + │ + └──▶ keycloak-0 에게 "캐시에서 지워라" ✗ 7800 이 막혀 못 갔다 + │ + keycloak-0 은 자기 캐시로 200 을 준다 ◀────────────┘ +``` + +### **A-0 의 결론을 정정한다** + +A-0 에서 나는 이렇게 썼다. + +> 로그아웃과 함께 DB 행이 사라졌다 → 무효화가 DB 삭제로 전파된다 + +**그 인과는 틀렸다.** DB 행 삭제는 일어나지만, **반대편 노드는 DB 를 다시 +읽지 않는다.** 자기 캐시에 있으면 그걸로 답한다. + +> **룩어사이드 캐시는 읽을 때 DB 와 대조하지 않는다.** +> 캐시 무효화는 **클러스터 메시지(7800)를 타고** 간다. + +A-0 에서 400 이 나온 것은 DB 덕분이 아니라 **그때는 7800 이 살아 있어서**였다. +두 실험을 붙여야 비로소 정확한 그림이 나온다. + +| | 세션 **조회** | 세션 **무효화** | +|---|---|---| +| 경로 | PostgreSQL | **클러스터 메시지 (7800)** | +| 7800 차단 시 | 정상 | **전파되지 않음** | + +--- + +## 8. 그런데 안전장치가 있었다 — 예상 못 한 발견 + +`keycloak-0` 이 `Ready=false` 였다. 이유를 물었더니 + +```json +{ "status": "DOWN", + "checks": [ + { "name": "Keycloak cluster health check", "status": "DOWN", + "data": { "Failing since": "2026-09-04 02:45:14,251" } }, + { "name": "Keycloak database connections async health check", "status": "UP" } + ] } +``` + +**Keycloak 은 클러스터 분단을 readiness 로 신고한다.** 그리고 쿠버네티스가 +그 신고를 받아 처리했다. + +``` +=== Service 엔드포인트 === + ready 주소: [10.42.0.35] ← keycloak-1 만 트래픽을 받는다 + notReady : [10.42.1.67] ← keycloak-0 은 제외되었다 + +=== 외부 진입점 === + https://auth.hyeonworks.com/realms/master HTTP 200 + 토큰 발급 HTTP 200 +``` + +**분단된 노드가 스스로 로드밸런서에서 빠졌고, 서비스는 계속되었다.** + +### 그래서 7절의 로그아웃 우회는 어떻게 봐야 하나 + +| | | +|---|---| +| 내가 한 것 | Service 를 우회해 **파드 IP 로 직접** 호출 | +| 실제 사용자 | nginx → Traefik → **Service** → Ready 인 파드만 | + +**정문으로 들어오면 낡은 캐시에 닿지 않는다.** readiness 게이트가 막는다. + +> 다만 이건 **비대칭이라서 살았다.** `keycloak-1` 은 원래 뷰에서 멤버가 하나 +> 줄어든 정상적인 사건이라 Ready 를 유지했고, `keycloak-0` 은 합류 자체를 +> 못 해 DOWN 이 되었다. **양쪽이 동시에 DOWN 이 되는 경로가 있다면 전면 장애다.** +> A-5(비대칭 파티션)에서 이어서 본다. + +--- + +## 9. 복구 + +```bash +kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport +``` + +``` + +30초 keycloak-0=1 keycloak-1=1 + +60초 keycloak-0=1 keycloak-1=1 + +90초 keycloak-0=2 keycloak-1=2 ← 재형성 +``` + +**90초 만에 자동으로 다시 붙었다. 사람 손이 필요 없었다.** + +``` +=== MERGE3 가 합쳤는가 === + merge_events keycloak-0 = 1 + merge_events keycloak-1 = 1 +``` + +**MERGE3 가 한 일이다.** split brain 을 감지해 뷰를 병합하는 프로토콜이며, +지표가 `0 → 1` 로 올라간 것이 그 증거다. + +``` +=== JGROUPS_PING === + keycloak-0-26403 | 10.42.1.67:7800 | t + keycloak-1-48749 | 10.42.0.35:7800 | f ← 코디네이터가 하나로 돌아왔다 +``` + +**코디네이터가 keycloak-1 에서 keycloak-0 으로 넘어갔다.** 코디네이터는 +특권이 아니라 역할이며, 병합 시 재선출된다. + +--- + +## 10. 개념 정리 + +### conntrack — 연결 추적 + +리눅스 커널이 **진행 중인 연결을 기억**하는 표. 패킷마다 규칙을 다시 평가하지 +않기 위해 존재한다. + +| 상태 | 뜻 | +|---|---| +| `NEW` | 첫 패킷(SYN) | +| **`ESTABLISHED`** | **양방향 통신이 성립함 — 규칙 평가를 건너뛴다** | +| `[ASSURED]` | 충분히 오래된 연결. 표가 꽉 차도 안 지워진다 | +| `SYN_SENT [UNREPLIED]` | 보냈는데 답이 없음 = **차단되고 있다** | + +```bash +sudo conntrack -L | grep 7800 +sudo conntrack -D -p tcp -s -d --sport --dport +``` + +### FD_SOCK2 와 포트 규약 + +| 프로토콜 | 포트 | 하는 일 | +|---|---|---| +| TCP (트랜스포트) | **7800** | 클러스터 메시지 | +| **FD_SOCK2** | **57800** = 7800 + 50000 | 소켓으로 상대 생존 감시 | + +**방화벽 규칙을 손으로 쓸 때 57800 을 빠뜨리기 쉽다.** + +### MERGE3 + +split brain 이 생긴 뒤 **갈라진 뷰를 다시 합치는** JGroups 프로토콜. +주기적으로 다른 코디네이터의 존재를 확인하고, 발견하면 병합을 개시한다. + +```promql +vendor_jgroups_merge3_get_num_merge_events +``` + +### readiness 프로브와 Service 엔드포인트 + +``` + readiness 실패 → 파드가 Service 의 notReadyAddresses 로 이동 + → kube-proxy 가 그 파드로 라우팅하지 않음 + → 살아 있지만 트래픽은 안 받음 +``` + +**liveness 와 다르다.** liveness 실패는 **재시작**, readiness 실패는 +**격리**다. 클러스터 분단처럼 "재시작해도 안 나아지는" 문제에는 readiness 가 +맞는 신호다. + +--- + +## 11. 재현 절차 (명령어) + +```bash +# 0. 기준선 +kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ + -c "select name, ip, coord from jgroups_ping order by name" +kubectl -n observability port-forward svc/prometheus 19090:9090 & +curl -s "http://localhost:19090/api/v1/query?query=vendor_cluster_size" + +# 1. 차단 +kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml + +# 2. 주입이 의도한 것만 건드렸는지 확인 (8080/9000 은 살아 있어야 한다) +kubectl -n keycloak-lab get pods -o wide | grep keycloak # restarts=0 확인 + +# 3. 기존 연결이 남아 있음을 확인 — 이걸 안 하면 실험이 무효다 +ssh kc-lab-1 'sudo conntrack -L | grep 7800' + +# 4. conntrack 삭제 (양쪽 노드, 양쪽 방향). 반영까지 약 3분 +ssh kc-lab-1 'sudo conntrack -D -p tcp -s -d --sport --dport 7800' +ssh kc-lab-2 'sudo conntrack -D -p tcp -s -d --sport 7800 --dport ' + +# 5. 분단 확인 +curl -s "http://localhost:19090/api/v1/query?query=vendor_cluster_size" +kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ + -c "select name, coord from jgroups_ping" # coord=t 가 둘이면 split brain + +# 6. 복구 +kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport +curl -s "http://localhost:19090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events" +``` + +--- + +## 12. 다음 실험에 남기는 것 + +| 실험 | 이 실험이 준 것 | +|---|---| +| **A-5** 비대칭 파티션 | **양쪽이 동시에 NotReady 가 되는 경로가 있는가.** 여기서는 비대칭이라 살았다 | +| **A-2** DB 정지 | 캐시가 DB 와 대조하지 않는다는 사실 → **캐시에 있는 세션은 DB 없이도 읽힐 수 있다** | +| **A-7** volatile 비교 | 같은 주입에서 세션 공유가 **깨져야** 한다. 이 실험이 그 대조군 | +| 전체 | **주입이 실제로 걸렸는지 먼저 확인한다.** NetworkPolicy 는 기존 연결을 못 끊는다 |