Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b689e715 | ||
|
|
4177fb6a48 |
@@ -19,3 +19,23 @@
|
|||||||
[ 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
|
[ 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
|
[ 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
|
[ 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,55 @@
|
|||||||
|
[ 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
|
||||||
|
[ 113467ms] [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
|
||||||
|
[ 121972ms] [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
|
||||||
|
[ 139681ms] [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
|
||||||
|
[ 150971ms] [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
|
||||||
|
[ 161211ms] [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
|
||||||
|
[ 180744ms] [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
|
||||||
|
[ 191401ms] [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
|
||||||
|
[ 210668ms] [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
|
||||||
|
[ 229082ms] [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
|
||||||
|
[ 239345ms] [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
|
||||||
|
[ 252119ms] [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
|
||||||
|
[ 266045ms] [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
|
||||||
|
[ 276705ms] [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
|
||||||
|
[ 289911ms] [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
|
||||||
|
[ 296875ms] [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
|
||||||
|
[ 315099ms] [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
|
||||||
|
[ 325431ms] [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
|
||||||
|
[ 328418ms] [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
|
||||||
|
[ 343774ms] [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
|
||||||
|
[ 347054ms] [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
|
||||||
|
[ 352405ms] [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
|
||||||
|
[ 361291ms] [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
|
||||||
|
[ 370639ms] [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
|
||||||
|
[ 374286ms] [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
|
||||||
|
[ 377413ms] [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
|
||||||
|
[ 394362ms] [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
|
||||||
|
[ 405423ms] [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
|
||||||
|
[ 425476ms] [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
|
||||||
|
[ 434299ms] [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
|
||||||
|
[ 441775ms] [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
|
||||||
|
[ 450786ms] [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
|
||||||
|
[ 456829ms] [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
|
||||||
|
[ 468812ms] [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
|
||||||
|
[ 478913ms] [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
|
||||||
|
[ 491241ms] [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
|
||||||
|
[ 495228ms] [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
|
||||||
|
[ 502295ms] [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
|
||||||
|
[ 511625ms] [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
|
||||||
|
[ 524619ms] [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,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,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,19 @@
|
|||||||
|
=== [준비] 손실 측정 설계 확인 ===
|
||||||
|
LAST_SESSION_REFRESH 는 integer(초) — 200ms 손실은 보이지 않는다
|
||||||
|
created_on | integer | | not null |
|
||||||
|
last_session_refresh | integer | | not null | 0
|
||||||
|
"idx_user_session_expiration_created" btree (realm_id, offline_flag, remember_me, created_on, user_session_id, user_id)
|
||||||
|
"idx_user_session_expiration_last_refresh" btree (realm_id, offline_flag, remember_me, last_session_refresh, user_session_id, user_id)
|
||||||
|
→ 대신 행 존재 여부로 잰다. 로그인 하나 = 행 하나 = 이진 판정
|
||||||
|
|
||||||
|
전역 synchronous_commit: on
|
||||||
|
|
||||||
|
=== [1] 빠른 연속 로그인을 백그라운드로 시작 ===
|
||||||
|
루프 시작
|
||||||
|
6초 경과 — 지금까지 성공한 로그인: 0
|
||||||
|
|
||||||
|
=== [2] PostgreSQL 강제 종료 (SIGKILL) ===
|
||||||
|
종료 시각: 12:00:26.511
|
||||||
|
pod "postgres-7b474b88c8-xc2vt" force deleted from keycloak-lab namespace
|
||||||
|
삭제 반환: 12:00:26.586
|
||||||
|
클라이언트가 200 을 받은 로그인 수: 0
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
deployment "postgres" successfully rolled out
|
||||||
|
|
||||||
|
=== crash recovery 가 실행되었는가 (강제 종료의 흔적) ===
|
||||||
|
2026-09-04 02:58:41.036 UTC [1] LOG: database system is ready to accept connections
|
||||||
|
|
||||||
|
=== [설계 확인] 로그인 트랜잭션도 synchronous_commit 을 끄는가 ===
|
||||||
|
--- 로그인 트랜잭션 (INSERT 가 있는 것) ---
|
||||||
|
2:BEGIN
|
||||||
|
5:COMMIT
|
||||||
|
6:BEGIN
|
||||||
|
9:insert into OFFLINE_USER_SESSION (BROKER_SESSION_ID,CREATED_ON,DATA,LAST_SESSION_REFRESH,REALM_ID,REMEMBER_ME,USER_ID,VERSION,OFFLINE_FLAG,USER_SESSION_ID) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||||
|
10:insert into OFFLINE_CLIENT_SESSION (DATA,REALM_ID,TIMESTAMP,VERSION,CLIENT_ID,CLIENT_STORAGE_PROVIDER,EXTERNAL_CLIENT_ID,OFFLINE_FLAG,USER_SESSION_ID) values ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||||
|
11:SET LOCAL synchronous_commit TO OFF
|
||||||
|
12:COMMIT
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
=== [1] 로그인 루프 시작 (호스트에서 백그라운드로 exec — 세션이 살아 있어야 한다) ===
|
||||||
|
8초 동안 클라이언트가 200 을 받은 로그인: 106 건
|
||||||
|
|
||||||
|
=== [2] SIGKILL ===
|
||||||
|
종료: 12:01:32.981
|
||||||
|
반환: 12:01:33.236
|
||||||
|
최종 성공 로그인 수: 110 건
|
||||||
|
마지막 sid: FimM-krSybBACP2qIvshLWwU
|
||||||
|
마지막 sid: EwFfFwOIfqiv8N5GQ5OjtsVq
|
||||||
|
마지막 sid: CJX-PxFkS7rUc_9FQgB7iw1f
|
||||||
|
마지막 sid: 1EFK7SgUA4M7tq_SkC_BD2er
|
||||||
|
마지막 sid: _LiqTczuyxlpOs3T3xs25SLv
|
||||||
|
|
||||||
|
=== [3] PostgreSQL 재기동 후 crash recovery 확인 ===
|
||||||
|
Waiting for deployment "postgres" rollout to finish: 0 of 1 updated replicas are available...
|
||||||
|
deployment "postgres" successfully rolled out
|
||||||
|
2026-09-04 02:59:48.427 UTC [1] LOG: database system is ready to accept connections
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
=== [4] 클라이언트가 받은 sid 가 DB 에 있는가 ===
|
||||||
|
클라이언트가 200 을 받은 sid: 291 건
|
||||||
|
DB 온라인 세션 총계: 375
|
||||||
|
|
||||||
|
--- 마지막 15건을 하나씩 조회 ---
|
||||||
|
TCCOYnVlN30Y2fGyJEsVJ_Lq 있음
|
||||||
|
vBcllIkKWovh-FXN9tSzmxAe 있음
|
||||||
|
eMpN_ywUdRTks9uBE9aTumPK 있음
|
||||||
|
aPC_T0yrlMjrlskcLAp0AvY6 있음
|
||||||
|
UBPxmduB-ahGHBg636sY3AEz 있음
|
||||||
|
HLnBloNX9R3qQJSkpOnkNqL4 있음
|
||||||
|
_KPJS30IAqVhkTJGHcCxKvrM 있음
|
||||||
|
rq3caZ9MkyMYlFSSzRjQLygD 있음
|
||||||
|
ivvQm70hjl55DPpF7vpYmz_E 있음
|
||||||
|
81mx-rmi-tAeogHx3-su3z2q 있음
|
||||||
|
WnvNDH93uzcz1XNFbaMSXk1A 있음
|
||||||
|
DXPAIhjO5sGpCUlcD8IEoS8L 있음
|
||||||
|
aZMvl4IwPdK-rC_7bG005Z5C 있음
|
||||||
|
eIuBCprfWA5x0glcgSKYrrX0 있음
|
||||||
|
ozES5kEeu2IFf_cfcC_jFlbF 있음
|
||||||
|
|
||||||
|
마지막 15건 중 유실: 0 건
|
||||||
|
|
||||||
|
=== [5] 전체 대조 — 몇 건이나 사라졌는가 ===
|
||||||
|
클라이언트 성공: 291 건
|
||||||
|
DB 에 존재: 291 건
|
||||||
|
★ 유실: 0 건
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
=== [정리] 세션 테이블 비우고 루프 잔여 확인 ===
|
||||||
|
DELETE 375
|
||||||
|
남은 세션: 0
|
||||||
|
|
||||||
|
=== [재주입] postmaster(PID 1)에 SIGKILL — 진짜 크래시 ===
|
||||||
|
8초 후 성공 로그인: 110 건
|
||||||
|
SIGKILL: 12:03:21.441
|
||||||
|
최종 성공 로그인: 139 건
|
||||||
|
|
||||||
|
=== [검증] 이번엔 crash recovery 가 돌았는가 ===
|
||||||
|
deployment "postgres" successfully rolled out
|
||||||
|
2026-09-04 02:59:48.427 UTC [1] LOG: database system is ready to accept connections
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
=== 로그인 루프 시작 ===
|
||||||
|
8초 후: 112 건
|
||||||
|
|
||||||
|
=== 백엔드 프로세스에 SIGKILL → postmaster 가 재초기화한다 ===
|
||||||
|
시각: 12:04:22.063
|
||||||
|
최종 성공 로그인: 153 건
|
||||||
|
|
||||||
|
=== [검증] crash recovery 가 돌았는가 ===
|
||||||
|
2026-09-04 02:59:48.427 UTC [1] LOG: database system is ready to accept connections
|
||||||
|
2026-09-04 03:02:35.807 UTC [1] LOG: server process (PID 40) was terminated by signal 9: Killed
|
||||||
|
2026-09-04 03:02:35.807 UTC [1] LOG: terminating any other active server processes
|
||||||
|
2026-09-04 03:02:35.814 UTC [1] LOG: all server processes terminated; reinitializing
|
||||||
|
2026-09-04 03:02:35.896 UTC [2585] LOG: database system was not properly shut down; automatic recovery in progress
|
||||||
|
2026-09-04 03:02:35.899 UTC [2585] LOG: redo starts at 0/23CAB68
|
||||||
|
2026-09-04 03:02:35.904 UTC [2585] LOG: redo done at 0/2529E40 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
|
||||||
|
2026-09-04 03:02:35.923 UTC [2586] LOG: checkpoint complete: wrote 113 buffers (0.7%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.004 s, sync=0.004 s, total=0.015 s; sync files=27, longest=0.003 s, average=0.001 s; distance=1405 kB, estimate=1405 kB; lsn=0/252A048, redo lsn=0/252A048
|
||||||
|
2026-09-04 03:02:35.926 UTC [1] LOG: database system is ready to accept connections
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
=== 크래시 전후 대조 ===
|
||||||
|
클라이언트가 200 과 토큰을 받은 로그인 : 153 건
|
||||||
|
그중 DB 에 실제로 존재 : 149 건
|
||||||
|
★ 유실 : 4 건
|
||||||
|
DB 전체 온라인 세션 : 150 건
|
||||||
|
|
||||||
|
=== 유실된 sid 목록 ===
|
||||||
|
★ CQUfg9HLH29xvhiu6pVlfWOo ← 토큰은 발급됐는데 세션이 없다
|
||||||
|
★ 5gLP4fqmpZBbjhH_d-0TPMMr ← 토큰은 발급됐는데 세션이 없다
|
||||||
|
★ hkcOv1QskUFmYveMLB6Hljra ← 토큰은 발급됐는데 세션이 없다
|
||||||
|
★ p5XybeQIYmAs818gO4Vl_5ea ← 토큰은 발급됐는데 세션이 없다
|
||||||
|
|
||||||
|
=== 그 토큰이 지금 실제로 쓰이는가 (마지막 sid 로 확인) ===
|
||||||
|
마지막 sid: 8do0Bw6tkVLDVxgxotE7GosH
|
||||||
|
user_session_id | created_on | last_session_refresh
|
||||||
|
--------------------------+------------+----------------------
|
||||||
|
8do0Bw6tkVLDVxgxotE7GosH | 1788490958 | 1788490958
|
||||||
|
(1 row)
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# A-3 — DB 강제 종료와 데이터 손실 증거
|
||||||
|
|
||||||
|
2026-09-04 12:00–12:05 KST · Keycloak 26.7.0 / PostgreSQL 16
|
||||||
|
해설: [`docs/experiment-a3-database-crash.md`](../../experiment-a3-database-crash.md)
|
||||||
|
|
||||||
|
| 파일 | 무엇을 보여주는가 |
|
||||||
|
|---|---|
|
||||||
|
| `01-crash-injection.txt` | 첫 시도 실패 — 파드 안 백그라운드 루프가 `exec` 종료와 함께 죽어 0건 수집 |
|
||||||
|
| `02-design-check.txt` | **핵심 설계 확인** — 로그인 트랜잭션도 `SET LOCAL synchronous_commit TO OFF` 로 커밋한다 |
|
||||||
|
| `03-loss-measurement.txt` | `--grace-period=0 --force` 주입 |
|
||||||
|
| `04-comparison.txt` | **유실 0건** — 그러나 crash recovery 가 안 돌았다. 죽인 적이 없는 것 |
|
||||||
|
| `05-true-crash.txt` | `kill -9 1` 시도 — **컨테이너 안에서 PID 1 은 SIGKILL 을 무시한다** |
|
||||||
|
| `06-backend-kill-crash.txt` | **성공한 주입** — 백엔드에 SIGKILL → `not properly shut down` / `redo starts` / `redo done` |
|
||||||
|
| `07-loss-result.txt` | **결과: 153건 중 4건 유실.** 토큰은 발급됐는데 세션 행이 없는 sid 목록 |
|
||||||
|
|
||||||
|
## 핵심 세 줄
|
||||||
|
|
||||||
|
1. **로그인도 비동기 커밋이다.** refresh 시각뿐 아니라 **로그인 자체**가 사라질 수 있다.
|
||||||
|
2. **153건 중 4건(약 2.6%) 유실** — 초당 19건 기준 마지막 0.2초 분량, `wal_writer_delay` 기본값과 일치.
|
||||||
|
3. **주입을 세 번 시도해 세 번째에 성공했다.** 앞의 둘은 "손실 0"으로 보였지만 실제로는 크래시가 아니었다.
|
||||||
@@ -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 가 지표로 안 남는다** — 보완 필요 |
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
# A-3 — DB 를 강제로 죽이면 무엇을 잃는가 (RPO)
|
||||||
|
|
||||||
|
브랜치 `feature/keycloak-a3-database-crash` ·
|
||||||
|
증거 [`docs/evidence/a3-database-crash/`](evidence/a3-database-crash/) ·
|
||||||
|
2026-09-04 12:00–12:05 KST · Keycloak 26.7.0 / PostgreSQL 16
|
||||||
|
|
||||||
|
선행: [`A-0`](experiment-00-session-replication.md) ·
|
||||||
|
[`A-2`](experiment-a2-database-loss.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 결론부터
|
||||||
|
|
||||||
|
```
|
||||||
|
클라이언트가 200 과 토큰을 받은 로그인 : 153 건
|
||||||
|
그중 DB 에 실제로 존재 : 149 건
|
||||||
|
★ 유실 : 4 건
|
||||||
|
```
|
||||||
|
|
||||||
|
**로그인이 성공했다고 응답받았는데 세션이 존재하지 않는다.**
|
||||||
|
|
||||||
|
A-0 에서 발견한 `SET LOCAL synchronous_commit TO OFF` 의 대가를 실측했다.
|
||||||
|
버그가 아니라 **의도된 설계**이며, 그 비용이 얼마인지를 숫자로 확인한 것이다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 설계 — 무엇을 재야 손실이 보이는가
|
||||||
|
|
||||||
|
### 1-1. `LAST_SESSION_REFRESH` 로는 못 잰다
|
||||||
|
|
||||||
|
처음 계획은 "세션 갱신 시각이 되감기는지" 보는 것이었다. 스키마를 보고 접었다.
|
||||||
|
|
||||||
|
```
|
||||||
|
created_on | integer
|
||||||
|
last_session_refresh | integer ← 초 단위
|
||||||
|
```
|
||||||
|
|
||||||
|
**손실 창은 수백 밀리초**인데 눈금이 **1초**다. 보일 리가 없다.
|
||||||
|
|
||||||
|
### 1-2. 행 존재 여부로 잰다 — 이진 판정
|
||||||
|
|
||||||
|
```
|
||||||
|
로그인 1회 = OFFLINE_USER_SESSION 행 1개
|
||||||
|
클라이언트가 sid 를 받았다 = 서버가 COMMIT 했다고 응답했다
|
||||||
|
크래시 후 그 sid 가 없다 = 잃은 것
|
||||||
|
```
|
||||||
|
|
||||||
|
**있거나 없거나**이므로 눈금 문제가 없다.
|
||||||
|
|
||||||
|
### 1-3. 그런데 로그인도 비동기 커밋인가 — **먼저 확인해야 한다**
|
||||||
|
|
||||||
|
A-0 에서 잡은 것은 **refresh** 트랜잭션이었다. 로그인(INSERT)도 그런지는
|
||||||
|
확인하지 않았다. 아니라면 이 측정 설계 자체가 성립하지 않는다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||||
|
psql -U keycloak -d keycloak -c "alter system set log_statement='all'"
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||||
|
psql -U keycloak -d keycloak -c "select pg_reload_conf()"
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
BEGIN
|
||||||
|
insert into OFFLINE_USER_SESSION (...) values (...)
|
||||||
|
insert into OFFLINE_CLIENT_SESSION (...) values (...)
|
||||||
|
SET LOCAL synchronous_commit TO OFF ← 로그인도 비동기 커밋이다
|
||||||
|
COMMIT
|
||||||
|
```
|
||||||
|
|
||||||
|
**확인됐고, 함의가 refresh 보다 훨씬 무겁다.**
|
||||||
|
|
||||||
|
| | 잃으면 |
|
||||||
|
|---|---|
|
||||||
|
| refresh 갱신 시각 | 세션 수명이 조금 짧아진다. 사용자는 모른다 |
|
||||||
|
| **로그인 자체** | **토큰은 손에 있는데 세션이 없다.** 다음 요청부터 실패 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 실패한 주입 ① — `--grace-period=0 --force` 는 크래시가 아니다
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n keycloak-lab delete pod -l app=postgres --grace-period=0 --force
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
클라이언트 성공: 291 건
|
||||||
|
DB 에 존재: 291 건
|
||||||
|
★ 유실: 0 건
|
||||||
|
```
|
||||||
|
|
||||||
|
**0건.** 그런데 이건 "안 잃었다"가 아니라 **죽인 적이 없는 것**이다.
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 재기동 로그 ===
|
||||||
|
database system is ready to accept connections
|
||||||
|
(그뿐. "not properly shut down" 이 없다)
|
||||||
|
```
|
||||||
|
|
||||||
|
**crash recovery 가 돌지 않았다 = 깨끗하게 내려갔다.**
|
||||||
|
|
||||||
|
| 신호 | PostgreSQL 의 반응 |
|
||||||
|
|---|---|
|
||||||
|
| **SIGTERM** | **fast shutdown** — 진행 중 트랜잭션을 롤백하고 **WAL 을 플러시**한 뒤 종료 |
|
||||||
|
| SIGINT | smart shutdown — 연결이 끊기길 기다린다 |
|
||||||
|
| **SIGKILL** | **즉사** — 플러시 없음. 다음 기동에 crash recovery |
|
||||||
|
|
||||||
|
`--force --grace-period=0` 는 API 오브젝트를 즉시 지우지만 컨테이너 런타임은
|
||||||
|
여전히 정상 종료 절차를 밟는다. **PostgreSQL 은 SIGTERM 을 받고 얌전히
|
||||||
|
플러시했다.**
|
||||||
|
|
||||||
|
> **A-1 에서 배운 것이 또 나왔다** — 주입이 실제로 걸렸는지 먼저 확인하지
|
||||||
|
> 않으면 **"아무 일도 없었다"를 결과로 착각한다.**
|
||||||
|
> 여기서는 **crash recovery 메시지가 그 확인 수단**이다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 실패한 주입 ② — 컨테이너 안에서 PID 1 은 SIGKILL 을 받지 않는다
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- kill -9 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**아무 일도 일어나지 않았다.** 파드는 재시작하지 않았고 로그 시각도 그대로였다.
|
||||||
|
|
||||||
|
### 개념 — PID 1 의 시그널 보호
|
||||||
|
|
||||||
|
리눅스 커널은 **PID 1 을 특별 취급**한다. 자기 PID 네임스페이스 안에서 온
|
||||||
|
시그널은 **핸들러가 등록된 것만** 전달된다. **SIGKILL 도 예외가 아니다.**
|
||||||
|
|
||||||
|
```
|
||||||
|
같은 네임스페이스 안에서 → PID 1 은 등록하지 않은 시그널을 무시한다
|
||||||
|
조상 네임스페이스에서 → 전달된다 (노드에서 kill -9 하면 죽는다)
|
||||||
|
```
|
||||||
|
|
||||||
|
부팅 초기에 init 을 실수로 죽여 시스템이 멈추는 것을 막기 위한 장치인데,
|
||||||
|
컨테이너에서는 **"안에서는 PID 1 을 못 죽인다"** 로 나타난다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 성공한 주입 — 백엔드 프로세스를 죽인다
|
||||||
|
|
||||||
|
PostgreSQL 은 **postmaster(부모) + 연결마다 백엔드(자식)** 구조다.
|
||||||
|
자식 하나가 비정상 종료하면 **postmaster 는 공유 메모리가 오염됐다고 보고
|
||||||
|
전체를 재초기화**한다. 그게 곧 crash recovery 다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||||
|
sh -c 'kill -9 $(pgrep -f "postgres: keycloak keycloak" | head -1)'
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
server process (PID 40) was terminated by signal 9: Killed
|
||||||
|
terminating any other active server processes
|
||||||
|
all server processes terminated; reinitializing
|
||||||
|
database system was not properly shut down; automatic recovery in progress
|
||||||
|
redo starts at 0/23CAB68
|
||||||
|
redo done at 0/2529E40
|
||||||
|
checkpoint complete: wrote 113 buffers ...
|
||||||
|
database system is ready to accept connections
|
||||||
|
```
|
||||||
|
|
||||||
|
**이번엔 주입이 걸렸다.** `not properly shut down` + `redo` 가 증거다.
|
||||||
|
|
||||||
|
파드는 재시작하지 않는다 (`restarts=0`) — 컨테이너의 PID 1 인 postmaster 는
|
||||||
|
살아 있고, 자식만 갈아치운 것이다. **데이터 관점에서는 전원이 나간 것과 같다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 결과
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 크래시 전후 대조 ===
|
||||||
|
클라이언트가 200 과 토큰을 받은 로그인 : 153 건
|
||||||
|
그중 DB 에 실제로 존재 : 149 건
|
||||||
|
★ 유실 : 4 건
|
||||||
|
|
||||||
|
=== 유실된 sid ===
|
||||||
|
★ CQUfg9HLH29xvhiu6pVlfWOo ← 토큰은 발급됐는데 세션이 없다
|
||||||
|
★ 5gLP4fqmpZBbjhH_d-0TPMMr
|
||||||
|
★ hkcOv1QskUFmYveMLB6Hljra
|
||||||
|
★ p5XybeQIYmAs818gO4Vl_5ea
|
||||||
|
```
|
||||||
|
|
||||||
|
**약 2.6% 유실.** 초당 19건 정도 로그인하던 중이었으므로
|
||||||
|
**대략 마지막 0.2초 분량**이다 — `wal_writer_delay` 기본값(200ms)과 맞는다.
|
||||||
|
|
||||||
|
### 사용자에게 어떻게 보이는가
|
||||||
|
|
||||||
|
```
|
||||||
|
로그인 성공 → access token + refresh token 을 받음
|
||||||
|
│
|
||||||
|
│ (크래시)
|
||||||
|
▼
|
||||||
|
다음 요청 → access token 은 60초간 통한다
|
||||||
|
│ (서명만 보는 경로라면)
|
||||||
|
▼
|
||||||
|
60초 후 refresh → "Session not active" → 다시 로그인
|
||||||
|
```
|
||||||
|
|
||||||
|
**즉시 드러나지 않는다.** access token 수명 동안은 정상으로 보이다가
|
||||||
|
갱신 시점에 끊긴다. 장애와 증상 사이에 **최대 60초의 시차**가 있다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 개념
|
||||||
|
|
||||||
|
### WAL 과 `synchronous_commit`
|
||||||
|
|
||||||
|
```
|
||||||
|
COMMIT
|
||||||
|
│
|
||||||
|
├─ WAL 버퍼(메모리)에 기록 ← 항상 한다
|
||||||
|
│
|
||||||
|
├─ synchronous_commit = on : 디스크 플러시를 기다렸다가 응답
|
||||||
|
└─ synchronous_commit = off : 기다리지 않고 즉시 응답 ← Keycloak
|
||||||
|
│
|
||||||
|
└─ 크래시 시 이 구간이 사라진다
|
||||||
|
```
|
||||||
|
|
||||||
|
| 설정 | 응답 속도 | 잃는 것 |
|
||||||
|
|---|---|---|
|
||||||
|
| `on` (PostgreSQL 기본) | 느리다 (디스크 대기) | 없다 |
|
||||||
|
| **`off`** | 빠르다 | **최대 `wal_writer_delay` × 3 분량** |
|
||||||
|
|
||||||
|
**전역 설정은 `on` 이었다.**
|
||||||
|
|
||||||
|
```
|
||||||
|
전역 synchronous_commit: on
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keycloak 이 자기 트랜잭션에만 `SET LOCAL` 로 끈다.** DBA 가 서버 설정만
|
||||||
|
보고 "우리는 동기 커밋"이라 믿으면 틀린다. **애플리케이션이 트랜잭션 단위로
|
||||||
|
뒤집을 수 있다.**
|
||||||
|
|
||||||
|
### crash recovery
|
||||||
|
|
||||||
|
```
|
||||||
|
기동 시 pg_control 을 읽는다
|
||||||
|
└─ "깨끗하게 종료됨" 표시가 없다
|
||||||
|
└─ "database system was not properly shut down"
|
||||||
|
└─ 마지막 체크포인트부터 WAL 을 재생(redo)
|
||||||
|
└─ 디스크에 안 내려간 커밋은 복구할 수 없다 ← 손실
|
||||||
|
```
|
||||||
|
|
||||||
|
`redo starts at 0/23CAB68` → `redo done at 0/2529E40` 사이가 재생된 구간이다.
|
||||||
|
**WAL 에 없는 것은 재생할 수도 없다.**
|
||||||
|
|
||||||
|
### 이 손실이 "허용된" 이유
|
||||||
|
|
||||||
|
Keycloak 의 판단은 이렇게 읽힌다.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| 세션 쓰기는 **매우 잦다** | 로그인마다, refresh 마다 |
|
||||||
|
| 잃어도 **회복 가능하다** | 사용자가 다시 로그인하면 된다 |
|
||||||
|
| 동기 커밋의 비용은 **모든 요청에 붙는다** | 크래시는 드물다 |
|
||||||
|
|
||||||
|
**드문 사고의 비용을 상시 지연으로 지불하지 않겠다는 선택**이다.
|
||||||
|
합리적이지만, **선택했다는 사실을 알고 있어야 한다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 운영에 주는 것
|
||||||
|
|
||||||
|
| 알게 된 것 | 함의 |
|
||||||
|
|---|---|
|
||||||
|
| 로그인도 비동기 커밋 | **RPO 가 0 이 아니다.** 크래시 시 마지막 수백 ms 로그인은 사라진다 |
|
||||||
|
| 전역 `on` 인데 세션만 `off` | **서버 설정으로 판단하면 안 된다.** 애플리케이션이 뒤집는다 |
|
||||||
|
| 손실이 즉시 안 보인다 | access token 수명만큼 시차. **모니터링은 갱신 실패율을 봐야 한다** |
|
||||||
|
| `--grace-period=0` 은 크래시가 아니다 | **장애 훈련이 훈련이 안 될 수 있다** |
|
||||||
|
| 컨테이너 안에서 PID 1 을 못 죽인다 | 크래시 재현은 **자식 프로세스**나 **노드에서** |
|
||||||
|
|
||||||
|
### 바꿀 수 있는가
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 세션 트랜잭션까지 동기 커밋으로 강제하려면 (지연 대가를 치른다)
|
||||||
|
ALTER DATABASE keycloak SET synchronous_commit = on; -- SET LOCAL 이 이깁니다
|
||||||
|
```
|
||||||
|
|
||||||
|
**`SET LOCAL` 이 우선하므로 이것으로는 못 막는다.** Keycloak 설정이나
|
||||||
|
소스 수준의 문제이며, **RPO 0 이 필요하면 복제(streaming replication)로
|
||||||
|
푸는 것이 맞다** — 동기 스탠바이가 있으면 `synchronous_commit` 의 의미가
|
||||||
|
달라진다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 재현 절차 (명령어)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 0. 설계 확인 — 로그인도 비동기 커밋인지 먼저 본다
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||||
|
-c "alter system set log_statement='all'" -c "select pg_reload_conf()"
|
||||||
|
# → 로그인 1회 후 로그에서 "SET LOCAL synchronous_commit TO OFF" 확인
|
||||||
|
|
||||||
|
# 1. 세션 테이블 비우기
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||||
|
-c "delete from offline_user_session"
|
||||||
|
|
||||||
|
# 2. 로그인 루프 (호스트에서 백그라운드 exec — 파드 안 & 는 exec 종료와 함께 죽는다)
|
||||||
|
kubectl -n keycloak-lab exec a2-probe -- sh -c '<로그인 반복, sid 를 /tmp/sids 에>' &
|
||||||
|
|
||||||
|
# 3. 진짜 크래시 — 백엔드 프로세스에 SIGKILL
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||||
|
sh -c 'kill -9 $(pgrep -f "postgres: keycloak keycloak" | head -1)'
|
||||||
|
|
||||||
|
# 4. 주입이 걸렸는지 확인 — 이게 없으면 결과를 해석하지 않는다
|
||||||
|
kubectl -n keycloak-lab logs deploy/postgres | grep -E "not properly shut down|redo"
|
||||||
|
|
||||||
|
# 5. 대조
|
||||||
|
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||||
|
"select count(*) from offline_user_session where user_session_id in (<sid 목록>)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 다음 실험에 남기는 것
|
||||||
|
|
||||||
|
| 실험 | 이 실험이 준 것 |
|
||||||
|
|---|---|
|
||||||
|
| **D-1** 백업·복구 | RPO 는 **백업 주기 + 이 손실**이다. 둘을 더해야 진짜 RPO |
|
||||||
|
| **B-6** Redis 영속화 | `appendfsync everysec` 은 **같은 모양의 트레이드오프** |
|
||||||
|
| **A-4** 노드 상실 | 노드가 죽으면 이것도 함께 일어난다 (postgres 가 kc-lab-2) |
|
||||||
|
| 전체 | **주입 성공 신호를 미리 정한다.** 여기서는 crash recovery 로그 |
|
||||||
Reference in New Issue
Block a user