<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Zhou Xingyu</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://zhouxingyu.dev/</id>
  <link href="https://zhouxingyu.dev/" rel="alternate"/>
  <link href="https://zhouxingyu.dev/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Zhou Xingyu</rights>
  <subtitle>Software Engineering, Cloud Native, DevOps and AI Infra notes</subtitle>
  <title>Zhou Xingyu</title>
  <updated>2026-10-09T02:08:19.152Z</updated>
  <entry>
    <author>
      <name>Zhou Xingyu</name>
    </author>
    <category term="DevOps" scheme="https://zhouxingyu.dev/categories/DevOps/"/>
    <category term="CI/CD" scheme="https://zhouxingyu.dev/tags/CI-CD/"/>
    <category term="Jenkins" scheme="https://zhouxingyu.dev/tags/Jenkins/"/>
    <category term="GitHub Actions" scheme="https://zhouxingyu.dev/tags/GitHub-Actions/"/>
    <category term="GitLab CI" scheme="https://zhouxingyu.dev/tags/GitLab-CI/"/>
    <category term="Argo CD" scheme="https://zhouxingyu.dev/tags/Argo-CD/"/>
    <category term="GitOps" scheme="https://zhouxingyu.dev/tags/GitOps/"/>
    <category term="Kubernetes" scheme="https://zhouxingyu.dev/tags/Kubernetes/"/>
    <content>
      <![CDATA[<h1 id="CI-CD-工具与架构对比：从-GitHub-Enterprise-Jenkins-到-GitLab-CI、GitHub-Actions-与-Argo-CD"><a href="#CI-CD-工具与架构对比：从-GitHub-Enterprise-Jenkins-到-GitLab-CI、GitHub-Actions-与-Argo-CD" class="headerlink" title="CI&#x2F;CD 工具与架构对比：从 GitHub Enterprise + Jenkins 到 GitLab CI、GitHub Actions 与 Argo CD"></a>CI&#x2F;CD 工具与架构对比：从 GitHub Enterprise + Jenkins 到 GitLab CI、GitHub Actions 与 Argo CD</h1><h2 id="1-为什么要做这次对比？"><a href="#1-为什么要做这次对比？" class="headerlink" title="1. 为什么要做这次对比？"></a>1. 为什么要做这次对比？</h2><p>此前我的主要工作经验来自 GitHub Enterprise + Jenkins 的企业研发环境，涉及 PR 检查、Maven 构建、Artifactory 制品管理、测试环境部署、自动化回归和生产发布流程。</p><p>最近又在 WSL2 上搭建 GitLab CE，实践了 Shell&#x2F;Docker Runner、Docker-in-Docker、Artifact、Cache、Container Registry 和资源并发控制。</p><p>在整理这些经验时，一个问题逐渐变得清晰：</p><p><strong>GitHub Enterprise、Jenkins、GitHub Actions、GitLab CI&#x2F;CD、Argo CD 到底是相互替代的工具，还是可以组合使用的不同系统？</strong></p><p>答案是两者都有，但首先要明确各自负责的层次。</p><h2 id="2-CI-CD-不等于一个工具"><a href="#2-CI-CD-不等于一个工具" class="headerlink" title="2. CI&#x2F;CD 不等于一个工具"></a>2. CI&#x2F;CD 不等于一个工具</h2><p>从开发提交代码到应用运行，通常可以分成几个阶段：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">Source Code</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Code Review / Merge Gate</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">CI: Build + Static Analysis + Test</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Artifact / Container Registry</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">CD: Deploy + Verify + Rollback</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Runtime Environment</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Observability / Operations</span><br></pre></td></tr></table></figure><p>这些职责不一定由同一个产品完成。</p><p>例如：</p><ul><li>GitHub Enterprise 可以负责代码、PR 和分支保护。</li><li>Jenkins 可以负责构建、自动化测试和部署。</li><li>Artifactory 可以负责保存构建制品。</li><li>Argo CD 可以负责将 Kubernetes 集群同步到 Git 中声明的目标状态。</li><li>Prometheus&#x2F;Grafana 可以负责运行时监控。</li></ul><p>因此，一套 CI&#x2F;CD 系统更准确的描述方式应该是<strong>工具链架构</strong>，而不是只说“我们使用 Jenkins”或“我们使用 GitHub”。</p><p>还需要区分两个概念：</p><p><strong>Continuous Delivery（持续交付）</strong>：软件持续保持可发布状态，生产发布可以有人为审批。</p><p><strong>Continuous Deployment（持续部署）</strong>：满足自动化门禁后，变更可以自动进入生产环境。</p><p>使用了自动化流水线，不代表企业一定采用完全自动化的生产部署。</p><h2 id="3-常见工具的职责分类"><a href="#3-常见工具的职责分类" class="headerlink" title="3. 常见工具的职责分类"></a>3. 常见工具的职责分类</h2><table><thead><tr><th>工具</th><th>主要定位</th><th>能否执行 CI</th><th>能否执行部署</th></tr></thead><tbody><tr><td>GitHub Enterprise</td><td>企业代码托管与协作平台</td><td>通过 Actions 或外部 CI</td><td>通过集成工具</td></tr><tr><td>Jenkins</td><td>通用自动化与 CI&#x2F;CD 引擎</td><td>是</td><td>是</td></tr><tr><td>GitHub Actions</td><td>GitHub 集成式工作流与 CI&#x2F;CD</td><td>是</td><td>是</td></tr><tr><td>GitLab CI&#x2F;CD</td><td>GitLab 集成式 CI&#x2F;CD</td><td>是</td><td>是</td></tr><tr><td>Azure Pipelines</td><td>Azure DevOps CI&#x2F;CD 平台</td><td>是</td><td>是</td></tr><tr><td>CircleCI</td><td>CI&#x2F;CD 平台</td><td>是</td><td>是</td></tr><tr><td>Tekton</td><td>Kubernetes-native Pipeline 引擎</td><td>是</td><td>是</td></tr><tr><td>Argo CD</td><td>Kubernetes GitOps CD Controller</td><td>非主要职责</td><td>是</td></tr><tr><td>Flux CD</td><td>Kubernetes GitOps CD Controller</td><td>非主要职责</td><td>是</td></tr></tbody></table><p>这里最容易误解的是 Argo CD。</p><p><strong>Argo CD 并不是 Jenkins 或 GitHub Actions 的直接一对一替代品。</strong></p><p>Argo CD 主要解决的是 Kubernetes 应用如何持续保持在期望的部署状态。编译 Java、执行单元测试、生成 JAR、构建容器镜像等工作，通常仍由 Jenkins、GitHub Actions、GitLab CI 或 Tekton 承担。</p><h2 id="4-Jenkins：独立于代码平台的自动化引擎"><a href="#4-Jenkins：独立于代码平台的自动化引擎" class="headerlink" title="4. Jenkins：独立于代码平台的自动化引擎"></a>4. Jenkins：独立于代码平台的自动化引擎</h2><p>Jenkins 的典型架构：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">GitHub Enterprise / GitLab / Git</span><br><span class="line">                │</span><br><span class="line">         Webhook / Polling</span><br><span class="line">                │</span><br><span class="line">                ▼</span><br><span class="line">        Jenkins Controller</span><br><span class="line">                │</span><br><span class="line">        Scheduling / Pipeline</span><br><span class="line">                │</span><br><span class="line">       ┌────────┴────────┐</span><br><span class="line">       ▼                 ▼</span><br><span class="line">   Build Agent       Test Agent</span><br><span class="line">       │                 │</span><br><span class="line">   Maven / JDK       Testbed / Tools</span><br><span class="line">       │                 │</span><br><span class="line">       └────────┬────────┘</span><br><span class="line">                ▼</span><br><span class="line">        Artifact Repository</span><br><span class="line">                │</span><br><span class="line">                ▼</span><br><span class="line">           Deployment</span><br></pre></td></tr></table></figure><p>Jenkins Controller 负责管理 Job 和调度 Agent；实际工作通常交给 Agent 上的 Executor 执行。</p><p>Jenkins 可以通过 <code>Jenkinsfile</code> 实现 Pipeline as Code，例如：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">pipeline &#123;</span><br><span class="line">    agent any</span><br><span class="line"></span><br><span class="line">    stages &#123;</span><br><span class="line">        stage(<span class="string">&#x27;Build&#x27;</span>) &#123;</span><br><span class="line">            steps &#123;</span><br><span class="line">                sh <span class="string">&#x27;mvn clean package&#x27;</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        stage(<span class="string">&#x27;Test&#x27;</span>) &#123;</span><br><span class="line">            steps &#123;</span><br><span class="line">                sh <span class="string">&#x27;mvn test&#x27;</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但并不是所有 Jenkins 环境都使用 <code>Jenkinsfile</code>。传统 Job、Freestyle Job、UI 配置、Groovy 脚本以及多层 Job 调用都可能存在。</p><h3 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h3><ul><li>与代码托管平台解耦。</li><li>插件和脚本扩展能力强。</li><li>适合异构构建环境、专用物理机及复杂测试流程。</li><li>可以集成较老的系统和工具。</li></ul><h3 id="代价"><a href="#代价" class="headerlink" title="代价"></a>代价</h3><ul><li>Controller、Agent、插件、凭据和升级需要自行治理。</li><li>历史 Job 容易积累隐式依赖。</li><li>UI 配置过多会降低版本可追踪性。</li><li>多个 Pipeline 共用环境时，需要额外管理资源互斥和权限。</li></ul><p>对于依赖昂贵物理 Testbed、专用硬件、历史自动化框架的企业，Jenkins 的灵活性具有明显价值。</p><h2 id="5-GitHub-Enterprise-与-GitHub-Actions"><a href="#5-GitHub-Enterprise-与-GitHub-Actions" class="headerlink" title="5. GitHub Enterprise 与 GitHub Actions"></a>5. GitHub Enterprise 与 GitHub Actions</h2><p>GitHub Enterprise 首先是企业级 GitHub 平台，并不等于 GitHub Actions。</p><p>需要区分：</p><ul><li><strong>GitHub Enterprise Cloud</strong>：GitHub 托管的企业服务。</li><li><strong>GitHub Enterprise Server（GHES）</strong>：企业自行部署的 GitHub 实例。</li><li><strong>GitHub Actions</strong>：能够在支持的 GitHub 平台上启用的自动化工作流系统。</li></ul><p>GitHub Enterprise Server 的 Actions 需要管理员启用和配置，自托管 Runner 是其执行 Job 的主要方式。</p><p>典型 GitHub Actions 架构：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">GitHub Repository</span><br><span class="line">    │</span><br><span class="line">    ├── Pull Request</span><br><span class="line">    └── .github/workflows/ci.yml</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">       GitHub Actions</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">      Hosted / Self-hosted Runner</span><br><span class="line">             │</span><br><span class="line">        Build / Test / Deploy</span><br></pre></td></tr></table></figure><p>最小 Workflow：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Java</span> <span class="string">CI</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-java@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">distribution:</span> <span class="string">temurin</span></span><br><span class="line">          <span class="attr">java-version:</span> <span class="string">&#x27;21&#x27;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">mvn</span> <span class="string">clean</span> <span class="string">verify</span></span><br></pre></td></tr></table></figure><p>这里 <code>ubuntu-latest</code> 是 GitHub-hosted Runner 的常见标签；如果运行在 GitHub Enterprise Server，需要使用适当的 self-hosted Runner 配置。</p><p>GitHub Actions 的优势在于与 PR、Checks、Secrets、Environments 等 GitHub 功能集成紧密。</p><p>但它并不是 GitHub Enterprise 的必选组件。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">GitHub Enterprise</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">      Jenkins</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">   GitHub Commit Status / Checks</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">    PR Merge Gate</span><br></pre></td></tr></table></figure><p>同样可以实现“CI 不通过，PR 不允许合并”。</p><p>GitHub 的 Required Status Checks 可以接受外部 CI 系统报告的检查状态。这意味着，<strong>在 GitHub PR 页面看到 Jenkins 检查结果，并不能据此认定使用了 GitHub Actions。</strong></p><h2 id="6-GitLab-CI-CD：代码平台与-CI-CD-的集成"><a href="#6-GitLab-CI-CD：代码平台与-CI-CD-的集成" class="headerlink" title="6. GitLab CI&#x2F;CD：代码平台与 CI&#x2F;CD 的集成"></a>6. GitLab CI&#x2F;CD：代码平台与 CI&#x2F;CD 的集成</h2><p>GitLab CI&#x2F;CD 的配置一般位于项目根目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.gitlab-ci.yml</span><br></pre></td></tr></table></figure><p>架构类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">GitLab Server</span><br><span class="line">    │</span><br><span class="line">    │ Pipeline / Job Scheduling</span><br><span class="line">    ▼</span><br><span class="line">GitLab Runner</span><br><span class="line">    │</span><br><span class="line">    ├── Shell Executor</span><br><span class="line">    ├── Docker Executor</span><br><span class="line">    └── Kubernetes Executor</span><br></pre></td></tr></table></figure><p>与 Jenkins 相比，GitLab CI 的 Pipeline 配置和项目协作工作流集成得更紧密。</p><p>例如：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">build</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line"><span class="attr">build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">package</span></span><br><span class="line"></span><br><span class="line"><span class="attr">test:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><p>我在本地实验中进一步验证了：</p><ul><li>Shell 与 Docker Executor 的环境隔离差异。</li><li>Artifacts 与 Cache 的不同生命周期。</li><li><code>needs</code> 对默认 Stage 顺序的影响。</li><li>Docker Socket Binding 与 DinD 的安全和镜像存储差异。</li><li>GitLab Container Registry 的镜像 Push&#x2F;Pull。</li><li><code>resource_group</code> 对有限 Testbed 的互斥控制。</li></ul><p>与 Jenkins 一样，GitLab CI 也依赖真实的执行资源。Runner 的容量、网络、镜像仓库可用性、缓存和凭据管理同样会决定 Pipeline 的可靠性。</p><h2 id="7-Jenkins、GitHub-Actions、GitLab-CI-的对应关系"><a href="#7-Jenkins、GitHub-Actions、GitLab-CI-的对应关系" class="headerlink" title="7. Jenkins、GitHub Actions、GitLab CI 的对应关系"></a>7. Jenkins、GitHub Actions、GitLab CI 的对应关系</h2><table><thead><tr><th>概念</th><th>Jenkins</th><th>GitHub Actions</th><th>GitLab CI</th></tr></thead><tbody><tr><td>配置</td><td>Jenkinsfile &#x2F; Job Config</td><td><code>.github/workflows/*.yml</code></td><td><code>.gitlab-ci.yml</code></td></tr><tr><td>顶层编排</td><td>Pipeline</td><td>Workflow</td><td>Pipeline</td></tr><tr><td>工作单元</td><td>Stage &#x2F; Steps &#x2F; Job</td><td>Job &#x2F; Steps</td><td>Job &#x2F; Script</td></tr><tr><td>执行节点</td><td>Agent &#x2F; Executor</td><td>Runner</td><td>Runner &#x2F; Executor</td></tr><tr><td>依赖编排</td><td>Stage &#x2F; Parallel 等</td><td><code>needs</code></td><td><code>needs</code> &#x2F; Stages</td></tr><tr><td>缓存、制品</td><td>插件&#x2F;外部存储</td><td>Cache &#x2F; Artifacts</td><td>Cache &#x2F; Artifacts</td></tr><tr><td>人工发布门禁</td><td>Input &#x2F; 插件等</td><td>Environment Approval</td><td>Manual Job &#x2F; 审批功能</td></tr><tr><td>资源互斥</td><td>Lockable Resources 等</td><td>Concurrency Groups</td><td><code>resource_group</code></td></tr></tbody></table><p>这张表是概念对应，不表示这些机制完全等价。尤其是并发范围、权限边界、审批版本限制和执行环境清理方式，都有实现差异。</p><p>从 Jenkins 转向 GitLab CI 或 GitHub Actions，最大的困难通常不是 YAML 或 Groovy 语法，而是理解：</p><p><strong>谁负责调度？谁实际执行？状态保存在哪里？如何控制共享资源？失败发生在哪一层？</strong></p><h2 id="8-Argo-CD：从-Push-Deployment-到-GitOps-Reconciliation"><a href="#8-Argo-CD：从-Push-Deployment-到-GitOps-Reconciliation" class="headerlink" title="8. Argo CD：从 Push Deployment 到 GitOps Reconciliation"></a>8. Argo CD：从 Push Deployment 到 GitOps Reconciliation</h2><p>传统的命令式部署通常类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">CI Pipeline</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Build Artifact / Image</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Deploy Command</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Runtime Environment</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cf push</span><br></pre></td></tr></table></figure><p>或：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl apply -f deployment.yaml</span><br></pre></td></tr></table></figure><p>执行部署的 Pipeline 或操作人员主动向目标环境提交变更。</p><p>Argo CD 采用另一种思路：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Application Repository</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">CI: Build + Test + Push Image</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Update Deployment Manifest</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">GitOps Repository</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Argo CD Controller</span><br><span class="line">        │</span><br><span class="line">        ├── Desired State in Git</span><br><span class="line">        └── Live State in Kubernetes</span><br><span class="line">                    │</span><br><span class="line">                    ▼</span><br><span class="line">              Compare / Sync</span><br><span class="line">                    │</span><br><span class="line">                    ▼</span><br><span class="line">             Kubernetes Cluster</span><br></pre></td></tr></table></figure><p>Argo CD 以 Git 中的声明式配置为目标状态，持续比较 Kubernetes 当前状态与目标状态，并根据同步策略执行变更。</p><p>这个过程通常被称为 <strong>Reconciliation（调谐）</strong>。</p><p>与一次性执行 <code>kubectl apply</code> 相比，GitOps 更强调：</p><ul><li>Git 中的版本化期望状态。</li><li>对实际状态与期望状态差异的持续检测。</li><li>声明式变更、审计与回滚。</li><li>由集群侧控制器执行同步，而不是每次都让 CI 获得直接部署权限。</li></ul><p>但 GitOps 并不自动解决所有问题：错误配置同样可能被自动同步，数据库迁移、回滚安全性和环境权限仍然需要单独设计。</p><h3 id="关键区别"><a href="#关键区别" class="headerlink" title="关键区别"></a>关键区别</h3><p><strong>Jenkins &#x2F; GitHub Actions &#x2F; GitLab CI 负责生产可部署制品；Argo CD 负责让 Kubernetes 环境达到声明的部署状态。</strong></p><p>它们可以组合使用。</p><h2 id="9-Tekton、Flux-CD-和其他平台"><a href="#9-Tekton、Flux-CD-和其他平台" class="headerlink" title="9. Tekton、Flux CD 和其他平台"></a>9. Tekton、Flux CD 和其他平台</h2><h3 id="Tekton"><a href="#Tekton" class="headerlink" title="Tekton"></a>Tekton</h3><p>Tekton 是 Kubernetes-native CI&#x2F;CD 构建组件。它通过 Kubernetes CRD 定义：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Task</span><br><span class="line">TaskRun</span><br><span class="line">Pipeline</span><br><span class="line">PipelineRun</span><br></pre></td></tr></table></figure><p>Task 通常在 Kubernetes Pod 中执行。</p><p>因此，Tekton 更接近 Jenkins、GitLab CI 的流水线执行层，而不是 Argo CD 的集群持续调谐层。</p><h3 id="Flux-CD"><a href="#Flux-CD" class="headerlink" title="Flux CD"></a>Flux CD</h3><p>Flux CD 与 Argo CD 属于更接近的比较对象。</p><p>两者都强调声明式状态和 Kubernetes GitOps Reconciliation。</p><p>粗略理解：</p><ul><li>Argo CD：提供以 Application 为中心的应用管理与同步模型，带有常用的 Web UI。</li><li>Flux CD：采用可组合的 Kubernetes Controllers 与 CRD，管理 Git、OCI、Helm 等来源及期望状态。</li></ul><h3 id="Azure-Pipelines-与-CircleCI"><a href="#Azure-Pipelines-与-CircleCI" class="headerlink" title="Azure Pipelines 与 CircleCI"></a>Azure Pipelines 与 CircleCI</h3><p>Azure Pipelines 使用 YAML 或 Classic Pipeline，支持 Agent、Build、Test、Artifacts 与多环境部署。</p><p>CircleCI 使用 <code>.circleci/config.yml</code>，通过 Workflow、Job 和不同类型的 Executor 编排任务。</p><p>这两类平台与 Jenkins、GitHub Actions、GitLab CI 更具有直接可比性。</p><h2 id="10-回顾以前使用的-GitHub-Enterprise-Jenkins"><a href="#10-回顾以前使用的-GitHub-Enterprise-Jenkins" class="headerlink" title="10. 回顾以前使用的 GitHub Enterprise + Jenkins"></a>10. 回顾以前使用的 GitHub Enterprise + Jenkins</h2><p>基于我目前能回忆和确认的流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">GitHub Enterprise</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Pull Request</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Jenkins Precheck / Build / Test</span><br><span class="line">    │</span><br><span class="line">    ├── Maven</span><br><span class="line">    ├── Static Checks</span><br><span class="line">    ├── Regression Jobs</span><br><span class="line">    └── Integration Tests</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Artifactory / Deployment</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Cloud Foundry (cf push)</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Environment Verification</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">Approval / Release Process</span><br></pre></td></tr></table></figure><p>其中 Jenkins Pipeline 的执行、Job 调用、回归测试和 Artifactory 集成，是有实际操作经历支撑的部分。</p><p><strong>不能据此确认的部分：</strong></p><ul><li>GitHub Actions 是否也承担了部分自动化工作。</li><li>GitHub Checks 是由 Jenkins 插件、Commit Status API 还是其他集成方式更新的。</li><li>公司其他团队是否使用了 Argo CD。</li><li>后续 Kubernetes 迁移计划是否引入 GitOps。</li></ul><p>尤其是 GitHub Enterprise 本身并不能证明 GitHub Actions 被启用。</p><p>另外，我实际接触的 Cloud Foundry <code>cf push</code> 发布流程，并不能直接推断为 Kubernetes + Argo CD 架构。</p><p>这段经历本质上更接近：</p><p><strong>GitHub Enterprise（SCM &#x2F; PR） + Jenkins（CI&#x2F;CD Orchestration） + Artifactory（Artifact Repository） + Cloud Foundry（Runtime）</strong>。</p><h2 id="11-如何选择一套-CI-CD-架构？"><a href="#11-如何选择一套-CI-CD-架构？" class="headerlink" title="11. 如何选择一套 CI&#x2F;CD 架构？"></a>11. 如何选择一套 CI&#x2F;CD 架构？</h2><p>没有任何一种方案在所有场景下都最好。</p><table><thead><tr><th>场景</th><th>值得优先评估的方案</th></tr></thead><tbody><tr><td>已大量使用 Jenkins、专用 Agent 和物理 Testbed</td><td>Jenkins</td></tr><tr><td>GitHub 代码托管、希望原生集成 PR 自动化</td><td>GitHub Actions</td></tr><tr><td>GitLab 承担 SCM、MR、CI 和 Registry</td><td>GitLab CI&#x2F;CD</td></tr><tr><td>大量使用 Azure DevOps &#x2F; Microsoft 生态</td><td>Azure Pipelines</td></tr><tr><td>希望采用托管 CI 服务</td><td>CircleCI 等</td></tr><tr><td>CI 工作负载需要 Kubernetes 原生编排</td><td>Tekton</td></tr><tr><td>Kubernetes 声明式持续部署</td><td>Argo CD &#x2F; Flux CD</td></tr><tr><td>企业已有 Jenkins，但计划引入 Kubernetes GitOps</td><td>Jenkins + Argo CD &#x2F; Flux CD</td></tr></tbody></table><p>选择时，需要分别考虑：</p><ol><li>现有源码平台与权限模型。</li><li>Runner&#x2F;Agent 的运行环境和维护成本。</li><li>是否依赖物理硬件、GPU、专用工具或不可替代的老系统。</li><li>构建制品与镜像仓库的管理方式。</li><li>生产审批、审计、安全和隔离要求。</li><li>Kubernetes 是否已成为主要部署平台。</li><li>故障排查、运维团队能力和迁移成本。</li></ol><p>尤其不应该因为使用 Kubernetes，就默认所有 Jenkins Pipeline 都应该迁移到 Tekton，也不应该因为采用 Argo CD，就认为 CI 引擎不再需要了。</p><h2 id="12-对下一阶段学习的启示"><a href="#12-对下一阶段学习的启示" class="headerlink" title="12. 对下一阶段学习的启示"></a>12. 对下一阶段学习的启示</h2><p>经过 GitLab CI 实验和这次横向对比，我对整个交付链路的职责边界有了更清楚的认识：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">GitHub / GitLab</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">Jenkins / Actions / GitLab CI / Tekton</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">Artifact / Container Registry</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">Argo CD / Flux CD / CI Deployment Job</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">Kubernetes / Cloud Foundry / VM</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">Prometheus / Grafana / Logging</span><br></pre></td></tr></table></figure><p>下一步我不打算再增加新的 CI 平台实验，而是进入 Kubernetes 阶段：</p><ul><li>Deployment、ReplicaSet、Pod。</li><li>Service 与网络访问。</li><li>Readiness &#x2F; Liveness &#x2F; Startup Probes。</li><li>Rolling Update 与 Rollback。</li><li>Pod 故障、调度和资源限制排查。</li><li>最终将已完成的 GitLab CI 与 Kubernetes 部署连接起来。</li></ul><p><strong>CI&#x2F;CD 工具的目标不是让 YAML 更复杂，而是让代码到运行环境的交付过程可靠、可重复、可审计、可恢复。</strong></p><hr><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>以下资料以官方文档为主：</p><ul><li><a href="https://docs.github.com/en/actions/tutorials/migrate-to-github-actions/manual-migrations/migrate-from-jenkins">GitHub：从 Jenkins 迁移到 GitHub Actions</a></li><li><a href="https://docs.github.com/en/enterprise-server@3.18/admin/managing-github-actions-for-your-enterprise/getting-started-with-github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-server">GitHub Enterprise Server：配置 GitHub Actions</a></li><li><a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches">GitHub：Protected Branches</a></li><li><a href="https://www.jenkins.io/doc/book/pipeline/pipeline-as-code/">Jenkins：Pipeline as Code</a></li><li><a href="https://docs.gitlab.com/ci/pipelines/">GitLab：CI&#x2F;CD Pipelines</a></li><li><a href="https://docs.gitlab.com/ci/runners/">GitLab：Runners</a></li><li><a href="https://argo-cd.readthedocs.io/en/stable/">Argo CD：Overview</a></li><li><a href="https://tekton.dev/docs/pipelines/">Tekton：Tasks and Pipelines</a></li><li><a href="https://fluxcd.io/flux/concepts/">Flux：Core Concepts</a></li><li><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/pipelines-get-started">Azure Pipelines：YAML vs Classic</a></li><li><a href="https://circleci.com/docs/reference/configuration-reference/">CircleCI：Configuration Reference</a></li></ul>]]>
    </content>
    <id>https://zhouxingyu.dev/posts/CI-CD-Tools-and-Architecture-Comparison/</id>
    <link href="https://zhouxingyu.dev/posts/CI-CD-Tools-and-Architecture-Comparison/"/>
    <published>2026-10-09T02:00:00.000Z</published>
    <summary>结合 Jenkins 企业实践和 GitLab CI 实验，比较主流 CI/CD 平台、执行器、发布模式及 Kubernetes GitOps 架构。</summary>
    <title>CI/CD 工具与架构对比：从 GitHub Enterprise + Jenkins 到 GitLab CI、GitHub Actions 与 Argo CD</title>
    <updated>2026-10-09T02:08:19.152Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhou Xingyu</name>
    </author>
    <category term="GitLab" scheme="https://zhouxingyu.dev/categories/GitLab/"/>
    <category term="CI/CD" scheme="https://zhouxingyu.dev/tags/CI-CD/"/>
    <category term="GitLab" scheme="https://zhouxingyu.dev/tags/GitLab/"/>
    <category term="WSL2" scheme="https://zhouxingyu.dev/tags/WSL2/"/>
    <category term="DevOps" scheme="https://zhouxingyu.dev/tags/DevOps/"/>
    <content>
      <![CDATA[<h1 id="WSL2-本地-GitLab-CI-CD-实验：Runner、DinD-与-Container-Registry"><a href="#WSL2-本地-GitLab-CI-CD-实验：Runner、DinD-与-Container-Registry" class="headerlink" title="WSL2 本地 GitLab CI&#x2F;CD 实验：Runner、DinD 与 Container Registry"></a>WSL2 本地 GitLab CI&#x2F;CD 实验：Runner、DinD 与 Container Registry</h1><blockquote><p><strong>实验状态：已完成（2026-10-08）</strong><br>实操范围：GitLab CE、Shell &#x2F; Docker Executor、MR Pipeline、Protected Branch、Artifact、Cache、Docker Socket Binding、Docker-in-Docker（DinD）、GitLab Container Registry、Runner 并发与 <code>resource_group</code>。<br><strong>Kubernetes Executor 仅做架构梳理，没有部署集群。</strong><br>本文是分阶段实际操作与排障记录，不是可直接投产的安全配置手册。文中的 HTTP Registry、<code>privileged</code> DinD、Docker Socket 挂载仅用于隔离的本地 Lab。</p></blockquote><p>实验后期的主要交付链路：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Merge Request</span><br><span class="line">    ↓</span><br><span class="line">Build Job ── Artifact ──┐</span><br><span class="line">Maven Build ────────────┴─ needs ─→ DinD Build</span><br><span class="line">                                      ↓</span><br><span class="line">                           GitLab Container Registry</span><br><span class="line">                                      ↓</span><br><span class="line">                            Remove → Pull → Verify</span><br></pre></td></tr></table></figure><hr><h1 id="1-实验目标"><a href="#1-实验目标" class="headerlink" title="1. 实验目标"></a>1. 实验目标</h1><p>我过去使用 Jenkins 做过 CI&#x2F;CD、自动化测试、测试环境部署和生产 SRE，但没有长期实际使用过 GitLab CI。</p><p>因此这次实验并不是单纯学习 <code>.gitlab-ci.yml</code> 的语法，而是希望通过一个本地可控环境理解：</p><ul><li>GitLab CI Pipeline 如何触发；</li><li>GitLab Runner 在整个系统中的角色；</li><li>Shell Executor 和 Docker Executor 有什么区别；</li><li>Job 之间如何传递文件；</li><li>Cache 和 Artifact 有什么区别；</li><li>Runner 如何和受限测试资源配合；</li><li>Docker Executor 为什么会引入新的网络、镜像仓库和安全问题；</li><li>CI Job 应该如何选择运行镜像；</li><li>这些机制和过去使用 Jenkins 时的经验如何对应。</li></ul><p>实验环境：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Windows</span><br><span class="line">└── WSL2 Ubuntu</span><br><span class="line">    ├── GitLab CE</span><br><span class="line">    ├── GitLab Runner</span><br><span class="line">    └── Docker Engine</span><br></pre></td></tr></table></figure><hr><h1 id="2-GitLab-CE-本地部署"><a href="#2-GitLab-CE-本地部署" class="headerlink" title="2. GitLab CE 本地部署"></a>2. GitLab CE 本地部署</h1><p>GitLab CE 运行在 Docker 中。</p><p>初期部署时简化后的 Compose 配置（Registry 配置后来在第 28 节加入）：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">gitlab:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">gitlab/gitlab-ce:latest</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">hostname:</span> <span class="string">gitlab.test</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">GITLAB_OMNIBUS_CONFIG:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        external_url &#x27;http://gitlab.test:8929&#x27;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8929:8929&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;2224:22&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./config:/etc/gitlab</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./logs:/var/log/gitlab</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/var/opt/gitlab</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">shm_size:</span> <span class="string">&quot;256m&quot;</span></span><br></pre></td></tr></table></figure><p>部署初期在 Windows 本机使用过如下 hosts 映射：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1 gitlab.test</span><br></pre></td></tr></table></figure><p>后来改为多台开发机访问同一个 GitLab，统一使用 E5 Windows 的局域网地址 <code>192.168.5.3</code>。WSL 实际解析结果也曾是 <code>gitlab.test → 192.168.5.3</code>，因此后续必须处理 Windows → WSL2 的端口转发。不要把早期的 <code>127.0.0.1</code> hosts 示例当作所有设备最终的配置。</p><p>最初使用过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.local</span><br></pre></td></tr></table></figure><p>后来改为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.test</span><br></pre></td></tr></table></figure><p>原因之一是 <code>.local</code> 通常与 mDNS 有特殊关联，本地实验使用 <code>.test</code> 更明确。</p><p>GitLab 内部实际使用的 URL 可以通过：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> gitlab \</span><br><span class="line">  gitlab-rails runner <span class="string">&#x27;puts Gitlab.config.gitlab.url&#x27;</span></span><br></pre></td></tr></table></figure><p>检查。</p><p>期望结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://gitlab.test:8929</span><br></pre></td></tr></table></figure><p>这个配置后来非常重要，因为 GitLab Runner clone repository 时会使用 GitLab 生成的 repository URL。</p><hr><h1 id="3-用户、Group-与-Project"><a href="#3-用户、Group-与-Project" class="headerlink" title="3. 用户、Group 与 Project"></a>3. 用户、Group 与 Project</h1><p>为了避免所有操作都使用 root，我创建了普通用户：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jason</span><br></pre></td></tr></table></figure><p>然后创建 Group：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">devops-lab</span><br></pre></td></tr></table></figure><p>以及实验项目：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">devops-lab/ci-playground</span><br><span class="line">devops-lab/waterfall-ci-playground</span><br></pre></td></tr></table></figure><p>root 只作为 GitLab Instance Administrator 使用。</p><p>日常代码、MR、Pipeline 等操作都使用普通用户。</p><p>这样更接近真实企业里的权限模型。</p><hr><h1 id="4-Protected-Branch-实验"><a href="#4-Protected-Branch-实验" class="headerlink" title="4. Protected Branch 实验"></a>4. Protected Branch 实验</h1><p>创建另一个 Developer 用户后，对 <code>main</code> 设置 Protected Branch。</p><p>实验结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Developer</span><br><span class="line">├── 可以 push feature branch</span><br><span class="line">├── 可以创建 Merge Request</span><br><span class="line">└── 不能直接 push main</span><br></pre></td></tr></table></figure><p>也就是说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">feature branch</span><br><span class="line">      ↓</span><br><span class="line">Merge Request</span><br><span class="line">      ↓</span><br><span class="line">CI</span><br><span class="line">      ↓</span><br><span class="line">Merge</span><br><span class="line">      ↓</span><br><span class="line">main</span><br></pre></td></tr></table></figure><p>而不能直接绕过流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Developer</span><br><span class="line">   ↓</span><br><span class="line">main</span><br></pre></td></tr></table></figure><p>Protected Environment 在当前 GitLab CE &#x2F; Free 环境不可用，因此暂时没有继续做该实验。</p><hr><h1 id="5-GitLab-Runner：Shell-Executor"><a href="#5-GitLab-Runner：Shell-Executor" class="headerlink" title="5. GitLab Runner：Shell Executor"></a>5. GitLab Runner：Shell Executor</h1><p>首先安装了 GitLab Runner，并使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Shell Executor</span><br></pre></td></tr></table></figure><p>Runner 最终作为 systemd service 运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl status gitlab-runner</span><br></pre></td></tr></table></figure><p>服务实际类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner run</span><br><span class="line">--config /etc/gitlab-runner/config.toml</span><br><span class="line">--working-directory /home/gitlab-runner</span><br><span class="line">--user gitlab-runner</span><br></pre></td></tr></table></figure><hr><h2 id="5-1-User-Mode-Runner-的问题"><a href="#5-1-User-Mode-Runner-的问题" class="headerlink" title="5.1 User Mode Runner 的问题"></a>5.1 User Mode Runner 的问题</h2><p>最开始手动运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner run</span><br></pre></td></tr></table></figure><p>导致 Runner 工作目录受到启动位置影响。</p><p>出现过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">rm: cannot remove ... gitlab_runner_env:</span><br><span class="line">Permission denied</span><br></pre></td></tr></table></figure><p>原因是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner 用户</span><br></pre></td></tr></table></figure><p>在操作：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">zxy 用户拥有的目录</span><br></pre></td></tr></table></figure><p>之后改为 systemd system-mode Runner。</p><p>这样 Runner：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">运行用户：gitlab-runner</span><br><span class="line">工作目录：/home/gitlab-runner</span><br><span class="line">配置：/etc/gitlab-runner/config.toml</span><br></pre></td></tr></table></figure><p>职责边界更清楚。</p><hr><h2 id="5-2-bash-logout-导致-Prepare-Environment-失败"><a href="#5-2-bash-logout-导致-Prepare-Environment-失败" class="headerlink" title="5.2 .bash_logout 导致 Prepare Environment 失败"></a>5.2 <code>.bash_logout</code> 导致 Prepare Environment 失败</h2><p>切换到 system-mode 后遇到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Preparing environment</span><br><span class="line"></span><br><span class="line">ERROR: Job failed:</span><br><span class="line">prepare environment: exit status 1</span><br></pre></td></tr></table></figure><p>最终定位到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/gitlab-runner/.bash_logout</span><br></pre></td></tr></table></figure><p>删除该文件后 Shell Executor 恢复正常。</p><p>这个问题让我意识到：</p><blockquote><p>Shell Executor 并不是简单执行几条 shell 命令，它还受到 Runner 用户 shell profile 的影响。</p></blockquote><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">.bashrc</span><br><span class="line">.profile</span><br><span class="line">.bash_logout</span><br></pre></td></tr></table></figure><p>都可能影响 CI Job。</p><hr><h1 id="6-Runner-Tags"><a href="#6-Runner-Tags" class="headerlink" title="6. Runner Tags"></a>6. Runner Tags</h1><p>最初 Runner 在线，但 Job 一直处于 Pending。</p><p>Runner Tag：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">shell</span><br></pre></td></tr></table></figure><p>Job 没有：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">tags:</span></span><br></pre></td></tr></table></figure><p>同时 Runner 又没有允许：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Run untagged jobs</span><br></pre></td></tr></table></figure><p>因此无法调度。</p><p>关系可以理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Job tags</span><br><span class="line">   ↓</span><br><span class="line">Runner tags</span><br><span class="line">   ↓</span><br><span class="line">匹配</span><br><span class="line">   ↓</span><br><span class="line">Runner 执行 Job</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">job:</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">shell</span></span><br></pre></td></tr></table></figure><p>只能由包含：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">shell</span><br></pre></td></tr></table></figure><p>tag 的 Runner 执行。</p><hr><h1 id="7-最小-Pipeline"><a href="#7-最小-Pipeline" class="headerlink" title="7. 最小 Pipeline"></a>7. 最小 Pipeline</h1><p>最初的 Pipeline：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">build</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">test</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">deploy</span></span><br></pre></td></tr></table></figure><p>Build：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">build-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">build</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;hello gitlab ci&quot;</span> <span class="string">&gt;</span> <span class="string">build/app.txt</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">build/</span></span><br></pre></td></tr></table></figure><p>Test：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">test-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">test</span> <span class="string">-f</span> <span class="string">build/app.txt</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">grep</span> <span class="string">&quot;hello gitlab ci&quot;</span> <span class="string">build/app.txt</span></span><br></pre></td></tr></table></figure><p>Deploy：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">build/app.txt</span></span><br></pre></td></tr></table></figure><p>这完成了最基本的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Build</span><br><span class="line"> ↓</span><br><span class="line">Test</span><br><span class="line"> ↓</span><br><span class="line">Deploy</span><br></pre></td></tr></table></figure><hr><h1 id="8-Artifact"><a href="#8-Artifact" class="headerlink" title="8. Artifact"></a>8. Artifact</h1><p>Build Job 中：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">artifacts:</span></span><br><span class="line">  <span class="attr">paths:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">build/</span></span><br></pre></td></tr></table></figure><p>会让 GitLab 保存：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">build/app.txt</span><br></pre></td></tr></table></figure><p>后续 Job 可以继续使用。</p><p>这个机制后来在 Docker Executor 实验中被进一步验证。</p><hr><h1 id="9-Docker-Executor"><a href="#9-Docker-Executor" class="headerlink" title="9. Docker Executor"></a>9. Docker Executor</h1><p>除了 Shell Runner，又注册了一个：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">devops-lab-docker-runner</span><br></pre></td></tr></table></figure><p>Executor：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker</span><br></pre></td></tr></table></figure><p>测试 Job：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-env-test:</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">image:</span> <span class="string">alpine:latest</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">whoami</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">hostname</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">pwd</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">/etc/os-release</span></span><br></pre></td></tr></table></figure><p>实际结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">whoami</span><br><span class="line">→ root</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hostname</span><br><span class="line">→ runner-aww8idcik-project-3-concurrent-0</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pwd</span><br><span class="line">→ /builds/devops-lab/waterfall-ci-playground</span><br></pre></td></tr></table></figure><p>OS：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Alpine Linux</span><br></pre></td></tr></table></figure><p>这和 Shell Executor 有非常明显的区别。</p><hr><h1 id="10-Shell-Executor-vs-Docker-Executor"><a href="#10-Shell-Executor-vs-Docker-Executor" class="headerlink" title="10. Shell Executor vs Docker Executor"></a>10. Shell Executor vs Docker Executor</h1><table><thead><tr><th>项目</th><th>Shell Executor</th><th>Docker Executor</th></tr></thead><tbody><tr><td>执行位置</td><td>Runner Host</td><td>临时 Container</td></tr><tr><td>OS</td><td>Host OS</td><td>Image 决定</td></tr><tr><td>用户</td><td>gitlab-runner</td><td>Image 决定</td></tr><tr><td>工具链</td><td>Host 安装</td><td>Image 提供</td></tr><tr><td>文件系统隔离</td><td>较弱</td><td>较强</td></tr><tr><td>Job 结束</td><td>环境仍存在</td><td>Container 销毁</td></tr><tr><td>依赖环境</td><td>Host</td><td>Docker + Registry</td></tr></tbody></table><p>Docker Executor 最大的一个优点是：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">python:3.13</span></span><br></pre></td></tr></table></figure><p>或者：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br></pre></td></tr></table></figure><p>Job 可以自己定义工具链。</p><p>不需要在 Runner Host 上安装所有版本的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Java</span><br><span class="line">Python</span><br><span class="line">Node.js</span><br><span class="line">Maven</span><br><span class="line">Gradle</span><br><span class="line">...</span><br></pre></td></tr></table></figure><hr><h1 id="11-Docker-Executor-网络问题"><a href="#11-Docker-Executor-网络问题" class="headerlink" title="11. Docker Executor 网络问题"></a>11. Docker Executor 网络问题</h1><p>Docker Executor 第一次 clone repository 时出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Failed to connect to gitlab.test:8929</span><br></pre></td></tr></table></figure><p>Shell Executor 可以访问：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.test</span><br></pre></td></tr></table></figure><p>但 Container 不能。</p><p>原因是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Host 网络命名空间</span><br><span class="line">≠</span><br><span class="line">Container 网络命名空间</span><br></pre></td></tr></table></figure><p>对于 Container：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1</span><br></pre></td></tr></table></figure><p>指向的是 Container 自己，而不是 WSL Host。</p><p>Runner 配置中增加：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">extra_hosts</span> = [</span><br><span class="line">  <span class="string">&quot;gitlab.test:host-gateway&quot;</span></span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>之后 Job Container 可以访问：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.test</span><br></pre></td></tr></table></figure><p>这也是 Docker Executor 相比 Shell Executor 很重要的区别：</p><blockquote><p>Host 能访问某个服务，不代表 CI Container 也能访问。</p></blockquote><p>后续如果使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">GitLab</span><br><span class="line">Registry</span><br><span class="line">Artifactory</span><br><span class="line">Internal API</span><br><span class="line">Test Environment</span><br></pre></td></tr></table></figure><p>都必须站在 Job Container 的网络视角重新检查。</p><hr><h1 id="12-Docker-Registry-网络波动"><a href="#12-Docker-Registry-网络波动" class="headerlink" title="12. Docker Registry 网络波动"></a>12. Docker Registry 网络波动</h1><p>Docker Executor 还遇到过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Pulling docker image alpine:latest</span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net/http: timeout awaiting response headers</span><br></pre></td></tr></table></figure><p>Retry 后成功。</p><p>Maven Image 也出现过长时间卡在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Pulling docker image</span><br><span class="line">maven:3.9-eclipse-temurin-21</span><br></pre></td></tr></table></figure><p>这说明 Docker Executor 又增加了一层依赖：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">GitLab Runner</span><br><span class="line">   ↓</span><br><span class="line">Docker daemon</span><br><span class="line">   ↓</span><br><span class="line">Container Registry</span><br></pre></td></tr></table></figure><p>所以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Pipeline 失败</span><br></pre></td></tr></table></figure><p>不一定意味着：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">script 失败</span><br></pre></td></tr></table></figure><p>也可能发生在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Preparing Docker Executor</span><br></pre></td></tr></table></figure><p>阶段。</p><p>常见排查方向：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Registry</span><br><span class="line">DNS</span><br><span class="line">Proxy</span><br><span class="line">Network</span><br><span class="line">Authentication</span><br><span class="line">Pull Policy</span><br></pre></td></tr></table></figure><hr><h1 id="13-Docker-Job-文件隔离"><a href="#13-Docker-Job-文件隔离" class="headerlink" title="13. Docker Job 文件隔离"></a>13. Docker Job 文件隔离</h1><p>为了验证不同 Docker Job 是否共享文件系统，做了如下实验。</p><p>Job A：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-write:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">alpine</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;hello&quot;</span> <span class="string">&gt;</span> <span class="string">/tmp/docker-test</span></span><br></pre></td></tr></table></figure><p>Job B：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-read:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">alpine</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">/tmp/docker-test</span></span><br></pre></td></tr></table></figure><p>结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cat: can&#x27;t open &#x27;/tmp/docker-test&#x27;:</span><br><span class="line">No such file or directory</span><br></pre></td></tr></table></figure><p>原因：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Job A</span><br><span class="line">→ Container A</span><br><span class="line"></span><br><span class="line">Job B</span><br><span class="line">→ Container B</span><br></pre></td></tr></table></figure><p>两个 Job 并不是同一个 Container。</p><hr><h1 id="14-使用-Artifact-跨-Job-传递文件"><a href="#14-使用-Artifact-跨-Job-传递文件" class="headerlink" title="14. 使用 Artifact 跨 Job 传递文件"></a>14. 使用 Artifact 跨 Job 传递文件</h1><p>将 Job A 修改为：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-write:</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">output</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;hello&quot;</span> <span class="string">&gt;</span> <span class="string">output/result.txt</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">output/</span></span><br></pre></td></tr></table></figure><p>Job B：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-read:</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">output/result.txt</span></span><br></pre></td></tr></table></figure><p>之后成功。</p><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Job Container A</span><br><span class="line">      ↓</span><br><span class="line">   Artifact</span><br><span class="line">      ↓</span><br><span class="line">GitLab / Runner</span><br><span class="line">      ↓</span><br><span class="line">Job Container B</span><br></pre></td></tr></table></figure><p>而不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Container A filesystem</span><br><span class="line">      ↓</span><br><span class="line">Container B</span><br></pre></td></tr></table></figure><hr><h1 id="15-Cache"><a href="#15-Cache" class="headerlink" title="15. Cache"></a>15. Cache</h1><p>接下来验证 Cache。</p><p>测试配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">cache-build:</span></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">image:</span> <span class="string">alpine</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">cache:</span></span><br><span class="line">    <span class="attr">key:</span> <span class="string">demo-cache</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">.cache/</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">.cache</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      if [ -f .cache/dependency.txt ]; then</span></span><br><span class="line"><span class="string">        echo &quot;Cache HIT&quot;</span></span><br><span class="line"><span class="string">        cat .cache/dependency.txt</span></span><br><span class="line"><span class="string">      else</span></span><br><span class="line"><span class="string">        echo &quot;Cache MISS&quot;</span></span><br><span class="line"><span class="string">        echo &quot;downloaded dependency&quot; &gt; .cache/dependency.txt</span></span><br><span class="line"><span class="string">      fi</span></span><br></pre></td></tr></table></figure><p>第一次：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cache MISS</span><br></pre></td></tr></table></figure><p>后续 Pipeline：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Cache HIT</span><br><span class="line">downloaded dependency</span><br></pre></td></tr></table></figure><p>Runner 日志：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Creating cache demo-cache-protected...</span><br><span class="line"></span><br><span class="line">.cache/: found 2 matching artifact files and directories</span><br><span class="line"></span><br><span class="line">Primary cache does not exist remotely,</span><br><span class="line">uploading existing archive</span><br><span class="line"></span><br><span class="line">No URL provided,</span><br><span class="line">cache will not be uploaded to shared cache server.</span><br><span class="line"></span><br><span class="line">Cache will be stored only locally.</span><br><span class="line"></span><br><span class="line">Created cache</span><br></pre></td></tr></table></figure><hr><h1 id="16-Local-Cache"><a href="#16-Local-Cache" class="headerlink" title="16. Local Cache"></a>16. Local Cache</h1><p>这里的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">No URL provided</span><br></pre></td></tr></table></figure><p>并不是错误。</p><p>当前没有配置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">S3</span><br><span class="line">GCS</span><br><span class="line">MinIO</span><br></pre></td></tr></table></figure><p>等 Shared Cache Backend。</p><p>所以 Cache 保存在当前 Runner 本地。</p><p>可以把 cache 看成一种：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Disposable</span><br><span class="line">Shared</span><br><span class="line">Mutable</span><br><span class="line">Performance Optimization</span><br></pre></td></tr></table></figure><p>即：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Cache 正常</span><br><span class="line">→ Build 更快</span><br><span class="line"></span><br><span class="line">Cache Miss</span><br><span class="line">→ 重新生成</span><br><span class="line">→ Build 变慢</span><br><span class="line"></span><br><span class="line">Cache 损坏</span><br><span class="line">→ 清理 / 重新生成</span><br><span class="line">→ Build 应该仍然能够恢复</span><br></pre></td></tr></table></figure><p>因此：</p><blockquote><p>Cache 不应该成为构建正确性的唯一来源。</p></blockquote><hr><h2 id="16-1-并发-Job-与-Cache"><a href="#16-1-并发-Job-与-Cache" class="headerlink" title="16.1 并发 Job 与 Cache"></a>16.1 并发 Job 与 Cache</h2><p>如果多个 Job 使用同一个 cache key：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">cache:</span></span><br><span class="line">  <span class="attr">key:</span> <span class="string">maven-cache</span></span><br></pre></td></tr></table></figure><p>同时又都允许：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pull + push</span><br></pre></td></tr></table></figure><p>那么多个 Job 可能同时恢复 cache，再分别写入并上传。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Job A             Job B</span><br><span class="line">  │                 │</span><br><span class="line">restore           restore</span><br><span class="line">  │                 │</span><br><span class="line">modify            modify</span><br><span class="line">  │                 │</span><br><span class="line">save              save</span><br><span class="line">  └──────┬──────────┘</span><br><span class="line">         ↓</span><br><span class="line">     same cache</span><br></pre></td></tr></table></figure><p>因此共享 cache 本质上属于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Shared Mutable State</span><br></pre></td></tr></table></figure><p>在大规模并发环境中，需要考虑：</p><ul><li>Cache key 设计；</li><li>哪些 Job 可以写 cache；</li><li>哪些 Job 只读取 cache；</li><li>Cache 损坏后的恢复方式；</li><li>Local Cache 与 Shared Cache 的故障域。</li></ul><p>当前实验暂时不继续深入这一部分。</p><hr><h1 id="17-Artifact-vs-Cache"><a href="#17-Artifact-vs-Cache" class="headerlink" title="17. Artifact vs Cache"></a>17. Artifact vs Cache</h1><p>目前实验后，我对两者的理解是：</p><h2 id="Artifact"><a href="#Artifact" class="headerlink" title="Artifact"></a>Artifact</h2><p>目的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">传递 Pipeline 的构建结果</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">JAR</span><br><span class="line">ZIP</span><br><span class="line">Binary</span><br><span class="line">Test Report</span><br><span class="line">Build Output</span><br></pre></td></tr></table></figure><p>典型流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Build</span><br><span class="line"> ↓</span><br><span class="line">Artifact</span><br><span class="line"> ↓</span><br><span class="line">Test / Deploy</span><br></pre></td></tr></table></figure><p>Artifact 更接近：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Pipeline Output</span><br><span class="line">Versioned by Job / Pipeline</span><br><span class="line">Correctness Relevant</span><br></pre></td></tr></table></figure><hr><h2 id="Cache"><a href="#Cache" class="headerlink" title="Cache"></a>Cache</h2><p>目的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">减少重复下载或重复计算</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">.m2/repository</span><br><span class="line">npm cache</span><br><span class="line">pip cache</span><br><span class="line">Gradle cache</span><br></pre></td></tr></table></figure><p>即使 Cache 被删除：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Build 仍然应该能够完成</span><br></pre></td></tr></table></figure><p>只是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">速度变慢</span><br></pre></td></tr></table></figure><p>因此一个重要原则是：</p><blockquote><p>Cache 应该是可重新生成的。</p></blockquote><hr><h1 id="18-Maven-Cache"><a href="#18-Maven-Cache" class="headerlink" title="18. Maven Cache"></a>18. Maven Cache</h1><p>为了验证真实 Dependency Cache，创建了一个最小 Maven 项目。</p><p>CI：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">maven-build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">variables:</span></span><br><span class="line">    <span class="attr">MAVEN_OPTS:</span> <span class="string">&gt;</span></span><br><span class="line"><span class="string">      -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">  <span class="attr">cache:</span></span><br><span class="line">    <span class="attr">key:</span> <span class="string">maven-cache</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">.m2/repository/</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== Maven cache before build ===&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">du</span> <span class="string">-sh</span> <span class="string">.m2/repository</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">||</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">clean</span> <span class="string">package</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== Maven cache after build ===&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">du</span> <span class="string">-sh</span> <span class="string">.m2/repository</span></span><br></pre></td></tr></table></figure><p>第一次运行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Maven 下载 dependencies</span><br></pre></td></tr></table></figure><p>Runner 保存：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.m2/repository</span><br></pre></td></tr></table></figure><p>第二次运行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Maven 没有重新下载这些 dependencies</span><br></pre></td></tr></table></figure><p>说明 Maven Cache 生效。</p><p>完整流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Pipeline #1</span><br><span class="line">     ↓</span><br><span class="line">.m2 empty</span><br><span class="line">     ↓</span><br><span class="line">Maven download</span><br><span class="line">     ↓</span><br><span class="line">Runner cache</span><br><span class="line"></span><br><span class="line">Pipeline #2</span><br><span class="line">     ↓</span><br><span class="line">Restore cache</span><br><span class="line">     ↓</span><br><span class="line">.m2 already exists</span><br><span class="line">     ↓</span><br><span class="line">Maven reuse dependencies</span><br></pre></td></tr></table></figure><hr><h1 id="19-MR-Pipeline"><a href="#19-MR-Pipeline" class="headerlink" title="19. MR Pipeline"></a>19. MR Pipeline</h1><p>最开始配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">workflow:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_BRANCH&#x27;</span></span><br></pre></td></tr></table></figure><p>这样会产生：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Branch Pipeline</span><br><span class="line">+</span><br><span class="line">Merge Request Pipeline</span><br></pre></td></tr></table></figure><p>也就是 feature branch push 一次，创建 MR 后又跑一次。</p><p>实际需求并不是每一个开发过程中的 push 都执行完整 CI。</p><p>所以最终改成：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">workflow:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">when:</span> <span class="string">never</span></span><br></pre></td></tr></table></figure><p>最终效果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Feature Push</span><br><span class="line">→ No Pipeline</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Create / Update MR</span><br><span class="line">→ CI Pipeline</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Merge main</span><br><span class="line">→ main Pipeline</span><br></pre></td></tr></table></figure><p>这更符合希望模拟的开发流程。</p><hr><h1 id="20-Waterfall-CI-Model"><a href="#20-Waterfall-CI-Model" class="headerlink" title="20. Waterfall CI Model"></a>20. Waterfall CI Model</h1><p>另一个项目：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">waterfall-ci-playground</span><br></pre></td></tr></table></figure><p>用于模拟以前接触过的一种更偏传统产品研发的流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Feature</span><br><span class="line"> ↓</span><br><span class="line">PR / MR</span><br><span class="line"> ↓</span><br><span class="line">Build</span><br><span class="line"> ↓</span><br><span class="line">Deploy Testbed</span><br><span class="line"> ↓</span><br><span class="line">Integration Test</span><br><span class="line"> ↓</span><br><span class="line">允许 Merge</span><br></pre></td></tr></table></figure><p>Merge 后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">不会立即发布正式版本</span><br></pre></td></tr></table></figure><p>而是在某个 milestone：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">创建正式 Release</span><br></pre></td></tr></table></figure><p>因此 Workflow：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">workflow:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_TAG&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">when:</span> <span class="string">never</span></span><br></pre></td></tr></table></figure><p>也就是说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Feature Push</span><br><span class="line">→ No Pipeline</span><br><span class="line"></span><br><span class="line">MR</span><br><span class="line">→ Build + Integration</span><br><span class="line"></span><br><span class="line">Merge</span><br><span class="line">→ No Release</span><br><span class="line"></span><br><span class="line">Tag</span><br><span class="line">→ Release</span><br></pre></td></tr></table></figure><hr><h1 id="21-有限-Testbed-与-resource-group"><a href="#21-有限-Testbed-与-resource-group" class="headerlink" title="21. 有限 Testbed 与 resource_group"></a>21. 有限 Testbed 与 resource_group</h1><p>实际硬件测试中，一个非常重要的问题是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Testbed 数量有限</span><br></pre></td></tr></table></figure><p>两个 Pipeline 如果同时部署同一台机器：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Pipeline A Deploy</span><br><span class="line">        ↓</span><br><span class="line">Pipeline B Deploy</span><br><span class="line">        ↓</span><br><span class="line">Pipeline A Test</span><br></pre></td></tr></table></figure><p>Pipeline A 测到的已经可能不是自己的代码。</p><p>GitLab 可以使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">resource_group:</span> <span class="string">testbed-01</span></span><br></pre></td></tr></table></figure><p>限制：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">同一个 resource_group</span><br><span class="line">一次只允许一个 Job 执行</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Pipeline A</span><br><span class="line">Deploy + Test</span><br><span class="line">      ↓</span><br><span class="line">release resource</span><br><span class="line">      ↓</span><br><span class="line">Pipeline B</span><br><span class="line">Deploy + Test</span><br></pre></td></tr></table></figure><p>这里还有一个重要设计点：</p><blockquote><p>对于本次模拟的独占 Testbed，Deploy 和 Test 最好放在同一个占有 <code>resource_group</code> 的 Job 内。</p></blockquote><p>否则：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Deploy Job</span><br><span class="line">→ resource released</span><br><span class="line"></span><br><span class="line">Test Job</span><br><span class="line">→ later</span><br></pre></td></tr></table></figure><p>中间可能被其他 Pipeline 抢走 Testbed。</p><hr><h1 id="22-Docker-Image-与-CI-Job-Image"><a href="#22-Docker-Image-与-CI-Job-Image" class="headerlink" title="22. Docker Image 与 CI Job Image"></a>22. Docker Image 与 CI Job Image</h1><p>在开始 Docker Image Build 之前，需要先明确：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">docker:cli</span></span><br></pre></td></tr></table></figure><p>并不等于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker exec -it &lt;container&gt; sh</span><br></pre></td></tr></table></figure><p><code>docker exec</code> 的含义是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">已经存在的 Container</span><br><span class="line">      ↓</span><br><span class="line">进入其中执行命令</span><br></pre></td></tr></table></figure><p>而 GitLab CI 中：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">docker:cli</span></span><br></pre></td></tr></table></figure><p>表示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">GitLab Runner</span><br><span class="line">      ↓</span><br><span class="line">根据 docker:cli Image</span><br><span class="line">      ↓</span><br><span class="line">创建新的 Job Container</span><br><span class="line">      ↓</span><br><span class="line">执行 script</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">docker:cli image</span><br><span class="line">       ↓</span><br><span class="line">┌─────────────────────────┐</span><br><span class="line">│ CI Job Container        │</span><br><span class="line">│                         │</span><br><span class="line">│ /bin/sh                 │</span><br><span class="line">│ docker CLI              │</span><br><span class="line">│ basic utilities         │</span><br><span class="line">└─────────────────────────┘</span><br></pre></td></tr></table></figure><p>因此：</p><blockquote><p>CI Job 的 <code>image:</code> 可以理解为这个 Job 的“临时工作机模板”。</p></blockquote><hr><h1 id="23-如何选择-CI-Job-Image"><a href="#23-如何选择-CI-Job-Image" class="headerlink" title="23. 如何选择 CI Job Image"></a>23. 如何选择 CI Job Image</h1><p>选择 Image 时，不需要背大量 Image 名字。</p><p>更合理的思路是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">这个 Job 要执行什么命令？</span><br><span class="line">        ↓</span><br><span class="line">需要哪些 Runtime / Tools？</span><br><span class="line">        ↓</span><br><span class="line">寻找最接近需求的可信 Image</span><br><span class="line">        ↓</span><br><span class="line">选择合适的版本 Tag</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mvn + JDK</span><br><span class="line">→ maven image</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Python</span><br><span class="line">→ python image</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm / node</span><br><span class="line">→ node image</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Docker CLI</span><br><span class="line">→ docker:cli</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">简单 shell 工具</span><br><span class="line">→ alpine</span><br></pre></td></tr></table></figure><hr><h2 id="23-1-Image-可以包含多个工具"><a href="#23-1-Image-可以包含多个工具" class="headerlink" title="23.1 Image 可以包含多个工具"></a>23.1 Image 可以包含多个工具</h2><p>Image 并不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">JDK</span><br><span class="line">Maven</span><br><span class="line">Node</span><br><span class="line">Docker</span><br></pre></td></tr></table></figure><p>只能四选一。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">maven:3.9-eclipse-temurin-21</span><br></pre></td></tr></table></figure><p>本身就是一个组合环境：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Maven 3.9</span><br><span class="line">+</span><br><span class="line">Eclipse Temurin JDK 21</span><br></pre></td></tr></table></figure><p>因此 Java Maven Build 可以直接：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">java-build:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">clean</span> <span class="string">package</span></span><br></pre></td></tr></table></figure><hr><h2 id="23-2-Spring-Boot-通常不是-CI-环境中的安装工具"><a href="#23-2-Spring-Boot-通常不是-CI-环境中的安装工具" class="headerlink" title="23.2 Spring Boot 通常不是 CI 环境中的安装工具"></a>23.2 Spring Boot 通常不是 CI 环境中的安装工具</h2><p>对于 Spring Boot 项目：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">JDK</span><br><span class="line">Maven</span><br><span class="line">Spring Boot</span><br></pre></td></tr></table></figure><p>并不是需要在 Job Container 中分别安装三个软件。</p><p>通常结构是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Job Container</span><br><span class="line">├── JDK</span><br><span class="line">├── Maven</span><br><span class="line">└── checked-out repository</span><br><span class="line">    ├── pom.xml</span><br><span class="line">    └── src/</span><br></pre></td></tr></table></figure><p>Spring Boot：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">由 pom.xml 定义</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Maven</span><br><span class="line">  ↓</span><br><span class="line">读取 pom.xml</span><br><span class="line">  ↓</span><br><span class="line">下载 Spring Boot dependencies</span><br><span class="line">  ↓</span><br><span class="line">Compile / Test / Package</span><br></pre></td></tr></table></figure><p>因此最普通的 Spring Boot Job 可能只有：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">java-build:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">clean</span> <span class="string">test</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">package</span></span><br></pre></td></tr></table></figure><hr><h2 id="23-3-一个-Job-同时需要很多工具怎么办"><a href="#23-3-一个-Job-同时需要很多工具怎么办" class="headerlink" title="23.3 一个 Job 同时需要很多工具怎么办"></a>23.3 一个 Job 同时需要很多工具怎么办</h2><p>如果只缺少少量附加工具，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl</span><br><span class="line">jq</span><br></pre></td></tr></table></figure><p>可以临时安装：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">maven:3.9-eclipse-temurin-21</span></span><br><span class="line"></span><br><span class="line"><span class="attr">before_script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">apt-get</span> <span class="string">update</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">apt-get</span> <span class="string">install</span> <span class="string">-y</span> <span class="string">curl</span> <span class="string">jq</span></span><br><span class="line"></span><br><span class="line"><span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">mvn</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><p>如果团队长期固定需要：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">JDK</span><br><span class="line">Maven</span><br><span class="line">Docker CLI</span><br><span class="line">kubectl</span><br><span class="line">helm</span><br><span class="line">curl</span><br><span class="line">jq</span><br></pre></td></tr></table></figure><p>则更适合维护自己的 CI Image：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">registry.company.com/ci/java-build:2026.09</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> maven:<span class="number">3.9</span>-eclipse-temurin-<span class="number">21</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; apt-get install -y curl jq</span></span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">registry.company.com/ci/java-build:2026.09</span></span><br></pre></td></tr></table></figure><p>这种方式与过去 Jenkins Agent 预装工具的思路很相似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Jenkins Agent</span><br><span class="line">= 长期机器上预装工具</span><br><span class="line"></span><br><span class="line">Docker Executor</span><br><span class="line">= 把工具环境固化成 Image</span><br><span class="line">  每个 Job 临时创建</span><br></pre></td></tr></table></figure><hr><h2 id="23-4-不要把所有职责都塞进一个-Job"><a href="#23-4-不要把所有职责都塞进一个-Job" class="headerlink" title="23.4 不要把所有职责都塞进一个 Job"></a>23.4 不要把所有职责都塞进一个 Job</h2><p>另一种方法是拆 Job。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Maven Build</span><br><span class="line">→ Docker Build</span><br><span class="line">→ Kubernetes Deploy</span><br></pre></td></tr></table></figure><p>可以拆成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Job A</span><br><span class="line">Maven + JDK</span><br><span class="line">→ Build JAR</span><br><span class="line">      │</span><br><span class="line">      │ Artifact</span><br><span class="line">      ▼</span><br><span class="line">Job B</span><br><span class="line">Docker CLI</span><br><span class="line">→ Build Container Image</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">Job C</span><br><span class="line">kubectl / Helm</span><br><span class="line">→ Deploy</span><br></pre></td></tr></table></figure><p>所以可以使用这样的判断：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">少量附加工具</span><br><span class="line">→ Job 中临时安装</span><br><span class="line"></span><br><span class="line">稳定且反复使用的组合工具</span><br><span class="line">→ 自定义 CI Image</span><br><span class="line"></span><br><span class="line">职责明显不同</span><br><span class="line">→ 拆成多个 Job</span><br></pre></td></tr></table></figure><hr><h2 id="23-5-本地检查陌生-Image"><a href="#23-5-本地检查陌生-Image" class="headerlink" title="23.5 本地检查陌生 Image"></a>23.5 本地检查陌生 Image</h2><p>如果不确定某个 Image 里面有什么，可以先本地检查。</p><p>例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> docker:cli docker version</span><br></pre></td></tr></table></figure><p>查看 OS：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> docker:cli <span class="built_in">cat</span> /etc/os-release</span><br></pre></td></tr></table></figure><p>进入 shell：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> -it \</span><br><span class="line">  --entrypoint sh \</span><br><span class="line">  docker:cli</span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">which</span> docker</span><br><span class="line"><span class="built_in">which</span> sh</span><br><span class="line"><span class="built_in">cat</span> /etc/os-release</span><br></pre></td></tr></table></figure><p>Maven Image：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> \</span><br><span class="line">  maven:3.9-eclipse-temurin-21 \</span><br><span class="line">  mvn --version</span><br></pre></td></tr></table></figure><p>因此 Image 选择不需要完全依赖名称猜测。</p><hr><h1 id="24-Docker-Socket-Binding-——-已验证"><a href="#24-Docker-Socket-Binding-——-已验证" class="headerlink" title="24. Docker Socket Binding —— 已验证"></a>24. Docker Socket Binding —— 已验证</h1><p>为了让 Docker Executor 中的 Job 使用 Docker CLI，Runner 配置增加：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[runners.docker]</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes</span> = [</span><br><span class="line">  <span class="string">&quot;/cache&quot;</span>,</span><br><span class="line">  <span class="string">&quot;/var/run/docker.sock:/var/run/docker.sock&quot;</span></span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>然后使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-socket-test:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">tags:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">image:</span> <span class="string">docker:cli</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== Docker version ===&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">version</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== Docker info ===&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">info</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== Visible containers ===&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">ps</span></span><br></pre></td></tr></table></figure><p>实验成功。</p><hr><h2 id="24-1-docker-version"><a href="#24-1-docker-version" class="headerlink" title="24.1 docker version"></a>24.1 <code>docker version</code></h2><p>实际输出同时存在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Client:</span><br><span class="line"> Version: 29.8.1</span><br></pre></td></tr></table></figure><p>以及：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Server: Docker Engine - Community</span><br><span class="line"> Version: 29.8.0</span><br></pre></td></tr></table></figure><p>其中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Client</span><br></pre></td></tr></table></figure><p>来自：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker:cli Job Container</span><br></pre></td></tr></table></figure><p>而：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Server</span><br></pre></td></tr></table></figure><p>来自：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WSL Host Docker daemon</span><br></pre></td></tr></table></figure><p>两者版本不完全相同，但 Docker API 都是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1.56</span><br></pre></td></tr></table></figure><p>因此能够正常通信。</p><hr><h2 id="24-2-docker-info"><a href="#24-2-docker-info" class="headerlink" title="24.2 docker info"></a>24.2 <code>docker info</code></h2><p>更加明确的证据来自：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Operating System: Ubuntu 26.04.1 LTS</span><br><span class="line">Kernel Version: 6.18.33.2-microsoft-standard-WSL2</span><br><span class="line">Name: DESKTOP-P5CGV6I</span><br><span class="line">CPUs: 36</span><br><span class="line">Total Memory: 31.29GiB</span><br><span class="line">Docker Root Dir: /var/lib/docker</span><br></pre></td></tr></table></figure><p>当前 CI Job 使用的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker:cli</span><br></pre></td></tr></table></figure><p>Image。</p><p>但 <code>docker info</code> 返回的却是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WSL2 Ubuntu Host</span><br></pre></td></tr></table></figure><p>的信息。</p><p>这证明：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Job Container</span><br><span class="line">   │</span><br><span class="line">   │ Docker CLI</span><br><span class="line">   │</span><br><span class="line">   │ /var/run/docker.sock</span><br><span class="line">   ▼</span><br><span class="line">WSL Host Docker daemon</span><br></pre></td></tr></table></figure><hr><h2 id="24-3-Job-可以看到-Host-Container"><a href="#24-3-Job-可以看到-Host-Container" class="headerlink" title="24.3 Job 可以看到 Host Container"></a>24.3 Job 可以看到 Host Container</h2><p><code>docker ps</code> 实际看到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">runner-aww8idcik-project-3-concurrent-0-...-build</span><br><span class="line">gitlab</span><br></pre></td></tr></table></figure><p>其中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab</span><br></pre></td></tr></table></figure><p>是宿主机长期运行的 GitLab Container。</p><p>另外：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">runner-...-build</span><br></pre></td></tr></table></figure><p>正是当前正在运行这个 CI Job 的 Container。</p><p>也就是说：</p><blockquote><p>Job Container 通过 Host Docker daemon，甚至可以看到“自己”。</p></blockquote><hr><h2 id="24-4-这不是-Docker-in-Docker"><a href="#24-4-这不是-Docker-in-Docker" class="headerlink" title="24.4 这不是 Docker-in-Docker"></a>24.4 这不是 Docker-in-Docker</h2><p>当前结构并不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Host Docker</span><br><span class="line">└── Job Container</span><br><span class="line">    └── Docker daemon</span><br><span class="line">        └── Container</span><br></pre></td></tr></table></figure><p>而更接近：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">WSL Host Docker daemon</span><br><span class="line">│</span><br><span class="line">├── GitLab</span><br><span class="line">│</span><br><span class="line">├── CI Job Container</span><br><span class="line">│    └── docker CLI</span><br><span class="line">│</span><br><span class="line">└── CI 创建的其他 Container</span><br></pre></td></tr></table></figure><p>Job Container 中只有：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Docker CLI</span><br></pre></td></tr></table></figure><p>它通过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/var/run/docker.sock</span><br></pre></td></tr></table></figure><p>直接向宿主机 Docker daemon 发命令。</p><p>因此这种结构也常被理解为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Sibling Containers</span><br></pre></td></tr></table></figure><p>而不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Nested Containers</span><br></pre></td></tr></table></figure><hr><h1 id="25-Docker-Socket-的安全边界"><a href="#25-Docker-Socket-的安全边界" class="headerlink" title="25. Docker Socket 的安全边界"></a>25. Docker Socket 的安全边界</h1><p>当前 Runner 没有要求：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">privileged</span> = <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>但：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/var/run/docker.sock</span><br></pre></td></tr></table></figure><p>本身已经提供了很高权限。</p><p>如果 Job 可以控制 Host Docker daemon，那么它理论上可以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">创建 Container</span><br><span class="line">删除 Container</span><br><span class="line">查看其他 Container</span><br><span class="line">管理 Docker Volume</span><br><span class="line">管理 Docker Network</span><br><span class="line">挂载 Host Directory</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Docker Job Container isolation</span><br><span class="line">           ↓</span><br><span class="line">mount docker.sock</span><br><span class="line">           ↓</span><br><span class="line">Security boundary significantly reduced</span><br></pre></td></tr></table></figure><p>这也是为什么：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">privileged = false</span><br></pre></td></tr></table></figure><p>并不意味着当前 Job 就是低权限的。</p><p>第 27 节通过 DinD 对比：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Socket Binding</span><br><span class="line">vs</span><br><span class="line">Docker-in-Docker</span><br></pre></td></tr></table></figure><p>理解二者不同的安全和资源边界。</p><hr><h1 id="26-Docker-Socket-Binding：实际构建-Docker-Image"><a href="#26-Docker-Socket-Binding：实际构建-Docker-Image" class="headerlink" title="26. Docker Socket Binding：实际构建 Docker Image"></a>26. Docker Socket Binding：实际构建 Docker Image</h1><p>第 24～25 节已经证明 CI 中的 Docker CLI 能经由挂载的 <code>/var/run/docker.sock</code> 访问 WSL 宿主机 Docker Daemon。这次进一步完成真正的 Image Build。</p><p>实验用 Dockerfile：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> alpine:<span class="number">3.24</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> build/app.txt /app/app.txt</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">&quot;cat&quot;</span>, <span class="string">&quot;/app/app.txt&quot;</span>]</span></span><br></pre></td></tr></table></figure><p><code>build-job</code> 生成 <code>build/app.txt</code> 并保存为 Artifact，镜像构建 Job 通过 <code>needs</code> 获取该文件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-image-build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">image</span></span><br><span class="line">  <span class="attr">tags:</span> [<span class="string">docker</span>]</span><br><span class="line">  <span class="attr">image:</span> <span class="string">docker:cli</span></span><br><span class="line">  <span class="attr">needs:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">job:</span> <span class="string">build-job</span></span><br><span class="line">      <span class="attr">artifacts:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">build/app.txt</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">build</span> <span class="string">-t</span> <span class="string">waterfall-demo:$CI_COMMIT_SHORT_SHA</span> <span class="string">.</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">image</span> <span class="string">inspect</span> <span class="string">waterfall-demo:$CI_COMMIT_SHORT_SHA</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">run</span> <span class="string">--rm</span> <span class="string">waterfall-demo:$CI_COMMIT_SHORT_SHA</span></span><br></pre></td></tr></table></figure><p>这段脚本只展示实验所需的关键 Job；项目还包含 Maven 等其他 Job，实际提交需要保持 <code>.gitlab-ci.yml</code> 中对应的 Stage 和 Workflow 一致。</p><p>第一次 <code>docker build</code> 在访问 Docker Hub Token 服务时失败：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">failed to fetch anonymous token</span><br><span class="line">lookup auth.docker.io on 172.19.96.1:53: i/o timeout</span><br></pre></td></tr></table></figure><p>随后在 WSL 宿主机手动拉取 <code>alpine:3.24</code>，重新运行该 Job 后成功。先前已证明 Job 使用的是宿主机 Daemon，因此它能利用宿主机已有的基础镜像。</p><p>通过后，<code>docker image ls waterfall-demo</code> 可以在 WSL 宿主机看到镜像；这是 Socket Binding 与下一节 DinD 镜像存储行为的关键区别。</p><hr><h1 id="27-Docker-in-Docker：独立-Docker-Daemon"><a href="#27-Docker-in-Docker：独立-Docker-Daemon" class="headerlink" title="27. Docker-in-Docker：独立 Docker Daemon"></a>27. Docker-in-Docker：独立 Docker Daemon</h1><h2 id="27-1-为-DinD-单独注册-Group-Runner"><a href="#27-1-为-DinD-单独注册-Group-Runner" class="headerlink" title="27.1 为 DinD 单独注册 Group Runner"></a>27.1 为 DinD 单独注册 Group Runner</h2><p>我没有直接在原 Docker Runner 上开启 <code>privileged</code>，而是在 <code>devops-lab</code> Group 下新建 Runner：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Runner name: devops-lab-dind-runner</span><br><span class="line">Scope: Group Runner</span><br><span class="line">Executor: docker</span><br><span class="line">Tag: dind</span><br><span class="line">Run untagged jobs: disabled</span><br></pre></td></tr></table></figure><p>注册时容易误把 <code>dind</code> 当成 Executor。实际上 <code>gitlab-runner register</code> 提示 <code>Enter an executor</code> 时仍应输入 <code>docker</code>；<code>dind</code> 是选用的 Runner Tag 和 CI 服务模式，并不是 Executor 类型。</p><p>新 Runner 的核心配置（注册产生的 Token 等字段省略）：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[[runners]]</span></span><br><span class="line">  <span class="attr">name</span> = <span class="string">&quot;devops-lab-dind-runner&quot;</span></span><br><span class="line">  <span class="attr">executor</span> = <span class="string">&quot;docker&quot;</span></span><br><span class="line">  <span class="section">[runners.docker]</span></span><br><span class="line">    <span class="attr">image</span> = <span class="string">&quot;docker:cli&quot;</span></span><br><span class="line">    <span class="attr">privileged</span> = <span class="literal">true</span></span><br><span class="line">    <span class="attr">volumes</span> = [<span class="string">&quot;/cache&quot;</span>]</span><br><span class="line">    <span class="attr">extra_hosts</span> = [<span class="string">&quot;gitlab.test:host-gateway&quot;</span>]</span><br><span class="line">    <span class="attr">pull_policy</span> = <span class="string">&quot;if-not-present&quot;</span></span><br><span class="line">    <span class="attr">dns</span> = [<span class="string">&quot;192.168.5.1&quot;</span>, <span class="string">&quot;223.5.5.5&quot;</span>]</span><br></pre></td></tr></table></figure><p>这里<strong>没有挂载宿主机 <code>/var/run/docker.sock</code></strong>。传统 DinD 要启动自己的 Docker Daemon，因此本次 Lab 的 Runner 开启了 <code>privileged = true</code>。这属于高风险设置，不能直接作为面向不可信代码的公共 Runner 配置。关闭 Untagged Job 仅避免误调度，Runner Tag 本身也不是权限隔离边界。</p><h2 id="27-2-DinD-Job"><a href="#27-2-DinD-Job" class="headerlink" title="27.2 DinD Job"></a>27.2 DinD Job</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-dind-build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">image</span></span><br><span class="line">  <span class="attr">tags:</span> [<span class="string">dind</span>]</span><br><span class="line">  <span class="attr">image:</span> <span class="string">docker:cli</span></span><br><span class="line">  <span class="attr">services:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">docker:dind</span></span><br><span class="line">      <span class="attr">alias:</span> <span class="string">docker</span></span><br><span class="line">      <span class="attr">command:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;--dns=192.168.5.1&quot;</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;--dns=223.5.5.5&quot;</span></span><br><span class="line">  <span class="attr">variables:</span></span><br><span class="line">    <span class="attr">DOCKER_HOST:</span> <span class="string">&quot;tcp://docker:2375&quot;</span></span><br><span class="line">    <span class="attr">DOCKER_TLS_CERTDIR:</span> <span class="string">&quot;&quot;</span></span><br><span class="line">  <span class="attr">needs:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">job:</span> <span class="string">build-job</span></span><br><span class="line">      <span class="attr">artifacts:</span> <span class="literal">true</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">job:</span> <span class="string">maven-build</span></span><br><span class="line">      <span class="attr">artifacts:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">version</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">build</span> <span class="string">-t</span> <span class="string">waterfall-dind:$CI_COMMIT_SHORT_SHA</span> <span class="string">.</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">run</span> <span class="string">--rm</span> <span class="string">waterfall-dind:$CI_COMMIT_SHORT_SHA</span></span><br></pre></td></tr></table></figure><p>上面是 <strong>DinD 基础构建实验的目标配置</strong>；第 28 节的 Registry 实验改用了另一个 Dockerfile，以避开 Docker Hub 访问问题。</p><p><code>DOCKER_HOST=tcp://docker:2375</code> 指向 DinD Service。<code>DOCKER_TLS_CERTDIR=&quot;&quot;</code> 关闭该实验网络内 DinD 的 TLS，不可照搬到开放网络。常规做法是限制服务网络并优先采用更安全的构建架构。</p><h2 id="27-3-DNS-故障：Maven-与-DinD-同时受影响"><a href="#27-3-DNS-故障：Maven-与-DinD-同时受影响" class="headerlink" title="27.3 DNS 故障：Maven 与 DinD 同时受影响"></a>27.3 DNS 故障：Maven 与 DinD 同时受影响</h2><p>最初 Maven 失败：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">repo.maven.apache.org: Temporary failure in name resolution</span><br><span class="line">Plugin org.apache.maven.plugins:maven-clean-plugin:3.2.0</span><br><span class="line">could not be resolved</span><br></pre></td></tr></table></figure><p>DinD 构建则失败在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lookup registry-1.docker.io on 172.19.96.1:53: i/o timeout</span><br></pre></td></tr></table></figure><p>Windows <code>ipconfig</code> 显示 <code>vEthernet (WSL)</code> 为 <code>172.19.96.1/20</code>。这个 IP 是当时 Windows 侧 WSL 虚拟网卡地址，不是人为给 WSL 固定的服务 IP，也不能因为 DNS 出问题就删除它。</p><p>我分别在普通 Docker 容器内尝试默认 DNS、<code>223.5.5.5</code> 和路由器 <code>192.168.5.1</code>：默认 DNS 解析失败，后两者成功。因此针对 <strong>两个 Docker Executor Runner</strong> 增加：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[runners.docker]</span></span><br><span class="line">  <span class="attr">dns</span> = [<span class="string">&quot;192.168.5.1&quot;</span>, <span class="string">&quot;223.5.5.5&quot;</span>]</span><br></pre></td></tr></table></figure><p>对 DinD 的独立 Daemon 还增加了服务参数：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">docker:dind</span></span><br><span class="line">    <span class="attr">alias:</span> <span class="string">docker</span></span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;--dns=192.168.5.1&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;--dns=223.5.5.5&quot;</span></span><br></pre></td></tr></table></figure><p>调整后 Maven 与相应 DinD Job 均曾构建通过，确认该 DNS 问题获得缓解。但之后又遇到 <code>registry-1.docker.io:443</code> 的连接超时：<strong>DNS 能解析，不代表 Docker Hub HTTPS 链路就一定可达</strong>。国内网络访问受限时，DinD 的临时镜像存储也不能直接复用 WSL 宿主机已拉取的 Alpine Image。</p><h2 id="27-4-Socket-Binding-与-DinD-对比"><a href="#27-4-Socket-Binding-与-DinD-对比" class="headerlink" title="27.4 Socket Binding 与 DinD 对比"></a>27.4 Socket Binding 与 DinD 对比</h2><table><thead><tr><th>维度</th><th>Docker Socket Binding</th><th>Docker-in-Docker</th></tr></thead><tbody><tr><td>Daemon</td><td>WSL 宿主机 Docker Daemon</td><td>DinD Service 内独立 Daemon</td></tr><tr><td>连接方式</td><td>Unix Socket</td><td>本实验 TCP 2375</td></tr><tr><td>镜像存储</td><td>宿主机镜像存储</td><td>DinD Service 自己的临时存储</td></tr><tr><td>基础镜像缓存</td><td>可直接复用宿主机缓存</td><td>默认无法复用宿主机缓存</td></tr><tr><td>主要风险</td><td>Job 可控制宿主机 Docker Daemon</td><td>传统 privileged DinD 会削弱隔离</td></tr></tbody></table><p>本实验中两者均能执行 Docker 命令，但它们控制的是不同的 Daemon。后续用 Container Registry 将镜像从 DinD 环境交付出去。</p><hr><h1 id="28-GitLab-Container-Registry：Build-→-Push-→-Pull"><a href="#28-GitLab-Container-Registry：Build-→-Push-→-Pull" class="headerlink" title="28. GitLab Container Registry：Build → Push → Pull"></a>28. GitLab Container Registry：Build → Push → Pull</h1><h2 id="28-1-启用本地-Registry"><a href="#28-1-启用本地-Registry" class="headerlink" title="28.1 启用本地 Registry"></a>28.1 启用本地 Registry</h2><p>在现有 GitLab CE Docker Compose 的 <code>GITLAB_OMNIBUS_CONFIG</code> 中增加：</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">external_url <span class="string">&#x27;http://gitlab.test:8929&#x27;</span></span><br><span class="line">registry_external_url <span class="string">&#x27;http://gitlab.test:5600&#x27;</span></span><br></pre></td></tr></table></figure><p>端口映射对应增加：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ports:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;8929:8929&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;2224:22&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;5600:5600&quot;</span></span><br></pre></td></tr></table></figure><p>执行 <code>docker compose up -d</code> 重新创建容器后，保持原有 <code>config</code>、<code>logs</code>、<code>data</code> 持久化目录不变。通过 <code>docker exec gitlab gitlab-ctl status</code> 可看到 <code>registry</code> 进程运行。</p><p>最初尝试的是端口 <code>5050</code>，但它遇到了 Windows&#x2F;WSL2 网络访问问题，最终改为 <code>5600</code>。</p><h2 id="28-2-代理、portproxy-与-Windows-排除端口"><a href="#28-2-代理、portproxy-与-Windows-排除端口" class="headerlink" title="28.2 代理、portproxy 与 Windows 排除端口"></a>28.2 代理、portproxy 与 Windows 排除端口</h2><p>首次执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -i http://gitlab.test:5050/v2/</span><br></pre></td></tr></table></figure><p>得到的竟是 <code>503 Forwarding failure (Privoxy@localhost)</code>。这不是 Registry 的 503，而是 WSL 的 HTTP 代理尝试通过 SOCKS5 转发本地服务失败。使用下面的命令可以排除代理干扰：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -i --noproxy <span class="string">&#x27;*&#x27;</span> http://gitlab.test:5050/v2/</span><br></pre></td></tr></table></figure><p>此时转为 <code>Connection refused</code>。进一步检查：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">WSL → 127.0.0.1:5050/v2/       HTTP 401（正常）</span><br><span class="line">WSL → gitlab.test:5050/v2/     Connection refused</span><br><span class="line">getent ahostsv4 gitlab.test     192.168.5.3</span><br></pre></td></tr></table></figure><p>说明 Registry 内部正常，问题发生在访问 Windows LAN 地址 <code>192.168.5.3</code> 的路径上。查看 Windows：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">netsh interface portproxy show all</span><br><span class="line"><span class="built_in">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">5050</span> <span class="literal">-State</span> Listen</span><br><span class="line">netsh interface ipv4 show excludedportrange protocol=tcp</span><br></pre></td></tr></table></figure><p>虽然 <code>portproxy</code> 已有 <code>0.0.0.0:5050 → 172.19.106.103:5050</code>，但 Windows 没有创建监听。原因最终定位为 Windows 的排除端口范围：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">5041    5140</span><br></pre></td></tr></table></figure><p><code>5050</code> 落在这个范围内。因此没有去删除系统保留区间，而是调整 Registry 对外端口为 <code>5600</code>，同步修改 GitLab 配置、Docker Compose、Windows <code>portproxy</code> 和允许访问的 Windows 防火墙规则。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在管理员 PowerShell 执行；WSL IP 应先用 hostname -I 核对</span></span><br><span class="line">netsh interface portproxy delete v4tov4 listenaddress=<span class="number">0.0</span>.<span class="number">0.0</span> listenport=<span class="number">5050</span></span><br><span class="line">netsh interface portproxy add v4tov4 listenaddress=<span class="number">0.0</span>.<span class="number">0.0</span> listenport=<span class="number">5600</span> connectaddress=<span class="number">172.19</span>.<span class="number">106.103</span> connectport=<span class="number">5600</span></span><br></pre></td></tr></table></figure><p>最终：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -i --noproxy <span class="string">&#x27;*&#x27;</span> http://gitlab.test:5600/v2/</span><br></pre></td></tr></table></figure><p>返回：</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">401</span> Unauthorized</span><br><span class="line"><span class="attribute">Docker-Distribution-Api-Version</span><span class="punctuation">: </span>registry/2.0</span><br><span class="line"><span class="attribute">Www-Authenticate</span><span class="punctuation">: </span>Bearer realm=&quot;http://gitlab.test:8929/jwt/auth&quot;,service=&quot;container_registry&quot;</span><br></pre></td></tr></table></figure><p><strong>这里的 401 是预期行为</strong>：Registry 已经可达，但尚未携带认证信息。WSL IP 可能随网络重建而变化，Windows <code>portproxy</code> 的目标 IP 需要相应核对和更新。</p><h2 id="28-3-Docker-Hub-不稳定时，用-scratch-进行封闭实验"><a href="#28-3-Docker-Hub-不稳定时，用-scratch-进行封闭实验" class="headerlink" title="28.3 Docker Hub 不稳定时，用 scratch 进行封闭实验"></a>28.3 Docker Hub 不稳定时，用 <code>scratch</code> 进行封闭实验</h2><p>DinD 访问 Docker Hub 一度出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Head https://registry-1.docker.io/v2/library/alpine/manifests/3.24</span><br><span class="line">... dial tcp 75.126.164.178:443: i/o timeout</span><br></pre></td></tr></table></figure><p>为避免外部 Registry 阻断本次<strong>内部 Registry Push&#x2F;Pull</strong>实验，新建 <code>Dockerfile.registry-test</code>：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> scratch</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> build/app.txt /app/app.txt</span></span><br></pre></td></tr></table></figure><p><code>scratch</code> 是 Docker 的特殊空基础镜像，不会为基础镜像层请求 Docker Hub；它也没有 shell 或 <code>cat</code>，所以不能像 Alpine 镜像一样直接 <code>docker run ... cat</code>，验证采用 <code>docker create</code> + <code>docker cp</code>。</p><h2 id="28-4-DinD-登录、Push、删除、Pull-与文件校验"><a href="#28-4-DinD-登录、Push、删除、Pull-与文件校验" class="headerlink" title="28.4 DinD 登录、Push、删除、Pull 与文件校验"></a>28.4 DinD 登录、Push、删除、Pull 与文件校验</h2><p>下面是本次成功实验的核心 Job 配置；<code>build-job</code> 和 <code>maven-build</code> 的定义仍在其他位置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker-dind-build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">image</span></span><br><span class="line">  <span class="attr">tags:</span> [<span class="string">dind</span>]</span><br><span class="line">  <span class="attr">image:</span> <span class="string">docker:cli</span></span><br><span class="line">  <span class="attr">services:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">docker:dind</span></span><br><span class="line">      <span class="attr">alias:</span> <span class="string">docker</span></span><br><span class="line">      <span class="attr">command:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;--insecure-registry=gitlab.test:5600&quot;</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;--dns=192.168.5.1&quot;</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;--dns=223.5.5.5&quot;</span></span><br><span class="line">  <span class="attr">variables:</span></span><br><span class="line">    <span class="attr">DOCKER_HOST:</span> <span class="string">&quot;tcp://docker:2375&quot;</span></span><br><span class="line">    <span class="attr">DOCKER_TLS_CERTDIR:</span> <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="attr">BUILDX_NO_DEFAULT_ATTESTATIONS:</span> <span class="string">&quot;1&quot;</span></span><br><span class="line">    <span class="attr">NO_PROXY:</span> <span class="string">&quot;localhost,127.0.0.1,gitlab.test,192.168.5.3,docker&quot;</span></span><br><span class="line">    <span class="attr">no_proxy:</span> <span class="string">&quot;localhost,127.0.0.1,gitlab.test,192.168.5.3,docker&quot;</span></span><br><span class="line">  <span class="attr">needs:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">job:</span> <span class="string">build-job</span></span><br><span class="line">      <span class="attr">artifacts:</span> <span class="literal">true</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">job:</span> <span class="string">maven-build</span></span><br><span class="line">      <span class="attr">artifacts:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">version</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">printf</span> <span class="string">&#x27;%s&#x27;</span> <span class="string">&quot;$CI_REGISTRY_PASSWORD&quot;</span> <span class="string">|</span> <span class="string">docker</span> <span class="string">login</span> <span class="string">&quot;$CI_REGISTRY&quot;</span> <span class="string">-u</span> <span class="string">&quot;$CI_REGISTRY_USER&quot;</span> <span class="string">--password-stdin</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">build</span> <span class="string">-f</span> <span class="string">Dockerfile.registry-test</span> <span class="string">-t</span> <span class="string">&quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA&quot;</span> <span class="string">.</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">push</span> <span class="string">&quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">image</span> <span class="string">rm</span> <span class="string">&quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">pull</span> <span class="string">&quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">image</span> <span class="string">inspect</span> <span class="string">&quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">create</span> <span class="string">--name</span> <span class="string">registry-test</span> <span class="string">&quot;$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA&quot;</span> <span class="string">/unused</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">cp</span> <span class="string">registry-test:/app/app.txt</span> <span class="string">./registry-result.txt</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">registry-result.txt</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">docker</span> <span class="string">rm</span> <span class="string">registry-test</span></span><br></pre></td></tr></table></figure><p>这里使用了 GitLab CI 内置的 <code>CI_REGISTRY</code>、<code>CI_REGISTRY_IMAGE</code>、<code>CI_REGISTRY_USER</code>、<code>CI_REGISTRY_PASSWORD</code>。<code>--password-stdin</code> 避免把认证密码直接写进命令参数；不要在日志中打印密码。</p><p>由于 Registry 是 HTTP，本次受控 Lab 在 <strong>DinD Daemon</strong> 上指定 <code>--insecure-registry=gitlab.test:5600</code>。这不等于给 WSL 宿主机 Docker Daemon 配置了同样的 HTTP Registry 信任；不同 Daemon 需分别管理。生产环境应使用 TLS。</p><h2 id="28-5-Layer-上传成功，但-Manifest-提交失败"><a href="#28-5-Layer-上传成功，但-Manifest-提交失败" class="headerlink" title="28.5 Layer 上传成功，但 Manifest 提交失败"></a>28.5 Layer 上传成功，但 Manifest 提交失败</h2><p>首次运行时，各 Layer 已显示 <code>Pushed</code>，最后仍报：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">error from registry: blob unknown to registry - sha256:21bb52...</span><br></pre></td></tr></table></figure><p>这说明镜像 Push 不只是传输各个 Layer，还要由 Registry 接受最终 Manifest &#x2F; Image Index。排查方向指向 BuildKit 默认 Attestation 与 Registry 校验之间的兼容性；<strong>不能仅凭 Layer 显示 Pushed 判断镜像上传完成</strong>。</p><p>在 Job 变量中加入：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">BUILDX_NO_DEFAULT_ATTESTATIONS:</span> <span class="string">&quot;1&quot;</span></span><br></pre></td></tr></table></figure><p>重新运行后 Pipeline 通过，完成镜像 Push、从 DinD 本地删除、重新 Pull 和文件校验。这个 Workaround 是为了验证实验链路，不能简单推广为所有 Registry 生产环境的标准做法。</p><p>至此确认：<strong>DinD 临时环境内构建的镜像，可以通过 GitLab Container Registry 保存并重新获取</strong>。本次没有验证从 WSL 宿主机另一个 Docker Daemon 拉取该 HTTP Registry 镜像；也没有通过 <code>scratch</code> 镜像验证应用容器实际启动。</p><hr><h1 id="29-Runner-并发与-resource-group-实验"><a href="#29-Runner-并发与-resource-group-实验" class="headerlink" title="29. Runner 并发与 resource_group 实验"></a>29. Runner 并发与 <code>resource_group</code> 实验</h1><h2 id="29-1-为什么最初看不出资源锁效果"><a href="#29-1-为什么最初看不出资源锁效果" class="headerlink" title="29.1 为什么最初看不出资源锁效果"></a>29.1 为什么最初看不出资源锁效果</h2><p>在当前 Runner 服务的 <code>/etc/gitlab-runner/config.toml</code> 中发现：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">concurrent</span> = <span class="number">1</span></span><br></pre></td></tr></table></figure><p>开始使用两个 Pipeline 进行验证时，第一个 <code>testbed-exclusive</code> 正在等待 90 秒，第二个 Pipeline 连 <code>maven-build</code> 等 Build Job 都停留在 Pending。这并不意味着所有 Job 被 <code>resource_group</code> 锁住：<strong>全局 Runner 并发为 1，本身就足以使无关 Job 排队</strong>。</p><p>将全局并发改为：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">concurrent</span> = <span class="number">4</span></span><br></pre></td></tr></table></figure><p>重新运行后，另一个 <code>testbed-exclusive</code> 曾在界面显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">此作业正在等待资源：testbed-01</span><br></pre></td></tr></table></figure><p>这确认 GitLab 能识别资源占用，但跨 Pipeline 演示又受到 Maven Central 下载缓慢影响：等第二条 Pipeline 跑到 Testbed 阶段时，第一条早已结束。因此改为一个<strong>不依赖 Maven 的最小实验</strong>。</p><h2 id="29-2-同一-Pipeline-两个-Job-直接竞争-Testbed"><a href="#29-2-同一-Pipeline-两个-Job-直接竞争-Testbed" class="headerlink" title="29.2 同一 Pipeline 两个 Job 直接竞争 Testbed"></a>29.2 同一 Pipeline 两个 Job 直接竞争 Testbed</h2><p>保留 <code>testbed</code> Stage，在同一 Pipeline 添加两个临时 Job：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">testbed-lock-a:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">testbed</span></span><br><span class="line">  <span class="attr">tags:</span> [<span class="string">shell</span>]</span><br><span class="line">  <span class="attr">needs:</span> []</span><br><span class="line">  <span class="attr">resource_group:</span> <span class="string">testbed-01</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Job A started, pipeline=$CI_PIPELINE_ID&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">date</span> <span class="string">-u</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">sleep</span> <span class="number">30</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Job A finished&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">date</span> <span class="string">-u</span></span><br><span class="line"></span><br><span class="line"><span class="attr">testbed-lock-b:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">testbed</span></span><br><span class="line">  <span class="attr">tags:</span> [<span class="string">shell</span>]</span><br><span class="line">  <span class="attr">needs:</span> []</span><br><span class="line">  <span class="attr">resource_group:</span> <span class="string">testbed-01</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Job B started, pipeline=$CI_PIPELINE_ID&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">date</span> <span class="string">-u</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">sleep</span> <span class="number">30</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Job B finished&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">date</span> <span class="string">-u</span></span><br></pre></td></tr></table></figure><p>这里的 <code>needs: []</code> <strong>是实验用的显式无依赖声明</strong>，让两个 Job 可以在 Pipeline 创建时直接进入调度，而不等待前面 <code>build</code>、<code>image</code> 等 Stage。它不是普通部署 Job 必须采用的配置。</p><p>验证结果：两个 Job 没有同时持有 <code>testbed-01</code>，后一个等待前一个释放资源后执行；实验通过。随后删除两个临时 Job，恢复原有 Pipeline。</p><h2 id="29-3-最终理解"><a href="#29-3-最终理解" class="headerlink" title="29.3 最终理解"></a>29.3 最终理解</h2><table><thead><tr><th>机制</th><th>层级</th><th>目的</th></tr></thead><tbody><tr><td><code>concurrent</code></td><td>GitLab Runner 进程</td><td>限制总体同时执行的 Job 数</td></tr><tr><td><code>limit</code></td><td>单个 Runner</td><td>限制该 Runner 可同时执行的 Job 数</td></tr><tr><td><code>needs</code></td><td>Pipeline 内 Job DAG</td><td>明确依赖并允许跨 Stage 提前调度</td></tr><tr><td><code>resource_group</code></td><td>同一 GitLab 项目内</td><td>让同一资源组的 Job 互斥执行</td></tr></tbody></table><p><code>resource_group</code> 并不是整个 GitLab Group 中的全局分布式锁。对于真实物理 Testbed，必须结合资源所有权、超时回收和 Deploy&#x2F;Test 锁范围设计。对本实验而言，已验证的是同一项目中 Job 的资源互斥。</p><hr><h1 id="30-Kubernetes-Executor：架构学习（未实操）"><a href="#30-Kubernetes-Executor：架构学习（未实操）" class="headerlink" title="30. Kubernetes Executor：架构学习（未实操）"></a>30. Kubernetes Executor：架构学习（未实操）</h1><p>本次没有安装 Kubernetes Runner，也没有创建 Kubernetes CI Pod。这里只总结 Kubernetes Executor 的典型工作方式，不把理论当作实验结果。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">GitLab Server</span><br><span class="line">     │ Job</span><br><span class="line">     ▼</span><br><span class="line">GitLab Runner（Kubernetes Executor）</span><br><span class="line">     │ Kubernetes API</span><br><span class="line">     ▼</span><br><span class="line">Kubernetes Cluster</span><br><span class="line">     └── Job Pod</span><br><span class="line">          ├── Build Container    # 执行 CI script</span><br><span class="line">          ├── Helper Container   # Git、Artifact、Cache 等辅助操作</span><br><span class="line">          └── Service Container  # 按配置提供数据库等服务（可选）</span><br></pre></td></tr></table></figure><p>与已经体验过的两种 Executor 对比：</p><table><thead><tr><th>维度</th><th>Shell Executor</th><th>Docker Executor</th><th>Kubernetes Executor</th></tr></thead><tbody><tr><td>运行位置</td><td>Runner Host</td><td>Docker 容器</td><td>Kubernetes Pod</td></tr><tr><td>工具来源</td><td>Host 环境</td><td>Job Image</td><td>Job Image</td></tr><tr><td>隔离边界</td><td>较弱</td><td>容器</td><td>Pod &#x2F; 容器及集群策略</td></tr><tr><td>扩缩容</td><td>依赖 Host</td><td>依赖 Docker Host</td><td>利用集群资源调度</td></tr><tr><td>典型排障</td><td>Shell Profile &#x2F; 权限</td><td>网络 &#x2F; 镜像 &#x2F; Daemon</td><td>Pod Events &#x2F; 调度 &#x2F; ServiceAccount &#x2F; NetworkPolicy</td></tr></tbody></table><p><strong>Kubernetes Executor 不自动提供 Docker Daemon。</strong> 在 Kubernetes Job 里构建容器镜像，仍需选择适当的构建方式和权限模型，例如专门的 BuildKit 方案；不能直接假设 <code>docker build</code> 一定可用。</p><hr><h1 id="31-总结：与-Jenkins-经验的对应关系"><a href="#31-总结：与-Jenkins-经验的对应关系" class="headerlink" title="31. 总结：与 Jenkins 经验的对应关系"></a>31. 总结：与 Jenkins 经验的对应关系</h1><p>从最初的 GitLab CE 部署到最终的 Registry 和资源互斥实验，这次最大的收获不是记住几个 YAML 关键字，而是把 CI&#x2F;CD 拆成明确的边界：</p><ul><li><strong>GitLab Server</strong> 管理项目、MR、Pipeline、Artifact、Registry 和资源组调度；</li><li><strong>GitLab Runner</strong> 实际领取和执行 Job，Executor 决定执行环境；</li><li><strong>Artifact</strong> 传递准确的构建产物，<strong>Cache</strong> 只负责加速可重建的数据；</li><li><strong><code>needs</code></strong> 表达依赖关系，<strong>Stage</strong> 表达默认阶段顺序，<strong><code>resource_group</code></strong> 处理有限资源互斥；</li><li><strong>Socket Binding</strong> 和 <strong>DinD</strong> 看似都运行 <code>docker build</code>，实则有不同的 Daemon、安全边界和镜像存储；</li><li><strong>Container Registry</strong> 才是脱离单个构建环境、让镜像可交付和复用的关键环节。</li></ul><p>实际踩坑也非常集中：Shell Runner 的 <code>.bash_logout</code>、容器访问 <code>gitlab.test</code>、Docker Hub&#x2F;Maven Central 的 DNS 与网络、WSL2 Windows portproxy 的端口排除、Privoxy 误代理本地 Registry、BuildKit Attestation 与 Registry Manifest 校验、Runner 全局并发掩盖 <code>resource_group</code> 等。</p><p>这些问题提醒我：<strong>Pipeline Failed 不一定是代码错误；需要先定位失败发生在 Git、Runner 准备环境、工具下载、构建、镜像上传，还是资源调度环节。</strong></p><p>此次实验到这里告一段落。后续如果继续扩展，我更希望围绕安全的构建方案、可靠的内部镜像仓库、Runner 可观测性和 Kubernetes Executor 的真实资源限制展开，而不是继续为单纯增加组件而增加组件。</p>]]>
    </content>
    <id>https://zhouxingyu.dev/posts/GitLab-CI-CD-Experiment/</id>
    <link href="https://zhouxingyu.dev/posts/GitLab-CI-CD-Experiment/"/>
    <published>2026-09-13T12:30:00.000Z</published>
    <summary>从 WSL2 部署 GitLab CE 到 Shell/Docker Runner、MR Pipeline、Artifact/Cache、Docker-in-Docker、Container Registry 和资源互斥，附实际排障记录。</summary>
    <title>WSL2 本地 GitLab CI/CD 实验：Runner、DinD 与 Container Registry</title>
    <updated>2026-10-08T14:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhou Xingyu</name>
    </author>
    <category term="GitLab" scheme="https://zhouxingyu.dev/categories/GitLab/"/>
    <category term="CI/CD" scheme="https://zhouxingyu.dev/tags/CI-CD/"/>
    <category term="GitLab" scheme="https://zhouxingyu.dev/tags/GitLab/"/>
    <category term="WSL2" scheme="https://zhouxingyu.dev/tags/WSL2/"/>
    <category term="DevOps" scheme="https://zhouxingyu.dev/tags/DevOps/"/>
    <content>
      <![CDATA[<h1 id="WSL2-本地-GitLab-CI-CD-最小实验记录"><a href="#WSL2-本地-GitLab-CI-CD-最小实验记录" class="headerlink" title="WSL2 本地 GitLab CI&#x2F;CD 最小实验记录"></a>WSL2 本地 GitLab CI&#x2F;CD 最小实验记录</h1><blockquote><p>从零搭建 GitLab CE、GitLab Runner，并逐步验证用户权限、Runner、Pipeline、Artifact、Manual Deploy、Environment、Protected Branch、Merge Request Pipeline、稀缺 Testbed 调度与 Release Pipeline。</p></blockquote><h2 id="1-实验目标"><a href="#1-实验目标" class="headerlink" title="1. 实验目标"></a>1. 实验目标</h2><p>这套实验并不是为了单纯“安装一个 GitLab”，而是希望在本地复现真实企业 CI&#x2F;CD 中的一些核心概念：</p><ul><li>GitLab Self-Managed</li><li>GitLab 用户、Group、Project 与权限</li><li>GitLab Runner</li><li>Shell Executor</li><li>Pipeline &#x2F; Stage &#x2F; Job</li><li>Artifacts</li><li>Manual Deployment</li><li>Environment</li><li>Protected Branch</li><li>Merge Request Pipeline</li><li>MR-only Pipeline</li><li>Runner Tag</li><li>Runner Scope</li><li>稀缺测试资源调度</li><li>Release Pipeline</li></ul><p>实验环境：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Windows</span><br><span class="line">└── WSL2 Ubuntu</span><br><span class="line">    ├── Docker Engine</span><br><span class="line">    ├── GitLab CE</span><br><span class="line">    ├── GitLab Runner</span><br><span class="line">    └── Local Git repositories</span><br></pre></td></tr></table></figure><p>GitLab 使用本地域名：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://gitlab.test:8929</span><br></pre></td></tr></table></figure><hr><h1 id="2-使用-Docker-部署-GitLab-CE"><a href="#2-使用-Docker-部署-GitLab-CE" class="headerlink" title="2. 使用 Docker 部署 GitLab CE"></a>2. 使用 Docker 部署 GitLab CE</h1><p>创建目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /svc/gitlab</span><br><span class="line"><span class="built_in">cd</span> /svc/gitlab</span><br></pre></td></tr></table></figure><p>创建 <code>compose.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">gitlab:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">gitlab/gitlab-ce:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">gitlab</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">hostname:</span> <span class="string">gitlab.test</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">shm_size:</span> <span class="string">&#x27;256m&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">GITLAB_OMNIBUS_CONFIG:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        external_url &#x27;http://gitlab.test:8929&#x27;</span></span><br><span class="line"><span class="string">        gitlab_rails[&#x27;gitlab_shell_ssh_port&#x27;] = 2224</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8929:8929&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;2224:22&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./config:/etc/gitlab</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./logs:/var/log/gitlab</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/var/opt/gitlab</span></span><br></pre></td></tr></table></figure><p>启动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br></pre></td></tr></table></figure><p>检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker ps</span><br><span class="line">docker <span class="built_in">exec</span> -it gitlab gitlab-ctl status</span><br></pre></td></tr></table></figure><p>GitLab 内部实际上包含很多服务，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">nginx</span><br><span class="line">postgresql</span><br><span class="line">redis</span><br><span class="line">puma</span><br><span class="line">sidekiq</span><br><span class="line">gitaly</span><br><span class="line">prometheus</span><br><span class="line">alertmanager</span><br><span class="line">gitlab-workhorse</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>因此 GitLab 容器启动速度明显慢于普通 Web 服务。</p><hr><h1 id="3-GitLab-unhealthy-与-dev-shm"><a href="#3-GitLab-unhealthy-与-dev-shm" class="headerlink" title="3. GitLab unhealthy 与 /dev/shm"></a>3. GitLab unhealthy 与 <code>/dev/shm</code></h1><p>第一次启动后曾出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Up ... (unhealthy)</span><br></pre></td></tr></table></figure><p>但执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it gitlab gitlab-ctl status</span><br></pre></td></tr></table></figure><p>可以看到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">postgresql   run</span><br><span class="line">redis        run</span><br><span class="line">puma         run</span><br><span class="line">sidekiq      run</span><br><span class="line">nginx        run</span><br><span class="line">gitaly       run</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>说明 GitLab 核心组件其实都已经启动。</p><p>日志中出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">writing value to /dev/shm/gitlab/sidekiq/... failed</span><br></pre></td></tr></table></figure><p>Docker 默认共享内存较小，因此在 Compose 中显式增加：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">shm_size:</span> <span class="string">&#x27;256m&#x27;</span></span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> gitlab <span class="built_in">df</span> -h /dev/shm</span><br></pre></td></tr></table></figure><hr><h1 id="4-gitlab-local、gitlab-test-与域名解析"><a href="#4-gitlab-local、gitlab-test-与域名解析" class="headerlink" title="4. gitlab.local、gitlab.test 与域名解析"></a>4. <code>gitlab.local</code>、<code>gitlab.test</code> 与域名解析</h1><p>最开始使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.local</span><br></pre></td></tr></table></figure><p>后来切换为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.test</span><br></pre></td></tr></table></figure><p>原因之一是 <code>.local</code> 通常和 mDNS 有特殊关系，而 <code>.test</code> 更适合作为测试域名。</p><p>Windows：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">C:\Windows\System32\drivers\etc\hosts</span><br></pre></td></tr></table></figure><p>添加：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1 gitlab.test</span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ipconfig /flushdns</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ping gitlab.test</span><br></pre></td></tr></table></figure><p>GitLab 的：</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">external_url</span><br></pre></td></tr></table></figure><p>也必须同步修改：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">external_url</span> <span class="string">&#x27;http://gitlab.test:8929&#x27;</span></span><br></pre></td></tr></table></figure><p>否则会出现一个很隐蔽的问题：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Runner API URL</span><br><span class="line">→ gitlab.test</span><br><span class="line"></span><br><span class="line">但 GitLab 下发的 repository clone URL</span><br><span class="line">→ gitlab.local</span><br></pre></td></tr></table></figure><p>最终 Runner 报：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Could not resolve host: gitlab.local</span><br></pre></td></tr></table></figure><p>检查 GitLab 实际认为自己的 URL：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> gitlab \</span><br><span class="line">  gitlab-rails runner <span class="string">&#x27;puts Gitlab.config.gitlab.url&#x27;</span></span><br></pre></td></tr></table></figure><p>必须输出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://gitlab.test:8929</span><br></pre></td></tr></table></figure><hr><h1 id="5-创建普通用户，而不是一直使用-root"><a href="#5-创建普通用户，而不是一直使用-root" class="headerlink" title="5. 创建普通用户，而不是一直使用 root"></a>5. 创建普通用户，而不是一直使用 root</h1><p><code>root</code> 只用于 GitLab Instance 管理。</p><p>额外创建普通用户：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Username: jason</span><br><span class="line">Type: Regular user</span><br><span class="line">Admin: false</span><br></pre></td></tr></table></figure><p>之后日常操作使用 <code>jason</code>。</p><p>创建 Group：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">devops-lab</span><br></pre></td></tr></table></figure><p>然后创建项目：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">devops-lab/ci-playground</span><br></pre></td></tr></table></figure><p>这样权限层次变成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">GitLab Instance</span><br><span class="line">│</span><br><span class="line">├── root</span><br><span class="line">│   └── Administrator</span><br><span class="line">│</span><br><span class="line">└── devops-lab</span><br><span class="line">    └── jason</span><br><span class="line">        └── Owner</span><br></pre></td></tr></table></figure><hr><h1 id="6-Windows-WSL-中区分-GitHub-与本地-GitLab-Git-Identity"><a href="#6-Windows-WSL-中区分-GitHub-与本地-GitLab-Git-Identity" class="headerlink" title="6. Windows &#x2F; WSL 中区分 GitHub 与本地 GitLab Git Identity"></a>6. Windows &#x2F; WSL 中区分 GitHub 与本地 GitLab Git Identity</h1><p>Git 的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">user.name</span><br><span class="line">user.email</span><br></pre></td></tr></table></figure><p>决定的是 Commit Author。</p><p>可以使用 conditional include。</p><p>例如：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ~/.gitconfig</span></span><br><span class="line"></span><br><span class="line"><span class="section">[user]</span></span><br><span class="line">    <span class="attr">name</span> = Jason Zhou</span><br><span class="line">    <span class="attr">email</span> = github@example.com</span><br><span class="line"></span><br><span class="line"><span class="section">[includeIf &quot;gitdir:/home/zxy/workspace/gitlab-test/&quot;]</span></span><br><span class="line">    <span class="attr">path</span> = ~/.gitconfig-gitlab</span><br></pre></td></tr></table></figure><p>然后：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ~/.gitconfig-gitlab</span></span><br><span class="line"></span><br><span class="line"><span class="section">[user]</span></span><br><span class="line">    <span class="attr">name</span> = Jason Zhou</span><br><span class="line">    <span class="attr">email</span> = local-gitlab@example.com</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git config user.name</span><br><span class="line">git config user.email</span><br><span class="line"></span><br><span class="line">git config --show-origin --get-regexp <span class="string">&#x27;^user\.&#x27;</span></span><br></pre></td></tr></table></figure><p>注意：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Commit Author</span><br><span class="line">≠</span><br><span class="line">SSH authentication identity</span><br></pre></td></tr></table></figure><p>这是两套独立机制。</p><hr><h1 id="7-GitHub-与-Local-GitLab-使用不同-SSH-Key"><a href="#7-GitHub-与-Local-GitLab-使用不同-SSH-Key" class="headerlink" title="7. GitHub 与 Local GitLab 使用不同 SSH Key"></a>7. GitHub 与 Local GitLab 使用不同 SSH Key</h1><p>SSH alias 配置位于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/.ssh/config</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">Host github.com</span><br><span class="line">    HostName github.com</span><br><span class="line">    User git</span><br><span class="line">    IdentityFile ~/.ssh/id_ed25519_github</span><br><span class="line">    IdentitiesOnly yes</span><br><span class="line"></span><br><span class="line">Host gitlab-local</span><br><span class="line">    HostName gitlab.test</span><br><span class="line">    Port 2224</span><br><span class="line">    User git</span><br><span class="line">    IdentityFile ~/.ssh/id_ed25519_gitlab</span><br><span class="line">    IdentitiesOnly yes</span><br></pre></td></tr></table></figure><p><code>gitlab-local</code> 只是本机 SSH alias，不是 DNS。</p><p>因此：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -T git@gitlab-local</span><br></pre></td></tr></table></figure><p>实际上等价于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">HostName gitlab.test</span><br><span class="line">Port 2224</span><br><span class="line">IdentityFile ~/.ssh/id_ed25519_gitlab</span><br></pre></td></tr></table></figure><p>Clone 时：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> \</span><br><span class="line">  git@gitlab-local:devops-lab/ci-playground.git</span><br></pre></td></tr></table></figure><p>而直接：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> \</span><br><span class="line">  ssh://git@gitlab.test:2224/devops-lab/ci-playground.git</span><br></pre></td></tr></table></figure><p>不会匹配：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Host gitlab-local</span><br></pre></td></tr></table></figure><p>因此可能使用错误的 SSH Key，最终出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Permission denied (publickey)</span><br></pre></td></tr></table></figure><hr><h1 id="8-第一条-GitLab-Pipeline"><a href="#8-第一条-GitLab-Pipeline" class="headerlink" title="8. 第一条 GitLab Pipeline"></a>8. 第一条 GitLab Pipeline</h1><p>第一版 <code>.gitlab-ci.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">build</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">test</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">deploy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">build-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Building...&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">build</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;hello gitlab ci&quot;</span> <span class="string">&gt;</span> <span class="string">build/app.txt</span></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">build/</span></span><br><span class="line"></span><br><span class="line"><span class="attr">test-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Testing...&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">test</span> <span class="string">-f</span> <span class="string">build/app.txt</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">grep</span> <span class="string">&quot;hello gitlab ci&quot;</span> <span class="string">build/app.txt</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Deploying...&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">build/app.txt</span></span><br></pre></td></tr></table></figure><p>Push 后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Git push</span><br><span class="line">↓</span><br><span class="line">GitLab Pipeline</span><br><span class="line">↓</span><br><span class="line">build</span><br><span class="line">↓</span><br><span class="line">test</span><br><span class="line">↓</span><br><span class="line">deploy</span><br></pre></td></tr></table></figure><p>但一开始所有 Job 都是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Pending</span><br></pre></td></tr></table></figure><p>原因：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GitLab 没有可用 Runner</span><br></pre></td></tr></table></figure><hr><h1 id="9-GitLab-Runner：Project-Runner-与-Runner-Scope"><a href="#9-GitLab-Runner：Project-Runner-与-Runner-Scope" class="headerlink" title="9. GitLab Runner：Project Runner 与 Runner Scope"></a>9. GitLab Runner：Project Runner 与 Runner Scope</h1><p>最开始创建的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Project Runner</span><br></pre></td></tr></table></figure><p>它只属于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ci-playground</span><br></pre></td></tr></table></figure><p>新的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">waterfall-ci-playground</span><br></pre></td></tr></table></figure><p>不会自动拥有它。</p><p>Runner Scope 可以分为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Project Runner</span><br><span class="line">Group Runner</span><br><span class="line">Instance Runner</span><br></pre></td></tr></table></figure><p>概念上类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Project Runner</span><br><span class="line">→ 某一个/几个 Repo</span><br><span class="line"></span><br><span class="line">Group Runner</span><br><span class="line">→ 整个 devops-lab</span><br><span class="line"></span><br><span class="line">Instance Runner</span><br><span class="line">→ 整个 GitLab</span><br></pre></td></tr></table></figure><p>对于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">devops-lab/</span><br><span class="line">├── ci-playground</span><br><span class="line">├── waterfall-ci-playground</span><br><span class="line">└── future-projects</span><br></pre></td></tr></table></figure><p>Group Runner 会更合理。</p><hr><h1 id="10-Tagged-Job-与-Untagged-Job"><a href="#10-Tagged-Job-与-Untagged-Job" class="headerlink" title="10. Tagged Job 与 Untagged Job"></a>10. Tagged Job 与 Untagged Job</h1><p>Runner 可以设置 tag：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">shell</span><br></pre></td></tr></table></figure><p>Job 也可以：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">tags:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">shell</span></span><br></pre></td></tr></table></figure><p>意思是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这个 Job 必须找到包含 shell tag 的 Runner</span><br></pre></td></tr></table></figure><p>如果 Job 没有：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">tags:</span></span><br></pre></td></tr></table></figure><p>则属于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">untagged job</span><br></pre></td></tr></table></figure><p>这时候 Runner 必须启用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Run untagged jobs</span><br></pre></td></tr></table></figure><p>否则即使 Runner：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Online</span><br></pre></td></tr></table></figure><p>Job 仍然会：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Pending</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Runner Online</span><br><span class="line">≠</span><br><span class="line">Runner 一定可以执行当前 Job</span><br></pre></td></tr></table></figure><p>还要经过 Runner 匹配。</p><hr><h1 id="11-User-mode-Runner-与-System-mode-Runner"><a href="#11-User-mode-Runner-与-System-mode-Runner" class="headerlink" title="11. User-mode Runner 与 System-mode Runner"></a>11. User-mode Runner 与 System-mode Runner</h1><p>第一次手工启动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner run</span><br></pre></td></tr></table></figure><p>日志：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WARNING: Running in user-mode.</span><br></pre></td></tr></table></figure><p>配置文件：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/gitlab-runner/.gitlab-runner/config.toml</span><br></pre></td></tr></table></figure><p>后来改成 system-mode。</p><p>systemd：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl <span class="built_in">cat</span> gitlab-runner</span><br></pre></td></tr></table></figure><p>实际启动命令：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">/usr/local/bin/gitlab-runner run</span><br><span class="line">  --config /etc/gitlab-runner/config.toml</span><br><span class="line">  --working-directory /home/gitlab-runner</span><br><span class="line">  --service gitlab-runner</span><br><span class="line">  --user gitlab-runner</span><br></pre></td></tr></table></figure><p>这说明：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Config:</span><br><span class="line">/etc/gitlab-runner/config.toml</span><br><span class="line"></span><br><span class="line">Working Directory:</span><br><span class="line">/home/gitlab-runner</span><br><span class="line"></span><br><span class="line">Linux User:</span><br><span class="line">gitlab-runner</span><br></pre></td></tr></table></figure><p>启动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> gitlab-runner</span><br><span class="line"><span class="built_in">sudo</span> systemctl start gitlab-runner</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl is-enabled gitlab-runner</span><br><span class="line"><span class="built_in">sudo</span> systemctl is-active gitlab-runner</span><br><span class="line"><span class="built_in">sudo</span> gitlab-runner verify</span><br></pre></td></tr></table></figure><p>这样 WSL 启动后，Runner 也可以跟随 systemd 自动启动。</p><hr><h1 id="12-Shell-Executor-与-Linux-文件权限"><a href="#12-Shell-Executor-与-Linux-文件权限" class="headerlink" title="12. Shell Executor 与 Linux 文件权限"></a>12. Shell Executor 与 Linux 文件权限</h1><p>第一次 user-mode Runner 是在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/zxy/workspace/gitlab-test/ci-playground</span><br></pre></td></tr></table></figure><p>目录中启动的：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner run</span><br></pre></td></tr></table></figure><p>于是默认 working directory 继承了当前目录，导致 Runner 创建：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/zxy/workspace/gitlab-test/ci-playground/builds/</span><br></pre></td></tr></table></figure><p>但执行用户是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner</span><br></pre></td></tr></table></figure><p>最终：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rm: cannot remove ... Permission denied</span><br></pre></td></tr></table></figure><p>这说明 Shell executor 本质上就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Runner</span><br><span class="line">→ 在宿主 Linux 上</span><br><span class="line">→ 以普通 Linux 用户</span><br><span class="line">→ 执行普通 shell process</span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Linux ownership</span><br><span class="line">permission</span><br><span class="line">PATH</span><br><span class="line">shell profile</span><br><span class="line">working directory</span><br></pre></td></tr></table></figure><p>都会影响 CI。</p><p>System-mode Runner 使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/gitlab-runner</span><br></pre></td></tr></table></figure><p>作为 working directory 后解决。</p><hr><h1 id="13-bash-logout-导致-Prepare-Environment-失败"><a href="#13-bash-logout-导致-Prepare-Environment-失败" class="headerlink" title="13. .bash_logout 导致 Prepare Environment 失败"></a>13. <code>.bash_logout</code> 导致 Prepare Environment 失败</h1><p>System Runner 第一次执行 Job：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Preparing environment</span><br><span class="line">ERROR: Job failed: prepare environment: exit status 1</span><br></pre></td></tr></table></figure><p>Shell executor 会启动 login shell，因此会加载：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">.profile</span><br><span class="line">.bashrc</span><br><span class="line">.bash_logout</span><br></pre></td></tr></table></figure><p><code>/home/gitlab-runner/.bash_logout</code> 中的默认逻辑导致 CI shell 退出失败。</p><p>删除：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /home/gitlab-runner/.bash_logout</span><br></pre></td></tr></table></figure><p>后解决。</p><p>这说明 CI 执行失败不一定来自 <code>.gitlab-ci.yml</code>。</p><p>也可能来自：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Runner host environment</span><br><span class="line">Linux user profile</span><br><span class="line">shell initialization</span><br></pre></td></tr></table></figure><hr><h1 id="14-Artifact：Job-之间如何传递文件"><a href="#14-Artifact：Job-之间如何传递文件" class="headerlink" title="14. Artifact：Job 之间如何传递文件"></a>14. Artifact：Job 之间如何传递文件</h1><p>Build：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">build-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">build</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;hello gitlab ci&quot;</span> <span class="string">&gt;</span> <span class="string">build/app.txt</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">build/</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">expire_in:</span> <span class="number">1</span> <span class="string">day</span></span><br></pre></td></tr></table></figure><p>Test：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">test-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">test</span> <span class="string">-f</span> <span class="string">build/app.txt</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">grep</span> <span class="string">&quot;hello gitlab ci&quot;</span> <span class="string">build/app.txt</span></span><br></pre></td></tr></table></figure><p>重要的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">build-job</span><br><span class="line">和</span><br><span class="line">test-job</span><br><span class="line"></span><br><span class="line">不是同一个 shell session</span><br></pre></td></tr></table></figure><p><code>build/app.txt</code> 能出现在 test job 中，是因为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">build</span><br><span class="line">↓</span><br><span class="line">GitLab 上传 artifact</span><br><span class="line">↓</span><br><span class="line">test 下载 artifact</span><br></pre></td></tr></table></figure><p>而不是因为两个 Job 共用了同一个工作目录。</p><hr><h1 id="15-Manual-Deployment"><a href="#15-Manual-Deployment" class="headerlink" title="15. Manual Deployment"></a>15. Manual Deployment</h1><p>增加：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy-job:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Deploying...&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">build/app.txt</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">when:</span> <span class="string">manual</span></span><br></pre></td></tr></table></figure><p>Pipeline：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">build ✅</span><br><span class="line">↓</span><br><span class="line">test ✅</span><br><span class="line">↓</span><br><span class="line">deploy ⏸ manual</span><br></pre></td></tr></table></figure><p>必须人工点击：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Play / Run</span><br></pre></td></tr></table></figure><p>才执行 deploy。</p><p>这个就是最简单的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Manual Gate</span><br></pre></td></tr></table></figure><hr><h1 id="16-GitLab-Environment"><a href="#16-GitLab-Environment" class="headerlink" title="16. GitLab Environment"></a>16. GitLab Environment</h1><p>把 Deployment 和 Environment 关联：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy-staging:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy-staging</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">/home/gitlab-runner/deployments/staging</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cp</span> <span class="string">build/app.txt</span> <span class="string">\</span></span><br><span class="line">      <span class="string">/home/gitlab-runner/deployments/staging/app.txt</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">environment:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">staging</span></span><br></pre></td></tr></table></figure><p>GitLab：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Operate</span><br><span class="line">→ Environments</span><br></pre></td></tr></table></figure><p>会出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">staging</span><br></pre></td></tr></table></figure><p>这时 GitLab 开始知道：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">哪个 commit</span><br><span class="line">通过哪个 pipeline</span><br><span class="line">什么时候</span><br><span class="line">部署到了哪个 environment</span><br></pre></td></tr></table></figure><hr><h1 id="17-Staging-Production"><a href="#17-Staging-Production" class="headerlink" title="17. Staging + Production"></a>17. Staging + Production</h1><p>Pipeline：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">build</span><br><span class="line">↓</span><br><span class="line">test</span><br><span class="line">↓</span><br><span class="line">staging</span><br><span class="line">↓</span><br><span class="line">production</span><br></pre></td></tr></table></figure><p>Staging 自动：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy-staging:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&#x27;</span></span><br></pre></td></tr></table></figure><p>Production manual：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy-production:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&#x27;</span></span><br><span class="line">      <span class="attr">when:</span> <span class="string">manual</span></span><br></pre></td></tr></table></figure><p>最终：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">main</span><br><span class="line">↓</span><br><span class="line">build</span><br><span class="line">↓</span><br><span class="line">test</span><br><span class="line">↓</span><br><span class="line">staging auto deploy</span><br><span class="line">↓</span><br><span class="line">production manual deploy</span><br></pre></td></tr></table></figure><hr><h1 id="18-Protected-Branch"><a href="#18-Protected-Branch" class="headerlink" title="18. Protected Branch"></a>18. Protected Branch</h1><p>为了模拟企业权限模型，创建：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">developer1</span><br></pre></td></tr></table></figure><p>角色：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Developer</span><br></pre></td></tr></table></figure><p><code>jason</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Owner</span><br></pre></td></tr></table></figure><p>保护：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main</span><br></pre></td></tr></table></figure><p>设置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Allowed to merge:</span><br><span class="line">Maintainers</span><br><span class="line"></span><br><span class="line">Allowed to push and merge:</span><br><span class="line">No one</span><br></pre></td></tr></table></figure><p>于是 Developer：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">可以：</span><br><span class="line">feature branch commit/push</span><br><span class="line">创建 MR</span><br><span class="line"></span><br><span class="line">不可以：</span><br><span class="line">直接 push main</span><br></pre></td></tr></table></figure><p>重要的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git commit</span><br></pre></td></tr></table></figure><p>永远发生在本地。</p><p>GitLab 无法禁止你本地 commit。</p><p>真正被 Server 阻止的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git push origin main</span><br></pre></td></tr></table></figure><hr><h1 id="19-MR-Pipeline-与-Branch-Pipeline"><a href="#19-MR-Pipeline-与-Branch-Pipeline" class="headerlink" title="19. MR Pipeline 与 Branch Pipeline"></a>19. MR Pipeline 与 Branch Pipeline</h1><p>一开始：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">workflow:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_BRANCH&#x27;</span></span><br></pre></td></tr></table></figure><p>结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">feature push</span><br><span class="line">→ branch pipeline</span><br><span class="line"></span><br><span class="line">创建 MR</span><br><span class="line">→ MR pipeline</span><br></pre></td></tr></table></figure><p>如果 MR 已存在，再 push：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">branch pipeline</span><br><span class="line">+</span><br><span class="line">MR pipeline</span><br></pre></td></tr></table></figure><p>同一个 commit 被 CI 两次。</p><p>对于当前实验，这没有必要。</p><hr><h1 id="20-MR-only-Pipeline"><a href="#20-MR-only-Pipeline" class="headerlink" title="20. MR-only Pipeline"></a>20. MR-only Pipeline</h1><p>最终使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">workflow:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">when:</span> <span class="string">never</span></span><br></pre></td></tr></table></figure><p>于是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">feature branch push</span><br><span class="line">→ no pipeline</span><br><span class="line"></span><br><span class="line">Create MR</span><br><span class="line">→ build</span><br><span class="line">→ test</span><br><span class="line"></span><br><span class="line">MR branch push new commit</span><br><span class="line">→ MR pipeline again</span><br><span class="line"></span><br><span class="line">Merge main</span><br><span class="line">→ main pipeline</span><br><span class="line">→ build</span><br><span class="line">→ test</span><br><span class="line">→ staging</span><br><span class="line">→ production manual</span><br></pre></td></tr></table></figure><p>这套逻辑非常清晰：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">开发代码</span><br><span class="line">↓</span><br><span class="line">MR</span><br><span class="line">↓</span><br><span class="line">CI validation</span><br><span class="line">↓</span><br><span class="line">Merge</span><br><span class="line">↓</span><br><span class="line">CD</span><br></pre></td></tr></table></figure><hr><h1 id="21-CloudIQ-风格的持续部署模型"><a href="#21-CloudIQ-风格的持续部署模型" class="headerlink" title="21. CloudIQ 风格的持续部署模型"></a>21. CloudIQ 风格的持续部署模型</h1><p>当前 <code>ci-playground</code> 模拟的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">feature</span><br><span class="line">↓</span><br><span class="line">MR</span><br><span class="line">↓</span><br><span class="line">build</span><br><span class="line">↓</span><br><span class="line">test</span><br><span class="line">↓</span><br><span class="line">merge main</span><br><span class="line">↓</span><br><span class="line">main pipeline</span><br><span class="line">↓</span><br><span class="line">staging</span><br><span class="line">↓</span><br><span class="line">production manual</span><br></pre></td></tr></table></figure><p>这比较接近持续交付系统。</p><p>实际更复杂的系统中，源码 Pipeline 与 Deployment Pipeline 甚至可能是两套系统。</p><p>例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Application Repository</span><br><span class="line">↓</span><br><span class="line">build artifact</span><br><span class="line">↓</span><br><span class="line">Deployment Portal</span><br><span class="line">↓</span><br><span class="line">CI</span><br><span class="line">↓</span><br><span class="line">INT</span><br><span class="line">↓</span><br><span class="line">PRD</span><br></pre></td></tr></table></figure><p>后续可以再通过 GitLab Multi-project Pipeline 模拟。</p><hr><h1 id="22-Waterfall-Unity-PowerStore-风格-Pipeline"><a href="#22-Waterfall-Unity-PowerStore-风格-Pipeline" class="headerlink" title="22. Waterfall &#x2F; Unity &#x2F; PowerStore 风格 Pipeline"></a>22. Waterfall &#x2F; Unity &#x2F; PowerStore 风格 Pipeline</h1><p>另外建立：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">devops-lab/waterfall-ci-playground</span><br></pre></td></tr></table></figure><p>这个模型不同。</p><p>核心逻辑：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">feature branch</span><br><span class="line">↓</span><br><span class="line">MR</span><br><span class="line">↓</span><br><span class="line">build</span><br><span class="line">↓</span><br><span class="line">deploy testbed</span><br><span class="line">↓</span><br><span class="line">integration test</span><br><span class="line">↓</span><br><span class="line">全部通过</span><br><span class="line">↓</span><br><span class="line">允许 merge</span><br><span class="line">↓</span><br><span class="line">main</span><br><span class="line"></span><br><span class="line">merge main 后</span><br><span class="line">→ 不立即 deploy</span><br><span class="line"></span><br><span class="line">到 milestone</span><br><span class="line">↓</span><br><span class="line">official release</span><br></pre></td></tr></table></figure><p>这属于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PR / MR 是完整质量 Gate</span><br></pre></td></tr></table></figure><p>而不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">merge 后继续逐环境持续部署</span><br></pre></td></tr></table></figure><hr><h1 id="23-Waterfall-Pipeline：只允许-MR-与-Release-Tag"><a href="#23-Waterfall-Pipeline：只允许-MR-与-Release-Tag" class="headerlink" title="23. Waterfall Pipeline：只允许 MR 与 Release Tag"></a>23. Waterfall Pipeline：只允许 MR 与 Release Tag</h1><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">workflow:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_TAG&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">when:</span> <span class="string">never</span></span><br></pre></td></tr></table></figure><p>于是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">push feature branch</span><br><span class="line">→ no pipeline</span><br><span class="line"></span><br><span class="line">create MR</span><br><span class="line">→ validation pipeline</span><br><span class="line"></span><br><span class="line">push tag</span><br><span class="line">→ release pipeline</span><br></pre></td></tr></table></figure><hr><h1 id="24-模拟有限的真实-Testbed"><a href="#24-模拟有限的真实-Testbed" class="headerlink" title="24. 模拟有限的真实 Testbed"></a>24. 模拟有限的真实 Testbed</h1><p>Integration Job：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">integration-testbed:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">integration</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">resource_group:</span> <span class="string">testbed-01</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== RESERVE TESTBED ===&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">\</span></span><br><span class="line">      <span class="string">/home/gitlab-runner/testbeds/testbed-01</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cp</span> <span class="string">build/app.txt</span> <span class="string">\</span></span><br><span class="line">      <span class="string">/home/gitlab-runner/testbeds/testbed-01/app.txt</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== DEPLOY ===&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cat</span> <span class="string">\</span></span><br><span class="line">      <span class="string">/home/gitlab-runner/testbeds/testbed-01/app.txt</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== INTEGRATION TEST ===&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">grep</span> <span class="string">&quot;version=&quot;</span> <span class="string">\</span></span><br><span class="line">      <span class="string">/home/gitlab-runner/testbeds/testbed-01/app.txt</span></span><br></pre></td></tr></table></figure><p>关键：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">resource_group:</span> <span class="string">testbed-01</span></span><br></pre></td></tr></table></figure><p>这表示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">同一时间只能有一个 Job 占用 testbed-01</span><br></pre></td></tr></table></figure><p>例如两个 MR：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">MR A build ✅</span><br><span class="line">MR B build ✅</span><br><span class="line"></span><br><span class="line">MR A integration</span><br><span class="line">→ Running</span><br><span class="line"></span><br><span class="line">MR B integration</span><br><span class="line">→ Waiting</span><br></pre></td></tr></table></figure><p>A 完成后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">MR B</span><br><span class="line">→ 获得 testbed</span><br></pre></td></tr></table></figure><p>这可以模拟真实环境里：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">昂贵物理机</span><br><span class="line">有限 testbed</span><br><span class="line">reserve</span><br><span class="line">机器竞争</span><br></pre></td></tr></table></figure><hr><h1 id="25-为什么-Deploy-Test-放在同一个-Job"><a href="#25-为什么-Deploy-Test-放在同一个-Job" class="headerlink" title="25. 为什么 Deploy + Test 放在同一个 Job"></a>25. 为什么 Deploy + Test 放在同一个 Job</h1><p>如果写成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">deploy-job</span><br><span class="line">↓</span><br><span class="line">test-job</span><br></pre></td></tr></table></figure><p>即使两边都使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">resource_group:</span> <span class="string">testbed-01</span></span><br></pre></td></tr></table></figure><p>Deploy Job 结束后 resource 就被释放。</p><p>另一条 Pipeline 可能立刻：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">deploy 到同一台 testbed</span><br></pre></td></tr></table></figure><p>覆盖环境。</p><p>随后第一条 Pipeline 再测试时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">测试的已经不是自己部署的内容</span><br></pre></td></tr></table></figure><p>因此如果需求是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">从部署开始</span><br><span class="line">一直到测试完成</span><br><span class="line"></span><br><span class="line">必须独占 testbed</span><br></pre></td></tr></table></figure><p>最简单的方法就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">deploy + integration test</span><br><span class="line">→ 同一个 Job</span><br></pre></td></tr></table></figure><p>整个 Job 使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">resource_group</span></span><br></pre></td></tr></table></figure><hr><h1 id="26-Merge-Gate"><a href="#26-Merge-Gate" class="headerlink" title="26. Merge Gate"></a>26. Merge Gate</h1><p>项目设置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Settings</span><br><span class="line">→ Merge Requests</span><br><span class="line">→ Pipelines must succeed</span><br></pre></td></tr></table></figure><p>然后故意测试失败：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="string">grep</span> <span class="string">&quot;THIS_WILL_FAIL&quot;</span> <span class="string">\</span></span><br><span class="line">  <span class="string">/home/gitlab-runner/testbeds/testbed-01/app.txt</span></span><br></pre></td></tr></table></figure><p>结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">build ✅</span><br><span class="line">integration ❌</span><br><span class="line">↓</span><br><span class="line">MR merge blocked</span><br></pre></td></tr></table></figure><p>修复后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">build ✅</span><br><span class="line">integration ✅</span><br><span class="line">↓</span><br><span class="line">Merge allowed</span><br></pre></td></tr></table></figure><p>这就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CI 结果成为代码进入 main 的 Gate</span><br></pre></td></tr></table></figure><hr><h1 id="27-Official-Release-Pipeline"><a href="#27-Official-Release-Pipeline" class="headerlink" title="27. Official Release Pipeline"></a>27. Official Release Pipeline</h1><p>Merge 到 main 后：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">不自动部署</span><br></pre></td></tr></table></figure><p>到了 milestone 后打 Tag：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">git checkout main</span><br><span class="line">git pull</span><br><span class="line"></span><br><span class="line">git tag -a v1.0.0 \</span><br><span class="line">  -m <span class="string">&quot;Official release v1.0.0&quot;</span></span><br><span class="line"></span><br><span class="line">git push origin v1.0.0</span><br></pre></td></tr></table></figure><p>Release Job：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">release-package:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">release</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;=== OFFICIAL RELEASE ===&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&#x27;echo &quot;Release tag: $CI_COMMIT_TAG&quot;&#x27;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">build</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">echo</span> <span class="string">\</span></span><br><span class="line">      <span class="string">&quot;official-release=$CI_COMMIT_TAG&quot;</span> <span class="string">\</span></span><br><span class="line">      <span class="string">&gt;</span> <span class="string">build/app.txt</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mkdir</span> <span class="string">-p</span> <span class="string">\</span></span><br><span class="line">      <span class="string">&quot;/home/gitlab-runner/releases/$CI_COMMIT_TAG&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="bullet">-</span> <span class="string">cp</span> <span class="string">build/app.txt</span> <span class="string">\</span></span><br><span class="line">      <span class="string">&quot;/home/gitlab-runner/releases/$CI_COMMIT_TAG/app.txt&quot;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">build/</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">&#x27;$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/&#x27;</span></span><br></pre></td></tr></table></figure><p>因此：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">MR Pipeline</span><br><span class="line">和</span><br><span class="line">Official Release Pipeline</span><br><span class="line"></span><br><span class="line">完全独立</span><br></pre></td></tr></table></figure><hr><h1 id="28-一个-YAML-小坑：script-中的冒号"><a href="#28-一个-YAML-小坑：script-中的冒号" class="headerlink" title="28. 一个 YAML 小坑：script 中的冒号"></a>28. 一个 YAML 小坑：script 中的冒号</h1><p>曾出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">jobs:release-package:script config should be</span><br><span class="line">a string or a nested array of strings</span><br></pre></td></tr></table></figure><p>原因是：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;Release tag: $CI_COMMIT_TAG&quot;</span></span><br></pre></td></tr></table></figure><p>其中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:</span><br></pre></td></tr></table></figure><p>可能被 YAML 解析器误认为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">key: value</span><br></pre></td></tr></table></figure><p>因此更安全的写法：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="string">&#x27;echo &quot;Release tag: $CI_COMMIT_TAG&quot;&#x27;</span></span><br></pre></td></tr></table></figure><p>CI YAML 不是普通 shell script。</p><p>它首先必须经过：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">YAML parser</span><br><span class="line">↓</span><br><span class="line">GitLab CI parser</span><br><span class="line">↓</span><br><span class="line">Shell</span><br></pre></td></tr></table></figure><hr><h1 id="29-当前两套-Pipeline-模型"><a href="#29-当前两套-Pipeline-模型" class="headerlink" title="29. 当前两套 Pipeline 模型"></a>29. 当前两套 Pipeline 模型</h1><p>目前实验已经形成两个独立模型。</p><h2 id="模型-A：持续交付"><a href="#模型-A：持续交付" class="headerlink" title="模型 A：持续交付"></a>模型 A：持续交付</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">feature</span><br><span class="line">↓</span><br><span class="line">MR</span><br><span class="line">↓</span><br><span class="line">build + test</span><br><span class="line">↓</span><br><span class="line">merge</span><br><span class="line">↓</span><br><span class="line">main</span><br><span class="line">↓</span><br><span class="line">staging</span><br><span class="line">↓</span><br><span class="line">production manual</span><br></pre></td></tr></table></figure><p>适合：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Web Service</span><br><span class="line">SaaS</span><br><span class="line">Cloud Platform</span><br><span class="line">持续部署系统</span><br></pre></td></tr></table></figure><hr><h2 id="模型-B：大型产品-Waterfall-Release"><a href="#模型-B：大型产品-Waterfall-Release" class="headerlink" title="模型 B：大型产品 &#x2F; Waterfall Release"></a>模型 B：大型产品 &#x2F; Waterfall Release</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">feature</span><br><span class="line">↓</span><br><span class="line">MR</span><br><span class="line">↓</span><br><span class="line">build</span><br><span class="line">↓</span><br><span class="line">deploy testbed</span><br><span class="line">↓</span><br><span class="line">integration / regression</span><br><span class="line">↓</span><br><span class="line">merge</span><br><span class="line">↓</span><br><span class="line">main</span><br><span class="line">↓</span><br><span class="line">等待 milestone</span><br><span class="line">↓</span><br><span class="line">official release</span><br></pre></td></tr></table></figure><p>适合：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">大型产品</span><br><span class="line">硬件相关测试</span><br><span class="line">昂贵 Testbed</span><br><span class="line">长 Release Cycle</span><br></pre></td></tr></table></figure><hr><h1 id="30-Jenkins-与-GitLab-CI-概念映射"><a href="#30-Jenkins-与-GitLab-CI-概念映射" class="headerlink" title="30. Jenkins 与 GitLab CI 概念映射"></a>30. Jenkins 与 GitLab CI 概念映射</h1><p>通过实验后，可以做一个大致映射：</p><table><thead><tr><th>Jenkins</th><th>GitLab</th></tr></thead><tbody><tr><td>Jenkins Controller</td><td>GitLab</td></tr><tr><td>Jenkins Agent &#x2F; Node</td><td>GitLab Runner</td></tr><tr><td>Node Label</td><td>Runner Tag</td></tr><tr><td>Jenkinsfile</td><td><code>.gitlab-ci.yml</code></td></tr><tr><td>Stage</td><td>Stage</td></tr><tr><td>Job &#x2F; Step</td><td>Job &#x2F; Script</td></tr><tr><td>archiveArtifacts</td><td>artifacts</td></tr><tr><td>input</td><td>manual job</td></tr><tr><td>Agent Pool</td><td>Runner Pool</td></tr><tr><td>Testbed Reservation</td><td>resource_group</td></tr><tr><td>PR Validation</td><td>MR Pipeline</td></tr><tr><td>Release Job</td><td>Tag Pipeline</td></tr></tbody></table><p>尤其是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GitLab Runner</span><br><span class="line">≈</span><br><span class="line">Jenkins Agent / Node</span><br></pre></td></tr></table></figure><p>它本身并不复杂。</p><p>复杂的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Runner 调度</span><br><span class="line">Executor</span><br><span class="line">资源</span><br><span class="line">权限</span><br><span class="line">隔离</span><br><span class="line">缓存</span><br><span class="line">网络</span><br><span class="line">并发</span><br><span class="line">Testbed</span><br><span class="line">稳定性</span><br></pre></td></tr></table></figure><hr><h1 id="31-已遇到的真实故障"><a href="#31-已遇到的真实故障" class="headerlink" title="31. 已遇到的真实故障"></a>31. 已遇到的真实故障</h1><p>整个实验中实际遇到过：</p><h3 id="1-GitLab-Container-Unhealthy"><a href="#1-GitLab-Container-Unhealthy" class="headerlink" title="1. GitLab Container Unhealthy"></a>1. GitLab Container Unhealthy</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/dev/shm</span><br></pre></td></tr></table></figure><p>相关。</p><hr><h3 id="2-gitlab-local-无法解析"><a href="#2-gitlab-local-无法解析" class="headerlink" title="2. gitlab.local 无法解析"></a>2. <code>gitlab.local</code> 无法解析</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Windows hosts</span><br><span class="line">GitLab external_url</span><br><span class="line">Runner URL</span><br><span class="line">clone URL</span><br></pre></td></tr></table></figure><p>没有统一。</p><hr><h3 id="3-Runner-Online-但-Job-Pending"><a href="#3-Runner-Online-但-Job-Pending" class="headerlink" title="3. Runner Online 但 Job Pending"></a>3. Runner Online 但 Job Pending</h3><p>原因：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Tagged / Untagged Runner mismatch</span><br></pre></td></tr></table></figure><hr><h3 id="4-Shell-Executor-Permission-Denied"><a href="#4-Shell-Executor-Permission-Denied" class="headerlink" title="4. Shell Executor Permission Denied"></a>4. Shell Executor Permission Denied</h3><p>原因：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">gitlab-runner user</span><br><span class="line"></span><br><span class="line">运行在</span><br><span class="line"></span><br><span class="line">/home/zxy/...</span><br></pre></td></tr></table></figure><p>权限不匹配。</p><hr><h3 id="5-Prepare-Environment-Failed"><a href="#5-Prepare-Environment-Failed" class="headerlink" title="5. Prepare Environment Failed"></a>5. Prepare Environment Failed</h3><p>原因：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/home/gitlab-runner/.bash_logout</span><br></pre></td></tr></table></figure><hr><h3 id="6-Runner-Clone-失败"><a href="#6-Runner-Clone-失败" class="headerlink" title="6. Runner Clone 失败"></a>6. Runner Clone 失败</h3><p>Runner：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.test</span><br></pre></td></tr></table></figure><p>GitLab repository URL：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.local</span><br></pre></td></tr></table></figure><hr><h3 id="7-SSH-Clone-Permission-Denied"><a href="#7-SSH-Clone-Permission-Denied" class="headerlink" title="7. SSH Clone Permission Denied"></a>7. SSH Clone Permission Denied</h3><p>直接使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gitlab.test</span><br></pre></td></tr></table></figure><p>没有命中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Host gitlab-local</span><br></pre></td></tr></table></figure><p>导致 SSH Key 不正确。</p><hr><h3 id="8-CI-YAML-Invalid"><a href="#8-CI-YAML-Invalid" class="headerlink" title="8. CI YAML Invalid"></a>8. CI YAML Invalid</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">echo</span> <span class="string">&quot;Release tag: ...&quot;</span></span><br></pre></td></tr></table></figure><p>中的冒号触发 YAML 解析问题。</p><hr><h1 id="32-当前实验覆盖范围"><a href="#32-当前实验覆盖范围" class="headerlink" title="32. 当前实验覆盖范围"></a>32. 当前实验覆盖范围</h1><p>目前已经实际验证：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">GitLab CE</span><br><span class="line">Docker deployment</span><br><span class="line">User / Group / Project</span><br><span class="line">Git identity separation</span><br><span class="line">SSH aliases</span><br><span class="line">Project Runner</span><br><span class="line">Runner Scope</span><br><span class="line">Shell Executor</span><br><span class="line">systemd Runner</span><br><span class="line">Runner working directory</span><br><span class="line">Linux permission</span><br><span class="line">Runner shell profile</span><br><span class="line">Tagged / Untagged Jobs</span><br><span class="line">Build</span><br><span class="line">Test</span><br><span class="line">Deploy</span><br><span class="line">Artifacts</span><br><span class="line">Manual Job</span><br><span class="line">Environment</span><br><span class="line">Protected Branch</span><br><span class="line">Developer / Owner permissions</span><br><span class="line">MR Pipeline</span><br><span class="line">MR-only Workflow</span><br><span class="line">Main Branch CD</span><br><span class="line">resource_group</span><br><span class="line">Testbed simulation</span><br><span class="line">Merge Gate</span><br><span class="line">Tag Release</span><br></pre></td></tr></table></figure><p>还没有完成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Docker Executor</span><br><span class="line">Docker image build</span><br><span class="line">Runner Cache</span><br><span class="line">Docker-in-Docker</span><br><span class="line">Kubernetes Executor</span><br><span class="line">Autoscaling Runner</span><br><span class="line">Runner Metrics</span><br><span class="line">Multi-project Pipeline</span><br><span class="line">Deployment Project / Deploy Portal simulation</span><br></pre></td></tr></table></figure><p>这些可以作为下一阶段。</p><hr><h1 id="33-最重要的认识"><a href="#33-最重要的认识" class="headerlink" title="33. 最重要的认识"></a>33. 最重要的认识</h1><p>最开始看 GitLab Runner 时，很容易觉得：</p><blockquote><p>Runner 不就是一台执行 Job 的机器吗？</p></blockquote><p>实验之后会发现 Runner 本身确实很简单。</p><p>真正复杂的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">哪台 Runner 执行？</span><br><span class="line">使用什么 Executor？</span><br><span class="line">以什么 Linux 用户执行？</span><br><span class="line">Job 需要什么权限？</span><br><span class="line">如何访问内部网络？</span><br><span class="line">如何管理 Secret？</span><br><span class="line">不同 Pipeline 怎么抢有限资源？</span><br><span class="line">怎样隔离不同项目？</span><br><span class="line">如何限制 Production Deployment？</span><br><span class="line">如何处理 Runner Offline / Pending / Queue？</span><br><span class="line">怎样处理 Cache 和 Artifact？</span><br></pre></td></tr></table></figure><p>因此 JD 中单独写：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GitLab Runner</span><br></pre></td></tr></table></figure><p>通常并不是要求：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">会 gitlab-runner register</span><br></pre></td></tr></table></figure><p>而是希望候选人理解：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CI Execution Infrastructure</span><br></pre></td></tr></table></figure><p>这也是 GitLab Runner 和 Jenkins Agent &#x2F; Node 最接近的地方。</p><hr><h1 id="34-下一阶段"><a href="#34-下一阶段" class="headerlink" title="34. 下一阶段"></a>34. 下一阶段</h1><p>下一阶段计划：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Shell Executor</span><br><span class="line">↓</span><br><span class="line">Docker Executor</span><br><span class="line">↓</span><br><span class="line">不同 Docker image 执行 Job</span><br><span class="line">↓</span><br><span class="line">Cache</span><br><span class="line">↓</span><br><span class="line">Docker image build</span><br><span class="line">↓</span><br><span class="line">Kubernetes Executor</span><br></pre></td></tr></table></figure><p>然后再把 Cloud 风格 Pipeline 拆成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Application Project</span><br><span class="line">↓</span><br><span class="line">Artifact</span><br><span class="line">↓</span><br><span class="line">Deployment Project</span><br><span class="line">↓</span><br><span class="line">CI</span><br><span class="line">↓</span><br><span class="line">INT</span><br><span class="line">↓</span><br><span class="line">PRD</span><br></pre></td></tr></table></figure><p>进一步模拟真实的 Deploy Portal 与多项目交付体系。</p>]]>
    </content>
    <id>https://zhouxingyu.dev/posts/GitLab-CI-CD-Local-Lab/</id>
    <link href="https://zhouxingyu.dev/posts/GitLab-CI-CD-Local-Lab/"/>
    <published>2026-09-13T12:30:00.000Z</published>
    <summary>记录在WSL2中构建GitLab CI/CD的完整过程。</summary>
    <title>在 WSL2 中搭建 GitLab Community Edition</title>
    <updated>2026-09-16T02:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhou Xingyu</name>
    </author>
    <category term="Agentic AI" scheme="https://zhouxingyu.dev/categories/Agentic-AI/"/>
    <category term="Agentic AI" scheme="https://zhouxingyu.dev/tags/Agentic-AI/"/>
    <category term="Agents" scheme="https://zhouxingyu.dev/tags/Agents/"/>
    <category term="Workflow" scheme="https://zhouxingyu.dev/tags/Workflow/"/>
    <category term="Design Patterns" scheme="https://zhouxingyu.dev/tags/Design-Patterns/"/>
    <content>
      <![CDATA[<h3 id="Chapter-1-Prompt-Chaining"><a href="#Chapter-1-Prompt-Chaining" class="headerlink" title="Chapter 1 Prompt Chaining"></a>Chapter 1 Prompt Chaining</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install -U langchain langchain-community langchain-deepseek langgraph python-dotenv</span><br></pre></td></tr></table></figure><p>Hands-On Code Example</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> langchain_deepseek <span class="keyword">import</span> ChatDeepSeek</span><br><span class="line"><span class="keyword">from</span> langchain_core.prompts <span class="keyword">import</span> ChatPromptTemplate</span><br><span class="line"><span class="keyword">from</span> langchain_core.output_parsers <span class="keyword">import</span> StrOutputParser</span><br><span class="line"></span><br><span class="line"><span class="comment">## 为了更好地安全性，从 .env 文件加载环境变量</span></span><br><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br><span class="line">load_dotenv()</span><br><span class="line"><span class="comment">## 确保你的 DEEPSEEK_API_KEY 在 .env 文件中正确设置</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 初始化语言模型（此处使用ChatDeepSeek）</span></span><br><span class="line">llm = ChatDeepSeek(</span><br><span class="line">    model=<span class="string">&quot;deepseek-chat&quot;</span>,</span><br><span class="line">    temperature=<span class="number">0</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">## --- 提示词 1： 提取信息 ---</span></span><br><span class="line">prompt_extract = ChatPromptTemplate.from_template(</span><br><span class="line">    <span class="string">&quot;请从以下文本中提取技术规格：\n\n&#123;text_input&#125;&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">## --- 提示词 2： 转换为 JSON ---</span></span><br><span class="line">prompt_transform = ChatPromptTemplate.from_template(</span><br><span class="line">    <span class="string">&quot;请将以下规格转换为 JSON 对象，使用&#x27;cpu&#x27;、&#x27;memory&#x27; 和 &#x27;storage&#x27; 作为键：\n\n&#123;specifications&#125;&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">## --- 利用 LCEL 构建处理链</span></span><br><span class="line"><span class="comment">## StrOutputParser 用于将 LLM 的消息输出解析为简单字符串。</span></span><br><span class="line">extraction_chain = prompt_extract | llm | StrOutputParser()</span><br><span class="line"></span><br><span class="line"><span class="comment">## 完整的链将提取链的输出传递到转换提示词的 &#x27;specifications&#x27; 参数中</span></span><br><span class="line">full_chain = (</span><br><span class="line">    &#123; <span class="string">&quot;specifications&quot;</span>: extraction_chain &#125;</span><br><span class="line">    | prompt_transform</span><br><span class="line">    | llm</span><br><span class="line">    | StrOutputParser()</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">## --- 运行链 ---</span></span><br><span class="line">input_text = <span class="string">&quot;新款笔记本电脑配备了3.5 GHz 八核处理器，16GB 内存和1TB NVMe SSD存储。&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 使用输入文本字典执行链。</span></span><br><span class="line">final_result = full_chain.invoke(&#123; <span class="string">&quot;text_input&quot;</span>: input_text &#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">## 输出最终结果</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;\n--- 最终 JSON 输出---&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(final_result)</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="Chapter-2-Routing"><a href="#Chapter-2-Routing" class="headerlink" title="Chapter 2 Routing"></a>Chapter 2 Routing</h3><p>The mechanism of the Routing pattern:</p><ul><li>LLM-based Routing</li><li>Embedding-based Routing</li><li>Rule-based Routing</li><li>Machine Learning Model-Based Routing</li></ul><p>Practical Applications &amp; Use Cases</p><ul><li>Human-computer interaction</li><li>Automated data and document processing pipelines</li><li>Multiple Agents</li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># dependencies:</span></span><br><span class="line"><span class="comment"># pip install -U langchain langchain-deepseek python-dotenv</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> langchain_deepseek <span class="keyword">import</span> ChatDeepSeek</span><br><span class="line"><span class="keyword">from</span> langchain_core.prompts <span class="keyword">import</span> ChatPromptTemplate</span><br><span class="line"><span class="keyword">from</span> langchain_core.output_parsers <span class="keyword">import</span> StrOutputParser</span><br><span class="line"><span class="keyword">from</span> langchain_core.runnables <span class="keyword">import</span> RunnablePassthrough, RunnableBranch</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 1. Configuration</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Ensure your API key environment variable is set (e.g., DEEPSEEK_API_KEY)</span></span><br><span class="line"><span class="comment"># 确保你的API密钥环境变量已设置（如DEEPSEEK_API_KEY）</span></span><br><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br><span class="line">load_dotenv()</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    llm = ChatDeepSeek(</span><br><span class="line">        model=<span class="string">&quot;deepseek-v4-flash&quot;</span>,</span><br><span class="line">        temperature=<span class="number">0</span>,</span><br><span class="line">        extra_body=&#123;</span><br><span class="line">            <span class="string">&quot;thinking&quot;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;type&quot;</span>: <span class="string">&quot;disabled&quot;</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;Language model initialized: <span class="subst">&#123;llm.model_name&#125;</span>&quot;</span>)</span><br><span class="line"><span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;Error initializing language model: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">    llm = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 2. Simulated Sub-Agent Handlers</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">booking_handler</span>(<span class="params">request: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Simulates the Booking Agent handing a request.&quot;&quot;&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n--- DELEGATING TO BOOKING HANDLER ---&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        <span class="string">f&quot;Booking Handler processed request: &#x27;<span class="subst">&#123;request&#125;</span>&#x27;.&quot;</span></span><br><span class="line">        <span class="string">f&quot;confirmed for request: <span class="subst">&#123;request&#125;</span>&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">info_handler</span>(<span class="params">request: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Simulates the Info Agent handling a request.&quot;&quot;&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n--- DELEGATING TO INFO HANDLER ---&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        <span class="string">f&quot;Info Handler processed request: &#x27;<span class="subst">&#123;request&#125;</span>&#x27;. &quot;</span></span><br><span class="line">        <span class="string">f&quot;Result: Simulated information retrieval.&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">unclear_handler</span>(<span class="params">request: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Handles requests that couldn&#x27;t be delegated.&quot;&quot;&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\n--- HANDLING UNCLEAR REQUEST ---&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        <span class="string">f&quot;Coordinator could not delegate request: &#x27;<span class="subst">&#123;request&#125;</span>&#x27;. &quot;</span></span><br><span class="line">        <span class="string">f&quot;Please clarify.&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 3. Coordinator Router Prompt</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line">coordinator_router_prompt = ChatPromptTemplate.from_messages([</span><br><span class="line">    (<span class="string">&quot;system&quot;</span>, </span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">Analyze the user&#x27;s request and determine which specialist handler </span></span><br><span class="line"><span class="string">should process it.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- If the request is related to booking flights or hotels,</span></span><br><span class="line"><span class="string">  output &#x27;booker&#x27;.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- For all other general inforamtion questions,</span></span><br><span class="line"><span class="string">  output &#x27;info&#x27;.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- If the request is unclear or doesn&#x27;t fit either category,</span></span><br><span class="line"><span class="string">  output &#x27;unclear&#x27;.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">ONLY output one word:</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">booker</span></span><br><span class="line"><span class="string">info</span></span><br><span class="line"><span class="string">unclear</span></span><br><span class="line"><span class="string"> &quot;&quot;&quot;</span></span><br><span class="line">    ),</span><br><span class="line">    (<span class="string">&quot;user&quot;</span>, <span class="string">&quot;&#123;request&#125;&quot;</span>)</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 4. Router Chain</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> llm:</span><br><span class="line">    coordinator_router_chain = (</span><br><span class="line">        coordinator_router_prompt | llm | StrOutputParser()</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 5. Branch Handlers</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line">branches = &#123;</span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;booker&quot;</span>: RunnablePassthrough.assign(</span><br><span class="line">        output=<span class="keyword">lambda</span> x: booking_handler(</span><br><span class="line">            x[<span class="string">&#x27;request&#x27;</span>][<span class="string">&#x27;request&#x27;</span>]</span><br><span class="line">        )</span><br><span class="line">    ),</span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;info&quot;</span>: RunnablePassthrough.assign(</span><br><span class="line">        output=<span class="keyword">lambda</span> x: info_handler(</span><br><span class="line">            x[<span class="string">&#x27;request&#x27;</span>][<span class="string">&#x27;request&#x27;</span>]</span><br><span class="line">        )</span><br><span class="line">    ),</span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;unclear&quot;</span>: RunnablePassthrough.assign(</span><br><span class="line">        output=<span class="keyword">lambda</span> x: unclear_handler(</span><br><span class="line">            x[<span class="string">&#x27;request&#x27;</span>][<span class="string">&#x27;request&#x27;</span>]</span><br><span class="line">        )</span><br><span class="line">    ),</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 6. Delegation Logic</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line">delegation_branch = RunnableBranch(</span><br><span class="line">    (</span><br><span class="line">        <span class="keyword">lambda</span> x:x[<span class="string">&quot;decision&quot;</span>].strip().lower() == <span class="string">&quot;booker&quot;</span>,</span><br><span class="line">        branches[<span class="string">&quot;booker&quot;</span>]</span><br><span class="line">    ),</span><br><span class="line">    (</span><br><span class="line">        <span class="keyword">lambda</span> x:x[<span class="string">&quot;decision&quot;</span>].strip().lower() == <span class="string">&quot;info&quot;</span>,</span><br><span class="line">        branches[<span class="string">&quot;info&quot;</span>]</span><br><span class="line">    ),</span><br><span class="line">    <span class="comment"># Default branch for unclear requests</span></span><br><span class="line">    branches[<span class="string">&quot;unclear&quot;</span>]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 7. Coordinator Agent</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">debug_router_result</span>(<span class="params">x</span>):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;\nRouter Decision: <span class="subst">&#123;x[<span class="string">&#x27;decision&#x27;</span>].strip()&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span> x</span><br><span class="line"></span><br><span class="line">coordinator_agent = &#123;</span><br><span class="line">    <span class="comment"># Run request through LLM router</span></span><br><span class="line">    <span class="string">&quot;decision&quot;</span>: coordinator_router_chain,</span><br><span class="line">    <span class="comment"># Preserve original request</span></span><br><span class="line">    <span class="string">&quot;request&quot;</span>: RunnablePassthrough(),</span><br><span class="line">&#125; | debug_router_result | delegation_branch | (</span><br><span class="line">    <span class="keyword">lambda</span> x: x[<span class="string">&quot;output&quot;</span>]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"><span class="comment"># 8. Example Usage</span></span><br><span class="line"><span class="comment"># ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> llm:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;\nSkipping execution due to LLM initialization failure.&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;--- DeepSeek Routing Demo ---&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;Type &#x27;exit&#x27; or &#x27;quit&#x27; to stop.\n&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        request = <span class="built_in">input</span>(<span class="string">&quot;Please enter your request: &quot;</span>).strip()</span><br><span class="line"></span><br><span class="line">        <span class="comment"># Exit the interactive Loop</span></span><br><span class="line">        <span class="keyword">if</span> request.lower() <span class="keyword">in</span> [<span class="string">&quot;exit&quot;</span>, <span class="string">&quot;quit&quot;</span>]:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Exiting...&quot;</span>)</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># Ignore empty input</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> request:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Request cannot be empty.\n&quot;</span>)</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            result = coordinator_agent.invoke(&#123;</span><br><span class="line">                <span class="string">&quot;request&quot;</span>: request</span><br><span class="line">            &#125;)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;\nFinal Result: <span class="subst">&#123;result&#125;</span>\n&quot;</span>)</span><br><span class="line">        <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;\nError processing request: <span class="subst">&#123;e&#125;</span>\n&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">#    print(&quot;--- Running with a booking request ---&quot;)</span></span><br><span class="line"><span class="comment">#    request_a = &quot;Book me a flight to London.&quot;</span></span><br><span class="line"><span class="comment">#    result_a = coordinator_agent.invoke(&#123;&quot;request&quot;: request_a&#125;)</span></span><br><span class="line"><span class="comment">#    print(f&quot;Final Result A: &#123;result_a&#125;&quot;)</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment">#    print(&quot;\n--- Running with an info request ---&quot;)</span></span><br><span class="line"><span class="comment">#    request_b = &quot;What is the weather like in Paris?&quot;</span></span><br><span class="line"><span class="comment">#    result_b = coordinator_agent.invoke(&#123;&quot;request&quot;: request_b&#125;)</span></span><br><span class="line"><span class="comment">#    print(f&quot;Final Result B: &#123;result_b&#125;&quot;)</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment">#    print(&quot;\n--- Running with an unclear request ---&quot;)</span></span><br><span class="line"><span class="comment">#    request_c = &quot;I need something.&quot;</span></span><br><span class="line"><span class="comment">#    result_c = coordinator_agent.invoke(&#123;&quot;request&quot;: request_c&#125;)</span></span><br><span class="line"><span class="comment">#    print(f&quot;Final Result C: &#123;result_c&#125;&quot;)</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure><p>Google ADK Example:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br></pre></td><td class="code"><pre><span class="line"># Copyright (c) 2025 Marco Fago</span><br><span class="line">#</span><br><span class="line"># This code is licensed under the MIT License.</span><br><span class="line"># See the LICENSE file in the repository for the full license text.</span><br><span class="line"></span><br><span class="line">import uuid</span><br><span class="line">from typing import Dict, Any, Optional</span><br><span class="line"></span><br><span class="line">from google.adk.agents import Agent</span><br><span class="line">from google.adk.runners import InMemoryRunner</span><br><span class="line">from google.adk.tools import FunctionTool</span><br><span class="line">from google.genai import types</span><br><span class="line">from google.adk.events import Event</span><br><span class="line"></span><br><span class="line"># Colab 代码链接：https://colab.research.google.com/drive/10wxRlPyDJ70pPEtWWA3Aa3tjvfobrB3m</span><br><span class="line"></span><br><span class="line"># 安装依赖</span><br><span class="line"># pip install google-genai google-adk</span><br><span class="line"># export GOOGLE_API_KEY=...</span><br><span class="line"></span><br><span class="line"># --- Define Tool Functions ---</span><br><span class="line"># These functions simulate the actions of the specialist agents.</span><br><span class="line"># --- 定义工具函数 ---</span><br><span class="line"># 这些函数模拟了专业智能体的具体行动。</span><br><span class="line"></span><br><span class="line">def booking_handler(request: str) -&gt; str:</span><br><span class="line">    &quot;&quot;&quot;</span><br><span class="line">    Handles booking requests for flights and hotels.</span><br><span class="line">    Args:</span><br><span class="line">        request: The user&#x27;s request for a booking.</span><br><span class="line">    Returns:</span><br><span class="line">        A confirmation message that the booking was handled.</span><br><span class="line">    &quot;&quot;&quot;</span><br><span class="line">    print(&quot;-------------------------- Booking Handler Called ----------------------------&quot;)</span><br><span class="line">    return f&quot;Booking action for &#x27;&#123;request&#125;&#x27; has been simulated.&quot;</span><br><span class="line"></span><br><span class="line">def info_handler(request: str) -&gt; str:</span><br><span class="line">    &quot;&quot;&quot;</span><br><span class="line">    Handles general information requests.</span><br><span class="line">    Args:</span><br><span class="line">        request: The user&#x27;s question.</span><br><span class="line">    Returns:</span><br><span class="line">        A message indicating the information request was handled.</span><br><span class="line">    &quot;&quot;&quot;</span><br><span class="line">    print(&quot;-------------------------- Info Handler Called ----------------------------&quot;)</span><br><span class="line">    return f&quot;Information request for &#x27;&#123;request&#125;&#x27;. Result: Simulated information retrieval.&quot;</span><br><span class="line"></span><br><span class="line">def unclear_handler(request: str) -&gt; str:</span><br><span class="line">    &quot;&quot;&quot;Handles requests that couldn&#x27;t be delegated.&quot;&quot;&quot;</span><br><span class="line">    return f&quot;Coordinator could not delegate request: &#x27;&#123;request&#125;&#x27;. Please clarify.&quot;</span><br><span class="line"></span><br><span class="line"># --- Create Tools from Functions ---</span><br><span class="line"># --- 从函数创建工具 ---</span><br><span class="line">booking_tool = FunctionTool(booking_handler)</span><br><span class="line">info_tool = FunctionTool(info_handler)</span><br><span class="line"></span><br><span class="line"># Define specialized sub-agents equipped with their respective tools</span><br><span class="line"># --- 定义配备了各自工具的专业子智能体 ---</span><br><span class="line">booking_agent = Agent(</span><br><span class="line">    name=&quot;Booker&quot;,</span><br><span class="line">    model=&quot;gemini-2.0-flash&quot;,</span><br><span class="line">    description=&quot;A specialized agent that handles all flight and hotel booking requests by calling the booking tool.&quot;,</span><br><span class="line">    tools=[booking_tool]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">info_agent = Agent(</span><br><span class="line">    name=&quot;Info&quot;,</span><br><span class="line">    model=&quot;gemini-2.0-flash&quot;,</span><br><span class="line">    description=&quot;A specialized agent that provides general information and answers user questions by calling the info tool.&quot;,</span><br><span class="line">    tools=[info_tool]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"># Define the parent agent with explicit delegation instructions</span><br><span class="line"># --- 定义带有明确委派指令的父智能体 ---</span><br><span class="line">coordinator = Agent(</span><br><span class="line">    name=&quot;Coordinator&quot;,</span><br><span class="line">    model=&quot;gemini-2.0-flash&quot;,</span><br><span class="line">    instruction=(</span><br><span class="line">        &quot;You are the main coordinator. Your only task is to analyze incoming user requests &quot;</span><br><span class="line">        &quot;and delegate them to the appropriate specialist agent. Do not try to answer the user directly.\n&quot;</span><br><span class="line">        &quot;- For any requests related to booking flights or hotels, delegate to the &#x27;Booker&#x27; agent.\n&quot;</span><br><span class="line">        &quot;- For all other general information questions, delegate to the &#x27;Info&#x27; agent.&quot;</span><br><span class="line">    ),</span><br><span class="line">    description=&quot;A coordinator that routes user requests to the correct specialist agent.&quot;,</span><br><span class="line">    # The presence of sub_agents enables LLM-driven delegation (Auto-Flow) by default.</span><br><span class="line">    # 定义了 sub_agents 默认就会启用由大语言模型驱动的委派。</span><br><span class="line">    sub_agents=[booking_agent, info_agent]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"># --- Execution Logic ---</span><br><span class="line"># --- 执行逻辑 ---</span><br><span class="line">def run_coordinator(runner: InMemoryRunner, request: str):</span><br><span class="line">    &quot;&quot;&quot;Runs the coordinator agent with a given request and delegates.&quot;&quot;&quot;</span><br><span class="line">    &quot;&quot;&quot;用给定的请求运行协调员智能体并进行委派。&quot;&quot;&quot;</span><br><span class="line">    print(f&quot;\n--- Running Coordinator with request: &#x27;&#123;request&#125;&#x27; ---&quot;)</span><br><span class="line">    final_result = &quot;&quot;</span><br><span class="line">    try:</span><br><span class="line">        user_id = &quot;user_123&quot;</span><br><span class="line">        session_id = str(uuid.uuid4())</span><br><span class="line">        runner.session_service.create_session(</span><br><span class="line">            app_name=runner.app_name, user_id=user_id, session_id=session_id</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">        for event in runner.run(</span><br><span class="line">            user_id=user_id,</span><br><span class="line">            session_id=session_id,</span><br><span class="line">            new_message=types.Content(</span><br><span class="line">                role=&#x27;user&#x27;,</span><br><span class="line">                parts=[types.Part(text=request)]</span><br><span class="line">            ),</span><br><span class="line">        ):</span><br><span class="line">            if event.is_final_response() and event.content:</span><br><span class="line">                # Try to get text directly from event.content to avoid iterating parts</span><br><span class="line">                if hasattr(event.content, &#x27;text&#x27;) and event.content.text:</span><br><span class="line">                     final_result = event.content.text</span><br><span class="line">                elif event.content.parts:</span><br><span class="line">                    # Fallback: Iterate through parts and extract text (might trigger warning)</span><br><span class="line">                    text_parts = [part.text for part in event.content.parts if part.text]</span><br><span class="line">                    final_result = &quot;&quot;.join(text_parts)</span><br><span class="line">                # Assuming the loop should break after the final response</span><br><span class="line">                break</span><br><span class="line"></span><br><span class="line">        print(f&quot;Coordinator Final Response: &#123;final_result&#125;&quot;)</span><br><span class="line">        return final_result</span><br><span class="line">    except Exception as e:</span><br><span class="line">        print(f&quot;An error occurred while processing your request: &#123;e&#125;&quot;)</span><br><span class="line">        return f&quot;An error occurred while processing your request: &#123;e&#125;&quot;</span><br><span class="line"></span><br><span class="line">def main():</span><br><span class="line">    &quot;&quot;&quot;Main function to run the ADK example.&quot;&quot;&quot;</span><br><span class="line">    &quot;&quot;&quot;运行 ADK 示例的主函数。&quot;&quot;&quot;</span><br><span class="line">    print(&quot;--- Google ADK Routing Example (ADK Auto-Flow Style) ---&quot;)</span><br><span class="line">    print(&quot;Note: This requires Google ADK installed and authenticated.&quot;)</span><br><span class="line"></span><br><span class="line">    runner = InMemoryRunner(coordinator)</span><br><span class="line">    # Example Usage</span><br><span class="line">    # 使用示例</span><br><span class="line">    result_a = run_coordinator(runner, &quot;Book me a hotel in Paris.&quot;)</span><br><span class="line">    print(f&quot;Final Output A: &#123;result_a&#125;&quot;)</span><br><span class="line">    result_b = run_coordinator(runner, &quot;What is the highest mountain in the world?&quot;)</span><br><span class="line">    print(f&quot;Final Output B: &#123;result_b&#125;&quot;)</span><br><span class="line">    result_c = run_coordinator(runner, &quot;Tell me a random fact.&quot;) # Should go to Info</span><br><span class="line">    print(f&quot;Final Output C: &#123;result_c&#125;&quot;)</span><br><span class="line">    result_d = run_coordinator(runner, &quot;Find flights to Tokyo next month.&quot;) # Should go to Booker</span><br><span class="line">    print(f&quot;Final Output D: &#123;result_d&#125;&quot;)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">if __name__ == &quot;__main__&quot;:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://zhouxingyu.dev/posts/Agentic-Design-Patterns/</id>
    <link href="https://zhouxingyu.dev/posts/Agentic-Design-Patterns/"/>
    <published>2026-09-09T01:30:00.000Z</published>
    <summary>记录《Agentic Desin Patterns》阅读笔记以及localized source code。</summary>
    <title>《Agentic Design Patterns》阅读记录</title>
    <updated>2026-09-11T11:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhou Xingyu</name>
    </author>
    <category term="Observability" scheme="https://zhouxingyu.dev/categories/Observability/"/>
    <category term="WSL2" scheme="https://zhouxingyu.dev/tags/WSL2/"/>
    <category term="DevOps" scheme="https://zhouxingyu.dev/tags/DevOps/"/>
    <category term="Prometheus" scheme="https://zhouxingyu.dev/tags/Prometheus/"/>
    <category term="Grafana" scheme="https://zhouxingyu.dev/tags/Grafana/"/>
    <category term="Android 4.4.4 OS" scheme="https://zhouxingyu.dev/tags/Android-4-4-4-OS/"/>
    <content>
      <![CDATA[<h1 id="Legacy-Edge-Lab-阶段全记录：旧设备的边缘监控、状态显示与退役决策"><a href="#Legacy-Edge-Lab-阶段全记录：旧设备的边缘监控、状态显示与退役决策" class="headerlink" title="Legacy Edge Lab 阶段全记录：旧设备的边缘监控、状态显示与退役决策"></a>Legacy Edge Lab 阶段全记录：旧设备的边缘监控、状态显示与退役决策</h1><p>记录范围：2026 年 9 月 4 日至 9 月 9 日。文中时间均为北京时间，除非另有注明。</p><p>本文依据项目清单、实现说明和阶段验证记录整理。版本号、性能数据和设备状态描述的是本次实验环境，不代表最新软件版本，也不代表同型号设备普遍具备这些能力。外观和操作体验来自现场反馈，系统信息来自实际查询，连续性数据来自采集记录与 Prometheus。</p><h2 id="1-项目起点：给旧设备找到边界清楚的工作"><a href="#1-项目起点：给旧设备找到边界清楚的工作" class="headerlink" title="1. 项目起点：给旧设备找到边界清楚的工作"></a>1. 项目起点：给旧设备找到边界清楚的工作</h2><p>Legacy Edge Lab 的起点是一组闲置设备：小米手机 3、小米平板 1、Kindle、旧 iPhone 和旧笔记本。项目希望用它们学习边缘采集、网络可观测性、故障恢复、跨平台差异和设备生命周期管理。</p><p>最终得到的结果并不是五台设备全部上线。截至本轮收尾，小米 3 已成为通过验证的受控 Android 边缘探针；小米平板 1 和旧笔记本退役；Kindle 完成 Phase 0，适合作为按需唤醒的低频状态页；旧 iPhone 仍待评估。</p><p>这个结果符合项目最初的工程顺序：简单、稳定、可逆、低资源、可维护、可观测、容易恢复，并适配旧硬件。每增加一层组件，都需要说明它解决了什么已观察到的问题。</p><p>设备承担边缘采集、显示或兼容性测试，重计算留在主 Homelab。本轮没有把 Prometheus、Grafana 后端、数据库或容器平台搬到旧手机和平板上，也没有为了找到用途去 root、解锁或刷机。</p><h2 id="2-阶段路线与最终架构"><a href="#2-阶段路线与最终架构" class="headerlink" title="2. 阶段路线与最终架构"></a>2. 阶段路线与最终架构</h2><p>各设备的阶段编号独立。例如，小米 3 的 Phase 3C 完成后，小米平板仍从自己的 Phase 0 开始。</p><table><thead><tr><th>阶段</th><th>要回答的问题</th><th>实际结果</th></tr></thead><tbody><tr><td>小米 3 Phase 0 &#x2F; 0B</td><td>真机是什么状态，能否继续观察？</td><td>完成只读清单、基础网络检查和 30 分钟基线</td></tr><tr><td>小米 3 Phase 1</td><td>现有运行时能做什么？</td><td>选择系统 Shell、已有 Python 2.6.2 和主机 ADB 采集</td></tr><tr><td>小米 3 Phase 2A</td><td>最小探针是否可运行？</td><td>有界测量、稳定 JSON 和独立失败处理得到验证</td></tr><tr><td>小米 3 Phase 2B</td><td>USB 断开后会怎样？</td><td>断开期间持续输出，重新连接后自动恢复；修正失败分类</td></tr><tr><td>小米 3 Phase 2C</td><td>接入真实 VPS 后能连续运行吗？</td><td>两小时、121 次采集全部完成，零测量执行失败</td></tr><tr><td>小米 3 Phase 2D</td><td>更长运行中能否区分网络事件与探针故障？</td><td>八小时、481 次采集完整，外网异常自动恢复</td></tr><tr><td>小米 3 Phase 3A</td><td>怎样接入指标系统且不让抓取等待 ADB？</td><td>实现主机端后台采集与快照读取分离的 exporter</td></tr><tr><td>小米 3 Phase 3A.1</td><td>真正的 Prometheus 能否正确抓取、识别故障？</td><td>实际入库、PromQL、陈旧数据和恢复语义得到验证</td></tr><tr><td>小米 3 Phase 3B</td><td>完整链路能否持续运行一天？</td><td>精确 24 小时窗口内 2,881 次抓取完整，无传输或采集器故障</td></tr><tr><td>小米 3 Phase 3C</td><td>仪表盘能否解释实际发生过的问题？</td><td>完成 Grafana 集成，用历史异常验证诊断视图</td></tr><tr><td>小米平板 Phase 0A—0C</td><td>能否成为低维护显示终端？</td><td>完成清单，确认 Chrome 的 Grafana 脚本解析障碍和 HTTPS 信任问题</td></tr><tr><td>小米平板 Phase 0D</td><td>是否进入首次显示稳定性测试？</td><td>未开始；轻量页面测试访问再次中断，停止继续投入</td></tr><tr><td>设备生命周期收尾</td><td>哪些设备仍值得保留为 Lab 资源？</td><td>平板和无法开机的旧笔记本正式退役</td></tr><tr><td>Kindle Phase 0A—0B</td><td>外观、供电、型号和基础连接是否适合继续测试？</td><td>GREEN；确认第 10 代、当前固件、存储、Wi-Fi、浏览器与 USB 行为</td></tr><tr><td>Kindle Phase 0C—0D</td><td>内置浏览器适合哪一种显示层级？</td><td>HTML&#x2F;CSS&#x2F;PNG&#x2F;基础 JS&#x2F;HTTPS&#x2F;meta-refresh 可用，选择无 JS 的 Tier B</td></tr><tr><td>Kindle Phase 0F</td><td>低频刷新和睡眠恢复是否可接受？</td><td>10 分钟刷新及约 38 分钟窗口通过；默认自动睡眠，定位为按需唤醒显示</td></tr></tbody></table><p>最终已经验证的小米 3 路径如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">小米 3</span><br><span class="line">  ├─ Wi-Fi：实际发出 ICMP、DNS、TCP 测量</span><br><span class="line">  └─ 系统 Shell / 已有 Python 2.6.2</span><br><span class="line">          │</span><br><span class="line">          │ USB / ADB：主机发起临时命令并读取结果</span><br><span class="line">          ▼</span><br><span class="line">主 Homelab 的 Windows / WSL</span><br><span class="line">  └─ collector：约每 60 秒采集一次</span><br><span class="line">          ▼</span><br><span class="line">     exporter：后台采集 + 有界内存快照</span><br><span class="line">          │ 127.0.0.1:9108/metrics</span><br><span class="line">          ▼</span><br><span class="line">     Prometheus：每 30 秒抓取</span><br><span class="line">          │ 本机 TSDB</span><br><span class="line">          ▼</span><br><span class="line">     Grafana：本机回环访问，保留登录认证</span><br><span class="line">          ▼</span><br><span class="line">     电脑浏览器</span><br></pre></td></tr></table></figure><p>测量从手机的网络栈发出，结果通过 USB 返回主机。它仍有 Android 边缘观测价值，但不是已经脱离主机、能自主联网发送指标的独立探针。</p><h2 id="3-小米-3-Phase-0：先查实机，再讨论用途"><a href="#3-小米-3-Phase-0：先查实机，再讨论用途" class="headerlink" title="3. 小米 3 Phase 0：先查实机，再讨论用途"></a>3. 小米 3 Phase 0：先查实机，再讨论用途</h2><h3 id="3-1-硬件与系统基线"><a href="#3-1-硬件与系统基线" class="headerlink" title="3.1 硬件与系统基线"></a>3.1 硬件与系统基线</h3><p>2026 年 9 月 4 日，通过已授权的只读 ADB 查询得到以下基线：</p><table><thead><tr><th>项目</th><th>实测结果</th></tr></thead><tbody><tr><td>制造商 &#x2F; 型号 &#x2F; 代号</td><td>Xiaomi &#x2F; MI 3 &#x2F; <code>pisces</code></td></tr><tr><td>Android &#x2F; API</td><td>4.4.4 &#x2F; 19</td></tr><tr><td>MIUI &#x2F; 构建</td><td>V8 &#x2F; <code>V8.0.1.0.KXCCNDG</code> &#x2F; <code>KTU84P</code></td></tr><tr><td>安全补丁属性</td><td>2016-08-01</td></tr><tr><td>构建类型 &#x2F; 标签</td><td><code>user</code> &#x2F; <code>release-keys</code></td></tr><tr><td>内核</td><td>Linux 3.4.35-g8f1d7b1</td></tr><tr><td>CPU &#x2F; ABI</td><td>ARMv7 Processor rev 2，四个逻辑处理器；<code>armeabi-v7a</code>，兼容 <code>armeabi</code></td></tr><tr><td>系统可用总 RAM</td><td>2,015,048 kB，约 1.92 GiB</td></tr><tr><td>Swap 总量</td><td>786,428 kB</td></tr><tr><td>数据与共享存储</td><td><code>/data</code> 约 3.4 GiB；<code>/storage_int</code> 约 9.1 GiB</td></tr><tr><td>屏幕</td><td>Android 报告 1080×1920、480 dpi</td></tr><tr><td>USB</td><td><code>mtp,adb</code>，连接已授权</td></tr><tr><td>已有浏览器</td><td>小米浏览器 8.1.6</td></tr></tbody></table><p>没有把产品宣传页的规格直接填进清单。准确的 SoC 营销型号、物理闪存标称容量、实际 Wi-Fi 频段能力等，没有在本轮得到足够证据。</p><p>ADB Shell 是 UID 2000；查询时没有发现常见位置的 <code>su</code>，构建为 <code>user/release-keys</code>。这些观察不能证明 bootloader 已锁定，也不能证明设备从未改动过。</p><h3 id="3-2-安全与-30-分钟基线"><a href="#3-2-安全与-30-分钟基线" class="headerlink" title="3.2 安全与 30 分钟基线"></a>3.2 安全与 30 分钟基线</h3><p>用户反馈手机外观正常，无异味、无变形，使用原有配件。初次电池遥测为 13%、40.3°C；这一瞬时读数被保留，没有直接解释为电池故障，也没有把系统返回的 <code>good</code> 当成真实健康认证。</p><p>随后在 17:17—17:47 做了四次约间隔 10 分钟的只读采样：</p><ul><li>ADB 和 Wi-Fi 持续可用，uptime 单调增加，没有重启证据。</li><li>电量由 22% 增至 33%，USB 充电持续。</li><li>电池温度为 34.5—34.8°C，没有上升趋势。</li><li>内存、缓存和 Swap 存在波动，没有在这个短窗口内表现出持续恶化。</li></ul><p>这是低负载基线，不是显示测试、满负载测试或续航测试。安全等级定为 <strong>YELLOW</strong>：可以继续受控实验，尚未批准无人值守运行。</p><h3 id="3-3-最小网络能力"><a href="#3-3-最小网络能力" class="headerlink" title="3.3 最小网络能力"></a>3.3 最小网络能力</h3><p>实际验证了当前 Wi-Fi 连接上的 IPv4、到默认网关的有界 ICMP，以及公共域名解析后进行 IPv4 探测的路径。</p><p>IPv6 在内核层面没有被禁用，但当时只有回环和链路本地地址，没有全局 IPv6 地址。这个结论只描述所测连接，不能推导为设备完全不支持 IPv6。</p><p>首次检查还发现 <code>awk</code> 不可用，相关初始结果被丢弃，改用实际存在的工具重新验证。旧 Android 上不能假设一个看似普通的 Unix 命令一定存在。</p><h2 id="4-Phase-1：把已有运行时用在它能胜任的范围内"><a href="#4-Phase-1：把已有运行时用在它能胜任的范围内" class="headerlink" title="4. Phase 1：把已有运行时用在它能胜任的范围内"></a>4. Phase 1：把已有运行时用在它能胜任的范围内</h2><p>手机上有 <code>mksh</code>、<code>ping</code>、<code>ping6</code> 和一个裁剪过的 Python 2.6.2。Python 能导入 <code>socket</code>、<code>threading</code>、<code>select</code> 等模块，但缺少 <code>ssl</code>、常见 HTTP 库以及 <code>json</code>；系统也没有查到 <code>curl</code>、<code>wget</code>、OpenSSL 或独立的通用 <code>timeout</code>。</p><p>因此，不能看到“有 Python”就按完整 Python 环境设计，也不能假定现代 Termux 可以直接安装使用。</p><table><thead><tr><th>路线</th><th>本轮判断</th></tr></thead><tbody><tr><td>系统 Shell</td><td>用于设备检查、执行和解析有界 ping，开销低</td></tr><tr><td>已有 Python 2.6.2</td><td>限定用于 DNS、TCP socket 和诊断性明文 HTTP</td></tr><tr><td>增加兼容用户态工具</td><td>当时作为补齐现代 TLS 的可能路线，未在本轮执行</td></tr><tr><td>自行开发 APK</td><td>维护成本更高，没有进入实现</td></tr></tbody></table><p>最终选择 Shell 与已有 Python 的小组合，由主机通过 ADB 调度并生成 JSON。手机没有新增软件或持久化脚本。</p><p>Phase 1 曾记录一个“验证兼容 HTTPS 客户端”的候选实验，后续实际工作选择了先验证非 TLS 探针的可靠性。本文不把这个历史建议记成已完成安装；小米 3 的现代 HTTPS 测量至今仍未建立。</p><h2 id="5-Phase-2A：最小-ADB-探针"><a href="#5-Phase-2A：最小-ADB-探针" class="headerlink" title="5. Phase 2A：最小 ADB 探针"></a>5. Phase 2A：最小 ADB 探针</h2><p>采集器位于主机，提供稳定、带版本号的 JSON 输出。真正的目标值只从 Git 忽略的本地配置读取，输出使用 <code>gateway</code>、<code>internet</code>、<code>vps</code> 三个固定逻辑名称。</p><p>第一版包含 ICMP 延迟与丢包、全局 IPv6 状态、DNS 成功率与耗时、TCP 建连结果与耗时，以及 uptime、电池和 Wi-Fi 等低成本状态。原始 socket 的明文 HTTP 只作诊断，默认关闭。</p><p>有界执行是这一阶段的核心要求：</p><ul><li>ping 同时使用次数、总 deadline 和回复等待上限。</li><li>DNS 通过带 join deadline 的工作线程约束，避免只设置 socket timeout 却无法限制名称解析。</li><li>每次设备侧操作外面还有主机的 ADB 超时。</li><li>单个目标失败后，后续目标仍继续测量。</li></ul><p>正常单次测量约需 7—8 秒；可选明文 HTTP 验证约需 8.6 秒；组合故障测试在 18.4 秒内结束。独立主机 JSON 解析器能够读取输出，三次约 60 秒间隔的循环也保持了相同结构。</p><p>这里的 <code>vps</code> 代码路径最初用公共替代目标验证，不能算作真实 VPS 连通性验证。这个缺口在 Phase 2C 才补上。</p><h2 id="6-Phase-2B：两小时-USB-中断与恢复"><a href="#6-Phase-2B：两小时-USB-中断与恢复" class="headerlink" title="6. Phase 2B：两小时 USB 中断与恢复"></a>6. Phase 2B：两小时 USB 中断与恢复</h2><p>2026 年 9 月 5 日 14:36—16:36，采集器安排了 121 次测量：开始立即执行一次，随后每 60 秒一次。</p><p>第一次健康采集之后，用户执行了一次物理 USB 断开。线缆保持断开期间，第 2—53 次共 52 个周期不可用；重新连接后，第 54 次成功，恢复发生在连接可用后的第一个计划周期。</p><p><strong>52 个失败周期反映线缆断开的时长，不是恢复耗时。</strong></p><table><thead><tr><th>项目</th><th>结果</th></tr></thead><tbody><tr><td>预期 &#x2F; 输出 &#x2F; 可解析记录</td><td>121 &#x2F; 121 &#x2F; 121</td></tr><tr><td>成功执行记录</td><td>69，其中 1 次有 DNS 测量失败</td></tr><tr><td>ADB 传输失败</td><td>52</td></tr><tr><td>采集器故障</td><td>0</td></tr><tr><td>成功周期耗时中位数 &#x2F; 最大值</td><td>6.649 秒 &#x2F; 11.818 秒</td></tr><tr><td>传输失败周期耗时</td><td>0.118—0.150 秒</td></tr><tr><td>温度</td><td>31.0—34.4°C</td></tr></tbody></table><p>重新连接没有要求重启手机、重启 ADB 服务或再次授权。但这也暴露了架构代价：USB 是真实依赖，自动恢复不能弥补线缆断开期间没有测量数据。</p><h3 id="6-1-一个值得修正的实现问题"><a href="#6-1-一个值得修正的实现问题" class="headerlink" title="6.1 一个值得修正的实现问题"></a>6.1 一个值得修正的实现问题</h3><p>Schema v1 在传输不可用时，把没有执行的网络测量列为失败，并设置 <code>partial_failure=true</code>。这会让仪表盘把“没测到”误读成“网络都坏了”。</p><p>测试后做了聚焦修正，升级到 schema v2：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">传输失败：</span><br><span class="line">  transport_failure = true</span><br><span class="line">  failure_class = &quot;transport&quot;</span><br><span class="line">  measurement failure list = []</span><br><span class="line"></span><br><span class="line">测量失败：</span><br><span class="line">  ADB 和设备执行路径仍然可用</span><br><span class="line">  partial_failure 只表示已经执行的测量中存在失败</span><br></pre></td></tr></table></figure><p>修复使用主机端不可用 ADB 路径和独立 DNS 故障进行验证，没有重新设计调度，也没有为了修复分类再制造一次长时间物理断连。</p><p>这次两小时测试证明了恢复机制，但因为存在很长的传输空窗，不能代替连续两小时工作基线。</p><h2 id="7-Phase-2C：真实-VPS-的连续两小时基线"><a href="#7-Phase-2C：真实-VPS-的连续两小时基线" class="headerlink" title="7. Phase 2C：真实 VPS 的连续两小时基线"></a>7. Phase 2C：真实 VPS 的连续两小时基线</h2><p>同日 17:27—19:27，用 schema v2 和本地真实 VPS 配置运行连续测试。USB 保持连接，没有计划中断。</p><table><thead><tr><th>项目</th><th>结果</th></tr></thead><tbody><tr><td>完整记录</td><td>121 &#x2F; 121</td></tr><tr><td>传输故障 &#x2F; 采集器故障 &#x2F; 测量执行失败</td><td>0 &#x2F; 0 &#x2F; 0</td></tr><tr><td>调度间隔</td><td>所有 120 个间隔均为记录中的 60 秒</td></tr><tr><td>测量耗时中位数 &#x2F; 最大值</td><td>10.157 秒 &#x2F; 13.913 秒</td></tr><tr><td>DNS &#x2F; TCP 成功次数</td><td>121 &#x2F; 121，各自均为全部成功</td></tr><tr><td>真实 VPS ICMP</td><td>每轮至少收到一个回复</td></tr><tr><td>电池 &#x2F; USB</td><td>电量全程 100%，持续 USB 供电</td></tr><tr><td>温度</td><td>32.9—34.4°C</td></tr></tbody></table><p>“零测量执行失败”不表示“零丢包”。121 轮中，网关、Internet、VPS 分别有 11、18、48 轮报告非零丢包。VPS 每轮平均 RTT 的中位数为 421.720 ms，TCP 建连耗时中位数为 422.018 ms。</p><p>短 ping 批次存在丢包，同时 TCP 每轮都成功，这些是有意义的路径观察。它们不能直接用来诊断某个设备或链路的具体故障，也不应被当作采集器失效。</p><p>内存与缓存上下波动，未表现为单调泄漏；这仍不能排除更慢的泄漏。完整结果支持进入八小时阶段。</p><h2 id="8-Phase-2D：八小时测试中捕获真实网络事件"><a href="#8-Phase-2D：八小时测试中捕获真实网络事件" class="headerlink" title="8. Phase 2D：八小时测试中捕获真实网络事件"></a>8. Phase 2D：八小时测试中捕获真实网络事件</h2><p>2026 年 9 月 5 日 22:05 至次日 06:05，连续运行 481 次采集。</p><table><thead><tr><th>项目</th><th>结果</th></tr></thead><tbody><tr><td>输出与解析</td><td>481 &#x2F; 481 完整</td></tr><tr><td>ADB 传输失败 &#x2F; 采集器故障</td><td>0 &#x2F; 0</td></tr><tr><td>包含测量失败的周期</td><td>12</td></tr><tr><td>耗时中位数 &#x2F; 最大值</td><td>9.568 秒 &#x2F; 26.367 秒</td></tr><tr><td>DNS 成功</td><td>479 &#x2F; 481</td></tr><tr><td>TCP 成功</td><td>469 &#x2F; 481</td></tr><tr><td>电池与温度</td><td>全程 100%、USB 供电；32.1—32.9°C</td></tr></tbody></table><p>这次最有价值的发现是 02:20—02:29 的连续外部连接事件：TCP 在十个周期内失败，Internet 与 VPS 的 ICMP 在后九轮完全丢失，DNS 在最后两轮也失败。与此同时，网关 ICMP、ADB、Wi-Fi 状态采集、设备状态和 JSON 输出保持健康。</p><p>所有受影响测量随后无需干预就恢复，并继续运行约 3 小时 36 分钟，没有新的测量失败周期。另有两次早期孤立 TCP 失败，共同构成 12 次测量失败。</p><p>这支持将事件解释为所观测的外部路径异常，而不是采集器故障；具体故障设备或运营商原因并未确定。</p><p>八小时门槛通过后，才开始添加主机监控后端。手机仍是 YELLOW，未因阶段通过而自动获得无人值守许可。</p><h2 id="9-Phase-3A：把采集与-Prometheus-抓取解耦"><a href="#9-Phase-3A：把采集与-Prometheus-抓取解耦" class="headerlink" title="9. Phase 3A：把采集与 Prometheus 抓取解耦"></a>9. Phase 3A：把采集与 Prometheus 抓取解耦</h2><p>探针可能花费几秒到二十多秒。如果每次 Prometheus 访问 <code>/metrics</code> 都启动一轮 ADB，抓取延迟和设备测量就会绑在一起。</p><p>本轮 exporter 采用 Python 标准库实现：后台线程每 60 秒调用现有 collector；HTTP 服务只读取最新有界内存快照。抓取不会触发 ADB，也不会等待当前测量完成。</p><p>状态只保留最近一次尝试、最近一次传输健康时的测量及少量健康字段，没有持续增长的内存队列，也不写手机存储或连续访问日志。</p><h3 id="9-1-故障语义要能解释现实"><a href="#9-1-故障语义要能解释现实" class="headerlink" title="9.1 故障语义要能解释现实"></a>9.1 故障语义要能解释现实</h3><table><thead><tr><th>事件</th><th>应该看到的信号</th></tr></thead><tbody><tr><td>单个 DNS&#x2F;TCP&#x2F;ping 测量失败</td><td>传输与执行仍成功，partial failure 为 1，对应测量失败</td></tr><tr><td>ADB 不可用</td><td>传输与执行为 0；不制造一组虚假的网络测量失败</td></tr><tr><td>无法得到新测量</td><td>保留之前传输健康时的测量，同时明确增加数据年龄</td></tr><tr><td>数据超过 150 秒未更新</td><td>stale 为 1；首次有效测量之前也为 stale</td></tr><tr><td>主机调用 collector 意外异常</td><td>exporter collection success 为 0，错误计数增加</td></tr><tr><td>exporter 进程消失</td><td>Prometheus 自身的抓取 <code>up</code> 为 0</td></tr></tbody></table><p>保留旧值必须同时显示年龄和陈旧状态。否则一个仍然返回 HTTP 200 的接口，可能长期展示已经失效的“正常”数据。</p><h3 id="9-2-指标与基数控制"><a href="#9-2-指标与基数控制" class="headerlink" title="9.2 指标与基数控制"></a>9.2 指标与基数控制</h3><p>项目实际使用 <code>legacy_edge_</code> 前缀，时间统一按秒、丢包和电量按比例暴露。部分关键指标如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">legacy_edge_probe_transport_up</span><br><span class="line">legacy_edge_probe_execution_success</span><br><span class="line">legacy_edge_probe_partial_failure</span><br><span class="line">legacy_edge_probe_duration_seconds</span><br><span class="line">legacy_edge_probe_age_seconds</span><br><span class="line">legacy_edge_probe_stale</span><br><span class="line">legacy_edge_exporter_collection_success</span><br><span class="line">legacy_edge_exporter_collection_errors_total</span><br><span class="line">legacy_edge_ping_success</span><br><span class="line">legacy_edge_ping_rtt_seconds</span><br><span class="line">legacy_edge_ping_packet_loss_ratio</span><br><span class="line">legacy_edge_dns_success</span><br><span class="line">legacy_edge_dns_duration_seconds</span><br><span class="line">legacy_edge_tcp_success</span><br><span class="line">legacy_edge_tcp_connect_duration_seconds</span><br><span class="line">legacy_edge_ipv6_global_available</span><br><span class="line">legacy_edge_device_battery_temperature_celsius</span><br></pre></td></tr></table></figure><p>exporter 样本只使用固定设备标签，ping 额外使用三个固定目标标签。实际地址、时间戳、动态错误字符串、请求 ID 和完整 URL 不作为标签。Prometheus 后续会增加自己的 <code>job</code>、<code>instance</code> 标签。</p><p>Android 内存诊断值没有直接进入首批指标。<code>MemFree</code>、缓存与 Swap 可以辅助观察，但当时不足以提供清晰的“应用可用内存”语义，贸然画成告警指标容易误导。</p><h3 id="9-3-这一阶段到底验证了什么"><a href="#9-3-这一阶段到底验证了什么" class="headerlink" title="9.3 这一阶段到底验证了什么"></a>9.3 这一阶段到底验证了什么</h3><p>真实 <code>/metrics</code> 返回 HTTP 200，包含 21 个指标族、27 个样本，HELP&#x2F;TYPE 元数据完整。初次响应约 13.427 ms，在后台 ADB 正执行时的响应约 1.148 ms。六个单元测试覆盖单位、元数据、启动陈旧状态和不同故障类型。</p><p>当时环境没有 Prometheus 或 promtool。因此 Phase 3A 只能声称“指标暴露及语义已验证”，不能声称“真实 Prometheus 已抓取”。后者单独进入 Phase 3A.1。</p><h2 id="10-Phase-3A-1：真实-Prometheus-入库与故障验证"><a href="#10-Phase-3A-1：真实-Prometheus-入库与故障验证" class="headerlink" title="10. Phase 3A.1：真实 Prometheus 入库与故障验证"></a>10. Phase 3A.1：真实 Prometheus 入库与故障验证</h2><p>2026 年 9 月 6 日，在 WSL 主机安装并验证了 Prometheus 3.13.2 LTS 的官方 Linux amd64 独立发行包。第一次下载不完整，校验和与归档检查失败，因此没有安装；完整重传通过校验后才解压使用。</p><p>环境的 PID 1 不是 systemd。安装使用用户目录，没有引入 Docker、Kubernetes、系统包管理或自启动服务。</p><table><thead><tr><th>组件</th><th>本轮配置</th></tr></thead><tbody><tr><td>collector &#x2F; exporter</td><td>每 60 秒采集；监听 <code>127.0.0.1:9108</code></td></tr><tr><td>Prometheus</td><td>每 30 秒抓取；监听 <code>127.0.0.1:9090</code></td></tr><tr><td>陈旧阈值</td><td>150 秒，即当前采集间隔的 2.5 倍</td></tr><tr><td>TSDB 保留限制</td><td>7 天、1 GiB</td></tr><tr><td>运行状态</td><td>保存在 Git 外；前台进程手动启动</td></tr></tbody></table><p>promtool 配置检查通过，真实 target 为 UP，PromQL 能查到所需指标，抓取约 2 ms。</p><p>随后进行两种不同故障验证：</p><ol><li><strong>短暂拔掉 USB。</strong> exporter HTTP 仍可访问，Prometheus <code>up=1</code>，但传输状态变为 0；旧测量保留，年龄增长，在约 157.5 秒的观察点已超过阈值，stale 为 1。恢复 USB 后，下一个成功周期恢复传输和新鲜状态。</li><li><strong>停止 exporter。</strong> Prometheus 本身仍运行，下一次抓取将 <code>up</code> 置为 0；重新启动 exporter 后恢复。</li></ol><p>这证明接口可用、设备传输可用和测量新鲜度是不同层次。这里验证的是状态指标，还没有实现告警规则或通知流程。</p><h2 id="11-Phase-3B：完整链路的-24-小时验证"><a href="#11-Phase-3B：完整链路的-24-小时验证" class="headerlink" title="11. Phase 3B：完整链路的 24 小时验证"></a>11. Phase 3B：完整链路的 24 小时验证</h2><p>精确分析窗口为 2026 年 9 月 6 日 13:42:25 至 9 月 7 日 13:42:25，共 86,400 秒。由于人工返回时间较晚，实际进程约运行了 25 小时 42 分钟；核心连续性统计严格限定在原来的 24 小时内。</p><h3 id="11-1-采集与抓取连续性"><a href="#11-1-采集与抓取连续性" class="headerlink" title="11.1 采集与抓取连续性"></a>11.1 采集与抓取连续性</h3><table><thead><tr><th>指标</th><th>24 小时结果</th></tr></thead><tbody><tr><td>预期 &#x2F; 实际 Prometheus 抓取</td><td>2,881 &#x2F; 2,881</td></tr><tr><td>抓取间隔</td><td>30 秒，无缺口</td></tr><tr><td>独立探针快照</td><td>1,441</td></tr><tr><td>Prometheus target UP</td><td>100%</td></tr><tr><td>ADB 传输 &#x2F; 执行成功</td><td>100% &#x2F; 100%</td></tr><tr><td>exporter 采集错误</td><td>0</td></tr><tr><td>stale 事件</td><td>0</td></tr><tr><td>包含测量失败的快照</td><td>30，占 2.08%，分为 10 个事件段</td></tr><tr><td>探针耗时：中位数 &#x2F; p95 &#x2F; 最大值</td><td>9.771 &#x2F; 13.016 &#x2F; 27.369 秒</td></tr><tr><td>抓取耗时：中位数 &#x2F; p95 &#x2F; 最大值</td><td>1.789 &#x2F; 1.979 &#x2F; 2.594 ms</td></tr><tr><td>数据年龄最大值</td><td>52.384 秒，低于 150 秒阈值</td></tr></tbody></table><p>记录包含时间窗口两端的采样点，因此 24 小时、30 秒间隔会得到 2,881 个点；这些抓取也不等同于同样数量的独立手机测量。</p><h3 id="11-2-网络测量并没有“全绿”"><a href="#11-2-网络测量并没有“全绿”" class="headerlink" title="11.2 网络测量并没有“全绿”"></a>11.2 网络测量并没有“全绿”</h3><p>下表中“非零丢包轮次”与“全部丢包轮次”按每个短探测批次统计。RTT 表示有回复批次的平均 RTT 分布，不能当作整天所有单包时延的分布。</p><table><thead><tr><th>路径</th><th>非零丢包轮次</th><th>全部丢包轮次</th><th>RTT 中位数 &#x2F; p95 &#x2F; 最大值</th></tr></thead><tbody><tr><td>网关</td><td>98 &#x2F; 1,441</td><td>3</td><td>31.782 &#x2F; 133.072 &#x2F; 455.652 ms</td></tr><tr><td>Internet</td><td>179 &#x2F; 1,441</td><td>22</td><td>116.196 &#x2F; 431.199 &#x2F; 2,197.906 ms</td></tr><tr><td>VPS</td><td>463 &#x2F; 1,441</td><td>25</td><td>419.713 &#x2F; 561.327 &#x2F; 2,281.889 ms</td></tr></tbody></table><p>DNS 成功 1,427 &#x2F; 1,441 次，TCP 成功 1,416 &#x2F; 1,441 次。整个窗口内没有观测到全局 IPv6 可用。</p><p>两次明显的外部连接事件发生在 01:30—01:39 和 06:56—07:05，各持续十个探针周期。Internet&#x2F;VPS 的 ICMP 与 TCP 同时受影响，部分周期 DNS 也失败；网关、ADB、exporter 和 Prometheus 保持健康。</p><p>接近窗口结束时，还有一次持续两个周期、连网关也受影响的事件。它在额外运行窗口中自行恢复，因此“恢复”证据和“精确 24 小时统计”分开保留。</p><p>这些测量失败没有被清洗成成功，也没有被记成监控链路中断。四个六小时段中的部分测量失败数分别为 3、11、11、5，没有表现出随时间单调恶化。</p><h3 id="11-3-设备、主机资源和存储"><a href="#11-3-设备、主机资源和存储" class="headerlink" title="11.3 设备、主机资源和存储"></a>11.3 设备、主机资源和存储</h3><p>手机电量与 USB 供电状态在所有探针快照中保持 100% 和已供电，uptime 持续增加。电池温度为 32.3—35.0°C，中位数 33.4°C，没有持续上升趋势。</p><p>主机端 exporter RSS 从启动时 24,600 KiB 增至约 25.7 小时后的 25,832 KiB，增长约 1.2 MiB。这个变化没有影响工作，但不足以宣称不存在缓慢泄漏。Prometheus RSS 没有呈现持续增长。</p><p>TSDB 从启动前的 52,769 字节增至延迟收尾时的 669,423 字节，仍低于 1 MiB。因为收尾超过了精确 24 小时，而且存在压缩整理，这只是较长实际运行范围的存储观察，不能包装成精确的一天增长率。</p><p>promtool 对所分析 TSDB 块发现 32 条序列、五个标签名，未见标签 churn。这与 exporter 初始暴露的 27 个业务样本属于不同统计范围，不能直接混算。</p><h3 id="11-4-通过测试仍然是-YELLOW"><a href="#11-4-通过测试仍然是-YELLOW" class="headerlink" title="11.4 通过测试仍然是 YELLOW"></a>11.4 通过测试仍然是 YELLOW</h3><p>测试前、约 5.3 小时、约 19.8 小时及完成后都有用户外部检查，未报告异常发热、异味、变形、火花或供电不稳。夜间两次物理检查之间约有 12 小时间隔；连续遥测不等于连续人工看护。</p><p>阶段结论是：完整链路可继续用于受控 Homelab 实验。旧电池、旧系统、USB 依赖和主机进程生命周期仍然存在，安全等级保留 YELLOW，没有升级为无人值守 24×7 设备。</p><h2 id="12-Phase-3C：Grafana-展示真正的故障差异"><a href="#12-Phase-3C：Grafana-展示真正的故障差异" class="headerlink" title="12. Phase 3C：Grafana 展示真正的故障差异"></a>12. Phase 3C：Grafana 展示真正的故障差异</h2><p>阶段记录在 9 月 7 日完成 Grafana OSS 13.2.1 的安装与验证，相关提交在 9 月 8 日入库。它同样使用经过校验的用户目录独立安装，仅监听 <code>127.0.0.1:3000</code>，保持登录认证，禁用匿名访问和用户注册。</p><p>仪表盘 <strong>Legacy Edge — Xiaomi Mi 3</strong> 包含五个分区、20 个可视化面板，默认查看 24 小时并每 30 秒刷新：</p><ul><li>整体健康：Prometheus、exporter、ADB、执行状态、数据新鲜度。</li><li>网络路径：逻辑目标的 ICMP 成功、RTT、丢包，以及全局 IPv6 状态。</li><li>DNS 与 TCP：成功状态和耗时。</li><li>探针状态：持续时间、数据年龄和部分测量失败。</li><li>设备健康：电量、温度、USB 供电和 uptime。</li></ul><p>所有面板查询在验证时都返回数据。更重要的是，通过 Grafana 数据源回看 01:30—01:39 的真实事件，20 个抓取样本显示监控链路与网关健康、没有陈旧数据，同时 Internet&#x2F;VPS&#x2F;TCP 失败后恢复。</p><p>这让仪表盘能回答“网络异常发生时，探针自己是否还在正常工作”，而不仅是展示若干曲线。</p><p>20 次连续仪表盘 API 请求的中位耗时为 21.617 ms，最大 45.281 ms；查询后监控链路仍健康，Grafana 进程约占 340 MiB RSS。这些是主机端 API 与资源数据，不是平板页面渲染性能，也不是浏览器完整交互基准。</p><p>至此，小米 3 的边缘测量、主机采集、指标暴露、时序存储与可视化形成了完整链路。</p><h2 id="13-小米平板-Phase-0A—0B：先确认“屏幕-Wi-Fi-浏览器”的真实基础"><a href="#13-小米平板-Phase-0A—0B：先确认“屏幕-Wi-Fi-浏览器”的真实基础" class="headerlink" title="13. 小米平板 Phase 0A—0B：先确认“屏幕 + Wi-Fi + 浏览器”的真实基础"></a>13. 小米平板 Phase 0A—0B：先确认“屏幕 + Wi-Fi + 浏览器”的真实基础</h2><p>平板原本考虑作为 Grafana 或轻量 Homelab NOC 显示屏。后端仍留在主机，平板只负责展示。</p><p>用户先完成外部观察：外观完好、无鼓包异味、温度正常、能够开机，USB 接口完好无松动，报告稳定充电，拔掉 USB 后可以用电池运行，但续航未测。</p><p>确认已有 USB 调试授权后，才执行只读清单采集。</p><table><thead><tr><th>项目</th><th>实测结果</th></tr></thead><tbody><tr><td>型号 &#x2F; 代号</td><td>Xiaomi MI PAD &#x2F; <code>mocha</code></td></tr><tr><td>Android &#x2F; API</td><td>4.4.4 &#x2F; 19</td></tr><tr><td>MIUI &#x2F; 构建</td><td><code>V9.2.4.0.KXFCNEK</code> &#x2F; <code>KTU84P</code></td></tr><tr><td>安全补丁属性</td><td>2016-12-01</td></tr><tr><td>CPU &#x2F; 平台</td><td>ARMv7，四个逻辑 CPU；平台 <code>tegra</code>，sysfs family 为 <code>Tegra12</code></td></tr><tr><td>内存 &#x2F; Swap</td><td>系统总 RAM 约 1.88 GiB；无 Swap</td></tr><tr><td>数据分区</td><td>设备 df 报告总量 55.4G、剩余 20.2G；非物理闪存容量结论</td></tr><tr><td>屏幕</td><td>Android 报告 1536×2048、320 dpi</td></tr><tr><td>浏览器</td><td>小米浏览器 9.3.2；Chrome 62.0.3202.84</td></tr><tr><td>WebView</td><td>系统库文件存在；独立包未找到，具体引擎版本未验证</td></tr><tr><td>网络</td><td>Wi-Fi 开启、有 IPv4 地址，无全局 IPv6 地址</td></tr></tbody></table><p>电池离散快照为 33%&#x2F;33.7°C、29%&#x2F;35.5°C、29%&#x2F;34.5°C，系统显示 USB 充电。用户确认该段没有拔 USB。这个下降需要被记录，但没有连续功率采样或容量测试，不能断言电池损坏或 USB 供电不足。</p><p>屏幕超时值很大，但没有更改它，也没有据此宣称屏幕永远不会锁定。设备安全状态暂定 YELLOW，没有发现足以判断 RED 的已报告证据。</p><h2 id="14-Phase-0C：Grafana-显示为什么没有通过"><a href="#14-Phase-0C：Grafana-显示为什么没有通过" class="headerlink" title="14. Phase 0C：Grafana 显示为什么没有通过"></a>14. Phase 0C：Grafana 显示为什么没有通过</h2><h3 id="14-1-HTTPS-证书失败与基本页面渲染分开判断"><a href="#14-1-HTTPS-证书失败与基本页面渲染分开判断" class="headerlink" title="14.1 HTTPS 证书失败与基本页面渲染分开判断"></a>14.1 HTTPS 证书失败与基本页面渲染分开判断</h3><p>两个已安装浏览器访问公共 HTTPS 示例页都出现证书问题。Chrome 错误名为 <code>NET::ERR_CERT_AUTHORITY_INVALID</code>；输入 HTTP 示例地址后跳转 HTTPS，仍然失败。</p><p>平板与主机时间基本一致，没有明显日期偏差。没有安装证书或绕过警告，也没有仅凭错误名认定为旧根证书、特定 TLS 版本或中间设备所致。</p><p>随后公共 HTTP 测试页能够显示，说明至少基础页面加载可用。JavaScript 设置被报告为允许，但“允许脚本”与“脚本已经正确执行”仍是两种证据。</p><h3 id="14-2-先用-USB，保留-Grafana-的回环边界"><a href="#14-2-先用-USB，保留-Grafana-的回环边界" class="headerlink" title="14.2 先用 USB，保留 Grafana 的回环边界"></a>14.2 先用 USB，保留 Grafana 的回环边界</h3><p>Grafana 原本只监听主机回环，没有为平板测试直接开放 LAN。实验采用桌面 Chrome 的 USB 端口转发，尝试让平板访问主机本机服务。</p><p>过程中出现设备信息 stale、标签标题令人误解、inspect 调试窗口长时间空白等现象。后续通过直接调试协议确认 Chrome 62 使用协议 1.2，能够定位到目标标签并读取事件。</p><p>这里还有几个需要保留的排障边界：</p><ul><li>平板返回桌面的一次事件后来被确认是用户手动退出，不计为崩溃。</li><li><code>am start</code> 的旧系统参数和 Activity 权限限制导致自动打开页面尝试失败，没有绕过权限。</li><li>ADB 反向转发的只读查询返回 <code>closed</code>，不能因主机命令存在就认为设备支持。</li><li>主机 Grafana 正常、Windows 能访问时，平板曾返回 <code>ERR_CONNECTION_REFUSED</code>，且没有发现平板本地目标端口监听；这一轮属于访问路径问题。</li><li>临时用于直接调试的 ADB 转发在每次查询后撤销，没有把它们留成长期部署。</li></ul><p>这些现象说明测试工具链也需要验证，不能把所有报错都归到设备性能上。USB 显示即使成功，也不能代替最终 Wi-Fi 部署验证。</p><h3 id="14-3-真正捕获到的前端兼容障碍"><a href="#14-3-真正捕获到的前端兼容障碍" class="headerlink" title="14.3 真正捕获到的前端兼容障碍"></a>14.3 真正捕获到的前端兼容障碍</h3><p>恢复 Chrome USB 映射后，平板显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">If you&#x27;re seeing this Grafana has failed to load its application files...</span><br></pre></td></tr></table></figure><p>直接调试采集观察到初始 Document、五个 Script 和两个 Stylesheet 返回 HTTP 200，同时多次出现：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Uncaught SyntaxError: Unexpected token &#123;</span><br></pre></td></tr></table></figure><p>资源已被取到，但 Chrome 62 无法解析当前交付的至少部分脚本。精确的语法特性和 bundle 尚未定位，已有证据足以确认当前浏览器组合没通过前端启动门槛。</p><p>减少面板数量主要影响进入应用后的负载，无法解决相同 Grafana 前端的启动语法错误。因此没有再为这台平板制作简化 Grafana 仪表盘。</p><p>小米浏览器没有实际完成 Grafana 渲染测试，不能把 Chrome 的结果包装成两个浏览器都已实测失败。</p><h2 id="15-静态回退尝试与未开始的-Phase-0D"><a href="#15-静态回退尝试与未开始的-Phase-0D" class="headerlink" title="15. 静态回退尝试与未开始的 Phase 0D"></a>15. 静态回退尝试与未开始的 Phase 0D</h2><p>回退层级预先定义为：</p><table><thead><tr><th>层级</th><th>内容</th><th>本轮状态</th></tr></thead><tbody><tr><td>Tier A</td><td>完整 Grafana</td><td>Chrome 62 下前端启动失败，登录与面板未进入可测阶段</td></tr><tr><td>Tier B</td><td>更少、更轻的 Grafana 面板</td><td>未实施；无法解决已观察到的公共前端语法障碍</td></tr><tr><td>Tier C</td><td>服务端生成的轻量真实状态页</td><td>未实现真实监控集成</td></tr><tr><td>Tier D</td><td>定期刷新的静态 HTML &#x2F; 图片</td><td>模拟 HTML 初次显示成功，刷新和稳定性未完成验证</td></tr></tbody></table><p>为了验证 Tier D，主机临时运行了一个仅监听回环的静态 HTML 测试服务，页面包含明确标为模拟数据的状态卡片、简单时钟、触摸计数和每 60 秒刷新。</p><p>用户调整 USB 映射后，平板成功显示了 “LEGACY EDGE LAB” 和中文测试标题。随后，在没有操作的情况下，页面再次变成“无法访问此网站”。本次没有同步采到足以确定根因的错误码或监听状态，不能认定为浏览器崩溃、Wi-Fi 故障或静态 HTML 不兼容。</p><p>由于功能与访问路径尚未稳定，原计划的 <strong>30 分钟受控显示测试没有开始</strong>。没有进行更长测试，没有关闭热保护、电池保护或修改息屏设置。</p><p>这时继续投入已经不符合低维护显示终端的目标。临时轻量服务被停止，测试夹具没有变成项目组件。设备最终决定为 <strong>retired &#x2F; not worth further investment</strong>，Grafana&#x2F;NOC 角色及后续静态回退角色均关闭。</p><p>退役依据是已经观察到的前端兼容障碍、HTTPS 信任问题和测试访问中断带来的维护投入，而不是一个未证实的硬件故障。项目不再通过 root、ROM、替换浏览器、维修或进一步兼容性工作来保留这台平板的角色。</p><h2 id="16-旧笔记本：从供电约束到非运行状态退役"><a href="#16-旧笔记本：从供电约束到非运行状态退役" class="headerlink" title="16. 旧笔记本：从供电约束到非运行状态退役"></a>16. 旧笔记本：从供电约束到非运行状态退役</h2><p>旧笔记本最初有电池失效或不可用、外部供电可能不稳定的历史记录。因此它原本只被列为有条件的 Linux 实验机、故障测试节点、SSH 跳板、临时 Kubernetes worker、PXE 测试机或网络排障工作站。</p><p>2026 年 9 月 8 日，用户报告它现在无法开机，并决定退出 Lab。没有进行新的通电试验、拆机或硬件诊断；具体型号、故障位置和失败部件均未验证。</p><p>状态明确记录为 <strong>non-operational &#x2F; retired from the lab</strong>，所有历史候选角色关闭。不把无法开机进一步解释为主板、电池或适配器的确定故障，也不启动内部电气维修来维持这些设想。</p><h2 id="17-Kindle-Phase-0：把电子墨水屏当作低频终端"><a href="#17-Kindle-Phase-0：把电子墨水屏当作低频终端" class="headerlink" title="17. Kindle Phase 0：把电子墨水屏当作低频终端"></a>17. Kindle Phase 0：把电子墨水屏当作低频终端</h2><p>Kindle 的候选角色从一开始就不是运行监控后端，而是显示主机生成的轻量内容：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Homelab</span><br><span class="line">  → 服务端生成的状态内容</span><br><span class="line">  → Kindle 内置浏览器</span><br><span class="line">  → 电子墨水屏</span><br></pre></td></tr></table></figure><p>这条路线刻意避开完整 Grafana、实时动画和复杂交互。Phase 0 没有越狱、root、降级固件、安装非官方启动器或替代浏览器，也没有为了常亮去修改系统。</p><h3 id="17-1-外观、供电与设备基线"><a href="#17-1-外观、供电与设备基线" class="headerlink" title="17.1 外观、供电与设备基线"></a>17.1 外观、供电与设备基线</h3><p>用户报告设备外壳、屏幕和接缝平整，没有鼓包、变形、异味或异常发热；充电器、线缆和接口完好，连续充电一夜后正常启动。拔掉电源时电量为 98%，正常翻动十几页后仍为 98%，期间没有自动关机或重启。</p><p>这些结果支持 <strong>GREEN</strong> 安全等级，可以继续受控测试，但不能代替电池容量测量或 24×7 无人值守验收。</p><table><thead><tr><th>项目</th><th>实机结果</th></tr></thead><tbody><tr><td>设备</td><td>Kindle（第 10 代）</td></tr><tr><td>当前固件</td><td>5.18.1.1.1（4769670023）</td></tr><tr><td>升级前固件</td><td>5.16.9（4246260033）- 39728</td></tr><tr><td>用户存储</td><td>总量 2.75 GB，可用 2.66 GB</td></tr><tr><td>容量档位</td><td>与官方 4 GB &#x2F; 约 3 GB 用户空间档相符；属于推断，不是直接读取的物理容量</td></tr><tr><td>Wi-Fi</td><td>已连接并能浏览网页</td></tr><tr><td>USB</td><td>主机能够识别和查看 Kindle 存储，充电稳定，测试后正常弹出</td></tr><tr><td>浏览器</td><td>内置浏览器存在，版本号未识别</td></tr></tbody></table><p>用户在 Phase 0 期间通过官方路径把固件升级到 5.18.1.1.1，因此后续浏览器和稳定性结论都针对新版本。更新前后发生过一次重启，用户同时不确定此前电源键按压时长，唤醒时又遇到日文字体更新提示；这个事件无法归因于浏览器或硬件，单独保留为受更新过程干扰的观察。</p><p>屏幕像素分辨率、浏览器版本和真实电池容量没有从设置界面取得。没有为了补齐表格而采用侵入式诊断。</p><h3 id="17-2-浏览器能力：简单页面比“现代前端”更重要"><a href="#17-2-浏览器能力：简单页面比“现代前端”更重要" class="headerlink" title="17.2 浏览器能力：简单页面比“现代前端”更重要"></a>17.2 浏览器能力：简单页面比“现代前端”更重要</h3><p>浏览器启动时直接提示内存有限，适合简单网站，动画或复杂功能可能加载不佳甚至无法加载。这与项目的轻量页面方向一致，也提前给完整 Grafana 前端划出了边界。</p><p>公共页面测试中，<code>https://example.com</code> 正常显示且没有证书警告，证明当前网络与固件至少能完成一个现代 HTTPS 连接和证书验证。<code>http://neverssl.com</code> 返回 <code>ERR_EMPTY_RESPONSE</code>；由于目标站点、网络路径和浏览器都可能造成空响应，不能把这一次失败写成“不支持 HTTP”。</p><p>为隔离公共网络变量，主机临时提供了一个 LAN-only 测试页，只包含示例状态、简单表格、CSS 边框、一张 300×72 灰度 PNG、生成时间、一个最小 DOM 脚本和 meta-refresh。页面没有真实主机名、地址、凭据或监控值。</p><p>Kindle 实际显示了标题、表格和样式，成功加载 PNG，JavaScript 把测试文字从 <code>NOT RUN</code> 改为 <code>WORKS</code>。这只证明基础 DOM 操作能执行，不代表现代框架或复杂浏览器 API 可用。最终显示页也不需要依赖 JavaScript。</p><p>临时服务只允许私有 LAN 客户端访问。Phase 0 完成后，服务、监听端口和临时防火墙规则全部撤销，没有把测试路径变成长期暴露面。</p><h3 id="17-3-刷新、睡眠与-38-分钟窗口"><a href="#17-3-刷新、睡眠与-38-分钟窗口" class="headerlink" title="17.3 刷新、睡眠与 38 分钟窗口"></a>17.3 刷新、睡眠与 38 分钟窗口</h3><p>最初使用 60 秒 meta-refresh。浏览器按约 60—62 秒重新请求页面，但每次都会产生明显的电子墨水整页闪烁。这证明自动刷新有效，同时也说明一分钟一次不符合电子墨水显示的使用方式。</p><p>稳定性页面随后改为无 JavaScript、每 600 秒刷新。Kindle 在 17:11 加载页面和 PNG，17:21 按计划刷新。用户认为十分钟一次的闪烁可以接受。设备约十分钟自动睡眠；较长时间睡眠后唤醒，页面稍等片刻重新加载，服务端在 17:49 再次收到页面和图片请求。</p><p>这个窗口覆盖约 38 分钟，验证了页面保留、低频刷新和睡眠后的恢复。期间没有 Wi-Fi 断开、浏览器崩溃、卡死或再次重启；温度正常，文字和图像清晰，残影不影响阅读。结束电量为 96%，但没有记录稳定窗口开始时的即时电量，不能把更早的 98% 读数包装成精确掉电量。</p><p>默认自动睡眠是角色边界：Kindle 不能在当前设置下充当持续亮屏的 NOC。它更适合按需唤醒查看，唤醒后自动取得较新的状态。</p><h3 id="17-4-选择-Tier-B，保留-Tier-C-回退"><a href="#17-4-选择-Tier-B，保留-Tier-C-回退" class="headerlink" title="17.4 选择 Tier B，保留 Tier C 回退"></a>17.4 选择 Tier B，保留 Tier C 回退</h3><p>四个候选层级的结论如下：</p><table><thead><tr><th>层级</th><th>结论</th></tr></thead><tbody><tr><td>Tier A：轻量实时 HTML</td><td>不采用实时刷新；对电子墨水没有收益，频繁闪烁明显</td></tr><tr><td>Tier B：服务端 HTML、少量或无 JS、周期刷新</td><td><strong>选择</strong>；无 JS、十分钟刷新已经通过短时验证</td></tr><tr><td>Tier C：服务端生成 PNG&#x2F;JPEG</td><td>保留为排版或浏览器性能出现实际问题时的回退</td></tr><tr><td>Tier D：手动刷新&#x2F;每日摘要</td><td>当前无需退到这一层，但仍可作为服务不可用时的简单降级</td></tr></tbody></table><p>推荐的 Phase 1 架构是：主机通过回环地址执行固定 Prometheus 查询和 Grafana 健康检查，每项设置短超时并独立失败；随后生成不含凭据的高对比度 HTML，可选一张小型灰度趋势图。Kindle 只访问 LAN-only 只读端点，每十分钟 meta-refresh。Grafana UI、登录信息和 API token 都不发送到设备。</p><p>因此，Kindle <strong>值得集成</strong>，角色是按需唤醒、低频更新的 Tier B 状态页。Phase 0 只完成可行性验证；真实状态映射和最终服务仍属于待评审的 Phase 1，没有在本轮提前实现。</p><h2 id="18-当前状态、已交付内容与尚未完成的工作"><a href="#18-当前状态、已交付内容与尚未完成的工作" class="headerlink" title="18. 当前状态、已交付内容与尚未完成的工作"></a>18. 当前状态、已交付内容与尚未完成的工作</h2><h3 id="18-1-设备池"><a href="#18-1-设备池" class="headerlink" title="18.1 设备池"></a>18.1 设备池</h3><table><thead><tr><th>设备</th><th>收尾状态</th></tr></thead><tbody><tr><td>小米 3</td><td>已验证的受控 Android 边缘可观测性探针；YELLOW</td></tr><tr><td>小米平板 1</td><td>已退役，不值得继续投入；不再承担显示角色</td></tr><tr><td>旧笔记本</td><td>无法开机，非运行状态，已退役</td></tr><tr><td>Kindle</td><td>Phase 0 已验证；GREEN；适合作为按需唤醒、十分钟刷新、服务端渲染的 Tier B 状态页，待 Phase 1 评审</td></tr><tr><td>旧 iPhone</td><td>仍为候选，尚无实机清单或网络&#x2F;Safari 验证</td></tr></tbody></table><p>尚未完成 Android、iOS、Linux 的同网络横向比较。Kindle 已通过低刷新显示可行性门槛，但真实监控集成尚未开始；旧 iPhone 的兼容性测试仍只是候选方向。</p><h3 id="18-2-仓库交付"><a href="#18-2-仓库交付" class="headerlink" title="18.2 仓库交付"></a>18.2 仓库交付</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">AGENTS.md                         项目原则与当前退役约束</span><br><span class="line">inventory/                        真机清单、测试证据、退役理由</span><br><span class="line">docs/device-role-matrix.md         当前设备角色</span><br><span class="line">probes/android/xiaomi-mi3/</span><br><span class="line">  collector.py                    主机 ADB 采集器</span><br><span class="line">  exporter.py                     有界状态与 Prometheus 指标</span><br><span class="line">  test_exporter.py                指标与故障语义验证</span><br><span class="line">  config.example.json             已脱敏配置模板</span><br><span class="line">prometheus/                       实际配置、模板与运行说明</span><br><span class="line">grafana/                          回环配置、启动脚本、数据源与仪表盘 provisioning</span><br></pre></td></tr></table></figure><p>密码、真实目标、完整序列号、SSID、MAC、私有网络地址、原始运行日志和 TSDB 不进入博客稿。实际配置与运行状态保存在 Git 外；本文使用的 <code>127.0.0.1</code> 是回环地址，逻辑目标也不暴露真实拓扑。</p><h3 id="18-3-当前限制"><a href="#18-3-当前限制" class="headerlink" title="18.3 当前限制"></a>18.3 当前限制</h3><p>小米 3 的成功仍有清楚的适用范围：USB&#x2F;ADB 和 Windows&#x2F;WSL 是依赖，主机进程手动启动，没有完成进程监督或自启动；Grafana 是回环访问；告警规则和响应流程尚未实现；现代 HTTPS 探针与全局 IPv6 的实际连通性没有得到验证。</p><p>现有记录认为，后续有工程价值的工作包括主机进程监督、经评审后实现 Kindle 的轻量状态渲染器，以及在合适条件下对旧 iPhone 先做清单与安全检查。这些都属于待办，不是本篇已经完成的阶段；退役设备没有保留后续实验任务。</p><h2 id="19-这轮实践留下的工程认识"><a href="#19-这轮实践留下的工程认识" class="headerlink" title="19. 这轮实践留下的工程认识"></a>19. 这轮实践留下的工程认识</h2><p>第一，旧设备是否有价值取决于任务边界。小米 3 的系统虽然老，但已有工具足以完成小规模、低频网络测量；平板能显示网页，也不代表它适合承担当前 Grafana 前端。</p><p>第二，可观测性必须包含采集链路本身。网络失败、USB 断开、主机采集异常、exporter 不可达和数据过期，需要不同信号。否则仪表盘可能把完全不同的问题画成同一种红色。</p><p>第三，分阶段验证能保留判断依据。30 分钟的低负载观察、两小时恢复测试、连续两小时真实路径、八小时采集以及完整链路 24 小时测试，各自回答不同问题，不能互相替代。</p><p>第四，运行时间和恢复行为都重要。一次掉线后能自动恢复，不代表依赖消失；一天没有重启，也不等于电池和供电从此适合无人值守。</p><p>第五，停止投入可以成为完整的工程结果。平板的兼容性排查、临时显示路径与维护成本已经超过角色收益；旧笔记本则失去了基本运行能力。保留这些退役理由，比让设备永远停留在“以后还能折腾”的候选表里更有用。</p><p>第六，显示设备的系统行为会改变角色定义。Kindle 能可靠显示和刷新轻量页面，但默认自动睡眠使它更适合按需唤醒查看，而不是持续亮屏。接受这个边界，比为了“看起来像 NOC”去修改系统更符合项目目标。</p><h2 id="20-原始记录与复核入口"><a href="#20-原始记录与复核入口" class="headerlink" title="20. 原始记录与复核入口"></a>20. 原始记录与复核入口</h2><p>以下链接固定到本次总结依据的仓库提交 <code>d00adfc</code>，便于博客留档后复核，避免后续主分支变化改变本文的证据范围：</p><ul><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/AGENTS.md">项目原则与设备状态</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/inventory/xiaomi-mi3.md">小米 3 全阶段原始清单</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/probes/android/xiaomi-mi3/README.md">小米 3 collector &#x2F; exporter 说明</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/prometheus/README.md">Prometheus 部署与验证</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/grafana/README.md">Grafana 部署与验证</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/inventory/xiaomi-mipad1.md">小米平板清单、显示实验和退役决策</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/inventory/kindle.md">Kindle Phase 0 清单与显示可行性评估</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/inventory/old-laptop.md">旧笔记本退役记录</a></li><li><a href="https://github.com/jzops101/legacy-edge-lab/blob/d00adfc/docs/device-role-matrix.md">当前设备角色矩阵</a></li></ul>]]>
    </content>
    <id>https://zhouxingyu.dev/posts/legacy-edge-lab-phase-summary/</id>
    <link href="https://zhouxingyu.dev/posts/legacy-edge-lab-phase-summary/"/>
    <published>2026-09-08T09:00:00.000Z</published>
    <summary>记录在WSL2中搭建Prometheus/Grafana以应用家用旧设备进行边缘监控实践与退役决策</summary>
    <title>Legacy Edge Lab 阶段记录</title>
    <updated>2026-09-08T09:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhou Xingyu</name>
    </author>
    <category term="Kubernetes" scheme="https://zhouxingyu.dev/categories/Kubernetes/"/>
    <category term="Kubernetes" scheme="https://zhouxingyu.dev/tags/Kubernetes/"/>
    <category term="WSL2" scheme="https://zhouxingyu.dev/tags/WSL2/"/>
    <category term="DevOps" scheme="https://zhouxingyu.dev/tags/DevOps/"/>
    <category term="Minikube" scheme="https://zhouxingyu.dev/tags/Minikube/"/>
    <content>
      <![CDATA[<h2 id="为什么要在-WSL2-中搭建-Minikube"><a href="#为什么要在-WSL2-中搭建-Minikube" class="headerlink" title="为什么要在 WSL2 中搭建 Minikube"></a>为什么要在 WSL2 中搭建 Minikube</h2><p>我搭建这套环境的目的并不是为了在本地长期运行一个“生产级 Kubernetes 集群”，而是为了给 Kubernetes 学习、排障和面试准备提供一个可以反复折腾的实验环境。</p><p>相比只阅读文档，本地集群最大的价值是能够主动制造问题。例如：</p><ul><li>修改 Pod 的资源限制，观察 <code>Pending</code>、OOMKilled 等状态；</li><li>配置错误的镜像，观察 <code>ImagePullBackOff</code>；</li><li>修改 readiness &#x2F; liveness probe，观察 Pod 生命周期变化；</li><li>创建不同类型的 Service，理解 Kubernetes 网络访问路径；</li><li>修改 Deployment 副本数，观察调度和滚动更新；</li><li>查看 Events、Logs 和资源状态，练习排障流程；</li><li>安装 Prometheus、Grafana 等组件，熟悉 Kubernetes 中的可观测性体系。</li></ul><p>我的主要开发环境本身就在 WSL2 中，因此把 Kubernetes 实验环境也放在 WSL2 里有几个明显优势：</p><ol><li>可以直接使用 Linux 命令行和工具链；</li><li>不需要额外准备虚拟机；</li><li>可以直接使用现有的 Docker &#x2F; container runtime；</li><li>实验环境可以随时删除和重建；</li><li>与日常使用的 Git、Shell、VS Code 等工具能够自然结合。</li></ol><p>对于我的使用场景来说，“方便破坏、方便重建”比“尽量模拟生产环境”更加重要。</p><hr><h2 id="Minikube、kind-和-Docker-Desktop-Kubernetes-怎么选"><a href="#Minikube、kind-和-Docker-Desktop-Kubernetes-怎么选" class="headerlink" title="Minikube、kind 和 Docker Desktop Kubernetes 怎么选"></a>Minikube、kind 和 Docker Desktop Kubernetes 怎么选</h2><p>在本地学习 Kubernetes 时，常见选择包括 Minikube、kind 和 Docker Desktop Kubernetes，但三者的侧重点不同。</p><h3 id="Minikube"><a href="#Minikube" class="headerlink" title="Minikube"></a>Minikube</h3><p>Minikube 面向本地学习和开发，支持 Docker、KVM、Hyper-V 等 driver，并提供方便实验的命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">minikube dashboard</span><br><span class="line">minikube addons list</span><br><span class="line">minikube service &lt;service-name&gt;</span><br></pre></td></tr></table></figure><p>它操作直观，适合主动制造故障并观察 Kubernetes 的行为，因此我选择 Minikube 作为主要实验环境。</p><hr><h3 id="kind"><a href="#kind" class="headerlink" title="kind"></a>kind</h3><p>kind 即 Kubernetes IN Docker，使用 Docker container 模拟 Kubernetes node：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">kind create cluster</span><br><span class="line">kind delete cluster</span><br></pre></td></tr></table></figure><p>集群创建和销毁较快，也适合在 CI 中临时运行 integration test：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">CI</span><br><span class="line"> ↓</span><br><span class="line">创建 kind cluster</span><br><span class="line"> ↓</span><br><span class="line">部署待测试组件</span><br><span class="line"> ↓</span><br><span class="line">运行 integration test</span><br><span class="line"> ↓</span><br><span class="line">删除 cluster</span><br></pre></td></tr></table></figure><p>如果目标是 Controller &#x2F; Operator 开发、CI 集成测试或快速创建多个测试集群，kind 往往更合适；而我的重点是学习 Kubernetes 运维、排障和资源行为。</p><hr><h3 id="Docker-Desktop-Kubernetes"><a href="#Docker-Desktop-Kubernetes" class="headerlink" title="Docker Desktop Kubernetes"></a>Docker Desktop Kubernetes</h3><p>Docker Desktop 可以直接启用 Kubernetes，配置最少：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Docker Desktop</span><br><span class="line">    ↓</span><br><span class="line">Enable Kubernetes</span><br><span class="line">    ↓</span><br><span class="line">kubectl get nodes</span><br></pre></td></tr></table></figure><p>但我希望环境运行在 WSL2 的 Linux 工具链中，并且需要频繁执行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">创建集群</span><br><span class="line">→ 修改配置</span><br><span class="line">→ 制造故障</span><br><span class="line">→ 删除集群</span><br><span class="line">→ 重新创建</span><br></pre></td></tr></table></figure><p>Minikube 对这种实验式工作流更灵活。</p><hr><h2 id="最终选择"><a href="#最终选择" class="headerlink" title="最终选择"></a>最终选择</h2><p>因此我目前的选择是：</p><table><thead><tr><th>工具</th><th>更适合的场景</th></tr></thead><tbody><tr><td>Minikube</td><td>Kubernetes 学习、实验、排障</td></tr><tr><td>kind</td><td>CI、Operator 开发、快速测试集群</td></tr><tr><td>Docker Desktop Kubernetes</td><td>希望最少配置快速获得一个本地 Kubernetes</td></tr></tbody></table><p>我的目标是建立一个可以反复破坏和恢复的 Kubernetes 实验场，因此本文后续使用 <strong>Minikube + WSL2</strong>。</p><h2 id="我的环境"><a href="#我的环境" class="headerlink" title="我的环境"></a>我的环境</h2><p>这次排障使用的实际环境如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Windows 10 + WSL2</span><br><span class="line">Linux DESKTOP-P5CGV6I 6.18.33.2-microsoft-standard-WSL2</span><br><span class="line">Ubuntu 26.04.1 LTS (Resolute)</span><br><span class="line">CPU: 36 cores</span><br><span class="line">Memory: 31 GiB</span><br><span class="line">Swap: 8 GiB</span><br><span class="line">Docker Engine: 29.8.0（安装在 WSL 内）</span><br><span class="line">kubectl client: v1.34.11</span><br><span class="line">Minikube: v1.39.0</span><br><span class="line">Kubernetes: v1.37.0</span><br><span class="line">HTTP proxy: http://127.0.0.1:8118（Privoxy）</span><br></pre></td></tr></table></figure><h2 id="安装前准备"><a href="#安装前准备" class="headerlink" title="安装前准备"></a>安装前准备</h2><h3 id="安装-Docker-Engine"><a href="#安装-Docker-Engine" class="headerlink" title="安装 Docker Engine"></a>安装 Docker Engine</h3><p>Docker Engine 从 Docker 官方 apt 仓库安装，并直接运行在 WSL 中。安装后的第一次验证失败了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> docker run hello-world</span><br></pre></td></tr></table></figure><p>关键错误是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dial tcp ...:443: i/o timeout</span><br></pre></td></tr></table></figure><p>Shell 已经配置代理，并不代表 Docker daemon 会自动继承它。<code>docker pull</code> 的网络请求由 daemon 发起，因此需要给 Docker 的 systemd 服务单独配置代理：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">HTTP_PROXY</span>=http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8118</span></span><br><span class="line"><span class="attr">HTTPS_PROXY</span>=http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8118</span></span><br><span class="line"><span class="attr">NO_PROXY</span>=localhost,<span class="number">127.0</span>.<span class="number">0.1</span>,::<span class="number">1</span></span><br></pre></td></tr></table></figure><p>重新加载 systemd 配置并重启 Docker 后，再次运行 <code>docker run hello-world</code> 成功。这说明 WSL 内的 Docker daemon 已经可以通过 Privoxy 访问 Docker Hub。</p><p>随后把当前用户加入 <code>docker</code> 用户组，使后续 Docker 和 Minikube 操作不再依赖 <code>sudo</code>。</p><h3 id="安装-kubectl"><a href="#安装-kubectl" class="headerlink" title="安装 kubectl"></a>安装 kubectl</h3><p>安装后的客户端版本为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl client: v1.34.11</span><br></pre></td></tr></table></figure><p>本机 kubectl 为 v1.34.11，而 Minikube 集群为 Kubernetes v1.37.0，已超出 kubectl 官方支持的 ±1 个 minor 版本范围。因此本文后续统一使用 <code>minikube kubectl -- ...</code>，避免客户端版本偏差干扰实验：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube kubectl -- &lt;kubectl arguments&gt;</span><br></pre></td></tr></table></figure><h3 id="安装-Minikube"><a href="#安装-Minikube" class="headerlink" title="安装 Minikube"></a>安装 Minikube</h3><p>安装后的版本为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube version: v1.39.0</span><br></pre></td></tr></table></figure><p>Docker 已经能够运行普通容器，因此选择 Docker driver 启动 Minikube。这里需要先区分两个概念：driver 决定 Minikube 节点运行在哪里，而 Kubernetes container runtime 决定 Pod 镜像由谁拉取和运行。二者不一定相同。</p><h2 id="第一次启动-Minikube-集群"><a href="#第一次启动-Minikube-集群" class="headerlink" title="第一次启动 Minikube 集群"></a>第一次启动 Minikube 集群</h2><p>第一次使用 Docker driver 启动时，Minikube 为节点选择了 containerd 2.3.4 作为 Kubernetes container runtime：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Minikube driver: docker</span><br><span class="line">Kubernetes container runtime: containerd 2.3.4</span><br></pre></td></tr></table></figure><p>启动过程中首先出现 <code>NO_PROXY</code> 未包含 Minikube IP <code>192.168.49.2</code> 的警告。代理不应处理集群内部流量，因此将以下地址加入 <code>NO_PROXY</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">localhost,127.0.0.1,::1,192.168.49.0/24,10.96.0.0/12</span><br></pre></td></tr></table></figure><p>其中 <code>192.168.49.0/24</code> 覆盖 Minikube 节点网络，<code>10.96.0.0/12</code> 覆盖集群 Service 网络。这样访问节点、API Server 和 Service 时不会绕到外部代理。</p><p>Minikube 随后仍然警告无法访问 <code>registry.k8s.io</code>。集群表面上完成了启动，但节点一直处于 <code>NotReady</code>。</p><h2 id="从-Pod-状态定位镜像拉取失败"><a href="#从-Pod-状态定位镜像拉取失败" class="headerlink" title="从 Pod 状态定位镜像拉取失败"></a>从 Pod 状态定位镜像拉取失败</h2><p>先查看系统 Pod，而不是只看 <code>minikube start</code> 的最后一行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube kubectl -- get pods -A</span><br></pre></td></tr></table></figure><p>关键状态为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">kindnet                ImagePullBackOff</span><br><span class="line">coredns                Pending</span><br><span class="line">storage-provisioner    Pending</span><br></pre></td></tr></table></figure><p><code>coredns</code> 和 <code>storage-provisioner</code> 的 <code>Pending</code> 是结果，不一定是根因。继续使用 <code>kubectl describe pod</code> 查看 Events，发现最早、最具体的错误来自 kindnet：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker.io/kindest/kindnetd:v20260820-69b56db7</span><br><span class="line">direct TCP timeout to registry-1.docker.io</span><br></pre></td></tr></table></figure><p>因此排障方向从“为什么节点 NotReady”收敛为“谁在拉取 kindnet 镜像，以及它为什么没有走代理”。</p><h2 id="关键诊断：Docker-driver-不等于-container-runtime"><a href="#关键诊断：Docker-driver-不等于-container-runtime" class="headerlink" title="关键诊断：Docker driver 不等于 container runtime"></a>关键诊断：Docker driver 不等于 container runtime</h2><p>宿主 WSL 中的 Docker 已经可以拉取镜像，但这不能证明 Minikube 节点内的 containerd 也有代理。检查 containerd 服务环境：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube ssh -- <span class="string">&#x27;sudo systemctl show containerd --property=Environment&#x27;</span></span><br></pre></td></tr></table></figure><p>输出为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Environment=</span><br></pre></td></tr></table></figure><p>kubelet 的环境同样为空。这证明 Docker daemon 的 systemd 代理只解决了宿主 Docker 的网络请求，并没有传递给 Minikube 节点内部的 containerd。</p><p>直接沿着 Kubernetes 实际使用的 CRI 拉取路径复现问题：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">minikube ssh -- \</span><br><span class="line">  <span class="string">&#x27;sudo crictl pull docker.io/kindest/kindnetd:v20260820-69b56db7&#x27;</span></span><br></pre></td></tr></table></figure><p>结果仍然是超时。至此可以确认：失败点不在 kubectl、API Server 或宿主 Docker，而在没有代理环境的 containerd 拉取路径。</p><h2 id="验证-Minikube-节点到代理的网络路径"><a href="#验证-Minikube-节点到代理的网络路径" class="headerlink" title="验证 Minikube 节点到代理的网络路径"></a>验证 Minikube 节点到代理的网络路径</h2><p>Minikube 节点有自己的网络命名空间。节点内的 <code>127.0.0.1</code> 指向节点自身，不是 WSL 宿主机，因此不能把宿主代理地址原样写成 <code>http://127.0.0.1:8118</code>。</p><p>先让 Privoxy 监听 <code>0.0.0.0:8118</code>，使它能够接受来自 Minikube 节点的连接。Minikube 提供的宿主桥接名称在节点内解析为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">host.minikube.internal -&gt; 192.168.49.1</span><br></pre></td></tr></table></figure><p>验证 TCP 端口：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube ssh -- <span class="string">&#x27;nc -vz host.minikube.internal 8118&#x27;</span></span><br></pre></td></tr></table></figure><p>连接成功后，再分别从 WSL 和 Minikube 节点通过 Privoxy 请求 Registry v2 接口：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://registry.k8s.io/v2/ -&gt; HTTP 401</span><br></pre></td></tr></table></figure><p>这里的 <code>401 Unauthorized</code> 是预期结果。Docker Registry v2 会先返回认证挑战；能够收到 HTTP 401，说明 DNS、TCP、代理转发和 TLS 握手都已经成功。它不是网络不通的证据。</p><h2 id="最终修复：让实际拉取-Pod-镜像的-runtime-使用代理"><a href="#最终修复：让实际拉取-Pod-镜像的-runtime-使用代理" class="headerlink" title="最终修复：让实际拉取 Pod 镜像的 runtime 使用代理"></a>最终修复：让实际拉取 Pod 镜像的 runtime 使用代理</h2><p>这并不意味着 containerd 不适合使用代理；继续为 containerd 正确配置代理同样是有效方案。这里选择 Docker runtime，主要是为了复用已经验证成功的 Docker daemon 代理路径，减少学习环境中的额外变量。因此删除失败集群，并通过 <code>--docker-env</code> 将代理配置传入 Minikube 节点内的 Docker daemon。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">minikube delete</span><br><span class="line"></span><br><span class="line">minikube start \</span><br><span class="line">  --driver=docker \</span><br><span class="line">  --container-runtime=docker \</span><br><span class="line">  --docker-env HTTP_PROXY=http://host.minikube.internal:8118 \</span><br><span class="line">  --docker-env HTTPS_PROXY=http://host.minikube.internal:8118 \</span><br><span class="line">  --docker-env NO_PROXY=localhost,127.0.0.1,::1,192.168.49.0/24,10.96.0.0/12</span><br></pre></td></tr></table></figure><p>这次配置解决问题的关键不是简单地“换成 Docker”，而是让真正执行 Pod 镜像拉取的 runtime 获得可达的代理地址。</p><h2 id="验证最终集群"><a href="#验证最终集群" class="headerlink" title="验证最终集群"></a>验证最终集群</h2><p>查看节点：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube kubectl -- get nodes -o wide</span><br></pre></td></tr></table></figure><p>关键结果：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">STATUS              Ready</span><br><span class="line">Kubernetes          v1.37.0</span><br><span class="line">INTERNAL-IP         192.168.49.2</span><br><span class="line">CONTAINER-RUNTIME   docker://29.7.2</span><br></pre></td></tr></table></figure><p>查看 Minikube 组件状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube status</span><br></pre></td></tr></table></figure><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">host: Running</span><br><span class="line">kubelet: Running</span><br><span class="line">apiserver: Running</span><br><span class="line">kubeconfig: Configured</span><br></pre></td></tr></table></figure><p>再次检查 <code>kube-system</code>，以下 Pod 均为 <code>Running</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">coredns</span><br><span class="line">etcd</span><br><span class="line">kube-apiserver</span><br><span class="line">kube-controller-manager</span><br><span class="line">kube-proxy</span><br><span class="line">kube-scheduler</span><br><span class="line">storage-provisioner</span><br></pre></td></tr></table></figure><h2 id="部署一个最小测试应用"><a href="#部署一个最小测试应用" class="headerlink" title="部署一个最小测试应用"></a>部署一个最小测试应用</h2><p>创建第一个 Deployment：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube kubectl -- create deployment web --image=nginx:alpine</span><br></pre></td></tr></table></figure><p>依次查看 Deployment、ReplicaSet 和 Pod，可以观察控制器如何把声明的副本状态逐层转换为实际运行的容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">minikube kubectl -- get deployment web</span><br><span class="line">minikube kubectl -- get replicaset</span><br><span class="line">minikube kubectl -- get pod</span><br></pre></td></tr></table></figure><p>然后用 NodePort Service 暴露应用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">minikube kubectl -- expose deployment web --<span class="built_in">type</span>=NodePort --port=80</span><br><span class="line">minikube kubectl -- get service web</span><br><span class="line">minikube kubectl -- get endpoints web</span><br><span class="line">minikube kubectl -- get endpointslice -l kubernetes.io/service-name=web</span><br></pre></td></tr></table></figure><p>Service 通过标签选择 Pod；现代 Kubernetes 主要使用 EndpointSlice 表示 Service 对应的后端 Pod 端点，同时保留 <code>get endpoints</code> 便于对照。获取本地访问地址：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">minikube service web --url</span><br></pre></td></tr></table></figure><p>最终通过以下地址成功访问 Nginx 页面：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://127.0.0.1:43449</span><br></pre></td></tr></table></figure><p>在 WSL2 + Docker driver 环境下，<code>minikube service web --url</code> 可能返回 <code>127.0.0.1:&lt;随机端口&gt;</code> 形式的本地访问地址。这里的 <code>43449</code> 并不是 Service 本身的 NodePort，而是 Minikube 为当前环境建立的本地访问入口。</p><p>这次验证对应的对象和请求路径是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Deployment</span><br><span class="line">  -&gt; ReplicaSet</span><br><span class="line">  -&gt; Pod</span><br><span class="line">  -&gt; Service</span><br><span class="line">  -&gt; EndpointSlice</span><br><span class="line">  -&gt; HTTP request</span><br></pre></td></tr></table></figure><p>Deployment 管理期望副本，ReplicaSet 维持 Pod 数量，Pod 承载 Nginx 容器；Service 提供稳定入口，EndpointSlice 记录后端 Pod 端点，最后 HTTP 请求到达 Nginx。</p><h2 id="常用排障路径"><a href="#常用排障路径" class="headerlink" title="常用排障路径"></a>常用排障路径</h2><p>这次问题适合按以下顺序排查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">minikube status</span><br><span class="line">minikube kubectl -- get nodes -o wide</span><br><span class="line">minikube kubectl -- get pods -A</span><br><span class="line">minikube kubectl -- describe pod &lt;pod-name&gt; -n &lt;namespace&gt;</span><br><span class="line">minikube ssh -- <span class="string">&#x27;sudo systemctl show containerd --property=Environment&#x27;</span></span><br><span class="line">minikube ssh -- <span class="string">&#x27;sudo crictl pull &lt;image&gt;&#x27;</span></span><br><span class="line">minikube ssh -- <span class="string">&#x27;nc -vz host.minikube.internal 8118&#x27;</span></span><br></pre></td></tr></table></figure><p>顺序背后的思路是：先确认集群和节点状态，再从异常 Pod 的 Events 找到具体镜像，最后进入节点，沿实际 container runtime 的拉取路径复现网络错误。</p><h2 id="Lessons-learned"><a href="#Lessons-learned" class="headerlink" title="Lessons learned"></a>Lessons learned</h2><ol><li><strong>Docker driver 不等于 Kubernetes container runtime。</strong> 本次失败环境使用 Docker driver，但 Pod 镜像实际由节点内的 containerd 拉取。</li><li><strong>Shell 代理、Docker daemon 代理和 container runtime 代理互相独立。</strong> 一个组件能联网，不能证明另一个组件也继承了代理。</li><li><strong>Minikube 节点内的 <code>127.0.0.1</code> 不是 WSL 宿主 localhost。</strong> 它只指向节点自身。</li><li><strong>访问宿主服务应使用 <code>host.minikube.internal</code>。</strong> 本次它解析到 <code>192.168.49.1</code>，是节点访问 Privoxy 的正确桥梁。</li><li><strong>排查 <code>ImagePullBackOff</code> 要从 Pod Events 追到真实 runtime。</strong> <code>describe pod</code> 找到镜像和 Registry，再用 <code>crictl pull</code> 复现，能避免在错误层面反复修改配置。</li><li><strong>Registry <code>/v2/</code> 返回 HTTP 401 是正常认证挑战。</strong> 它证明代理网络和 TLS 路径已经打通，不代表连接失败。</li><li><strong>注意客户端与集群版本差异。</strong> 本次 kubectl 为 v1.34.11，Kubernetes 为 v1.37.0，因此文章使用 <code>minikube kubectl -- ...</code> 执行需要版本对齐的命令。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这次故障的表象是节点 <code>NotReady</code>、kindnet <code>ImagePullBackOff</code>，根因却是 Minikube 节点内负责拉取 Pod 镜像的 containerd 没有代理环境。</p><p>真正有效的排障不是重复执行 <code>minikube start</code>，而是从 Pod Events 找到失败镜像，确认实际 container runtime，再进入节点复现拉取请求并验证代理路径。最终重建集群，让 Docker runtime 使用 <code>host.minikube.internal:8118</code>，集群恢复 <code>Ready</code>，系统 Pod 和第一个 Nginx 工作负载均正常运行。</p>]]>
    </content>
    <id>https://zhouxingyu.dev/posts/WSL2-Minikube-Setup-and-Troubleshooting/</id>
    <link href="https://zhouxingyu.dev/posts/WSL2-Minikube-Setup-and-Troubleshooting/"/>
    <published>2026-09-04T05:30:00.000Z</published>
    <summary>记录在WSL2中搭建Minikube的完整过程，包括驱动选择、资源配置、网络问题、常见故障以及排障思路。</summary>
    <title>在 WSL2 中搭建 Minikube：从安装到常见故障排查</title>
    <updated>2026-09-04T05:30:00.000Z</updated>
  </entry>
</feed>
