Created
April 10, 2026 04:55
-
-
Save Y0lan/0a84a1d2af1a92ad19a2a2b28d06723d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GKE Balloon Pods: du niveau 5 ans au niveau expert</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,700;9..144,900&family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-1: #f5efe2; | |
| --bg-2: #fffaf0; | |
| --paper: rgba(255, 252, 246, 0.92); | |
| --ink: #1e2f2f; | |
| --muted: #4a6262; | |
| --subtle: #7a9494; | |
| --line: rgba(30, 47, 47, 0.10); | |
| --line-strong: rgba(30, 47, 47, 0.18); | |
| --gold: #f4bd43; | |
| --gold-2: #ffe3a0; | |
| --gold-bg: rgba(244, 189, 67, 0.12); | |
| --red: #e1634e; | |
| --red-2: #ffb2a0; | |
| --red-bg: rgba(225, 99, 78, 0.10); | |
| --green: #1b8c6f; | |
| --green-2: #b7eddc; | |
| --green-bg: rgba(27, 140, 111, 0.08); | |
| --blue: #1d5ae8; | |
| --blue-2: #d8e5ff; | |
| --blue-bg: rgba(29, 90, 232, 0.06); | |
| --node: #fff5de; | |
| --node-edge: #e7c269; | |
| --shadow: 0 20px 60px rgba(34, 44, 50, 0.10); | |
| --shadow-sm: 0 4px 16px rgba(34, 44, 50, 0.08); | |
| --radius-xl: 28px; | |
| --radius-lg: 20px; | |
| --radius-md: 14px; | |
| --radius-sm: 10px; | |
| --font-display: "Fraunces", "Georgia", serif; | |
| --font-body: "DM Sans", "Avenir Next", "Segoe UI", sans-serif; | |
| --font-mono: "JetBrains Mono", "SFMono-Regular", "Consolas", monospace; | |
| } | |
| * { box-sizing: border-box; margin: 0; } | |
| body { | |
| font-family: var(--font-body); | |
| color: var(--ink); | |
| background: | |
| radial-gradient(ellipse at 8% 6%, rgba(255, 231, 179, 0.55), transparent 22rem), | |
| radial-gradient(ellipse at 92% 8%, rgba(183, 237, 220, 0.6), transparent 24rem), | |
| radial-gradient(ellipse at 50% 80%, rgba(216, 229, 255, 0.3), transparent 30rem), | |
| linear-gradient(180deg, var(--bg-1), var(--bg-2)); | |
| min-height: 100vh; | |
| } | |
| .wrap { | |
| width: min(1200px, calc(100% - 2rem)); | |
| margin: 0 auto; | |
| padding: 2rem 0 5rem; | |
| display: grid; | |
| gap: 1.25rem; | |
| } | |
| /* --- Cards --- */ | |
| .card { | |
| background: var(--paper); | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius-xl); | |
| box-shadow: var(--shadow); | |
| backdrop-filter: blur(12px); | |
| padding: 2rem; | |
| } | |
| /* --- Typography --- */ | |
| h1 { | |
| font-family: var(--font-display); | |
| font-size: clamp(2.4rem, 5vw, 4rem); | |
| line-height: 1.0; | |
| font-weight: 900; | |
| letter-spacing: -0.02em; | |
| max-width: 14ch; | |
| margin-bottom: 1.2rem; | |
| } | |
| h2 { | |
| font-family: var(--font-display); | |
| font-size: 1.55rem; | |
| font-weight: 700; | |
| margin-bottom: 1rem; | |
| line-height: 1.2; | |
| } | |
| h3 { | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| } | |
| p { margin-top: 0; } | |
| .mono { font-family: var(--font-mono); font-size: 0.9em; } | |
| a { color: var(--blue); text-decoration-thickness: 1px; text-underline-offset: 2px; } | |
| a:hover { text-decoration-thickness: 2px; } | |
| /* --- Hero --- */ | |
| .hero { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hero::before, .hero::after { | |
| content: ""; | |
| position: absolute; | |
| border-radius: 999px; | |
| pointer-events: none; | |
| } | |
| .hero::before { | |
| width: 16rem; height: 16rem; | |
| right: -5rem; top: -5rem; | |
| background: radial-gradient(circle, rgba(255, 217, 128, 0.5), transparent 65%); | |
| } | |
| .hero::after { | |
| width: 20rem; height: 20rem; | |
| left: -7rem; bottom: -9rem; | |
| background: radial-gradient(circle, rgba(183, 237, 220, 0.6), transparent 60%); | |
| } | |
| .eyebrow { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.45rem 0.85rem; | |
| border-radius: 999px; | |
| background: rgba(255, 255, 255, 0.7); | |
| border: 1px solid var(--line); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| letter-spacing: 0.02em; | |
| color: var(--muted); | |
| margin-bottom: 1.2rem; | |
| } | |
| .hero-grid { | |
| display: grid; | |
| grid-template-columns: 1.2fr 0.8fr; | |
| gap: 1.5rem; | |
| align-items: start; | |
| } | |
| .lede { | |
| max-width: 60ch; | |
| color: var(--muted); | |
| font-size: 1.05rem; | |
| line-height: 1.7; | |
| margin-bottom: 1.2rem; | |
| } | |
| .badge-row { display: flex; gap: 0.6rem; flex-wrap: wrap; } | |
| .badge { | |
| display: inline-flex; | |
| padding: 0.6rem 0.85rem; | |
| border-radius: 999px; | |
| border: 1px solid var(--line); | |
| background: rgba(255, 255, 255, 0.6); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: var(--muted); | |
| } | |
| .hero-note { | |
| padding: 1.25rem; | |
| border-radius: var(--radius-lg); | |
| background: linear-gradient(180deg, var(--blue-bg), rgba(29, 90, 232, 0.02)); | |
| border: 1px solid rgba(29, 90, 232, 0.12); | |
| } | |
| .hero-note h3 { color: var(--blue); font-size: 0.95rem; letter-spacing: 0.02em; text-transform: uppercase; } | |
| .hero-note p { color: var(--muted); line-height: 1.65; margin-bottom: 0; font-size: 0.95rem; } | |
| /* --- Concept Diagram --- */ | |
| .concept-section { text-align: center; } | |
| .concept-section h2 { margin-bottom: 0.5rem; } | |
| .concept-subtitle { color: var(--muted); font-size: 0.95rem; margin-bottom: 1.5rem; } | |
| .concept-svg { | |
| width: 100%; | |
| max-width: 860px; | |
| margin: 0 auto; | |
| display: block; | |
| } | |
| .concept-svg text { font-family: var(--font-body); } | |
| .concept-svg .stage-title { font-weight: 700; font-size: 13px; fill: var(--ink); } | |
| .concept-svg .stage-desc { font-size: 11px; fill: var(--muted); } | |
| .concept-svg .arrow-label { font-size: 10.5px; font-weight: 600; } | |
| .concept-svg .cycle-label { font-size: 10px; fill: var(--subtle); font-style: italic; } | |
| .concept-svg .pod-label { font-size: 9.5px; font-weight: 600; } | |
| @keyframes placeholderPulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.75; } | |
| } | |
| .concept-svg .pulse { animation: placeholderPulse 3s ease-in-out infinite; } | |
| /* --- Legend --- */ | |
| .legend { | |
| display: flex; | |
| gap: 1.25rem; | |
| flex-wrap: wrap; | |
| padding: 0.75rem 1.1rem; | |
| border-radius: var(--radius-md); | |
| background: rgba(255, 255, 255, 0.6); | |
| border: 1px solid var(--line); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: var(--muted); | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.45rem; | |
| } | |
| .legend-dot { | |
| width: 14px; height: 14px; | |
| border-radius: 5px; | |
| flex-shrink: 0; | |
| } | |
| .legend-dot.placeholder-dot { background: linear-gradient(180deg, var(--gold-2), var(--gold)); } | |
| .legend-dot.real-dot { background: linear-gradient(180deg, var(--red-2), var(--red)); } | |
| .legend-dot.system-dot { background: linear-gradient(180deg, #d9efeb, #95d6c1); } | |
| .legend-dot.evicted-dot { background: linear-gradient(180deg, #f0f0f0, #ddd); border: 1px dashed #bbb; } | |
| /* --- Step Timeline --- */ | |
| .timeline { | |
| display: flex; | |
| align-items: center; | |
| gap: 0; | |
| padding: 0.25rem 0; | |
| } | |
| .timeline-step { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.65rem 1rem; | |
| border-radius: 999px; | |
| border: 2px solid var(--line); | |
| background: rgba(255, 255, 255, 0.7); | |
| font-weight: 700; | |
| font-size: 0.88rem; | |
| color: var(--muted); | |
| cursor: pointer; | |
| transition: all 200ms ease; | |
| white-space: nowrap; | |
| position: relative; | |
| } | |
| .timeline-step:hover { border-color: var(--blue); color: var(--ink); transform: translateY(-1px); } | |
| .timeline-step.active { | |
| background: var(--blue); | |
| color: white; | |
| border-color: var(--blue); | |
| box-shadow: 0 4px 16px rgba(29, 90, 232, 0.25); | |
| } | |
| .timeline-step .step-num { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 22px; height: 22px; | |
| border-radius: 50%; | |
| background: rgba(0,0,0,0.08); | |
| font-size: 0.78rem; | |
| font-weight: 800; | |
| } | |
| .timeline-step.active .step-num { background: rgba(255,255,255,0.25); } | |
| .timeline-connector { | |
| width: 28px; | |
| height: 2px; | |
| background: var(--line); | |
| flex-shrink: 0; | |
| } | |
| .timeline-connector.passed { background: var(--blue); } | |
| /* --- Tabs --- */ | |
| .tab-row { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; } | |
| .tab { | |
| font-family: var(--font-body); | |
| font-weight: 700; | |
| font-size: 0.85rem; | |
| padding: 0.55rem 0.9rem; | |
| border-radius: 999px; | |
| border: 1.5px solid var(--line); | |
| background: rgba(255, 255, 255, 0.7); | |
| color: var(--muted); | |
| cursor: pointer; | |
| transition: all 180ms ease; | |
| } | |
| .tab:hover { border-color: var(--green); color: var(--ink); } | |
| .tab.active { | |
| background: var(--green); | |
| color: white; | |
| border-color: var(--green); | |
| } | |
| /* --- Simulator Grid --- */ | |
| .sim-grid { | |
| display: grid; | |
| grid-template-columns: 1.3fr 0.7fr; | |
| gap: 1.25rem; | |
| margin-top: 1rem; | |
| } | |
| /* --- Scene (cluster container) --- */ | |
| .scene { | |
| border-radius: var(--radius-xl); | |
| border: 1px solid var(--line); | |
| padding: 1.25rem; | |
| background: | |
| radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.85), transparent 16rem), | |
| linear-gradient(180deg, #f7fbff, #fff7ea 65%, #eef8ef); | |
| } | |
| .story { | |
| padding: 1rem 1.15rem; | |
| border-radius: var(--radius-lg); | |
| background: var(--green-bg); | |
| border: 1px solid rgba(27, 140, 111, 0.12); | |
| margin-bottom: 1rem; | |
| line-height: 1.6; | |
| font-size: 0.95rem; | |
| } | |
| .story h3 { | |
| color: var(--green); | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.03em; | |
| margin-bottom: 0.35rem; | |
| } | |
| .story p { color: var(--muted); margin-bottom: 0; } | |
| /* --- Cluster --- */ | |
| .cluster { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 0.85rem; | |
| margin-top: 1rem; | |
| } | |
| .node { | |
| border-radius: 18px; | |
| background: linear-gradient(180deg, #fffdf7, var(--node)); | |
| border: 2px solid var(--node-edge); | |
| padding: 0.75rem; | |
| min-height: 220px; | |
| box-shadow: inset 0 -8px 0 rgba(231, 194, 105, 0.12); | |
| transition: transform 250ms ease, opacity 250ms ease, box-shadow 250ms ease, border-color 250ms ease; | |
| } | |
| .node h3 { margin-bottom: 0.5rem; font-size: 0.88rem; color: var(--ink); } | |
| .node.offline { opacity: 0.22; transform: scale(0.97); } | |
| .node.highlight { | |
| transform: translateY(-3px); | |
| box-shadow: inset 0 -8px 0 rgba(231, 194, 105, 0.12), 0 14px 32px rgba(34, 44, 50, 0.10); | |
| border-color: var(--gold); | |
| } | |
| .pod-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 0.5rem; | |
| } | |
| .pod { | |
| min-height: 64px; | |
| border-radius: 14px; | |
| padding: 0.45rem; | |
| display: grid; | |
| place-items: center; | |
| text-align: center; | |
| border: 1px solid rgba(30, 47, 47, 0.08); | |
| font-weight: 800; | |
| font-size: 0.78rem; | |
| line-height: 1.1; | |
| transition: transform 300ms ease, opacity 300ms ease, background 400ms ease; | |
| } | |
| .pod small { | |
| display: block; | |
| margin-top: 0.2rem; | |
| font-weight: 600; | |
| font-size: 0.65rem; | |
| opacity: 0.8; | |
| } | |
| .pod.placeholder { background: linear-gradient(180deg, var(--gold-2), var(--gold)); } | |
| .pod.real { background: linear-gradient(180deg, var(--red-2), var(--red)); color: #fff9f7; } | |
| .pod.system { background: linear-gradient(180deg, #d9efeb, #95d6c1); } | |
| .pod.evicted { | |
| opacity: 0.16; | |
| transform: scale(0.92); | |
| background: linear-gradient(180deg, #f4f4f4, #e4e4e4); | |
| color: #999; | |
| } | |
| /* --- Metrics --- */ | |
| .metrics { display: grid; gap: 0.75rem; margin-top: 1rem; } | |
| .meter-row { display: grid; gap: 0.3rem; } | |
| .meter-head { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 1rem; | |
| font-size: 0.82rem; | |
| color: var(--muted); | |
| } | |
| .meter-label { font-weight: 700; color: var(--ink); } | |
| .meter-value { font-family: var(--font-mono); font-weight: 500; font-size: 0.8rem; } | |
| .meter { | |
| height: 12px; | |
| border-radius: 999px; | |
| background: rgba(30, 47, 47, 0.06); | |
| overflow: hidden; | |
| } | |
| .fill { | |
| height: 100%; | |
| border-radius: inherit; | |
| width: 0; | |
| transition: width 400ms cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .fill.demand { background: linear-gradient(90deg, var(--red), #ff8a73); } | |
| .fill.spare { background: linear-gradient(90deg, var(--gold), #ffd880); } | |
| .fill.ready { background: linear-gradient(90deg, var(--green), #53c5a2); } | |
| /* --- Explain Panel --- */ | |
| .explain-panel h2 { font-size: 1.25rem; margin-bottom: 0.75rem; } | |
| .explain-panel h2:not(:first-of-type) { margin-top: 1.5rem; } | |
| .insight-list, .warning-list { display: grid; gap: 0.6rem; } | |
| .insight { | |
| padding: 0.85rem 1rem; | |
| border-radius: var(--radius-sm); | |
| border: 1px solid var(--line); | |
| background: rgba(255, 255, 255, 0.65); | |
| color: var(--muted); | |
| line-height: 1.6; | |
| font-size: 0.9rem; | |
| display: flex; | |
| gap: 0.6rem; | |
| align-items: flex-start; | |
| } | |
| .insight-icon { | |
| flex-shrink: 0; | |
| width: 22px; height: 22px; | |
| border-radius: 6px; | |
| display: grid; | |
| place-items: center; | |
| font-size: 0.7rem; | |
| font-weight: 800; | |
| background: var(--green-bg); | |
| color: var(--green); | |
| border: 1px solid rgba(27, 140, 111, 0.15); | |
| } | |
| .warning { | |
| padding: 0.85rem 1rem; | |
| border-radius: var(--radius-sm); | |
| border-left: 3px solid var(--gold); | |
| background: var(--gold-bg); | |
| line-height: 1.6; | |
| font-size: 0.9rem; | |
| } | |
| .warning strong { color: var(--ink); font-size: 0.88rem; } | |
| .warning p { color: var(--muted); margin-bottom: 0; margin-top: 0.2rem; font-size: 0.88rem; } | |
| /* --- Fact / Ops / YAML grids --- */ | |
| .grid-3 { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 1rem; | |
| } | |
| .grid-item { | |
| padding: 1.15rem; | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--line); | |
| background: rgba(255, 255, 255, 0.65); | |
| } | |
| .grid-item h3 { display: flex; align-items: center; gap: 0.5rem; } | |
| .grid-item .num { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 26px; height: 26px; | |
| border-radius: 8px; | |
| background: var(--blue-bg); | |
| color: var(--blue); | |
| font-size: 0.8rem; | |
| font-weight: 800; | |
| flex-shrink: 0; | |
| } | |
| .grid-item p { color: var(--muted); font-size: 0.9rem; line-height: 1.65; margin-bottom: 0; } | |
| /* --- YAML --- */ | |
| .yaml-block { | |
| padding: 1.15rem; | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--line); | |
| background: rgba(255, 255, 255, 0.65); | |
| } | |
| .yaml-block h3 { margin-bottom: 0.65rem; } | |
| .yaml-block pre { | |
| margin: 0; | |
| padding: 1rem; | |
| border-radius: var(--radius-md); | |
| background: #0f1f24; | |
| color: #e2f4ee; | |
| overflow-x: auto; | |
| font-family: var(--font-mono); | |
| font-size: 0.82rem; | |
| line-height: 1.5; | |
| tab-size: 2; | |
| } | |
| .yaml-block pre .kw { color: #7ec8e3; } | |
| .yaml-block pre .str { color: #a8d8a8; } | |
| .yaml-block pre .num { color: #f4bd43; } | |
| .yaml-block pre .cmt { color: #667; font-style: italic; } | |
| /* --- Sources --- */ | |
| .sources { color: var(--muted); font-size: 0.9rem; line-height: 1.65; } | |
| .sources h2 { font-size: 1.2rem; } | |
| .sources p { margin-bottom: 0.75rem; } | |
| .sources p:last-child { margin-bottom: 0; } | |
| /* --- Keyboard hint --- */ | |
| .kbd-hint { | |
| text-align: center; | |
| font-size: 0.78rem; | |
| color: var(--subtle); | |
| margin-top: 0.5rem; | |
| } | |
| .kbd-hint kbd { | |
| display: inline-block; | |
| padding: 0.15rem 0.45rem; | |
| border-radius: 4px; | |
| border: 1px solid var(--line-strong); | |
| background: rgba(255,255,255,0.6); | |
| font-family: var(--font-mono); | |
| font-size: 0.72rem; | |
| } | |
| /* --- Responsive --- */ | |
| @media (max-width: 1020px) { | |
| .hero-grid, .sim-grid { grid-template-columns: 1fr; } | |
| .cluster { grid-template-columns: repeat(2, minmax(0, 1fr)); } | |
| .grid-3 { grid-template-columns: 1fr; } | |
| h1 { max-width: none; } | |
| .timeline { flex-wrap: wrap; gap: 0.5rem; } | |
| .timeline-connector { display: none; } | |
| } | |
| @media (max-width: 600px) { | |
| .cluster { grid-template-columns: 1fr 1fr; } | |
| .wrap { padding: 1rem 0 3rem; } | |
| .card { padding: 1.25rem; } | |
| .concept-svg { padding: 0 0.5rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="wrap"> | |
| <!-- ============================================ --> | |
| <!-- HERO --> | |
| <!-- ============================================ --> | |
| <section class="card hero"> | |
| <div class="eyebrow">GKE · Placeholder Pods · Spare Capacity Provisioning</div> | |
| <div class="hero-grid"> | |
| <div> | |
| <h1>Le "ballon" GKE, expliqué de 5 ans à expert.</h1> | |
| <p class="lede"> | |
| Le principe est simple : on fait exprès d'occuper un peu de capacité avec des Pods jetables | |
| et très peu prioritaires, pour que le cluster garde des nœuds déjà prêts. | |
| Quand un vrai pic arrive, les Pods métiers plus prioritaires prennent cette place | |
| immédiatement, sans attendre qu'un nouveau nœud démarre. | |
| C'est une stratégie de <strong>surprovisionnement contrôlé</strong>, pas un gadget. | |
| </p> | |
| <div class="badge-row"> | |
| <span class="badge">Version enfant : "des faux passagers gardent les sièges"</span> | |
| <span class="badge">Version archi : capacité tampon préchauffée</span> | |
| <span class="badge">Version ops : priorité, préemption, coût</span> | |
| </div> | |
| </div> | |
| <aside class="hero-note"> | |
| <h3>Résumé expert</h3> | |
| <p> | |
| Un placeholder Pod déclare de vraies <span class="mono">requests</span> CPU/mémoire, | |
| mais n'exécute qu'un conteneur <span class="mono">pause</span>. | |
| Il force le Cluster Autoscaler à garder ou créer des nœuds. | |
| Lorsqu'un Pod de production avec une <span class="mono">PriorityClass</span> supérieure | |
| a besoin de place et que le scheduler ne trouve pas de capacité libre, | |
| il préempte les placeholders. | |
| Portés par un <span class="mono">Deployment</span>, ils se recréent et | |
| redéclenchent du spare capacity. | |
| Portés par un <span class="mono">Job</span>, ils servent plutôt à un besoin ponctuel. | |
| </p> | |
| </aside> | |
| </div> | |
| </section> | |
| <!-- ============================================ --> | |
| <!-- CONCEPT DIAGRAM (visual summary) --> | |
| <!-- ============================================ --> | |
| <section class="card concept-section"> | |
| <h2>Le concept en un coup d'œil</h2> | |
| <p class="concept-subtitle">Le cycle complet du pattern "balloon", en trois étapes.</p> | |
| <svg class="concept-svg" viewBox="0 0 860 290" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <defs> | |
| <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"> | |
| <path d="M 0 1 L 9 5 L 0 9 z" fill="#4a6262"/> | |
| </marker> | |
| <linearGradient id="gP" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#ffe3a0"/><stop offset="100%" stop-color="#f4bd43"/></linearGradient> | |
| <linearGradient id="gR" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#ffb2a0"/><stop offset="100%" stop-color="#e1634e"/></linearGradient> | |
| <linearGradient id="gS" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#d9efeb"/><stop offset="100%" stop-color="#95d6c1"/></linearGradient> | |
| <linearGradient id="gE" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#f2f2f2"/><stop offset="100%" stop-color="#ddd"/></linearGradient> | |
| </defs> | |
| <!-- STAGE 1: Reserve --> | |
| <g transform="translate(20, 50)"> | |
| <text x="95" y="-14" text-anchor="middle" class="stage-title">1. Réserve active</text> | |
| <rect x="0" y="0" width="190" height="140" rx="14" fill="#fffdf7" stroke="#e7c269" stroke-width="2" stroke-dasharray="6 4"/> | |
| <!-- Pod grid 2x2 --> | |
| <rect class="pulse" x="10" y="10" width="80" height="54" rx="10" fill="url(#gS)"/> | |
| <text x="50" y="42" text-anchor="middle" class="pod-label" fill="#1e2f2f">System</text> | |
| <rect x="100" y="10" width="80" height="54" rx="10" fill="url(#gR)"/> | |
| <text x="140" y="42" text-anchor="middle" class="pod-label" fill="white">API</text> | |
| <rect class="pulse" x="10" y="74" width="80" height="54" rx="10" fill="url(#gP)"/> | |
| <text x="50" y="106" text-anchor="middle" class="pod-label" fill="#5a4400">Placeholder</text> | |
| <rect class="pulse" x="100" y="74" width="80" height="54" rx="10" fill="url(#gP)"/> | |
| <text x="140" y="106" text-anchor="middle" class="pod-label" fill="#5a4400">Placeholder</text> | |
| <!-- Description --> | |
| <text x="95" y="162" text-anchor="middle" class="stage-desc">Les placeholders réservent</text> | |
| <text x="95" y="176" text-anchor="middle" class="stage-desc">de la capacité sur le nœud</text> | |
| </g> | |
| <!-- ARROW 1 --> | |
| <line x1="228" y1="120" x2="298" y2="120" stroke="#4a6262" stroke-width="2" marker-end="url(#arr)"/> | |
| <text x="263" y="108" text-anchor="middle" class="arrow-label" fill="#e1634e">Pic !</text> | |
| <!-- STAGE 2: Absorption --> | |
| <g transform="translate(315, 50)"> | |
| <text x="95" y="-14" text-anchor="middle" class="stage-title">2. Absorption immédiate</text> | |
| <rect x="0" y="0" width="190" height="140" rx="14" fill="#fffdf7" stroke="#e1634e" stroke-width="2"/> | |
| <rect x="10" y="10" width="80" height="54" rx="10" fill="url(#gS)"/> | |
| <text x="50" y="42" text-anchor="middle" class="pod-label" fill="#1e2f2f">System</text> | |
| <rect x="100" y="10" width="80" height="54" rx="10" fill="url(#gR)"/> | |
| <text x="140" y="42" text-anchor="middle" class="pod-label" fill="white">API</text> | |
| <rect x="10" y="74" width="80" height="54" rx="10" fill="url(#gR)"/> | |
| <text x="50" y="106" text-anchor="middle" class="pod-label" fill="white">Worker</text> | |
| <rect x="100" y="74" width="80" height="54" rx="10" fill="url(#gR)"/> | |
| <text x="140" y="106" text-anchor="middle" class="pod-label" fill="white">Queue</text> | |
| <!-- Eviction markers --> | |
| <text x="50" y="90" text-anchor="middle" font-size="8" fill="rgba(255,255,255,0.7)">← évincé</text> | |
| <text x="140" y="90" text-anchor="middle" font-size="8" fill="rgba(255,255,255,0.7)">← évincé</text> | |
| <text x="95" y="162" text-anchor="middle" class="stage-desc">Les vrais workloads</text> | |
| <text x="95" y="176" text-anchor="middle" class="stage-desc">prennent la place (0s d'attente)</text> | |
| </g> | |
| <!-- ARROW 2 --> | |
| <line x1="523" y1="120" x2="593" y2="120" stroke="#4a6262" stroke-width="2" marker-end="url(#arr)"/> | |
| <text x="558" y="108" text-anchor="middle" class="arrow-label" fill="#1b8c6f">Reprise</text> | |
| <!-- STAGE 3: Restoration --> | |
| <g transform="translate(610, 50)"> | |
| <text x="115" y="-14" text-anchor="middle" class="stage-title">3. Tampon restauré</text> | |
| <rect x="0" y="0" width="230" height="140" rx="14" fill="#fffdf7" stroke="#1b8c6f" stroke-width="2" stroke-dasharray="6 4"/> | |
| <!-- Original node --> | |
| <rect x="10" y="10" width="55" height="54" rx="8" fill="url(#gS)"/> | |
| <text x="37" y="42" text-anchor="middle" class="pod-label" fill="#1e2f2f">Sys</text> | |
| <rect x="73" y="10" width="55" height="54" rx="8" fill="url(#gR)"/> | |
| <text x="100" y="42" text-anchor="middle" class="pod-label" fill="white">API</text> | |
| <rect class="pulse" x="10" y="74" width="55" height="54" rx="8" fill="url(#gP)"/> | |
| <text x="37" y="106" text-anchor="middle" font-size="8" font-weight="600" fill="#5a4400">Recréé</text> | |
| <rect class="pulse" x="73" y="74" width="55" height="54" rx="8" fill="url(#gP)"/> | |
| <text x="100" y="106" text-anchor="middle" font-size="8" font-weight="600" fill="#5a4400">Recréé</text> | |
| <!-- New node indicator --> | |
| <rect x="140" y="10" width="80" height="118" rx="10" fill="#fffdf7" stroke="#e7c269" stroke-width="1.5" stroke-dasharray="4 3"/> | |
| <text x="180" y="55" text-anchor="middle" font-size="9" font-weight="600" fill="#e7c269">Nouveau</text> | |
| <text x="180" y="68" text-anchor="middle" font-size="9" font-weight="600" fill="#e7c269">nœud</text> | |
| <text x="180" y="100" text-anchor="middle" font-size="20">➕</text> | |
| <text x="115" y="162" text-anchor="middle" class="stage-desc">Le Deployment recrée les placeholders,</text> | |
| <text x="115" y="176" text-anchor="middle" class="stage-desc">l'autoscaler ajoute un nœud si nécessaire</text> | |
| </g> | |
| <!-- CYCLE ARROW --> | |
| <path d="M 770 235 C 770 275, 115 275, 115 235" fill="none" stroke="#4a6262" stroke-width="1.5" stroke-dasharray="5 3" marker-end="url(#arr)"/> | |
| <text x="440" y="277" text-anchor="middle" class="cycle-label">Cycle continu (si Deployment)</text> | |
| </svg> | |
| </section> | |
| <!-- ============================================ --> | |
| <!-- INTERACTIVE SIMULATOR --> | |
| <!-- ============================================ --> | |
| <section class="card"> | |
| <h2>Simulation interactive</h2> | |
| <!-- Step Timeline --> | |
| <div class="timeline" role="tablist" aria-label="Étapes de la simulation"> | |
| <button class="timeline-step active" data-step="calme" role="tab" aria-selected="true"> | |
| <span class="step-num">1</span> Calme | |
| </button> | |
| <div class="timeline-connector"></div> | |
| <button class="timeline-step" data-step="pic" role="tab" aria-selected="false"> | |
| <span class="step-num">2</span> Pic soudain | |
| </button> | |
| <div class="timeline-connector"></div> | |
| <button class="timeline-step" data-step="reconstitution" role="tab" aria-selected="false"> | |
| <span class="step-num">3</span> Reconstitution | |
| </button> | |
| <div class="timeline-connector"></div> | |
| <button class="timeline-step" data-step="ponctuel" role="tab" aria-selected="false"> | |
| <span class="step-num">4</span> Réserve ponctuelle | |
| </button> | |
| </div> | |
| <p class="kbd-hint">Naviguez avec <kbd>←</kbd> <kbd>→</kbd> ou cliquez</p> | |
| <!-- Legend --> | |
| <div class="legend" style="margin-top: 0.75rem;"> | |
| <div class="legend-item"><div class="legend-dot placeholder-dot"></div> Placeholder (priorité basse, préemptable)</div> | |
| <div class="legend-item"><div class="legend-dot real-dot"></div> Workload réel (priorité haute)</div> | |
| <div class="legend-item"><div class="legend-dot system-dot"></div> Pod système (kube-system)</div> | |
| <div class="legend-item"><div class="legend-dot evicted-dot"></div> Évincé / vide</div> | |
| </div> | |
| <div class="sim-grid"> | |
| <!-- LEFT: Cluster + Metrics --> | |
| <div> | |
| <!-- Tabs --> | |
| <div class="tab-row" aria-label="Niveau d'explication" style="margin-top: 1rem;"> | |
| <button class="tab active" data-tab="kid">Comme à 5 ans</button> | |
| <button class="tab" data-tab="tech">Ingénieur</button> | |
| <button class="tab" data-tab="ops">Ops / design</button> | |
| </div> | |
| <div class="scene"> | |
| <div class="story" id="story"></div> | |
| <div class="metrics"> | |
| <div class="meter-row"> | |
| <div class="meter-head"> | |
| <span class="meter-label">Demande métier</span> | |
| <span class="meter-value" id="demand-label">6 vCPU</span> | |
| </div> | |
| <div class="meter"><div class="fill demand" id="demand-fill"></div></div> | |
| </div> | |
| <div class="meter-row"> | |
| <div class="meter-head"> | |
| <span class="meter-label">Capacité réservée (placeholders)</span> | |
| <span class="meter-value" id="spare-label">4 vCPU</span> | |
| </div> | |
| <div class="meter"><div class="fill spare" id="spare-fill"></div></div> | |
| </div> | |
| <div class="meter-row"> | |
| <div class="meter-head"> | |
| <span class="meter-label">Capacité immédiatement exploitable</span> | |
| <span class="meter-value" id="ready-label">10 vCPU</span> | |
| </div> | |
| <div class="meter"><div class="fill ready" id="ready-fill"></div></div> | |
| </div> | |
| </div> | |
| <div class="cluster"> | |
| <article class="node highlight" id="node-a"> | |
| <h3>Nœud A</h3> | |
| <div class="pod-grid"> | |
| <div class="pod system">System<small>DNS / kube-system</small></div> | |
| <div class="pod real" data-slot="a2">API<small>prio haute</small></div> | |
| <div class="pod placeholder" data-slot="a3">Placeholder<small>prio basse</small></div> | |
| <div class="pod placeholder" data-slot="a4">Placeholder<small>prio basse</small></div> | |
| </div> | |
| </article> | |
| <article class="node" id="node-b"> | |
| <h3>Nœud B</h3> | |
| <div class="pod-grid"> | |
| <div class="pod system">System<small>observabilité</small></div> | |
| <div class="pod real" data-slot="b2">Worker<small>prio haute</small></div> | |
| <div class="pod placeholder" data-slot="b3">Placeholder<small>prio basse</small></div> | |
| <div class="pod placeholder" data-slot="b4">Placeholder<small>prio basse</small></div> | |
| </div> | |
| </article> | |
| <article class="node" id="node-c"> | |
| <h3>Nœud C</h3> | |
| <div class="pod-grid"> | |
| <div class="pod placeholder" data-slot="c1">Placeholder<small>prio basse</small></div> | |
| <div class="pod placeholder" data-slot="c2">Placeholder<small>prio basse</small></div> | |
| <div class="pod placeholder" data-slot="c3">Placeholder<small>prio basse</small></div> | |
| <div class="pod placeholder" data-slot="c4">Placeholder<small>prio basse</small></div> | |
| </div> | |
| </article> | |
| <article class="node offline" id="node-d"> | |
| <h3>Nœud D</h3> | |
| <div class="pod-grid"> | |
| <div class="pod evicted" data-slot="d1">En attente<small>pas encore créé</small></div> | |
| <div class="pod evicted" data-slot="d2">En attente<small>pas encore créé</small></div> | |
| <div class="pod evicted" data-slot="d3">En attente<small>pas encore créé</small></div> | |
| <div class="pod evicted" data-slot="d4">En attente<small>pas encore créé</small></div> | |
| </div> | |
| </article> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Explanation panel --> | |
| <aside class="explain-panel"> | |
| <h2>Ce qu'il faut comprendre</h2> | |
| <div class="insight-list" id="explain-panel"></div> | |
| <h2>Pièges classiques</h2> | |
| <div class="warning-list"> | |
| <div class="warning"> | |
| <strong>Ce n'est pas de la capacité gratuite.</strong> | |
| <p>Les placeholders déclarent de vraies <span class="mono">requests</span> CPU/mémoire. Le Cluster Autoscaler dimensionne les nœuds pour les héberger, donc ils influencent directement le coût du cluster.</p> | |
| </div> | |
| <div class="warning"> | |
| <strong>Ce n'est pas un remplacement du HPA, VPA ou Cluster Autoscaler.</strong> | |
| <p>C'est une couche tampon complémentaire pour réduire le délai d'absorption du premier pic. Les mécanismes classiques d'autoscaling restent indispensables.</p> | |
| </div> | |
| <div class="warning"> | |
| <strong>Le tampon doit être bien dimensionné.</strong> | |
| <p>Trop petit, les pics débordent et vos Pods restent <span class="mono">Pending</span>. Trop grand, vous payez pour du vide durable. Calibrez selon votre burst typique et la durée acceptable de Pending.</p> | |
| </div> | |
| <div class="warning"> | |
| <strong>La priorité doit être nette et explicite.</strong> | |
| <p>Les placeholders doivent avoir une <span class="mono">PriorityClass</span> clairement inférieure à tous les workloads à protéger. Oubliez une PriorityClass et le scheduler ne pourra pas les préempter.</p> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| </section> | |
| <!-- ============================================ --> | |
| <!-- DEEP DIVE --> | |
| <!-- ============================================ --> | |
| <section class="card"> | |
| <h2>De l'image mentale à la mécanique réelle</h2> | |
| <div class="grid-3"> | |
| <article class="grid-item"> | |
| <h3><span class="num">1</span> Ce que c'est</h3> | |
| <p> | |
| Un placeholder Pod est un Pod volontairement "inutile" : il exécute le conteneur | |
| <span class="mono">registry.k8s.io/pause</span> (qui ne fait rien) mais déclare de vraies | |
| <span class="mono">requests</span>. Son rôle est de matérialiser une réserve | |
| de CPU/mémoire dans le scheduling, forçant l'autoscaling à maintenir des nœuds prêts. | |
| </p> | |
| </article> | |
| <article class="grid-item"> | |
| <h3><span class="num">2</span> Pourquoi ça existe</h3> | |
| <p> | |
| Sans réserve, lors d'un pic soudain, les Pods applicatifs restent | |
| <span class="mono">Pending</span> le temps que le Cluster Autoscaler déclenche | |
| la création d'un nouveau nœud. La documentation GKE indique qu'un nœud | |
| prend typiquement <strong>environ 80 à 120 secondes</strong> à démarrer. | |
| Le tampon élimine cette attente pour le premier burst. | |
| </p> | |
| </article> | |
| <article class="grid-item"> | |
| <h3><span class="num">3</span> Comment ça marche</h3> | |
| <p> | |
| Les placeholders reçoivent une <span class="mono">PriorityClass</span> à valeur faible | |
| (souvent négative). Lorsqu'un Pod plus prioritaire arrive et que le scheduler | |
| ne trouve pas de capacité libre, il préempte les placeholders. | |
| Si un <span class="mono">Deployment</span> les gère, le contrôleur les recrée, | |
| ce qui pousse l'autoscaler à reconstituer la réserve. | |
| </p> | |
| </article> | |
| </div> | |
| </section> | |
| <!-- ============================================ --> | |
| <!-- ARCHITECTURE CHOICES --> | |
| <!-- ============================================ --> | |
| <section class="card"> | |
| <h2>Choix d'architecture et d'exploitation</h2> | |
| <div class="grid-3"> | |
| <article class="grid-item"> | |
| <h3>Réserve continue</h3> | |
| <p> | |
| Utilisez un <span class="mono">Deployment</span> de placeholders pour un tampon permanent. | |
| Les Pods évincés seront recréés automatiquement par le contrôleur, | |
| poussant le cluster à rester "chaud" en permanence. | |
| C'est le pattern standard pour les services avec des pics imprévisibles. | |
| </p> | |
| </article> | |
| <article class="grid-item"> | |
| <h3>Réserve ponctuelle</h3> | |
| <p> | |
| Utilisez un <span class="mono">Job</span> pour provisionner de la capacité en prévision | |
| d'un événement planifié (vente flash, campagne marketing). | |
| Réservez suffisamment tôt pour absorber le délai de boot des nœuds, | |
| puis supprimez le Job après l'événement pour arrêter de payer le surplus. | |
| </p> | |
| </article> | |
| <article class="grid-item"> | |
| <h3>Sizing</h3> | |
| <p> | |
| Dimensionnez le tampon selon la formule : | |
| <span class="mono">replicas × requests_par_pod</span>. | |
| Paramètres clés : nombre de Pods à absorber au burst, | |
| requests par Pod, marge de sécurité, et durée acceptable de | |
| <span class="mono">Pending</span> avant le scale-out complet. | |
| </p> | |
| </article> | |
| </div> | |
| </section> | |
| <!-- ============================================ --> | |
| <!-- YAML EXAMPLES --> | |
| <!-- ============================================ --> | |
| <section class="card"> | |
| <h2>Exemple de mise en œuvre minimale</h2> | |
| <div class="grid-3"> | |
| <article class="yaml-block"> | |
| <h3>PriorityClass faible</h3> | |
| <pre><span class="kw">apiVersion</span>: scheduling.k8s.io/v1 | |
| <span class="kw">kind</span>: PriorityClass | |
| <span class="kw">metadata</span>: | |
| <span class="kw">name</span>: <span class="str">placeholder</span> | |
| <span class="kw">value</span>: <span class="num">-1000</span> | |
| <span class="kw">globalDefault</span>: <span class="num">false</span> | |
| <span class="cmt"># preemptionPolicy: PreemptLowerPriority</span> | |
| <span class="cmt"># (valeur par défaut, pas besoin de le spécifier)</span> | |
| <span class="kw">description</span>: <span class="str">"Placeholder pods pour surprovisionnement"</span></pre> | |
| </article> | |
| <article class="yaml-block"> | |
| <h3>Deployment continu</h3> | |
| <pre><span class="kw">apiVersion</span>: apps/v1 | |
| <span class="kw">kind</span>: Deployment | |
| <span class="kw">metadata</span>: | |
| <span class="kw">name</span>: <span class="str">spare-capacity</span> | |
| <span class="kw">spec</span>: | |
| <span class="kw">replicas</span>: <span class="num">6</span> | |
| <span class="kw">selector</span>: | |
| <span class="kw">matchLabels</span>: | |
| <span class="kw">app</span>: <span class="str">spare-capacity</span> | |
| <span class="kw">template</span>: | |
| <span class="kw">metadata</span>: | |
| <span class="kw">labels</span>: | |
| <span class="kw">app</span>: <span class="str">spare-capacity</span> | |
| <span class="kw">spec</span>: | |
| <span class="kw">priorityClassName</span>: <span class="str">placeholder</span> | |
| <span class="kw">containers</span>: | |
| - <span class="kw">name</span>: <span class="str">pause</span> | |
| <span class="kw">image</span>: <span class="str">registry.k8s.io/pause</span> | |
| <span class="kw">resources</span>: | |
| <span class="kw">requests</span>: | |
| <span class="kw">cpu</span>: <span class="str">"500m"</span> | |
| <span class="kw">memory</span>: <span class="str">"512Mi"</span></pre> | |
| </article> | |
| <article class="yaml-block"> | |
| <h3>Lecture opérationnelle</h3> | |
| <pre><span class="cmt"># Tampon total = replicas × requests</span> | |
| <span class="kw">Exemple avec ce Deployment</span>: | |
| <span class="num">6</span> placeholders × <span class="str">500m</span> = <span class="num">3 vCPU</span> | |
| <span class="num">6</span> placeholders × <span class="str">512Mi</span> = <span class="num">3 GiB</span> | |
| <span class="cmt"># Cette réserve est :</span> | |
| <span class="cmt"># - préemptable par les Pods plus prioritaires</span> | |
| <span class="cmt"># - visible du scheduler (requests, pas limits)</span> | |
| <span class="cmt"># - prise en compte par le Cluster Autoscaler</span> | |
| <span class="cmt"># - payante tant qu'elle existe</span></pre> | |
| </article> | |
| </div> | |
| </section> | |
| <!-- ============================================ --> | |
| <!-- SOURCES --> | |
| <!-- ============================================ --> | |
| <section class="card sources"> | |
| <h2>Sources et cadrage</h2> | |
| <p> | |
| Vérifié le 10 avril 2026 à partir des documentations officielles : | |
| <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/capacity-provisioning" target="_blank" rel="noreferrer">GKE : Provision extra compute capacity for rapid Pod scaling</a>, | |
| <a href="https://kubernetes.io/docs/tasks/administer-cluster/node-overprovisioning/" target="_blank" rel="noreferrer">Kubernetes : Overprovision Node Capacity For A Cluster</a>, | |
| et la doc GKE sur le bursting | |
| <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/pod-bursting-gke" target="_blank" rel="noreferrer">Configure Pod bursting in GKE</a>. | |
| </p> | |
| <p> | |
| Les points repris ici viennent de ces sources : placeholders à faible priorité, | |
| préemption par des Pods plus prioritaires, usage d'un <span class="mono">Deployment</span> | |
| pour une réserve continue, usage d'un <span class="mono">Job</span> pour une réserve ponctuelle, | |
| et ordre de grandeur du démarrage d'un nouveau nœud autour de 80 à 120 secondes. | |
| </p> | |
| <p> | |
| <strong>Note :</strong> GKE propose aussi l'API | |
| <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/provisioningrequest" target="_blank" rel="noreferrer">ProvisioningRequest</a> | |
| (Capacity Provisioning) qui automatise ce pattern de manière native. | |
| Le pattern "balloon" décrit ici reste pertinent pour les clusters non-GKE | |
| ou pour un contrôle plus fin de la stratégie de surprovisionnement. | |
| </p> | |
| </section> | |
| </main> | |
| <script> | |
| /* ============================================ | |
| State & DOM refs | |
| ============================================ */ | |
| const timelineSteps = [...document.querySelectorAll(".timeline-step")]; | |
| const connectors = [...document.querySelectorAll(".timeline-connector")]; | |
| const tabs = [...document.querySelectorAll(".tab")]; | |
| const storyEl = document.getElementById("story"); | |
| const explainPanel = document.getElementById("explain-panel"); | |
| const demandFill = document.getElementById("demand-fill"); | |
| const spareFill = document.getElementById("spare-fill"); | |
| const readyFill = document.getElementById("ready-fill"); | |
| const demandLabel = document.getElementById("demand-label"); | |
| const spareLabel = document.getElementById("spare-label"); | |
| const readyLabel = document.getElementById("ready-label"); | |
| const slots = {}; | |
| document.querySelectorAll("[data-slot]").forEach(el => { slots[el.dataset.slot] = el; }); | |
| const nodes = { | |
| a: document.getElementById("node-a"), | |
| b: document.getElementById("node-b"), | |
| c: document.getElementById("node-c"), | |
| d: document.getElementById("node-d") | |
| }; | |
| const stepOrder = ["calme", "pic", "reconstitution", "ponctuel"]; | |
| let currentStep = "calme"; | |
| let currentTab = "kid"; | |
| /* ============================================ | |
| Pod helper | |
| ============================================ */ | |
| function setPod(slot, type, title, subtitle) { | |
| if (!slot) return; | |
| slot.className = `pod ${type}`; | |
| slot.innerHTML = `${title}<small>${subtitle}</small>`; | |
| } | |
| /* ============================================ | |
| Step definitions | |
| ============================================ */ | |
| const steps = { | |
| calme: { | |
| label: "1. Calme", | |
| metrics: { demand: 45, spare: 30, ready: 75, demandLabel: "6 vCPU", spareLabel: "4 vCPU", readyLabel: "10 vCPU" }, | |
| story: { | |
| kid: "Le cluster garde des si\u00e8ges occup\u00e9s par des faux voyageurs. Ils ne servent pas \u00e0 travailler, ils servent \u00e0 garder la place chaude pour les vrais passagers qui arriveront d'un coup.", | |
| tech: "Des placeholders avec de vraies requests CPU/m\u00e9moire tournent d\u00e9j\u00e0. Le scheduler voit cette consommation et le Cluster Autoscaler maintient assez de n\u0153uds pour les h\u00e9berger. Aucun workload r\u00e9el n'est impact\u00e9.", | |
| ops: "Vous payez volontairement une r\u00e9serve pr\u00e9emptable. Le b\u00e9n\u00e9fice attendu\u00a0: une meilleure absorption du burst initial, sans attendre un scale-out complet des n\u0153uds (~80-120s)." | |
| }, | |
| explain: { | |
| kid: [ | |
| "Les placeholders sont des faux passagers qui gardent des si\u00e8ges libres.", | |
| "Quand les vrais passagers arrivent d'un coup, les faux descendent en premier.", | |
| "C'est comme r\u00e9server des places au cin\u00e9ma pour ses amis." | |
| ], | |
| tech: [ | |
| "Les placeholders mat\u00e9rialisent une capacit\u00e9 tampon via leurs requests (CPU + m\u00e9moire).", | |
| "Le Cluster Autoscaler dimensionne le cluster pour ces Pods aussi\u00a0\u2014\u00a0c'est ce qui cr\u00e9e la r\u00e9serve.", | |
| "Leur priorit\u00e9 faible (PriorityClass n\u00e9gative) pr\u00e9pare leur \u00e9viction future par le scheduler." | |
| ], | |
| ops: [ | |
| "Le tampon doit couvrir le burst critique, pas toute la charge maximale.", | |
| "Cherchez l'\u00e9quilibre entre la latence d'absorption (souhait\u00e9e\u00a0: ~0s) et le co\u00fbt permanent de la r\u00e9serve.", | |
| "Surveillez les \u00e9victions, les Pods Pending et le d\u00e9lai r\u00e9el de scale-out dans vos m\u00e9triques." | |
| ] | |
| }, | |
| apply() { | |
| nodes.a.className = "node highlight"; | |
| nodes.b.className = "node"; | |
| nodes.c.className = "node"; | |
| nodes.d.className = "node offline"; | |
| setPod(slots.a2, "real", "API", "prio haute"); | |
| setPod(slots.a3, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.a4, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.b2, "real", "Worker", "prio haute"); | |
| setPod(slots.b3, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.b4, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.c1, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.c2, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.c3, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.c4, "placeholder", "Placeholder", "prio basse"); | |
| setPod(slots.d1, "evicted", "En attente", "pas encore cr\u00e9\u00e9"); | |
| setPod(slots.d2, "evicted", "En attente", "pas encore cr\u00e9\u00e9"); | |
| setPod(slots.d3, "evicted", "En attente", "pas encore cr\u00e9\u00e9"); | |
| setPod(slots.d4, "evicted", "En attente", "pas encore cr\u00e9\u00e9"); | |
| } | |
| }, | |
| pic: { | |
| label: "2. Pic soudain", | |
| metrics: { demand: 78, spare: 8, ready: 86, demandLabel: "10,5 vCPU", spareLabel: "1 vCPU", readyLabel: "11,5 vCPU" }, | |
| story: { | |
| kid: "D'un coup, plein de vrais passagers arrivent\u00a0! Les faux passagers descendent imm\u00e9diatement, et les vrais s'assoient tout de suite sans attendre un nouveau bus.", | |
| tech: "Des Pods applicatifs plus prioritaires arrivent. Le scheduler ne trouvant pas de capacit\u00e9 libre, il pr\u00e9empte les placeholders. Les ressources qu'ils r\u00e9servaient sont r\u00e9affect\u00e9es instantan\u00e9ment aux workloads m\u00e9tiers.", | |
| ops: "C'est la phase o\u00f9 vous gagnez du temps de r\u00e9ponse. Le cluster absorbe le pic gr\u00e2ce au tampon, pendant que le Cluster Autoscaler et le HPA continuent en arri\u00e8re-plan." | |
| }, | |
| explain: { | |
| kid: [ | |
| "Les faux passagers perdent leur si\u00e8ge en premier\u00a0\u2014\u00a0c'est fait expr\u00e8s.", | |
| "Les vrais peuvent entrer imm\u00e9diatement sans attendre un nouveau bus.", | |
| "Le service est beaucoup plus rapide quand il y a un rush." | |
| ], | |
| tech: [ | |
| "La cl\u00e9 n'est pas le Pod placeholder lui-m\u00eame, mais la capacit\u00e9 de n\u0153ud d\u00e9j\u00e0 allou\u00e9e qu'il mat\u00e9rialisait.", | |
| "La pr\u00e9emption est d\u00e9clench\u00e9e par le scheduler quand il ne trouve pas de capacit\u00e9 libre pour un Pod plus prioritaire.", | |
| "Le Pod m\u00e9tier \u00e9vite une attente longue en Pending (~80-120s pour un nouveau n\u0153ud)." | |
| ], | |
| ops: [ | |
| "Cette strat\u00e9gie r\u00e9duit surtout la latence du premier burst (temps avant que les Pods soient Running).", | |
| "Elle n'annule pas le besoin de scaler ensuite si le pic dure\u00a0\u2014\u00a0HPA + Cluster Autoscaler restent n\u00e9cessaires.", | |
| "V\u00e9rifiez que vos workloads critiques ont bien une PriorityClass sup\u00e9rieure aux placeholders." | |
| ] | |
| }, | |
| apply() { | |
| nodes.a.className = "node highlight"; | |
| nodes.b.className = "node highlight"; | |
| nodes.c.className = "node highlight"; | |
| nodes.d.className = "node offline"; | |
| setPod(slots.a2, "real", "API", "prio haute"); | |
| setPod(slots.a3, "real", "API x2", "a pris la place"); | |
| setPod(slots.a4, "real", "API x3", "a pris la place"); | |
| setPod(slots.b2, "real", "Worker", "prio haute"); | |
| setPod(slots.b3, "real", "Worker x2", "a pris la place"); | |
| setPod(slots.b4, "real", "Queue", "a pris la place"); | |
| setPod(slots.c1, "real", "Checkout", "a pris la place"); | |
| setPod(slots.c2, "real", "Checkout x2", "a pris la place"); | |
| setPod(slots.c3, "evicted", "\u00c9vinc\u00e9", "lib\u00e8re de la place"); | |
| setPod(slots.c4, "evicted", "\u00c9vinc\u00e9", "lib\u00e8re de la place"); | |
| setPod(slots.d1, "evicted", "Scale-out", "en cours (~80s)"); | |
| setPod(slots.d2, "evicted", "Scale-out", "en cours (~80s)"); | |
| setPod(slots.d3, "evicted", "Scale-out", "en cours (~80s)"); | |
| setPod(slots.d4, "evicted", "Scale-out", "en cours (~80s)"); | |
| } | |
| }, | |
| reconstitution: { | |
| label: "3. Reconstitution", | |
| metrics: { demand: 72, spare: 22, ready: 94, demandLabel: "9,5 vCPU", spareLabel: "3 vCPU", readyLabel: "12,5 vCPU" }, | |
| story: { | |
| kid: "Les faux passagers reviennent plus tard sur un nouveau bus, pour \u00eatre pr\u00eats pour la prochaine bousculade. On pr\u00e9pare d\u00e9j\u00e0 la suite\u00a0!", | |
| tech: "Comme les placeholders sont g\u00e9r\u00e9s par un Deployment, le contr\u00f4leur tente de les recr\u00e9er. L'autoscaler ajoute un n\u0153ud pour les h\u00e9berger et la r\u00e9serve se reconstitue progressivement.", | |
| ops: "C'est le vrai int\u00e9r\u00eat du pattern continu\u00a0: non seulement absorber le pic, mais restaurer automatiquement le tampon pour le prochain burst. Mesurez le d\u00e9lai de reconstitution." | |
| }, | |
| explain: { | |
| kid: [ | |
| "Les si\u00e8ges de secours reviennent apr\u00e8s la bousculade.", | |
| "On pr\u00e9pare d\u00e9j\u00e0 le prochain moment charg\u00e9.", | |
| "Le syst\u00e8me redevient pr\u00eat, tout seul." | |
| ], | |
| tech: [ | |
| "Le Deployment maintient le nombre voulu de replicas (placeholders).", | |
| "Le Cluster Autoscaler voit de nouveau ces Pods \u00e0 placer et provisionne un n\u0153ud.", | |
| "Un nouveau n\u0153ud appara\u00eet (~80-120s) pour recr\u00e9er la r\u00e9serve compl\u00e8te." | |
| ], | |
| ops: [ | |
| "Mesurez le d\u00e9lai de reconstitution du tampon apr\u00e8s un burst.", | |
| "Dimensionnez ce d\u00e9lai selon votre fr\u00e9quence de pics.", | |
| "Si le burst est continu, la r\u00e9serve peut rester consomm\u00e9e longtemps\u00a0\u2014\u00a0le pattern sert surtout pour les pics ponctuels." | |
| ] | |
| }, | |
| apply() { | |
| nodes.a.className = "node"; | |
| nodes.b.className = "node highlight"; | |
| nodes.c.className = "node"; | |
| nodes.d.className = "node highlight"; | |
| setPod(slots.a2, "real", "API", "prio haute"); | |
| setPod(slots.a3, "real", "API x2", "toujours actif"); | |
| setPod(slots.a4, "placeholder", "Placeholder", "recr\u00e9\u00e9"); | |
| setPod(slots.b2, "real", "Worker", "prio haute"); | |
| setPod(slots.b3, "real", "Worker x2", "toujours actif"); | |
| setPod(slots.b4, "real", "Queue", "toujours actif"); | |
| setPod(slots.c1, "real", "Checkout", "toujours actif"); | |
| setPod(slots.c2, "placeholder", "Placeholder", "recr\u00e9\u00e9"); | |
| setPod(slots.c3, "placeholder", "Placeholder", "recr\u00e9\u00e9"); | |
| setPod(slots.c4, "placeholder", "Placeholder", "recr\u00e9\u00e9"); | |
| setPod(slots.d1, "placeholder", "Placeholder", "nouveau n\u0153ud"); | |
| setPod(slots.d2, "placeholder", "Placeholder", "nouveau n\u0153ud"); | |
| setPod(slots.d3, "placeholder", "Placeholder", "nouveau n\u0153ud"); | |
| setPod(slots.d4, "placeholder", "Placeholder", "nouveau n\u0153ud"); | |
| } | |
| }, | |
| ponctuel: { | |
| label: "4. R\u00e9serve ponctuelle", | |
| metrics: { demand: 54, spare: 0, ready: 54, demandLabel: "7 vCPU", spareLabel: "0 vCPU", readyLabel: "7 vCPU" }, | |
| story: { | |
| kid: "Cette fois, on avait pr\u00e9par\u00e9 des si\u00e8ges juste pour une f\u00eate. La f\u00eate est finie, on ne garde plus les faux passagers. On range tout.", | |
| tech: "Un Job de placeholders provisionne de la capacit\u00e9 pour un \u00e9v\u00e9nement ponctuel. Apr\u00e8s usage, le Job se termine et la r\u00e9serve n'est pas reconstitu\u00e9e\u00a0\u2014\u00a0contrairement au pattern Deployment.", | |
| ops: "Bon choix pour un besoin planifi\u00e9 et court (vente flash, campagne). Apr\u00e8s l'\u00e9v\u00e9nement, la capacit\u00e9 redescend et vous arr\u00eatez de payer le surplus. Documentez la proc\u00e9dure d'activation et d'arr\u00eat." | |
| }, | |
| explain: { | |
| kid: [ | |
| "On pr\u00e9pare des si\u00e8ges seulement quand on sait qu'une foule va venir.", | |
| "Apr\u00e8s la f\u00eate, on range les si\u00e8ges de secours.", | |
| "On \u00e9vite de garder trop de place pour rien." | |
| ], | |
| tech: [ | |
| "Le Job sert une logique one-shot de provisioning de capacit\u00e9.", | |
| "Il ne maintient pas une r\u00e9serve auto-r\u00e9g\u00e9n\u00e9r\u00e9e apr\u00e8s \u00e9viction.", | |
| "Pattern adapt\u00e9 aux \u00e9v\u00e9nements pr\u00e9visibles o\u00f9 vous connaissez la date du pic." | |
| ], | |
| ops: [ | |
| "R\u00e9servez t\u00f4t avant l'\u00e9v\u00e9nement pour absorber le d\u00e9lai de boot des n\u0153uds.", | |
| "Supprimez le Job apr\u00e8s l'\u00e9v\u00e9nement pour r\u00e9duire le co\u00fbt.", | |
| "Documentez clairement la proc\u00e9dure d'activation et d'arr\u00eat dans un runbook." | |
| ] | |
| }, | |
| apply() { | |
| nodes.a.className = "node"; | |
| nodes.b.className = "node"; | |
| nodes.c.className = "node"; | |
| nodes.d.className = "node offline"; | |
| setPod(slots.a2, "real", "API", "prio haute"); | |
| setPod(slots.a3, "real", "API x2", "apr\u00e8s \u00e9v\u00e9nement"); | |
| setPod(slots.a4, "evicted", "Vide", "plus de r\u00e9serve"); | |
| setPod(slots.b2, "real", "Worker", "prio haute"); | |
| setPod(slots.b3, "real", "Queue", "apr\u00e8s \u00e9v\u00e9nement"); | |
| setPod(slots.b4, "evicted", "Vide", "plus de r\u00e9serve"); | |
| setPod(slots.c1, "evicted", "Vide", "Job termin\u00e9"); | |
| setPod(slots.c2, "evicted", "Vide", "Job termin\u00e9"); | |
| setPod(slots.c3, "evicted", "Vide", "Job termin\u00e9"); | |
| setPod(slots.c4, "evicted", "Vide", "Job termin\u00e9"); | |
| setPod(slots.d1, "evicted", "Arr\u00eat\u00e9", "n\u0153ud retir\u00e9"); | |
| setPod(slots.d2, "evicted", "Arr\u00eat\u00e9", "n\u0153ud retir\u00e9"); | |
| setPod(slots.d3, "evicted", "Arr\u00eat\u00e9", "n\u0153ud retir\u00e9"); | |
| setPod(slots.d4, "evicted", "Arr\u00eat\u00e9", "n\u0153ud retir\u00e9"); | |
| } | |
| } | |
| }; | |
| /* ============================================ | |
| Render | |
| ============================================ */ | |
| function renderExplanation(stepKey, tabKey) { | |
| const lines = steps[stepKey].explain[tabKey]; | |
| explainPanel.innerHTML = lines.map((line, i) => | |
| `<div class="insight"><div class="insight-icon">${i + 1}</div><span>${line}</span></div>` | |
| ).join(""); | |
| storyEl.innerHTML = `<h3>${steps[stepKey].label}</h3><p>${steps[stepKey].story[tabKey]}</p>`; | |
| } | |
| function renderMeters(stepKey) { | |
| const m = steps[stepKey].metrics; | |
| demandFill.style.width = `${m.demand}%`; | |
| spareFill.style.width = `${m.spare}%`; | |
| readyFill.style.width = `${m.ready}%`; | |
| demandLabel.textContent = m.demandLabel; | |
| spareLabel.textContent = m.spareLabel; | |
| readyLabel.textContent = m.readyLabel; | |
| } | |
| function updateTimeline() { | |
| const idx = stepOrder.indexOf(currentStep); | |
| timelineSteps.forEach((btn, i) => { | |
| btn.classList.toggle("active", btn.dataset.step === currentStep); | |
| btn.setAttribute("aria-selected", btn.dataset.step === currentStep); | |
| }); | |
| connectors.forEach((c, i) => { | |
| c.classList.toggle("passed", i < idx); | |
| }); | |
| } | |
| function render() { | |
| updateTimeline(); | |
| tabs.forEach(t => t.classList.toggle("active", t.dataset.tab === currentTab)); | |
| steps[currentStep].apply(); | |
| renderMeters(currentStep); | |
| renderExplanation(currentStep, currentTab); | |
| } | |
| /* ============================================ | |
| Events | |
| ============================================ */ | |
| timelineSteps.forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| currentStep = btn.dataset.step; | |
| render(); | |
| }); | |
| }); | |
| tabs.forEach(tab => { | |
| tab.addEventListener("click", () => { | |
| currentTab = tab.dataset.tab; | |
| render(); | |
| }); | |
| }); | |
| // Keyboard navigation | |
| document.addEventListener("keydown", (e) => { | |
| const idx = stepOrder.indexOf(currentStep); | |
| if (e.key === "ArrowRight" && idx < stepOrder.length - 1) { | |
| currentStep = stepOrder[idx + 1]; | |
| render(); | |
| e.preventDefault(); | |
| } else if (e.key === "ArrowLeft" && idx > 0) { | |
| currentStep = stepOrder[idx - 1]; | |
| render(); | |
| e.preventDefault(); | |
| } | |
| }); | |
| /* ============================================ | |
| Init | |
| ============================================ */ | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment