Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4177fb6a48 | ||
|
|
2a98ef1090 |
@@ -0,0 +1,41 @@
|
||||
[ 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] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ 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
|
||||
[ 182011ms] [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
|
||||
[ 188613ms] [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
|
||||
[ 206228ms] [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
|
||||
[ 218561ms] [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
|
||||
[ 229607ms] [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
|
||||
[ 235818ms] [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
|
||||
[ 237158ms] [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
|
||||
[ 252095ms] [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
|
||||
[ 261118ms] [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
|
||||
[ 273809ms] [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
|
||||
[ 280227ms] [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
|
||||
[ 292650ms] [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
|
||||
[ 306477ms] [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
|
||||
[ 309957ms] [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
|
||||
[ 311984ms] [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
|
||||
[ 325683ms] [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
|
||||
[ 344586ms] [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
|
||||
[ 357576ms] [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
|
||||
[ 359294ms] [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
|
||||
[ 364740ms] [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
|
||||
@@ -0,0 +1,16 @@
|
||||
[ 615ms] [WARNING] <meta name="apple-mobile-web-app-capable" content="yes"> is deprecated. Please include <meta name="mobile-web-app-capable" content="yes"> @ https://app2.hyeonworks.com/explore?schemaVersion=1&panes=%7B%22te0%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up+%E2%80%94+%7B%7Bpod%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-20m%22%2C%22to%22%3A%22now%22%7D%7D%7D&orgId=1:0
|
||||
[ 929ms] [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
|
||||
[ 2566ms] [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
|
||||
[ 5541ms] [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
|
||||
[ 7990ms] [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
|
||||
[ 12402ms] [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
|
||||
[ 17640ms] [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
|
||||
[ 20285ms] [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
|
||||
[ 28277ms] [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
|
||||
[ 34341ms] [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
|
||||
[ 34985ms] [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
|
||||
[ 50081ms] [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
|
||||
[ 65649ms] [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
|
||||
[ 75046ms] [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
|
||||
[ 75890ms] [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
|
||||
[ 96067ms] [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
|
||||
@@ -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
|
||||
@@ -0,0 +1,145 @@
|
||||
- generic [active] [ref=f3e1]:
|
||||
- generic [ref=f3e4]:
|
||||
- link "Skip to main content" [ref=f3e5] [cursor=pointer]:
|
||||
- /url: "#pageContent"
|
||||
- banner [ref=f3e7]:
|
||||
- generic [ref=f3e8]:
|
||||
- link [ref=f3e10] [cursor=pointer]:
|
||||
- /url: /
|
||||
- img "Grafana" [ref=f3e11]
|
||||
- generic [ref=f3e14]:
|
||||
- button "Search or jump to..." [ref=f3e18] [cursor=pointer]
|
||||
- generic [ref=f3e19]: ctrl+k
|
||||
- generic [ref=f3e23]:
|
||||
- button "New" [ref=f3e24] [cursor=pointer]
|
||||
- button "Help" [ref=f3e30] [cursor=pointer]
|
||||
- button "News" [ref=f3e33] [cursor=pointer]
|
||||
- button "Profile" [ref=f3e36] [cursor=pointer]:
|
||||
- img "User avatar" [ref=f3e37]
|
||||
- generic [ref=f3e38]:
|
||||
- button "Open menu" [ref=f3e40] [cursor=pointer]
|
||||
- navigation "Breadcrumbs" [ref=f3e43]:
|
||||
- list [ref=f3e44]:
|
||||
- listitem [ref=f3e45]:
|
||||
- link "Home" [ref=f3e46] [cursor=pointer]:
|
||||
- /url: /
|
||||
- listitem [ref=f3e50]:
|
||||
- link "Explore" [ref=f3e51] [cursor=pointer]:
|
||||
- /url: /explore
|
||||
- listitem [ref=f3e55]:
|
||||
- generic "Prometheus" [ref=f3e56]
|
||||
- generic [ref=f3e57]:
|
||||
- generic [ref=f3e60]:
|
||||
- button "Copy shortened URL" [ref=f3e61] [cursor=pointer]
|
||||
- button "Open copy link options" [ref=f3e64] [cursor=pointer]
|
||||
- button "Toggle top search bar" [ref=f3e68] [cursor=pointer]
|
||||
- main [ref=f3e74]:
|
||||
- generic [ref=f3e76]:
|
||||
- heading "Explore" [level=1] [ref=f3e77]
|
||||
- generic [ref=f3e82]:
|
||||
- navigation "Explore toolbar" [ref=f3e84]:
|
||||
- navigation "Search links" [ref=f3e86]:
|
||||
- generic [ref=f3e87]:
|
||||
- button "Content outline" [expanded] [ref=f3e89] [cursor=pointer]:
|
||||
- generic [ref=f3e92]: Outline
|
||||
- generic [ref=f3e97] [cursor=pointer]:
|
||||
- img "Prometheus logo" [ref=f3e99]
|
||||
- textbox "Select a data source" [ref=f3e100]:
|
||||
- /placeholder: ""
|
||||
- generic [ref=f3e104]:
|
||||
- button "Split the pane" [ref=f3e106] [cursor=pointer]:
|
||||
- generic [ref=f3e109]: Split
|
||||
- button "Add" [ref=f3e111] [cursor=pointer]
|
||||
- generic [ref=f3e116]:
|
||||
- 'button "Time range selected: Last 20 minutes" [ref=f3e117] [cursor=pointer]'
|
||||
- button "Zoom out time range" [ref=f3e122] [cursor=pointer]
|
||||
- generic [ref=f3e126]:
|
||||
- button "Run query" [ref=f3e127] [cursor=pointer]
|
||||
- button "Auto refresh turned off. Choose refresh time interval" [ref=f3e131] [cursor=pointer]
|
||||
- generic [ref=f3e135]:
|
||||
- generic [ref=f3e139]:
|
||||
- button "Collapse outline" [expanded] [ref=f3e141] [cursor=pointer]:
|
||||
- img "arrow-from-right" [ref=f3e142]
|
||||
- button "Queries" [ref=f3e145] [cursor=pointer]:
|
||||
- img "arrow" [ref=f3e146]
|
||||
- button "Graph" [ref=f3e150] [cursor=pointer]:
|
||||
- img "graph-bar" [ref=f3e151]
|
||||
- generic [ref=f3e158]:
|
||||
- generic [ref=f3e160]:
|
||||
- generic "Query editor row" [ref=f3e163]:
|
||||
- generic [ref=f3e164]:
|
||||
- generic [ref=f3e166]:
|
||||
- generic [ref=f3e167]:
|
||||
- button "Collapse query row" [expanded] [ref=f3e168] [cursor=pointer]
|
||||
- generic [ref=f3e171]:
|
||||
- button "Query editor row title A" [ref=f3e172] [cursor=pointer]:
|
||||
- generic [ref=f3e173]: A
|
||||
- emphasis [ref=f3e174]: (Prometheus)
|
||||
- generic [ref=f3e175]:
|
||||
- button "Show data source help" [ref=f3e177] [cursor=pointer]
|
||||
- button "Duplicate query" [ref=f3e181] [cursor=pointer]
|
||||
- button "Hide response" [ref=f3e185] [cursor=pointer]
|
||||
- button "Remove query" [ref=f3e189] [cursor=pointer]
|
||||
- button "Drag and drop to reorder" [ref=f3e192]:
|
||||
- img "Drag and drop to reorder" [ref=f3e193]
|
||||
- generic [ref=f3e196]:
|
||||
- generic [ref=f3e197]:
|
||||
- button "Kick start your query" [ref=f3e198] [cursor=pointer]
|
||||
- generic [ref=f3e201]:
|
||||
- generic [ref=f3e202] [cursor=pointer]: Explain
|
||||
- generic [ref=f3e203]:
|
||||
- checkbox "Explain Toggle switch" [ref=f3e204]
|
||||
- generic "Toggle switch" [ref=f3e205] [cursor=pointer]
|
||||
- radiogroup [ref=f3e210]:
|
||||
- generic [ref=f3e211]:
|
||||
- radio "Builder" [ref=f3e212] [cursor=pointer]
|
||||
- generic [ref=f3e213] [cursor=pointer]: Builder
|
||||
- generic [ref=f3e214]:
|
||||
- radio "Code" [checked] [ref=f3e215] [cursor=pointer]
|
||||
- generic [ref=f3e216] [cursor=pointer]: Code
|
||||
- generic [ref=f3e218]:
|
||||
- generic [ref=f3e220]:
|
||||
- button "Loading metrics..." [disabled] [ref=f3e221] [cursor=pointer]
|
||||
- code [ref=f3e228]:
|
||||
- generic [ref=f3e229]:
|
||||
- generic [ref=f3e234]: "up{job=\"keycloak\"}"
|
||||
- textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f3e239]: "up{job=\"keycloak\"}"
|
||||
- 'button "Options Legend: up — {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f3e245] [cursor=pointer]':
|
||||
- generic [ref=f3e249]:
|
||||
- heading "Options" [level=6] [ref=f3e250]
|
||||
- generic [ref=f3e251]:
|
||||
- generic [ref=f3e252]: "Legend: up — {{pod}}"
|
||||
- generic [ref=f3e253]: "Format: Time series"
|
||||
- generic [ref=f3e254]: "Step: auto"
|
||||
- generic [ref=f3e255]: "Type: Range"
|
||||
- generic [ref=f3e256]: "Exemplars: false"
|
||||
- generic [ref=f3e257]:
|
||||
- button "Add query" [ref=f3e258] [cursor=pointer]
|
||||
- button "Query history" [ref=f3e262] [cursor=pointer]
|
||||
- button "Query inspector" [ref=f3e266] [cursor=pointer]
|
||||
- main [ref=f3e270]:
|
||||
- region [ref=f3e272]:
|
||||
- generic [ref=f3e273]:
|
||||
- heading "Graph" [level=2] [ref=f3e275]
|
||||
- radiogroup [ref=f3e278]:
|
||||
- generic [ref=f3e279]:
|
||||
- radio "Lines" [checked] [ref=f3e280] [cursor=pointer]
|
||||
- generic [ref=f3e281] [cursor=pointer]: Lines
|
||||
- generic [ref=f3e282]:
|
||||
- radio "Bars" [ref=f3e283] [cursor=pointer]
|
||||
- generic [ref=f3e284] [cursor=pointer]: Bars
|
||||
- generic [ref=f3e285]:
|
||||
- radio "Points" [ref=f3e286] [cursor=pointer]
|
||||
- generic [ref=f3e287] [cursor=pointer]: Points
|
||||
- generic [ref=f3e288]:
|
||||
- radio "Stacked lines" [ref=f3e289] [cursor=pointer]
|
||||
- generic [ref=f3e290] [cursor=pointer]: Stacked lines
|
||||
- generic [ref=f3e291]:
|
||||
- radio "Stacked bars" [ref=f3e292] [cursor=pointer]
|
||||
- generic [ref=f3e293] [cursor=pointer]: Stacked bars
|
||||
- generic [ref=f3e294]: Loading plugin panel...
|
||||
- generic [ref=f3e299]:
|
||||
- alert
|
||||
- alert
|
||||
- complementary
|
||||
- complementary
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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) =
|
||||
→ 변화 감지
|
||||
@@ -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)
|
||||
|
||||
@@ -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 건
|
||||
@@ -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 ---
|
||||
@@ -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 건
|
||||
@@ -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
|
||||
@@ -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 세션 수 ===
|
||||
@@ -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",
|
||||
@@ -0,0 +1,25 @@
|
||||
=== 양쪽 노드의 readiness — 둘 다 DOWN 이면 전면 장애다 ===
|
||||
Traceback (most recent call last):
|
||||
File "<string>", line 3, in <module>
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 의 인과 해석을 정정한다.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,14 @@
|
||||
=== A-2 기준선 — 클러스터가 정상으로 돌아왔는가 ===
|
||||
keycloak-0 true 10.42.1.67 kc-lab-2
|
||||
keycloak-1 true 10.42.0.35 kc-lab-1
|
||||
postgres-7b474b88c8-sn9ff true 10.42.1.24 kc-lab-2
|
||||
|
||||
cluster_size keycloak-1 = 2
|
||||
cluster_size keycloak-0 = 2
|
||||
|
||||
=== 노드별 세션 캐시 (실험 설계에 필요) ===
|
||||
keycloak-1 kc-lab-1 = 0 건
|
||||
keycloak-0 kc-lab-2 = 0 건
|
||||
|
||||
=== DB 온라인 세션 ===
|
||||
2
|
||||
@@ -0,0 +1,10 @@
|
||||
pod/a2-probe condition met
|
||||
keycloak-0=10.42.1.67 keycloak-1=10.42.0.35
|
||||
|
||||
=== [준비] 양쪽 노드에 세션을 하나씩 만든다 ===
|
||||
keycloak-0 에서 로그인 sid=EAXV5HcG2J1BZ3vnwONf64AQ 토큰길이=613
|
||||
keycloak-1 에서 로그인 sid=McyTj5lj3n_JqApCXeuAHExc 토큰길이=613
|
||||
|
||||
=== [확인] 세션이 각자 노드에만 캐시되었는가 ===
|
||||
keycloak-1 = 0 건
|
||||
keycloak-0 = 1 건
|
||||
@@ -0,0 +1,16 @@
|
||||
=== [1] 토큰을 새로 발급 (access 수명 60초) ===
|
||||
발급 완료 sid=RKXQGAgkuLtouFMVPTFmp_0_
|
||||
|
||||
=== [2] PostgreSQL 정지 ===
|
||||
정지 시각: 11:56:04
|
||||
deployment.apps/postgres scaled
|
||||
pod/postgres-7b474b88c8-sn9ff condition met
|
||||
삭제 완료: 11:56:04
|
||||
|
||||
=== [3] 네 경로를 즉시 시험 ===
|
||||
④ 이미 발급된 access token 으로 관리 API HTTP 000000{"error":"HTTP 401 Unauthorized"}401
|
||||
① 캐시를 가진 노드(keycloak-0)에서 refresh HTTP 500
|
||||
② 캐시가 없는 노드(keycloak-1)에서 refresh HTTP 500
|
||||
③ 새 로그인 HTTP 500
|
||||
--- 오류 본문 (새 로그인) ---
|
||||
{"error":"unknown_error","error_description":"For more on this error consult the server log."}
|
||||
@@ -0,0 +1,29 @@
|
||||
=== 파드 Ready 상태 — DB 가 없으면 어떻게 되는가 ===
|
||||
keycloak-0 false 0
|
||||
keycloak-1 false 0
|
||||
|
||||
=== Service 엔드포인트 ===
|
||||
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
|
||||
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
|
||||
notReady: [10.42.0.35 10.42.1.67]
|
||||
=== health/ready 상세 ===
|
||||
전체: DOWN
|
||||
Graceful Shutdown UP
|
||||
Keycloak cluster health check UP
|
||||
Keycloak database connections async health check DOWN
|
||||
Keycloak Initialized UP
|
||||
|
||||
=== ④ 다시 — 서명 검증만 필요한 경로는 살아 있는가 ===
|
||||
JWKS 엔드포인트(realm 공개키) HTTP 200
|
||||
realm 메타데이터(.well-known) HTTP 200
|
||||
관리 API(세션 조회 필요) HTTP 500
|
||||
|
||||
=== 외부 진입점 ===
|
||||
https://auth.hyeonworks.com/realms/master HTTP 503
|
||||
|
||||
=== Keycloak 로그 — 실제 오류 ===
|
||||
at io.agroal.pool.ConnectionPool$CreateConnectionTask.call(ConnectionPool.java:664)
|
||||
at io.agroal.pool.ConnectionPool$CreateConnectionTask.call(ConnectionPool.java:645)
|
||||
Caused by: java.net.ConnectException: Connection refused
|
||||
at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:219)
|
||||
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:365)
|
||||
@@ -0,0 +1,21 @@
|
||||
=== ★ up 지표는 무엇을 말하는가 (프로세스는 살아 있다) ===
|
||||
up{pod=keycloak-1} = 1 ← 1 인데 서비스는 503 이다
|
||||
up{pod=keycloak-0} = 1 ← 1 인데 서비스는 503 이다
|
||||
|
||||
=== 복구 — PostgreSQL 재기동 ===
|
||||
재기동 시각: 11:57:09
|
||||
deployment.apps/postgres scaled
|
||||
Waiting for deployment "postgres" rollout to finish: 0 out of 1 new replicas have been updated...
|
||||
Waiting for deployment "postgres" rollout to finish: 0 of 1 updated replicas are available...
|
||||
deployment "postgres" successfully rolled out
|
||||
|
||||
=== Keycloak 이 스스로 회복하는가 (재시작 없이) ===
|
||||
+15초 keycloak-0 true keycloak-1 true | 외부 HTTP 200
|
||||
→ 서비스 복귀
|
||||
|
||||
=== 재시작 횟수 — 파드가 죽었다 살아난 것인가, 그대로 회복한 것인가 ===
|
||||
keycloak-0 0
|
||||
keycloak-1 0
|
||||
|
||||
=== 정지 전 세션이 살아남았는가 ===
|
||||
online 세션 5
|
||||
@@ -0,0 +1,19 @@
|
||||
# A-2 — PostgreSQL 정지 증거
|
||||
|
||||
2026-09-04 11:56–11:58 KST · Keycloak 26.7.0
|
||||
해설: [`docs/experiment-a2-database-loss.md`](../../experiment-a2-database-loss.md)
|
||||
|
||||
| 파일 | 무엇을 보여주는가 |
|
||||
|---|---|
|
||||
| `01-baseline.txt` | 정지 전 — 양쪽 Ready, `cluster_size=2` |
|
||||
| `02-setup-sessions.txt` | 양쪽 노드에 세션 하나씩. 캐시는 각자 노드에만 |
|
||||
| `03-four-paths.txt` | **네 경로 전부 `500`.** 캐시를 가진 노드도 실패 — refresh 는 쓰기다 |
|
||||
| `04-health-and-service.txt` | **전면 장애 증거** — Ready 파드 0개, `ready 주소=[]`, 외부 **503**, `database connections: DOWN`. JWKS·.well-known 은 `200` |
|
||||
| `05-recovery.txt` | **`up=1` 인 채로 503.** DB 복귀 15초 후 재시작 0회로 자동 회복, 세션 5건 생존 |
|
||||
| `a2-up-stayed-1-during-outage.png` | Grafana — `up{job="keycloak"}` 이 전면 장애 내내 **1에 평평** |
|
||||
|
||||
## 핵심 세 줄
|
||||
|
||||
1. **DB 는 단일 장애점이다.** Keycloak 을 몇 대로 늘려도 같이 죽는다 — Ready 파드 0개, 외부 503.
|
||||
2. **캐시는 읽기를 대신할 뿐 쓰기를 못 한다.** refresh 는 `UPDATE LAST_SESSION_REFRESH` 를 하므로 캐시가 있어도 실패한다.
|
||||
3. **`up` 은 이 장애를 못 잡는다.** 알림은 readiness 와 외부 응답 코드에 걸어야 한다.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -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
|
||||
```
|
||||
|
||||

|
||||
|
||||
정책이 걸린 채 `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 <src> -d <dst> --sport <sp> --dport <dp>
|
||||
```
|
||||
|
||||
### 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 <k1ip> -d <k0ip> --sport <sp> --dport 7800'
|
||||
ssh kc-lab-2 'sudo conntrack -D -p tcp -s <k0ip> -d <k1ip> --sport 7800 --dport <sp>'
|
||||
|
||||
# 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 는 기존 연결을 못 끊는다 |
|
||||
@@ -0,0 +1,308 @@
|
||||
# A-2 — PostgreSQL 이 죽으면 어떻게 되는가
|
||||
|
||||
브랜치 `feature/keycloak-a2-database-loss` ·
|
||||
증거 [`docs/evidence/a2-database-loss/`](evidence/a2-database-loss/) ·
|
||||
2026-09-04 11:56–11:58 KST · Keycloak 26.7.0
|
||||
|
||||
선행: [`A-0`](experiment-00-session-replication.md) ·
|
||||
[`A-1`](experiment-a1-jgroups-transport-block.md)
|
||||
|
||||
---
|
||||
|
||||
## 0. 결론부터
|
||||
|
||||
| 예측 | 결과 |
|
||||
|---|---|
|
||||
| 즉시 전면 장애 | **맞다.** 외부 진입점 **503**, 양쪽 노드 NotReady |
|
||||
| 캐시에 있어도 못 쓴다 | **맞다.** 캐시를 가진 노드도 `500` |
|
||||
| — | **`up = 1` 인 채로 전면 장애가 났다** (관측의 함정) |
|
||||
| — | **DB 복귀 15초 만에 재시작 없이 자동 회복** |
|
||||
|
||||
**A-1 과 정반대다.** A-1 은 한쪽만 빠지고 서비스가 계속됐지만,
|
||||
A-2 는 **살아남는 노드가 없다.**
|
||||
|
||||
---
|
||||
|
||||
## 1. 설계 — 네 경로를 구분해서 본다
|
||||
|
||||
A-1 에서 **"룩어사이드 캐시는 읽을 때 DB 와 대조하지 않는다"** 를 확인했다.
|
||||
그렇다면 캐시를 가진 노드는 DB 없이도 버틸지 모른다. 그 가설을 가른다.
|
||||
|
||||
| # | 경로 | 무엇을 보는가 |
|
||||
|---|---|---|
|
||||
| ① | **캐시를 가진 노드**에서 refresh | 캐시가 DB 를 대신할 수 있는가 |
|
||||
| ② | 캐시가 없는 노드에서 refresh | 완전한 DB 의존 |
|
||||
| ③ | 새 로그인 | 쓰기 경로 |
|
||||
| ④ | 이미 발급된 토큰으로 조회 | 서명만으로 되는 경로 |
|
||||
|
||||
**access token 수명이 60초**이므로, 토큰 발급 → DB 정지 → 시험을 그 안에
|
||||
끝내야 한다.
|
||||
|
||||
### 계측 도구를 바꿨다
|
||||
|
||||
A-1 에서 임시 curl 파드가 형편없는 계측 도구임을 확인했다. 여기서는
|
||||
**상주 탐침 파드**를 하나 띄우고 `exec` 로 단계를 이어간다. 토큰을 파드 안
|
||||
파일에 남겨 **DB 정지 전후로 같은 토큰**을 쓸 수 있다.
|
||||
|
||||
```bash
|
||||
kubectl -n keycloak-lab run a2-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never --command -- sleep 7200
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a2-probe --timeout=120s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 기준선
|
||||
|
||||
```
|
||||
keycloak-0 ready=true 10.42.1.67 kc-lab-2
|
||||
keycloak-1 ready=true 10.42.0.35 kc-lab-1
|
||||
postgres ready=true 10.42.1.24 kc-lab-2
|
||||
|
||||
cluster_size keycloak-0 = 2
|
||||
cluster_size keycloak-1 = 2
|
||||
```
|
||||
|
||||
세션을 양쪽에 하나씩 만들고, A-0 대로 **각자 자기 노드에만 캐시**되는 것을
|
||||
확인했다.
|
||||
|
||||
```
|
||||
keycloak-0 에서 로그인 sid=EAXV5HcG2J1BZ3vnwONf64AQ
|
||||
keycloak-1 에서 로그인 sid=McyTj5lj3n_JqApCXeuAHExc
|
||||
→ 캐시 keycloak-0 = 1 건 / keycloak-1 = 0 건 (스크레이프 지연)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 주입
|
||||
|
||||
```bash
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
```
|
||||
|
||||
```
|
||||
정지 시각: 11:56:04
|
||||
삭제 완료: 11:56:04 ← 즉시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 결과 — 네 경로
|
||||
|
||||
```
|
||||
① 캐시를 가진 노드(keycloak-0)에서 refresh HTTP 500
|
||||
② 캐시가 없는 노드(keycloak-1)에서 refresh HTTP 500
|
||||
③ 새 로그인 HTTP 500
|
||||
④ 관리 API (세션 조회 필요) HTTP 500
|
||||
|
||||
--- 오류 본문 ---
|
||||
{"error":"unknown_error","error_description":"For more on this error consult the server log."}
|
||||
```
|
||||
|
||||
### ① 이 500 인 것이 중요하다
|
||||
|
||||
**캐시에 세션을 들고 있어도 refresh 는 실패한다.**
|
||||
|
||||
A-1 에서는 로그아웃된 세션을 캐시로 `200` 을 줬다. 왜 여기서는 안 되는가.
|
||||
|
||||
```
|
||||
refresh 처리
|
||||
├── 세션이 존재하는가 → 캐시로 답할 수 있다
|
||||
└── LAST_SESSION_REFRESH 갱신 → DB 쓰기가 필요하다 ← 여기서 죽는다
|
||||
```
|
||||
|
||||
A-0 에서 잡은 SQL 그대로다.
|
||||
|
||||
```sql
|
||||
update OFFLINE_USER_SESSION set LAST_SESSION_REFRESH=$1, VERSION=$2 where ...
|
||||
```
|
||||
|
||||
> **캐시는 읽기를 대신할 뿐, 쓰기를 대신하지 못한다.**
|
||||
> refresh 는 이름과 달리 **쓰기 연산**이다.
|
||||
|
||||
### 로그가 말하는 원인
|
||||
|
||||
```
|
||||
Caused by: java.net.ConnectException: Connection refused
|
||||
at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect
|
||||
at io.agroal.pool.ConnectionPool$CreateConnectionTask.call
|
||||
```
|
||||
|
||||
`agroal` 은 Quarkus 의 커넥션 풀이다. 풀이 새 커넥션을 만들지 못한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 살아남은 것 — 상태가 필요 없는 경로
|
||||
|
||||
```
|
||||
JWKS 엔드포인트(realm 공개키) HTTP 200
|
||||
realm 메타데이터(.well-known) HTTP 200
|
||||
관리 API (세션 조회 필요) HTTP 500
|
||||
```
|
||||
|
||||
**realm 공개키와 메타데이터는 메모리에 있으므로 DB 없이도 응답한다.**
|
||||
|
||||
이론적으로는 **이미 JWKS 를 캐시한 리소스 서버는 토큰 검증을 계속할 수 있다**는
|
||||
뜻이다. 다만 이 실험대에는 독립 리소스 서버가 아직 없으므로 **여기까지가
|
||||
말할 수 있는 범위**다 — B층에서 확인한다.
|
||||
|
||||
> **그런데 정문으로는 이것도 못 쓴다.** 아래 6절 때문이다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 전면 장애 — 살아남는 노드가 없다
|
||||
|
||||
```
|
||||
=== 파드 Ready ===
|
||||
keycloak-0 false restarts=0
|
||||
keycloak-1 false restarts=0
|
||||
|
||||
=== Service 엔드포인트 ===
|
||||
ready : [] ← 비었다
|
||||
notReady: [10.42.0.35 10.42.1.67]
|
||||
|
||||
=== 외부 진입점 ===
|
||||
https://auth.hyeonworks.com/realms/master HTTP 503
|
||||
```
|
||||
|
||||
```json
|
||||
{ "status": "DOWN",
|
||||
"checks": [
|
||||
{ "name": "Keycloak cluster health check", "status": "UP" },
|
||||
{ "name": "Keycloak database connections async health check", "status": "DOWN" },
|
||||
{ "name": "Keycloak Initialized", "status": "UP" } ] }
|
||||
```
|
||||
|
||||
**`cluster health` 는 UP 인데 `database connections` 가 DOWN 이라 전체가 DOWN 이다.**
|
||||
헬스체크는 **모든 항목이 UP 이어야 UP** 이다.
|
||||
|
||||
### A-1 과의 대비가 이 실험의 핵심이다
|
||||
|
||||
| | A-1 (7800 차단) | **A-2 (DB 정지)** |
|
||||
|---|---|---|
|
||||
| Ready 인 파드 | keycloak-1 **1개 생존** | **0개** |
|
||||
| Service `ready` | `[10.42.0.35]` | **`[]`** |
|
||||
| 외부 응답 | **200** | **503** |
|
||||
| 성격 | 용량 저하 | **전면 장애** |
|
||||
|
||||
**노드를 몇 대로 늘려도 DB 가 죽으면 전부 같이 죽는다.**
|
||||
Keycloak 의 대수는 DB 장애에 아무 도움이 되지 않는다.
|
||||
|
||||
> 원래 질문 *"Redis 또는 DB가 뒤질 경우 어떻게 복구를 해야 되는지"* 에 대한
|
||||
> 첫 번째 답 — **복구 이전에, DB 이중화가 Keycloak 대수보다 우선한다.**
|
||||
|
||||
---
|
||||
|
||||
## 7. 관측의 함정 — `up = 1` 인 채로 전면 장애
|
||||
|
||||
```
|
||||
up{pod=keycloak-1} = 1
|
||||
up{pod=keycloak-0} = 1 ← 서비스는 503 인데
|
||||
```
|
||||
|
||||

|
||||
|
||||
**전 구간 평평하다.** (11:44 의 짧은 골은 A-1 에서 파드를 교체한 자국이다.)
|
||||
|
||||
`up` 은 **Prometheus 가 `/metrics` 를 긁는 데 성공했는가**만 말한다.
|
||||
프로세스는 멀쩡히 살아 메트릭을 내놓고 있었다. **기능은 전멸했는데.**
|
||||
|
||||
| 지표 | 이 장애에서 |
|
||||
|---|---|
|
||||
| `up` | **1 — 아무것도 알려주지 않는다** |
|
||||
| 파드 `Ready` | **false — 여기서 드러난다** |
|
||||
| 외부 HTTP 코드 | **503 — 사용자가 겪는 것** |
|
||||
|
||||
> **A-0 에서 나는 `up` 을 "가장 중요한 합성 지표"라고 썼다.**
|
||||
> 절반만 맞다. `up` 은 **대상이 사라진 것**을 잡지만 **대상이 살아서 못 쓰는 것**은
|
||||
> 못 잡는다. 후자가 운영에서 훨씬 흔하다.
|
||||
>
|
||||
> **알림은 `up` 이 아니라 readiness 와 외부 응답 코드에 걸어야 한다.**
|
||||
|
||||
이 실험대에는 아직 `kube-state-metrics` 가 없어 파드 readiness 가 지표로
|
||||
남지 않는다. **관측 스택에 빠진 것을 이 실험이 찾아냈다** — 보완 항목이다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 복구 — 자동이었다
|
||||
|
||||
```bash
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
```
|
||||
|
||||
```
|
||||
재기동 시각: 11:57:09
|
||||
+15초 keycloak-0 true keycloak-1 true | 외부 HTTP 200
|
||||
→ 서비스 복귀
|
||||
|
||||
재시작 횟수: keycloak-0 = 0, keycloak-1 = 0
|
||||
정지 전 세션: online 세션 5 건 살아남음
|
||||
```
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 회복 시간 | **약 15초** (DB Ready 이후) |
|
||||
| 사람 개입 | **없음** |
|
||||
| Keycloak 재시작 | **불필요** — `restarts=0` |
|
||||
| 세션 | **살아남음** — DB 에 있으므로 |
|
||||
|
||||
**커넥션 풀이 스스로 재연결하고 readiness 가 다시 UP 이 되면서 Service 에
|
||||
복귀했다.** `readiness` 를 쓴 설계의 이득이 여기서 나온다 — `liveness` 였다면
|
||||
파드가 재시작되어 캐시까지 날아갔을 것이다.
|
||||
|
||||
### 개념 — readiness 와 liveness 를 가르는 기준
|
||||
|
||||
| | 실패하면 | 언제 쓰나 |
|
||||
|---|---|---|
|
||||
| **liveness** | **재시작** | 재시작하면 나아지는 문제 (교착, 메모리 누수) |
|
||||
| **readiness** | **트래픽에서 격리** | 재시작해도 안 나아지는 문제 (**의존 대상이 죽음**) |
|
||||
|
||||
**DB 장애에 liveness 를 걸면 재앙이다.** 모든 파드가 무한 재시작하고,
|
||||
DB 가 돌아와도 CrashLoopBackOff 의 백오프 때문에 회복이 늦어진다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 재현 절차 (명령어)
|
||||
|
||||
```bash
|
||||
# 0. 상주 탐침 (임시 파드는 계측에 부적합 — A-1 참조)
|
||||
kubectl -n keycloak-lab run a2-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never --command -- sleep 7200
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a2-probe --timeout=120s
|
||||
|
||||
# 1. 토큰 발급 (access 60초 안에 시험을 끝내야 한다)
|
||||
kubectl -n keycloak-lab exec a2-probe -- sh -c \
|
||||
'curl -s -X POST http://<k0>:8080/realms/master/protocol/openid-connect/token \
|
||||
-d grant_type=password -d client_id=admin-cli \
|
||||
-d username=admin -d password=<pw> > /tmp/tok.json'
|
||||
|
||||
# 2. DB 정지
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
|
||||
# 3. 네 경로
|
||||
kubectl -n keycloak-lab exec a2-probe -- curl -s -o /dev/null -w '%{http_code}\n' ...
|
||||
|
||||
# 4. 영향 범위
|
||||
kubectl -n keycloak-lab get endpoints keycloak \
|
||||
-o jsonpath='{.subsets[*].addresses[*].ip}' # 비어 있으면 전면 장애
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
|
||||
# 5. up 이 거짓말하는 것을 확인
|
||||
curl -s "http://localhost:19090/api/v1/query?query=up%7Bjob=%22keycloak%22%7D"
|
||||
|
||||
# 6. 복구
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 다음 실험에 남기는 것
|
||||
|
||||
| 실험 | 이 실험이 준 것 |
|
||||
|---|---|
|
||||
| **A-3** DB 강제 종료 | 정상 정지는 데이터를 안 잃었다. **강제 종료는?** (`synchronous_commit OFF`) |
|
||||
| **A-4** 노드 상실 | postgres 가 kc-lab-2 에 있으므로 그 노드를 죽이면 **A-2 가 함께 일어난다** |
|
||||
| **D-1** 백업·복구 | 여기서는 DB 가 되살아났다. **데이터가 사라졌다면?** |
|
||||
| 관측 스택 | **`kube-state-metrics` 가 없어 파드 readiness 가 지표로 안 남는다** — 보완 필요 |
|
||||
Reference in New Issue
Block a user