<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>setKing</title>
  
  
  <link href="https://github.com/setking/setking.github.io/atom.xml" rel="self"/>
  
  <link href="https://github.com/setking/setking.github.io/"/>
  <updated>2026-03-10T09:35:58.386Z</updated>
  <id>https://github.com/setking/setking.github.io/</id>
  
  <author>
    <name>setKing</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Ubuntu安装RocketMQ v5.3.2和docker安装rocketmq-dashboard</title>
    <link href="https://github.com/setking/setking.github.io/2026/03/07/linux%E8%B8%A9%E5%9D%91-Linux%E5%AE%89%E8%A3%85rocketmq5-3-2/"/>
    <id>https://github.com/setking/setking.github.io/2026/03/07/linux%E8%B8%A9%E5%9D%91-Linux%E5%AE%89%E8%A3%85rocketmq5-3-2/</id>
    <published>2026-03-07T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.386Z</updated>
    
    <content type="html"><![CDATA[<p>本教程系统是Ubuntu 24.04，RocketMQ版本：5.3.2。RocketMQ安装在宿主机上rocketmq-dashboard安装在docker里</p><p>RocketMQ 需要 Java：</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></pre></td><td class="code"><pre><span class="line">sudo apt update</span><br><span class="line">sudo apt install -y openjdk-17-jdk</span><br><span class="line"></span><br><span class="line"># 验证</span><br><span class="line">java -version</span><br></pre></td></tr></table></figure><p>下载和移动rocketMQ二进制文件：</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></pre></td><td class="code"><pre><span class="line">cd ~</span><br><span class="line">wget https://archive.apache.org/dist/rocketmq/5.3.2/rocketmq-all-5.3.2-bin-release.zip</span><br><span class="line">unzip rocketmq-all-5.3.2-bin-release.zip</span><br><span class="line">sudo rm -rf /usr/local/rocketmq</span><br><span class="line">sudo mv rocketmq-all-5.3.2-bin-release /usr/local/rocketmq</span><br><span class="line">ls /usr/local/rocketmq/bin/</span><br></pre></td></tr></table></figure><p>设置自启动和后台启动</p><p>配置 systemd 自启动</p><blockquote><p>我的rocket安装目录：&#x2F;usr&#x2F;rocketmq&#x2F;</p></blockquote><ol><li><p>namesrv 服务</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">sudo nano /etc/systemd/system/rocketmq-namesrv.service</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><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">[Unit]</span><br><span class="line">Description=Apache RocketMQ NameServer</span><br><span class="line">After=network.target</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=simple</span><br><span class="line">Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64</span><br><span class="line">ExecStart=/usr/rocketmq/bin/mqnamesrv</span><br><span class="line">ExecStop=/usr/rocketmq/bin/mqshutdown namesrv</span><br><span class="line">Restart=on-failure</span><br><span class="line">RestartSec=5s</span><br><span class="line">LimitNOFILE=65536</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure></li><li><p>配置 Broker</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">sudo nano /usr/rocketmq/conf/broker.conf</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><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><br><span class="line">brokerClusterName = DefaultCluster</span><br><span class="line">brokerName = broker-a</span><br><span class="line">brokerId = 0</span><br><span class="line">namesrvAddr = 127.0.0.1:9876</span><br><span class="line">defaultTopicQueueNums = 4</span><br><span class="line">autoCreateTopicEnable = true</span><br><span class="line">autoCreateSubscriptionGroup = true</span><br><span class="line">listenPort = 10911</span><br><span class="line">deleteWhen = 04</span><br><span class="line">fileReservedTime = 48</span><br><span class="line">brokerRole = ASYNC_MASTER</span><br><span class="line">flushDiskType = ASYNC_FLUSH</span><br><span class="line"># 存储路径（可自定义）</span><br><span class="line">storePathRootDir = /opt/rocketmq/store</span><br><span class="line">storePathCommitLog = /opt/rocketmq/store/commitlog</span><br></pre></td></tr></table></figure></li><li><p>broker 服务</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">sudo nano /etc/systemd/system/rocketmq-broker.service</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><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">[Unit]</span><br><span class="line">Description=Apache RocketMQ Broker</span><br><span class="line">After=network.target rocketmq-namesrv.service</span><br><span class="line">Requires=rocketmq-namesrv.service</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=simple</span><br><span class="line">Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64</span><br><span class="line">ExecStart=/opt/rocketmq/bin/mqbroker -c /opt/rocketmq/conf/broker.conf</span><br><span class="line">ExecStop=/opt/rocketmq/bin/mqshutdown broker</span><br><span class="line">Restart=on-failure</span><br><span class="line">RestartSec=5s</span><br><span class="line">LimitNOFILE=65536</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure></li><li><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></pre></td><td class="code"><pre><span class="line"># 重载 systemd</span><br><span class="line">sudo systemctl daemon-reload</span><br><span class="line"></span><br><span class="line"># 启动 NameServer</span><br><span class="line">sudo systemctl enable --now rocketmq-namesrv</span><br><span class="line">sudo systemctl status rocketmq-namesrv</span><br><span class="line"></span><br><span class="line"># 启动 Broker</span><br><span class="line">sudo systemctl enable --now rocketmq-broker</span><br><span class="line">sudo systemctl status rocketmq-broker</span><br></pre></td></tr></table></figure></li><li><p>确认状态</p><figure class="highlight plaintext"><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">sudo systemctl status rocketmq-namesrv</span><br><span class="line">sudo systemctl status rocketmq-broker</span><br></pre></td></tr></table></figure></li><li><p>调整 JVM 内存（重要！）</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></pre></td><td class="code"><pre><span class="line">// broker</span><br><span class="line">sed -i &#x27;s/-Xms8g -Xmx8g/-Xms1g -Xmx1g/g&#x27; /usr/local/rocketmq/bin/runbroker.sh</span><br><span class="line">sed -i &#x27;s/-XX:MaxDirectMemorySize=15g/-XX:MaxDirectMemorySize=1g/g&#x27; /usr/local/rocketmq/bin/runbroker.sh</span><br><span class="line">// namesrv</span><br><span class="line">sed -i &#x27;s/-Xms4g -Xmx4g -Xmn2g/-Xms512m -Xmx512m -Xmn256m/g&#x27; /usr/local/rocketmq/bin/runserver.sh</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></pre></td><td class="code"><pre><span class="line">grep &quot;Xms&quot; /usr/local/rocketmq/bin/runbroker.sh</span><br><span class="line">grep &quot;Xms&quot; /usr/local/rocketmq/bin/runserver.sh</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></pre></td><td class="code"><pre><span class="line">sudo systemctl restart rocketmq-namesrv</span><br><span class="line">sudo systemctl restart rocketmq-broker</span><br><span class="line">free -h</span><br></pre></td></tr></table></figure></li></ol><p>使用docker安装dashboard</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></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name rocketmq-dashboard \</span><br><span class="line">  -e &quot;JAVA_OPTS=-Drocketmq.namesrv.addr=host.docker.internal:9876&quot; \</span><br><span class="line">  -p 8080:8082 \</span><br><span class="line">  --add-host=host.docker.internal:host-gateway \</span><br><span class="line">  apacherocketmq/rocketmq-dashboard:latest</span><br></pre></td></tr></table></figure><ol><li>确认容器正常运行：</li></ol><figure class="highlight plaintext"><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 logs rocketmq-dashboard</span><br></pre></td></tr></table></figure><ol start="2"><li>确认端口监听：</li></ol><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">ss -tlnp | grep 8080</span><br></pre></td></tr></table></figure><ol start="3"><li>确认防火墙：</li></ol><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></pre></td><td class="code"><pre><span class="line"># 查看防火墙状态</span><br><span class="line">ufw status</span><br><span class="line"></span><br><span class="line"># 如果防火墙开着，放行8080</span><br><span class="line">ufw allow 8080</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;本教程系统是Ubuntu 24.04，RocketMQ版本：5.3.2。RocketMQ安装在宿主机上rocketmq-dashboard安装在docker里&lt;/p&gt;
&lt;p&gt;RocketMQ 需要 Java：&lt;/p&gt;
&lt;figure class=&quot;highlight pla</summary>
      
    
    
    
    <category term="教程" scheme="https://github.com/setking/setking.github.io/categories/%E6%95%99%E7%A8%8B/"/>
    
    <category term="文档" scheme="https://github.com/setking/setking.github.io/categories/%E6%95%99%E7%A8%8B/%E6%96%87%E6%A1%A3/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="文档" scheme="https://github.com/setking/setking.github.io/tags/%E6%96%87%E6%A1%A3/"/>
    
  </entry>
  
  <entry>
    <title>Kubernetes监控</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/15/Kubernetes-20-Kubernetes%E7%9B%91%E6%8E%A7%E4%BD%93%E7%B3%BB/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/15/Kubernetes-20-Kubernetes%E7%9B%91%E6%8E%A7%E4%BD%93%E7%B3%BB/</id>
    <published>2025-12-15T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<h4 id="Prometheus"><a href="#Prometheus" class="headerlink" title="Prometheus"></a>Prometheus</h4><p>Prometheus工作方式如下图所示：</p><p><img src="/assets/img/Kubernetes/Prometheus.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Prometheus.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Prometheus 项目工作的核心，是使用 Pull （抓取）的方式去搜集被监控对象的 Metrics 数据（监控指标数据），然后，再把这些数据保存在一个 TSDB （时间序列数据库，比如 OpenTSDB、InfluxDB 等）当中，以便后续可以按照时间进行检索。</p><p>有了这套核心监控机制， Prometheus 剩下的组件就是用来配合这套机制的运行。比如 Pushgateway，可以允许被监控对象以 Push 的方式向 Prometheus 推送 Metrics 数据。而 Alertmanager，则可以根据 Metrics 信息灵活地设置报警。当然， Prometheus 最受用户欢迎的功能，还是通过 Grafana 对外暴露出的、可以灵活配置的监控数据可视化界面。</p><p>按照 Metrics 数据的来源，Kubernetes 的监控体系有以下几种：</p><p><strong>第一种 Metrics，是宿主机的监控数据</strong></p><p><strong>第二种 Metrics，是来自于 Kubernetes 的 API Server、kubelet 等组件的 &#x2F;metrics API</strong></p><p><strong>第三种 Metrics，是 Kubernetes 相关的监控数据</strong></p><h5 id="Custom-Metrics"><a href="#Custom-Metrics" class="headerlink" title="Custom Metrics"></a>Custom Metrics</h5><p>Custom Metrics 已经成为了 Kubernetes 的一项标准能力。并且，Kubernetes 的自动扩展器组件 Horizontal Pod Autoscaler （HPA）， 也可以直接使用 Custom Metrics 来执行用户指定的扩展策略，这里的整个过程都是非常灵活和可定制的。</p><p>Kubernetes 里的 Custom Metrics 机制，是借助 Aggregator APIServer 扩展机制来实现的。这里的具体原理是，当你把 Custom Metrics APIServer 启动之后，Kubernetes 里就会出现一个叫作<code>custom.metrics.k8s.io</code>的 API。而当你访问这个 URL 时，Aggregator 就会把你的请求转发给 Custom Metrics APIServer 。</p><p>Custom Metrics APIServer 的实现，是一个 Prometheus 项目的 Adaptor。</p><h5 id="容器日志收集与管理"><a href="#容器日志收集与管理" class="headerlink" title="容器日志收集与管理"></a>容器日志收集与管理</h5><p>Kubernetes 里面对容器日志的处理方式，都叫作 cluster-level-logging，即：这个日志处理系统，与容器、Pod 以及 Node 的生命周期都是完全无关的。这种设计是为了保证，无论是容器挂了、Pod 被删除，甚至节点宕机的时候，应用的日志依然可以被正常获取到。</p><p>对于一个容器来说，当应用把日志输出到 stdout 和 stderr 之后，容器项目在默认情况下就会把这些日志输出到宿主机上的一个 JSON 文件里。这样，你通过 kubectl logs 命令就可以看到这些容器的日志了。</p><p>Kubernetes 本身，不会做容器日志收集工作的，为了实现上述 cluster-level-logging，需要在部署集群的时候，提前对具体的日志方案进行规划。</p><p>**第一种，在 Node 上部署 logging agent，将日志文件转发到后端存储里保存起来。**这个方案的架构图如下所示。</p><p><img src="/assets/img/Kubernetes/logging-agent.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/logging-agent.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>这里的核心就在于 logging agent ，它一般都会以 DaemonSet 的方式运行在节点上，然后将宿主机上的容器日志目录挂载进去，最后由 logging-agent 把日志转发出去。</p><p>这种方案的不足之处就在于，它要求应用输出的日志，都必须是直接输出到容器的 stdout 和 stderr 里。</p><p>**第二种，是对这种特殊情况的一个处理，即：当容器的日志只能输出到某些文件里的时候，我们可以通过一个 sidecar 容器把这些日志文件重新输出到 sidecar 的 stdout 和 stderr 上，这样就能够继续使用第一种方案了。**这个方案的具体工作原理，如下所示。</p><p><img src="/assets/img/Kubernetes/logging-backend.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/logging-backend.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>这种方案宿主机上实际上会存在两份相同的日志文件：一份是应用自己写入的；另一份则是 sidecar 的 stdout 和 stderr 对应的 JSON 文件。这对磁盘是很大的浪费。</p><p>**第三种方案，就是通过一个 sidecar 容器，直接把应用的日志文件发送到远程存储里面去。**也就是相当于把方案一里的 logging agent，放在了应用 Pod 里。这种方案的架构如下所示：</p><p><img src="/assets/img/Kubernetes/logging-paln3.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/logging-paln3.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>在这种方案里，应用还可以直接把日志输出到固定的文件里而不是 stdout，logging-agent 还可以使用 fluentd，后端存储还可以是 ElasticSearch。只不过， fluentd 的输入源，变成了应用的日志文件。</p><p>当然也可以在编写应用的时候，就直接指定好日志的存储后端。在这种方案下，Kubernetes 就完全不必操心容器日志的收集了，这对于本身已经有完善的日志处理系统的公司来说，是一个非常好的选择。</p><p>无论是哪种方案，都必须要及时将这些日志文件从宿主机上清理掉，或者给日志目录专门挂载一些容量巨大的远程盘。否则，一旦主磁盘分区被打满，整个系统就可能会陷入奔溃状态，这是非常麻烦的。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h4 id=&quot;Prometheus&quot;&gt;&lt;a href=&quot;#Prometheus&quot; class=&quot;headerlink&quot; title=&quot;Prometheus&quot;&gt;&lt;/a&gt;Prometheus&lt;/h4&gt;&lt;p&gt;Prometheus工作方式如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>CRI和容器运行时</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/09/Kubernetes-19-CRI%E5%92%8C%E5%AE%B9%E5%99%A8%E8%BF%90%E8%A1%8C%E6%97%B6/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/09/Kubernetes-19-CRI%E5%92%8C%E5%AE%B9%E5%99%A8%E8%BF%90%E8%A1%8C%E6%97%B6/</id>
    <published>2025-12-09T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p>有了CRI的Kubernetes 的架构图：</p><p><img src="/assets/img/Kubernetes/CRI.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/CRI.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>CRI 机制能够发挥作用的核心，在于每一种容器项目现在都可以自己实现一个 CRI shim，自行对 CRI 请求进行处理。这样，Kubernetes 就有了一个统一的容器抽象层，使得下层容器运行时可以自由地对接进入 Kubernetes 当中。CRI shim，是容器项目的维护者们自由发挥的“场地”。</p><p>CRI的接口可以分为两组：</p><ul><li>第一组，是 RuntimeService。它提供的接口，主要是跟容器相关的操作。比如，创建和启动容器、删除容器、执行 exec 命令等等。</li><li>而第二组，则是 ImageService。它提供的接口，主要是容器镜像相关的操作，比如拉取镜像、删除镜像等等。</li></ul><p>RuntimeService<strong>接口本身，只关注容器，不关注 Pod。</strong></p><p><strong>首先</strong>，Pod 是 Kubernetes 的编排概念，而不是容器运行时的概念。所以，我们就不能假设所有下层容器项目，都能够暴露出可以直接映射为 Pod 的 API。</p><p><strong>其次</strong>，如果 CRI 里引入了关于 Pod 的概念，那么接下来只要 Pod API 对象的字段发生变化，那么 CRI 就很有可能需要变更。而在 Kubernetes 开发的前期，Pod 对象的变化还是比较频繁的，但对于 CRI 这样的标准接口来说，这个变更频率就有点麻烦了。</p><p>在 CRI 的设计里，并没有一个直接创建 Pod 或者启动 Pod 的接口。</p><p>CRI 里有一组叫作 RunPodSandbox 的接口。 PodSandbox是抽取了 Pod 里的一部分与容器运行时相关的字段，比如 HostName、DnsConfig、CgroupParent 等。PodSandbox 这个接口描述的，是 Kubernetes 将 Pod 这个概念映射到容器运行时层面所需要的字段，或者说是一个 Pod 对象子集。</p><p>下图展示了Kubernetes 期望的 Pod 模型：</p><p><img src="/assets/img/Kubernetes/Kubernetes-pod.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Kubernetes-pod.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>当执行 kubectl run 创建了一个名叫 foo 的、包括了 A、B 两个容器的 Pod 之后。这个 Pod 的信息最后来到 kubelet，kubelet 就会按照图中所示的顺序来调用 CRI 接口。</p><p>CRI有一个叫Streaming的API，用于实现 exec、logs 等接口，这些 gRPC 接口调用期间，kubelet 需要跟容器项目维护一个长连接来传输数据。</p><p>CRI shim 里对 Streaming API 的实现，依赖于一套独立的 Streaming Server 机制，如图所示：</p><p><img src="/assets/img/Kubernetes/StreamingAPI.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/StreamingAPI.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>当我们对一个容器执行 kubectl exec 命令的时候，这个请求首先交给 API Server，然后 API Server 就会调用 kubelet 的 Exec API。接着kubelet 就会调用 CRI 的 Exec 接口，而负责响应这个接口的，自然就是具体的 CRI shim。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;有了CRI的Kubernetes 的架构图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/img/Kubernetes/CRI.png&quot; class=&quot;lazyload placeholder&quot; data-srcset=&quot;/assets/img/Kubernetes/</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>SIG-Node与CRI</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/07/Kubernetes-18-SIG-Node%E4%B8%8ECRI/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/07/Kubernetes-18-SIG-Node%E4%B8%8ECRI/</id>
    <published>2025-12-07T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p>与 kubelet 以及容器运行时管理相关的内容，都属于 SIG-Node 的范畴。</p><p>kubelet 工作原理如图所示：</p><p><img src="/assets/img/Kubernetes/kubelet.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/kubelet.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>kubelet 的工作核心，就是一个控制循环，即：SyncLoop（图中的大圆圈）。而驱动这个控制循环运行的事件，包括四种：</p><ol><li>Pod 更新事件；</li><li>Pod 生命周期变化；</li><li>kubelet 本身设置的执行周期；</li><li>定时的清理事件。</li></ol><p>kubelet 启动的时候，要做的第一件事情，就是设置 Listers，也就是注册它所关心的各种事件的 Informer。这些 Informer，就是 SyncLoop 需要处理的数据的来源。kubelet 还负责维护着很多很多其他的子控制循环（也就是图中的小圆圈）。，比如 Volume Manager、Image Manager、Node Status Manager 等等。这些控制循环的责任，就是通过控制器模式，完成 kubelet 的某项具体职责。</p><p>kubelet 是通过 Watch 机制，监听了与自己相关的 Pod 对象的变化。这个 Watch 的过滤条件是该 Pod 的 nodeName 字段与自己相同。kubelet 会把这些 Pod 的信息缓存在自己的内存里。</p><p>当一个 Pod 完成调度、与一个 Node 绑定起来之后， 这个 Pod 的变化就会触发 kubelet 在控制循环里注册的 Handler，也就是上图中的 HandlePods 部分。此时，通过检查该 Pod 在 kubelet 内存里的状态，kubelet 就能够判断出这是一个新调度过来的 Pod，从而触发 Handler 里 ADD 事件对应的处理逻辑。</p><p>在具体的处理过程当中，kubelet 会启动一个名叫 Pod Update Worker 的、单独的 Goroutine 来完成对 Pod 的处理工作。</p><p><strong>kubelet 调用下层容器运行时的执行过程，而是通过一组叫作 CRI（Container Runtime Interface，容器运行时接口）的 gRPC 接口来间接执行的。</strong></p><p>有了CRI 之后，Kubernetes 以及 kubelet 的架构如图所示：</p><p><img src="/assets/img/Kubernetes/CRIkube.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/CRIkube.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>当 Kubernetes 通过编排能力创建了一个 Pod 之后，调度器会为这个 Pod 选择一个具体的节点来运行。这时候，kubelet 就会通过 SyncLoop 来判断需要执行的具体操作，比如创建一个 Pod。此时，kubelet 实际上就会调用一个叫作 GenericRuntime 的通用组件来发起创建 Pod 的 CRI 请求。</p><p>至于CRI请求，每台宿主机上都会单独安装一个负责响应 CRI 的组件，这个组件，一般被称作 CRI shim。CRI shim 的工作，就是扮演 kubelet 与容器项目之间的“垫片”（shim）。所以它的作用非常单一，那就是实现 CRI 规定的每个接口，然后把具体的 CRI 请求“翻译”成对后端容器项目的请求或者操作。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;与 kubelet 以及容器运行时管理相关的内容，都属于 SIG-Node 的范畴。&lt;/p&gt;
&lt;p&gt;kubelet 工作原理如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/img/Kubernetes/kubelet.png&quot; class=&quot;lazyload </summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>GPU管理和Device Plugin机制</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/05/Kubernetes-17-GPU%E7%AE%A1%E7%90%86%E5%92%8CDevice-Plugin%E6%9C%BA%E5%88%B6/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/05/Kubernetes-17-GPU%E7%AE%A1%E7%90%86%E5%92%8CDevice-Plugin%E6%9C%BA%E5%88%B6/</id>
    <published>2025-12-05T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p>在 Kubernetes 的 GPU 支持的实现里，kubelet 实际上就是将GPU 设备目录和GPU 驱动目录，设置在了创建该容器的 CRI （Container Runtime Interface）参数里面。这样，等到该容器启动之后，对应的容器里就会出现 GPU 设备和驱动的路径了。</p><p>Kubernetes 在 Pod 的 API 对象里，并没有为 GPU 专门设置一个资源类型字段，而是使用了一种叫作 Extended Resource（ER）的特殊字段来负责传递 GPU 的信息。</p><p>在 Kubernetes 的 GPU 支持方案里，并不需要真正去做上述关于 Extended Resource 的这些操作。在 Kubernetes 中，对所有硬件加速设备进行管理的功能，都是由一种叫作 Device Plugin 的插件来负责的。这其中，当然也就包括了对该硬件的 Extended Resource 进行汇报的逻辑。</p><p>下图展示了Device Plugin 机制：</p><p><img src="/assets/img/Kubernetes/Device-Plugin.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Device-Plugin.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>对于每一种硬件设备，都需要有它所对应的 Device Plugin 进行管理，这些 Device Plugin，都通过 gRPC 的方式，同 kubelet 连接起来。</p><p>右侧开始看起，这个 Device Plugin 会通过一个叫作 ListAndWatch 的 API，定期向 kubelet 汇报该 Node 上 GPU 的列表。ListAndWatch 向上汇报的信息，只有本机上 GPU 的 ID 列表，而不会有任何关于 GPU 设备本身的信息。而且 kubelet 在向 API Server 汇报的时候，只会汇报该 GPU 对应的 Extended Resource 的数量。kubelet 本身，会将这个 GPU 的 ID 列表保存在自己的内存里，并通过 ListAndWatch API 定时更新。当一个 Pod 想要使用一个 GPU 的时候，在 Pod 的 limits 字段声明<code>nvidia.com/gpu: 1</code>。Kubernetes 的调度器就会从它的缓存里，寻找 GPU 数量满足条件的 Node，然后将缓存里的 GPU 数量减 1，完成 Pod 与 Node 的绑定。</p><p>这个调度成功后的 Pod 信息，自然就会被对应的 kubelet 拿来进行容器操作。而当 kubelet 发现这个 Pod 的容器请求一个 GPU 的时候，kubelet 就会从自己持有的 GPU 列表里，为这个容器分配一个 GPU。此时，kubelet 就会向本机的 Device Plugin 发起一个 Allocate() 请求。这个请求携带的参数，正是即将分配给该容器的设备 ID 列表。</p><p>当 Device Plugin 收到 Allocate 请求之后，它就会根据 kubelet 传递过来的设备 ID，从 Device Plugin 里找到这些设备对应的设备路径和驱动目录。当然，这些信息，正是 Device Plugin 周期性的从本机查询到的。比如，在 NVIDIA Device Plugin 的实现里，它会定期访问 nvidia-docker 插件，从而获取到本机的 GPU 信息。</p><p>被分配 GPU 对应的设备路径和驱动目录信息被返回给 kubelet 之后，kubelet 就完成了为一个容器分配 GPU 的操作。接下来，kubelet 会把这些信息追加在创建该容器所对应的 CRI 请求当中。这样，当这个 CRI 请求发给 Docker 之后，Docker 为你创建出来的容器里，就会出现这个 GPU 设备，并把它所需要的驱动目录挂载进去。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在 Kubernetes 的 GPU 支持的实现里，kubelet 实际上就是将GPU 设备目录和GPU 驱动目录，设置在了创建该容器的 CRI （Container Runtime Interface）参数里面。这样，等到该容器启动之后，对应的容器里就会出现 GPU 设备</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>默认调度器</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/04/Kubernetes-16-%E9%BB%98%E8%AE%A4%E8%B0%83%E5%BA%A6%E5%99%A8/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/04/Kubernetes-16-%E9%BB%98%E8%AE%A4%E8%B0%83%E5%BA%A6%E5%99%A8/</id>
    <published>2025-12-04T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p><strong>在 Kubernetes 项目中，默认调度器的主要职责，就是为一个新创建出来的 Pod，寻找一个最合适的节点（Node）。</strong></p><p>这个最合适是指：</p><ol><li>从集群所有的节点中，根据调度算法挑选出所有可以运行该 Pod 的节点；</li><li>从第一步的结果中，再根据调度算法挑选一个最符合条件的节点作为最终结果。</li></ol><p>在具体的调度流程中，默认调度器会首先调用一组叫作 Predicate 的调度算法，来检查每个 Node。然后，再调用一组叫作 Priority 的调度算法，来给上一步得到的结果里的每个 Node 打分。最终的调度结果，就是得分最高的那个 Node。调度器对一个 Pod 调度成功，实际上就是将它的 spec.nodeName 字段填上调度结果的节点名字。</p><p>调度机制的工作原理如图所示：</p><p><img src="/assets/img/Kubernetes/scheduler.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/scheduler.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Kubernetes 的调度器的核心，实际上就是两个相互独立的控制循环。</p><p><strong>第一个控制循环，我们可以称之为 Informer Path</strong>。它的主要目的，是启动一系列 Informer，用来监听（Watch）Etcd 中 Pod、Node、Service 等与调度相关的 API 对象的变化。</p><p>默认情况下，Kubernetes 的调度队列是一个 PriorityQueue（优先级队列），并且当某些集群信息发生变化的时候，调度器还会对调度队列里的内容进行一些特殊操作。</p><p>Kubernetes 的默认调度器还要负责对调度器缓存（即：scheduler cache）进行更新。事实上，Kubernetes 调度部分进行性能优化的一个最根本原则，就是尽最大可能将集群信息 Cache 化，以便从根本上提高 Predicate 和 Priority 调度算法的执行效率。</p><p><strong>第二个控制循环，是调度器负责 Pod 调度的主循环，可以称之为 Scheduling Path。</strong></p><p>Scheduling Path 的主要逻辑，就是不断地从调度队列里出队一个 Pod。然后，调用 Predicates 算法进行“过滤”。这一步“过滤”得到的一组 Node，就是所有可以运行这个 Pod 的宿主机列表。当然，Predicates 算法需要的 Node 信息，都是从 Scheduler Cache 里直接拿到的，这是调度器保证算法执行效率的主要手段之一。接下来，调度器就会再调用 Priorities 算法为上述列表里的 Node 打分，分数从 0 到 10。得分最高的 Node，就会作为这次调度的结果。调度算法执行完成后，调度器就需要将 Pod 对象的 nodeName 字段的值，修改为上述 Node 的名字。<strong>这个步骤在 Kubernetes 里面被称作 Bind。</strong></p><p>为了不在关键调度路径里远程访问 APIServer，Kubernetes 的默认调度器在 Bind 阶段，只会更新 Scheduler Cache 里的 Pod 和 Node 的信息。<strong>这种基于“乐观”假设的 API 对象更新方式，在 Kubernetes 里被称作 Assume。</strong></p><p>Assume 之后，调度器才会创建一个 Goroutine 来异步地向 APIServer 发起更新 Pod 的请求，来真正完成 Bind 操作。如果这次异步的 Bind 过程失败了，其实也没有太大关系，等 Scheduler Cache 同步之后一切就会恢复正常。</p><p>由于上述 Kubernetes 调度器的“乐观”绑定的设计，当一个新的 Pod 完成调度需要在某个节点上运行起来之前，该节点上的 kubelet 还会通过一个叫作 Admit 的操作来再次验证该 Pod 是否确实能够运行在该节点上。这一步 Admit 操作，实际上就是把一组叫作 GeneralPredicates 的、最基本的调度算法，比如：“资源是否可用”“端口是否冲突”等再执行一遍，作为 kubelet 端的二次确认。</p><p><strong>除了“Cache 化”和“乐观绑定”，Kubernetes 默认调度器还有一个重要的设计，那就是“无锁化”。</strong></p><p>在 Scheduling Path 上，调度器会启动多个 Goroutine 以节点为粒度并发执行 Predicates 算法，从而提高这一阶段的执行效率。而与之类似的，Priorities 算法也会以 MapReduce 的方式并行计算然后再进行汇总。而在这些所有需要并发的路径上，调度器会避免设置任何全局的竞争资源，从而免去了使用锁进行同步带来的巨大的性能损耗。</p><p>Kubernetes 调度器只有对调度队列和 Scheduler Cache 进行操作时，才需要加锁。而这两部分操作，都不在 Scheduling Path 的算法执行路径上。</p><p>Kubernetes 默认调度器的可扩展性设计如图描述：</p><p><img src="/assets/img/Kubernetes/schedulerCycle.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/schedulerCycle.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>可以看到，默认调度器的可扩展机制，在 Kubernetes 里面叫作 Scheduler Framework。顾名思义，这个设计的主要目的，就是在调度器生命周期的各个关键点上，为用户暴露出可以进行扩展和实现的接口，从而实现由用户自定义调度器的能力。每一个绿色的箭头都是一个可以插入自定义逻辑的接口。比如，上面的 Queue 部分，就意味着你可以在这一部分提供一个自己的调度队列的实现，从而控制每个 Pod 开始被调度（出队）的时机。Predicates 部分，则意味着你可以提供自己的过滤算法实现，根据自己的需求，来决定选择哪些机器。</p><p><strong>上述这些可插拔式逻辑，都是标准的 Go 语言插件机制（Go plugin 机制）</strong>，也就是说，你需要在编译的时候选择把哪些插件编译进去。</p><h5 id="Predicates"><a href="#Predicates" class="headerlink" title="Predicates"></a>Predicates</h5><p><strong>Predicates 在调度过程中的作用，可以理解为 Filter</strong>，即：它按照调度策略，从当前集群的所有节点中，“过滤”出一系列符合条件的节点。这些节点，都是可以运行待调度 Pod 的宿主机。</p><p>默认的调度策略有如下几种：</p><p><strong>GeneralPredicates</strong> ：这一组过滤规则，负责的是最基础的调度策略。</p><ul><li><p>PodFitsResources ：计算宿主机的 CPU 和内存资源等是否够用。</p></li><li><p>PodFitsHost ：检查宿主机的名字是否跟 Pod 的 spec.nodeName 一致。</p></li><li><p>PodFitsHostPorts ：检查Pod 申请的宿主机端口（spec.nodePort）是不是跟已经被使用的端口有冲突。</p></li><li><p>PodMatchNodeSelector：检查Pod 的 nodeSelector 或者 nodeAffinity 指定的节点，是否与待考察节点匹配，等等。</p></li></ul><p><strong>Volume 相关的过滤规则</strong>：这一组过滤规则，负责的是跟容器持久化 Volume 相关的调度策略。</p><ul><li>NoDiskConflict ：检查的条件是多个 Pod 声明挂载的持久化 Volume 是否有冲突。</li><li>MaxPDVolumeCountPredicate ： 检查的条件是一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目，如果是的话，那么声明使用该类型持久化 Volume 的 Pod 就不能再调度到这个节点了。</li><li>VolumeZonePredicate：检查持久化 Volume 的 Zone（高可用域）标签，是否与待考察节点的 Zone 标签相匹配。</li><li>VolumeBindingPredicate：检查该 Pod 对应的 PV 的 nodeAffinity 字段，是否跟某个节点的标签相匹配。</li></ul><p><strong>宿主机相关的过滤规则</strong>：这一组规则，主要考察待调度 Pod 是否满足 Node 本身的某些条件。</p><ul><li>PodToleratesNodeTaints：负责检查Node 的“污点”机制。只有当 Pod 的 Toleration 字段与 Node 的 Taint 字段能够匹配的时候，这个 Pod 才能被调度到该节点上。</li><li>NodeMemoryPressurePredicate：检查的是当前节点的内存是不是已经不够充足，如果是的话，那么待调度 Pod 就不能被调度到该节点上</li></ul><p><strong>Pod 相关的过滤规则</strong>：这一组规则，跟 GeneralPredicates 大多数是重合的。有一个特殊的点</p><ul><li>PodAffinityPredicate：这个规则的作用，是检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密（affinity）和反亲密（anti-affinity）关系。</li></ul><p><strong>在具体执行的时候， 当开始调度一个 Pod 时，Kubernetes 调度器会同时启动 16 个 Goroutine，来并发地为集群里的所有 Node 计算 Predicates，最后返回可以运行这个 Pod 的宿主机列表。</strong></p><p>在为每个 Node 执行 Predicates 时，调度器会按照固定的顺序来进行检查。这个顺序，是按照 Predicates 本身的含义来确定的。比如，宿主机相关的 Predicates 会被放在相对靠前的位置进行检查。要不然的话，在一台资源已经严重不足的宿主机上，上来就开始计算 PodAffinityPredicate，是没有实际意义的。</p><h5 id="Priorities"><a href="#Priorities" class="headerlink" title="Priorities"></a>Priorities</h5><p>在 Predicates 阶段完成了节点的“过滤”之后，Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分，得分最高的节点就是最后被 Pod 绑定的最佳节点。</p><p>Priorities 里最常用到的一个打分规则，是 LeastRequestedPriority。它的计算方法，可以简单地总结为如下所示的公式：</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">score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2</span><br></pre></td></tr></table></figure><p>这个算法实际上就是在选择空闲资源（CPU 和 Memory）最多的宿主机</p><p>而与 LeastRequestedPriority 一起发挥作用的，还有 BalancedResourceAllocation。它的计算公式如下所示：</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">score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10</span><br></pre></td></tr></table></figure><p>其中，每种资源的 Fraction 的定义是 ：Pod 请求的资源 &#x2F; 节点上的可用资源。而 variance 算法的作用，则是计算每两种资源 Fraction 之间的“距离”。而最后选择的，则是资源 Fraction 差距最小的节点。</p><p>BalancedResourceAllocation 选择的，其实是调度完成后，所有节点里各种资源分配最均衡的那个节点，从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况。</p><p>默认 Priorities 里，还有一个叫作 ImageLocalityPriority 的策略，即：如果待调度 Pod 需要使用的镜像很大，并且已经存在于某些 Node 上，那么这些 Node 的得分就会比较高。</p><p>为了避免这个算法引发调度堆叠，调度器在计算得分的时候还会根据镜像的分布进行优化，即：如果大镜像分布的节点数目很少，那么这些节点的权重就会被调低，从而“对冲”掉引起调度堆叠的风险</p><p><strong>在实际的执行过程中，调度器里关于集群和 Pod 的信息都已经缓存化，所以这些算法的执行过程还是比较快的。</strong></p><h5 id="优先级（Priority-）和抢占（Preemption）机制"><a href="#优先级（Priority-）和抢占（Preemption）机制" class="headerlink" title="优先级（Priority ）和抢占（Preemption）机制"></a>优先级（Priority ）和抢占（Preemption）机制</h5><p>正常情况下，当一个 Pod 调度失败后，它就会被暂时“搁置”起来，直到 Pod 被更新，或者集群状态发生变化，调度器才会对这个 Pod 进行重新调度。在有时候，我们希望的是这样一个场景。当一个高优先级的 Pod 调度失败后，该 Pod 并不会被“搁置”，而是会“挤走”某个 Node 上的一些低优先级的 Pod 。这样就可以保证这个高优先级 Pod 的调度成功。这个特性，其实也是一直以来就存在于 Borg 以及 Mesos 等项目里的一个基本功能。</p><p>**Kubernetes 规定，优先级是一个 32 bit 的整数，最大值不超过 1000000000（10 亿，1 billion），并且值越大代表优先级越高。**而超出 10 亿的值，其实是被 Kubernetes 保留下来分配给系统 Pod 使用的。显然，这样做的目的，就是保证系统 Pod 不会被用户抢占掉</p><p>调度器里会维护着一个调度队列。当 Pod 拥有了优先级之后，高优先级的 Pod 就可能会比低优先级的 Pod 提前出队，从而尽早完成调度过程。<strong>这个过程，就是“优先级”这个概念在 Kubernetes 里的主要体现。</strong></p><p>而当一个高优先级的 Pod 调度失败的时候，调度器的抢占能力就会被触发。这时，调度器就会试图从当前集群里寻找一个节点，使得当这个节点上的一个或者多个低优先级 Pod 被删除后，待调度的高优先级 Pod 就可以被调度到这个节点上。<strong>这个过程，就是“抢占”这个概念在 Kubernetes 里的主要体现。</strong></p><p>抢占过程发生时，抢占者并不会立刻被调度到被抢占的 Node 上。事实上，调度器只会将抢占者的 spec.nominatedNodeName 字段，设置为被抢占的 Node 的名字。然后，抢占者会重新进入下一个调度周期，然后在新的调度周期里来决定是不是要运行在被抢占的节点上。这当然也就意味着，即使在下一个调度周期，调度器也不会保证抢占者一定会运行在被抢占的节点上。</p><p>抢占发生的原因，一定是一个高优先级的 Pod 调度失败。这一次，我们还是称这个 Pod 为“抢占者”，称被抢占的 Pod 为“牺牲者”（victims）。</p><p>Kubernetes在调度队列的实现里，使用了两个不同的队列。</p><p>**第一个队列，叫作 activeQ。**凡是在 activeQ 里的 Pod，都是下一个调度周期需要调度的对象。所以，当你在 Kubernetes 集群里新创建一个 Pod 的时候，调度器会将这个 Pod 入队到 activeQ 里面。</p><p><strong>第二个队列，叫作 unschedulableQ</strong>，专门用来存放调度失败的 Pod。当一个 unschedulableQ 里的 Pod 被更新之后，调度器会自动把这个 Pod 移动到 activeQ 里，从而给这些调度失败的 Pod “重新做人”的机会。</p><p><strong>调度器为抢占者寻找牺牲者的流程</strong>：</p><p><strong>第一步</strong>，调度器会检查这次失败事件的原因，来确认抢占是不是可以帮助抢占者找到一个新节点。这是因为有很多 Predicates 的失败是不能通过抢占来解决的。比如，PodFitsHost 算法（负责的是，检查 Pod 的 nodeSelector 与 Node 的名字是否匹配），这种情况下，除非 Node 的名字发生变化，否则你即使删除再多的 Pod，抢占者也不可能调度成功。</p><p><strong>第二步</strong>，如果确定抢占可以发生，那么调度器就会把自己缓存的所有节点信息复制一份，然后使用这个副本来模拟抢占过程。</p><p>这里的抢占过程很容易理解。调度器会检查缓存副本里的每一个节点，然后从该节点上最低优先级的 Pod 开始，逐一“删除”这些 Pod。而每删除一个低优先级 Pod，调度器都会检查一下抢占者是否能够运行在该 Node 上。一旦可以运行，调度器就记录下这个 Node 的名字和被删除 Pod 的列表，这就是一次抢占过程的结果了。当遍历完所有的节点之后，调度器会在模拟产生的所有抢占结果里做一个选择，找出最佳结果。而这一步的<strong>判断原则，就是尽量减少抢占对整个系统的影响</strong>。比如，需要抢占的 Pod 越少越好，需要抢占的 Pod 的优先级越低越好，等等。</p><p>在得到了最佳的抢占结果之后，这个结果里的 Node，就是即将被抢占的 Node；被删除的 Pod 列表，就是牺牲者。所以接下来，<strong>调度器就可以真正开始抢占的操作</strong>了，这个过程，可以分为三步</p><p><strong>第一步</strong>，调度器会检查牺牲者列表，清理这些 Pod 所携带的 nominatedNodeName 字段。</p><p><strong>第二步</strong>，调度器会把抢占者的 nominatedNodeName，设置为被抢占的 Node 的名字。</p><p><strong>第三步</strong>，调度器会开启一个 Goroutine，同步地删除牺牲者。</p><p>第二步对抢占者 Pod 的更新操作会触发到我前面提到的“重新做人”的流程。<strong>接下来，调度器就会通过正常的调度流程把抢占者调度成功</strong>。</p><p>在为某一对 Pod 和 Node 执行 Predicates 算法的时候，如果待检查的 Node 是一个即将被抢占的节点，即：调度队列里有 nominatedNodeName 字段值是该 Node 名字的 Pod 存在（可以称之为：“潜在的抢占者”）。那么，<strong>调度器就会对这个 Node ，将同样的 Predicates 算法运行两遍。</strong></p><p><strong>第一遍</strong>， 调度器会假设上述“潜在的抢占者”已经运行在这个节点上，然后执行 Predicates 算法；</p><p><strong>第二遍</strong>， 调度器会正常执行 Predicates 算法，即：不考虑任何“潜在的抢占者”。</p><p>而只有这两遍 Predicates 算法都能通过时，这个 Pod 和 Node 才会被认为是可以绑定（bind）的。需要执行第一遍 Predicates 算法的原因，是由于 InterPodAntiAffinity 规则的存在。需要执行第二遍 Predicates 算法的原因，则是因为“潜在的抢占者”最后不一定会运行在待考察的 Node 上。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;在 Kubernetes 项目中，默认调度器的主要职责，就是为一个新创建出来的 Pod，寻找一个最合适的节点（Node）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个最合适是指：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从集群所有的节点中，根据调度算法挑选出所有可以运行该 Po</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>资源模型与资源管理</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/03/Kubernetes-15-%E8%B5%84%E6%BA%90%E6%A8%A1%E5%9E%8B%E4%B8%8E%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/03/Kubernetes-15-%E8%B5%84%E6%BA%90%E6%A8%A1%E5%9E%8B%E4%B8%8E%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86/</id>
    <published>2025-12-03T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<h5 id="资源模型"><a href="#资源模型" class="headerlink" title="资源模型"></a>资源模型</h5><p>Kubernetes 里，Pod 是最小的原子调度单位。这也就意味着，所有跟调度和资源管理相关的属性都应该是属于 Pod 对象的字段。而这其中最重要的部分，就是 Pod 的 CPU 和内存配置</p><p>Kubernetes 中，像 CPU 这样的资源被称作“可压缩资源”（compressible resources）。它的典型特点是，当可压缩资源不足时，Pod 只会“饥饿”，但不会退出。</p><p>像内存这样的资源，则被称作“不可压缩资源（incompressible resources）。当不可压缩资源不足时，Pod 就会因为 OOM（Out-Of-Memory）被内核杀掉。</p><p>由于 Pod 可以由多个 Container 组成，所以 CPU 和内存资源的限额，是要配置在每个 Container 的定义上的。这样，Pod 整体的资源配置，就由这些 Container 的配置值累加得到。</p><p>Kubernetes 里为 CPU 设置的单位是“CPU 的个数”。比如，cpu&#x3D;1 指的就是，这个 Pod 的 CPU 限额是 1 个 CPU。当然，具体“1 个 CPU”在宿主机上如何解释，是 1 个 CPU 核心，还是 1 个 vCPU，还是 1 个 CPU 的超线程（Hyperthread），完全取决于宿主机的 CPU 实现方式。Kubernetes 只负责保证 Pod 能够使用到“1 个 CPU”的计算能力。Kubernetes 允许你将 CPU 限额设置为分数，比如在我们的例子里，CPU limits 的值就是 500m。所谓 500m，指的就是 500 millicpu，也就是 0.5 个 CPU 的意思。这样，这个 Pod 就会被分配到 1 个 CPU 一半的计算能力。</p><p>对于内存资源来说，它的单位自然就是 bytes。Kubernetes 支持你使用 Ei、Pi、Ti、Gi、Mi、Ki（或者 E、P、T、G、M、K）的方式来作为 bytes 的值。比如，在我们的例子里，Memory requests 的值就是 64MiB (2 的 26 次方 bytes) 。这里要注意区分 MiB（mebibyte）和 MB（megabyte）的区别。</p><blockquote><p>1Mi&#x3D;1024*1024；1M&#x3D;1000*1000</p></blockquote><p><strong>Kubernetes 里 Pod 的 CPU 和内存资源，实际上还要分为 limits 和 requests 两种情况</strong></p><p>两者的区别在于：在调度的时候，kube-scheduler 只会按照 requests 的值进行计算。而在真正设置 Cgroups 限制的时候，kubelet 则会按照 limits 的值来进行设置。</p><p><strong>Kubernetes 对 CPU 和内存资源限额的设计，参考了 Borg 论文中对“动态资源边界”的定义</strong>：容器化作业在提交时所设置的资源边界，并不一定是调度系统所必须严格遵守的，这是因为在实际场景中，大多数作业使用到的资源其实远小于它所请求的资源限额。</p><p>基于这种假设，Borg 在作业被提交后，会主动减小它的资源限额配置，以便容纳更多的作业、提升资源利用率。而当作业资源使用量增加到一定阈值时，Borg 会通过“快速恢复”过程，还原作业原始的资源限额，防止出现异常情况。Kubernetes 的 requests+limits 的做法，就是这个思路的一个简化版：用户在提交 Pod 时，可以声明一个相对较小的 requests 值供调度器使用，而 Kubernetes 真正设置给容器 Cgroups 的，则是相对较大的 limits 值。不难看到，这跟 Borg 的思路相通的。</p><h5 id="Kubernetes-里的-QoS-模型"><a href="#Kubernetes-里的-QoS-模型" class="headerlink" title="Kubernetes 里的 QoS 模型"></a>Kubernetes 里的 QoS 模型</h5><p>在 Kubernetes 中，不同的 requests 和 limits 的设置方式，会将这个 Pod 划分到不同的 QoS 级别当中。</p><p><strong>当 Pod 里的每一个 Container 都同时设置了 requests 和 limits，并且 requests 和 limits 值相等的时候，这个 Pod 就属于 Guaranteed 类别</strong></p><p>当Pod创建之后，qosClass 字段就会被 Kubernetes 自动设置为 Guaranteed。需要注意的是，当 Pod 仅设置了 limits 没有设置 requests 的时候，Kubernetes 会自动为它设置与 limits 相同的 requests 值，所以，这也属于 Guaranteed 情况。</p><p><strong>当 Pod 不满足 Guaranteed 的条件，但至少有一个 Container 设置了 requests。那么这个 Pod 就会被划分到 Burstable 类别</strong>。<strong>如果一个 Pod 既没有设置 requests，也没有设置 limits，那么它的 QoS 类别就是 BestEffort</strong></p><p><strong>QoS 划分的主要应用场景，是当宿主机资源紧张的时候，kubelet 对 Pod 进行 Eviction（即资源回收）时需要用到的。</strong></p><p>具体地说，当 Kubernetes 所管理的宿主机上不可压缩资源短缺时，就有可能触发 Eviction。比如，可用内存（memory.available）、可用的宿主机磁盘空间（nodefs.available），以及容器运行时镜像存储空间（imagefs.available）等等。</p><p>Eviction 的默认阈值是可配置的。<strong>Eviction 在 Kubernetes 里分为 Soft 和 Hard 两种模式</strong>。Soft Eviction 允许你为 Eviction 过程设置一段“优雅时间”，Hard Eviction 模式下，Eviction 过程就会在阈值达到之后立刻开始。宿主机的 Eviction 阈值达到后，就会进入 MemoryPressure 或者 DiskPressure 状态，从而避免新的 Pod 被调度到这台宿主机上。</p><p>Eviction 发生的时候，kubelet 具体会挑选哪些 Pod 进行删除操作，需要参考这些 Pod 的 QoS ：</p><ul><li>首当其冲的，自然是 BestEffort 类别的 Pod。</li><li>其次，是属于 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。</li><li>最后，才是 Guaranteed 类别。并且，Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制，或者宿主机本身正处于 Memory Pressure 状态时，Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。</li></ul><p>对于同 QoS 类别的 Pod 来说，Kubernetes 还会根据 Pod 的优先级来进行进一步地排序和选择。</p><h5 id="cpuset"><a href="#cpuset" class="headerlink" title="cpuset"></a>cpuset</h5><p>在使用容器的时候，可以通过设置 cpuset 把容器绑定到某个 CPU 的核上，而不是像 cpushare 那样共享 CPU 的计算能力。</p><p>这种情况下，由于操作系统在 CPU 之间进行上下文切换的次数大大减少，容器里应用的性能会得到大幅提升。事实上，<strong>cpuset 方式，是生产环境里部署在线应用类型的 Pod 时，非常常用的一种方式。</strong></p><p>在 Kubernetes里的实现如下：</p><ul><li>首先，你的 Pod 必须是 Guaranteed 的 QoS 类型；</li><li>然后，你只需要将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值即可。</li></ul><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></pre></td><td class="code"><pre><span class="line">spec:</span><br><span class="line">  containers:</span><br><span class="line">  - name: nginx</span><br><span class="line">    image: nginx</span><br><span class="line">    resources:</span><br><span class="line">      limits:</span><br><span class="line">        memory: &quot;200Mi&quot;</span><br><span class="line">        cpu: &quot;2&quot;</span><br><span class="line">      requests:</span><br><span class="line">        memory: &quot;200Mi&quot;</span><br><span class="line">        cpu: &quot;2&quot;</span><br></pre></td></tr></table></figure><p>这时候，该 Pod 就会被绑定在 2 个独占的 CPU 核上。当然，具体是哪两个 CPU 核，是由 kubelet 为你分配的。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h5 id=&quot;资源模型&quot;&gt;&lt;a href=&quot;#资源模型&quot; class=&quot;headerlink&quot; title=&quot;资源模型&quot;&gt;&lt;/a&gt;资源模型&lt;/h5&gt;&lt;p&gt;Kubernetes 里，Pod 是最小的原子调度单位。这也就意味着，所有跟调度和资源管理相关的属性都应该是属于 Pod 对</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>Service、DNS与服务发现</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/02/Kubernetes-14-Service%E3%80%81DNS-%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/02/Kubernetes-14-Service%E3%80%81DNS-%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0/</id>
    <published>2025-12-02T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p>Kubernetes 里的 Service<strong>是由 kube-proxy 组件，加上 iptables 来共同实现的</strong></p><p>Service 进行转发的具体原理:iptables 对流入的 IP 包设置了一个“标志”（–set-xmark）,然后经过三条DNAT过滤，访问 Service VIP 的 IP 包经过 iptables 处理之后，就已经变成了访问具体某一个后端 Pod 的 IP 包了</p><p>而 DNAT 规则的作用，就是在 PREROUTING 检查点之前，也就是在路由之前，将流入 IP 包的目的地址和端口，改成–to-destination 所指定的新的目的地址和端口。可以看到，这个目的地址和端口，正是被代理 Pod 的 IP 地址和端口。</p><p>这些 Endpoints 对应的 iptables 规则，正是 kube-proxy 通过监听 Pod 的变化事件，在宿主机上生成并维护的。</p><p>Kubernetes 的 kube-proxy 还支持一种叫作 IPVS 的模式</p><p>kube-proxy 通过 iptables 处理 Service 的过程，其实需要在宿主机上设置相当多的 iptables 规则。而且，kube-proxy 还需要在控制循环里不断地刷新这些规则来确保它们始终是正确的。</p><p>**一直以来，基于 iptables 的 Service 实现，都是制约 Kubernetes 项目承载更多量级的 Pod 的主要障碍。**IPVS 模式的 Service，就是解决这个问题的一个行之有效的方法。</p><p>IPVS 模式的工作原理，跟 iptables 模式类似。当我们创建了 Service 之后，kube-proxy 首先会在宿主机上创建一个虚拟网卡，并为它分配 Service VIP 作为 IP 地址。接下来，kube-proxy 就会通过 Linux 的 IPVS 模块，为这个 IP 地址设置三个 IPVS 虚拟主机，并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略。</p><p>相比于 iptables，IPVS 在内核中的实现其实也是基于 Netfilter 的 NAT 模式，所以在转发这一层上，理论上 IPVS 并没有显著的性能提升。但是，IPVS 并不需要在宿主机上为每个 Pod 设置 iptables 规则，而是把对这些“规则”的处理放到了内核态，从而极大地降低了维护这些规则的代价。</p><blockquote><p>IPVS 模块只负责上述的负载均衡和代理功能。而一个完整的 Service 流程正常工作所需要的包过滤、SNAT 等操作，还是要靠 iptables 来实现。只不过，这些辅助性的 iptables 规则数量有限，也不会随着 Pod 数量的增加而增加。</p></blockquote><p>在大规模集群里，非常建议为 kube-proxy 设置–proxy-mode&#x3D;ipvs 来开启这个功能。它为 Kubernetes 集群规模带来的提升，还是非常巨大的。</p><h5 id="Service-与-DNS-的关系"><a href="#Service-与-DNS-的关系" class="headerlink" title="Service 与 DNS 的关系"></a>Service 与 DNS 的关系</h5><p>在 Kubernetes 中，Service 和 Pod 都会被分配对应的 DNS A 记录（从域名解析 IP 的记录）。</p><p>对于 ClusterIP 模式的 Service 来说，它的 A 记录的格式是：..svc.cluster.local。当你访问这条 A 记录的时候，它解析到的就是该 Service 的 VIP 地址。</p><p>而对于指定了 clusterIP&#x3D;None 的 Headless Service 来说，它的 A 记录的格式也是：..svc.cluster.local。但是，当你访问这条 A 记录的时候，它返回的是所有被代理的 Pod 的 IP 地址的集合。当然，如果你的客户端没办法解析这个集合的话，它可能会只会拿到第一个 Pod 的 IP 地址。</p><p>对于 ClusterIP 模式的 Service 来说，它代理的 Pod 被自动分配的 A 记录的格式是：..pod.cluster.local。这条记录指向 Pod 的 IP 地址。</p><p>Headless Service 来说，它代理的 Pod 被自动分配的 A 记录的格式是：…svc.cluster.local。这条记录也指向 Pod 的 IP 地址。</p><p>**Service 的访问信息在 Kubernetes 集群之外，其实是无效的。**Service 的访问入口，其实就是每台宿主机上由 kube-proxy 生成的 iptables 规则，以及 kube-dns 生成的 DNS 记录。而一旦离开了这个集群，这些信息对用户来说，也就自然没有作用了。</p><p>在使用 Kubernetes 的 Service 时，一个必须要面对和解决的问题就是：<strong>如何从外部（Kubernetes 集群之外），访问到 Kubernetes 里创建的 Service？</strong></p><p>最常用的一种方式就：NodePort</p><p>第二种方式，适用于公有云上的 Kubernetes 服务：指定一个 LoadBalancer 类型的 Service。公有云提供的 Kubernetes 服务里，都使用了一个叫作 CloudProvider 的转接层，来跟公有云本身的 API 进行对接。所以，在上述 LoadBalancer 类型的 Service 被提交后，Kubernetes 就会调用 CloudProvider 在公有云上为你创建一个负载均衡服务，并且把被代理的 Pod 的 IP 地址配置给负载均衡服务做后端。</p><p>第三种方式：ExternalName</p><h5 id="Service-与-Ingress"><a href="#Service-与-Ingress" class="headerlink" title="Service 与 Ingress"></a>Service 与 Ingress</h5><p>由于每个 Service 都要有一个负载均衡服务，这个做法既浪费成本又高。所以Kubernetes能不能为我内置一个全局的负载均衡器。然后，通过我访问的 URL，把请求转发给不同的后端 Service。</p><p><strong>这种全局的、为了代理不同后端 Service 而设置的负载均衡服务，就是 Kubernetes 里的 Ingress 服务。</strong></p><p>Ingress 的功能很容易理解：<strong>Ingress，就是 Service 的“Service”。</strong></p><p>在Ingress控制器yaml里会有一个rules字段。在 Kubernetes 里，这个字段叫作：<strong>IngressRule</strong>。</p><p>IngressRule 的 Key，就叫做：host。它必须是一个标准的域名格式（Fully Qualified Domain Name）的字符串，而不能是 IP 地址。host 字段定义的值，就是这个 Ingress 的入口。这也就意味着，当用户访问的时候，实际上访问到的是这个 Ingress 对象。这样，Kubernetes 就能使用 IngressRule 来对你的请求进行下一步转发。</p><p>IngressRule 规则的定义，依赖于 path 字段。可以简单地理解为，这里的每一个 path 都对应一个后端 Service。</p><p>**Ingress 对象，其实就是 Kubernetes 项目对“反向代理”的一种抽象。**一个 Ingress 对象的主要内容，实际上就是一个“反向代理”服务（比如：Nginx）的配置文件的描述。而这个代理服务对应的转发规则，就是 IngressRule。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Kubernetes 里的 Service&lt;strong&gt;是由 kube-proxy 组件，加上 iptables 来共同实现的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Service 进行转发的具体原理:iptables 对流入的 IP 包设置了一个“标志”（–set-xmark</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>容器网络</title>
    <link href="https://github.com/setking/setking.github.io/2025/12/01/Kubernetes-13-%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C/"/>
    <id>https://github.com/setking/setking.github.io/2025/12/01/Kubernetes-13-%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C/</id>
    <published>2025-12-01T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p>Linux 容器能看见“网络栈”，被隔离在它自己的 Network Namespace 当中。“网络栈”包括了：网卡（Network Interface）、回环设备（Loopback Device）、路由表（Routing Table）和 iptables 规则。对于一个进程来说，这些要素，其实就构成了它发起和响应网络请求的基本环境。</p><blockquote><p>作为一个容器，它可以声明直接使用宿主机的网络栈（–net&#x3D;host），即：不开启 Network Namespace</p><p>在这种情况下，这个容器启动后，直接监听的就是宿主机的 80 端口。</p></blockquote><p>接使用宿主机网络栈的方式，虽然可以为容器提供良好的网络性能，但也会不可避免地引入共享网络资源的问题，比如端口冲突。所以，<strong>在大多数情况下，都希望容器进程能使用自己 Network Namespace 里的网络栈，即：拥有属于自己的 IP 地址和端口。</strong></p><h5 id="docker"><a href="#docker" class="headerlink" title="docker"></a>docker</h5><p>这个被隔离的容器进程,跟其他 Network Namespace 里的容器进程进行交互可以用是网桥（Bridge），它是一个工作在数据链路层（Data Link）的设备，主要功能是根据 MAC 地址学习来将数据包转发到网桥的不同端口（Port）上。为了实现这个交互，Docker会默认在宿主机上创建一个名叫 docker0 的网桥，凡是连接在 docker0 网桥上的容器，就可以通过它来进行通信。然后可以使用<strong>Veth Pair</strong>虚拟设备将容器“连接”到 docker0 网桥上。</p><p>Veth Pair 设备的特点是：它被创建出来后，总是以两张虚拟网卡（Veth Peer）的形式成对出现的。并且，从其中一个“网卡”发出的数据包，可以直接出现在与它对应的另一张“网卡”上，哪怕这两个“网卡”在不同的 Network Namespace 里。这就使得 Veth Pair 常常被用作连接不同 Network Namespace 的“网线”。</p><p>在默认情况下，<strong>被限制在 Network Namespace 里的容器进程，实际上是通过 Veth Pair 设备 + 宿主机网桥的方式，实现了跟同其他容器的数据交换。</strong></p><p><strong>当你遇到容器连不通“外网”的时候，你都应该先试试 docker0 网桥能不能 ping 通，然后查看一下跟 docker0 和 Veth Pair 设备相关的 iptables 规则是不是有异常，往往就能够找到问题的答案了。</strong></p><h5 id="容器夸主机通信"><a href="#容器夸主机通信" class="headerlink" title="容器夸主机通信"></a>容器夸主机通信</h5><p>构建容器夸主机通信的容器网络的核心在于：我们需要在已有的宿主机网络上，再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。所以，这种技术就被称为：Overlay Network（覆盖网络）。</p><p>Overlay Network 本身，可以由每台宿主机上的一个“特殊网桥”共同组成。比如，当 Node 1 上的 Container 1 要访问 Node 2 上的 Container 3 的时候，Node 1 上的“特殊网桥”在收到数据包之后，能够通过某种方式，把数据包发送到正确的宿主机，比如 Node 2 上。而 Node 2 上的“特殊网桥”在收到数据包后，也能够通过某种方式，把数据包转发给正确的容器，比如 Container 3。</p><h5 id="Flannel"><a href="#Flannel" class="headerlink" title="Flannel"></a>Flannel</h5><p>Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上，Flannel 项目本身只是一个框架，真正为我们提供容器网络功能的，是 Flannel 的后端实现。目前，Flannel 支持三种后端实现，分别是：</p><ol><li>VXLAN；</li><li>host-gw；</li><li>UDP。</li></ol><p>这三种不同的后端实现，正代表了三种容器跨主网络的主流实现方法。UDP 模式，是 Flannel 项目最早支持的一种方式，却也是性能最差的一种方式。因为Flannel UDP 模式的容器通信有一个flanneld 的处理过程会使得在发出IP包的过程中，需要经过三次用户态与内核态之间的切换，这个模式目前已经被弃用。</p><p>Flannel UDP 模式的跨主通信的基本原理如图所示：</p><p><img src="/assets/img/Kubernetes/FlannelUDP.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/FlannelUDP.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Flannel UDP 模式提供的其实是一个三层的 Overlay 网络，即：它首先对发出端的 IP 包进行 UDP 封装，然后在接收端进行解封装拿到原始的 IP 包，进而把这个 IP 包转发给目标容器。这就好比，Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”，使得这两个容器可以直接使用 IP 地址进行通信，而无需关心容器和宿主机的分布情况。</p><p>VXLAN，即 Virtual Extensible LAN（虚拟可扩展局域网），是 Linux 内核本身就支持的一种网络虚似化技术。，VXLAN 可以完全在内核态实现上述封装和解封装的工作，从而通过与UDP相似的“隧道”机制，构建出覆盖网络（Overlay Network）。</p><p>VXLAN 的覆盖网络的设计思想是：在现有的三层网络之上，“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络，使得连接在这个 VXLAN 二层网络上的“主机”（虚拟机或者容器都可以）之间，可以像在同一个局域网（LAN）里那样自由通信。当然，实际上，这些“主机”可能分布在不同的宿主机上，甚至是分布在不同的物理机房里。</p><p>而为了能够在二层网络上打通“隧道”，VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP，即：VXLAN Tunnel End Point（虚拟隧道端点）。</p><p>VTEP 设备的作用，其实跟flanneld 进程非常相似。只不过，它进行封装和解封装的对象，是二层数据帧（Ethernet frame）；而且这个工作的执行流程，全部是在内核里完成的（因为 VXLAN 本身就是 Linux 内核中的一个模块）。</p><p>下图展示了VTEP 设备进行“隧道”通信的流程：</p><p><img src="/assets/img/Kubernetes/VTEP.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/VTEP.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><h5 id="CNI-网桥"><a href="#CNI-网桥" class="headerlink" title="CNI 网桥"></a>CNI 网桥</h5><p>Kubernetes里的CNI网桥通信的基本原理如图所示：</p><p><img src="/assets/img/Kubernetes/CNI.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/CNI.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>CNI 的设计思想，是：<strong>Kubernetes 在启动 Infra 容器之后，就可以直接调用 CNI 网络插件，为这个 Infra 容器的 Network Namespace，配置符合预期的网络栈。</strong></p><p>CNI 的基础可执行文件，按照功能可以分为三类：</p><p><strong>第一类，叫作 Main 插件，它是用来创建具体网络设备的二进制文件</strong>。比如，bridge（网桥设备）、ipvlan、loopback（lo 设备）、macvlan、ptp（Veth Pair 设备），以及 vlan。</p><p>Flannel、Weave 等项目，都属于“网桥”类型的 CNI 插件。所以在具体的实现中，它们往往会调用 bridge 这个二进制文件。</p><p><strong>第二类，叫作 IPAM（IP Address Management）插件，它是负责分配 IP 地址的二进制文件</strong>。比如，dhcp，这个文件会向 DHCP 服务器发起请求；host-local，则会使用预先配置的 IP 地址段来进行分配。</p><p><strong>第三类，是由 CNI 社区维护的内置 CNI 插件</strong>。比如：flannel，就是专门为 Flannel 项目提供的 CNI 插件；tuning，是一个通过 sysctl 调整网络设备参数的二进制文件；portmap，是一个通过 iptables 配置端口映射的二进制文件；bandwidth，是一个使用 Token Bucket Filter (TBF) 来进行限流的二进制文件。</p><p>要实现一个给 Kubernetes 用的容器网络方案，需要做两部分工作，以 Flannel 项目为例：</p><p><strong>首先，实现这个网络方案本身</strong>。这一部分需要编写的，其实就是 flanneld 进程里的主要逻辑。比如，创建和配置 flannel.1 设备、配置宿主机路由、配置 ARP 和 FDB 表里的信息等等。</p><p><strong>然后，实现该网络方案对应的 CNI 插件</strong>。这一部分主要需要做的，就是配置 Infra 容器里面的网络栈，并把它连接在 CNI 网桥上。</p><h5 id="Kubernetes-三层网络方案"><a href="#Kubernetes-三层网络方案" class="headerlink" title="Kubernetes 三层网络方案"></a>Kubernetes 三层网络方案</h5><p>下图展示了 Flannel 的 host-gw 模式的工作原理：</p><p><img src="/assets/img/Kubernetes/host-gw.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/host-gw.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p><strong>host-gw 模式的工作原理，其实就是将每个 Flannel 子网（Flannel Subnet，比如：10.244.1.0&#x2F;24）的“下一跳”，设置成了该子网对应的宿主机的 IP 地址。</strong></p><p>也就是说，这台“主机”（Host）会充当这条容器通信路径里的“网关”（Gateway）。这也正是“host-gw”的含义。</p><p>Flannel 子网和主机的信息，都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化，然后实时更新路由表即可。</p><p>而在这种模式下，容器通信的过程就免除了额外的封包和解包带来的性能损耗。根据实际的测试，host-gw 的性能损失大约在 10% 左右，而其他所有基于 VXLAN“隧道”机制的网络方案，性能损失都在 20%~30% 左右。</p><p>host-gw 模式能够正常工作的核心，就在于 IP 包在封装成帧发送出去的时候，会使用路由表里的“下一跳”来设置目的 MAC 地址。这样，它就会经过二层网络到达目的宿主机。<strong>所以说，Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。</strong></p><h5 id="Calico"><a href="#Calico" class="headerlink" title="Calico"></a>Calico</h5><p>**相比于Flannel 通过 Etcd 和宿主机上的 flanneld 来维护路由信息的做法，Calico 项目使用了一个“重型武器”来自动地在整个集群中分发路由信息。**这个“重型武器”，就是 <strong>BGP</strong></p><p><strong>BGP 的全称是 Border Gateway Protocol，即：边界网关协议</strong>。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。</p><p>自治系统运行原理如下图所示：</p><p><img src="/assets/img/Kubernetes/%E8%87%AA%E6%B2%BB%E7%B3%BB%E7%BB%9F.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/%E8%87%AA%E6%B2%BB%E7%B3%BB%E7%BB%9F.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>在这个图中，我们有两个自治系统（Autonomous System，简称为 AS）：AS 1 和 AS 2。而所谓的一个自治系统，指的是一个组织管辖下的所有 IP 网络和路由器的全体。你可以把它想象成一个小公司里的所有主机和路由器。在正常情况下，自治系统之间不会有任何“来往”。</p><p>但是，如果这样两个自治系统里的主机，要通过 IP 地址直接进行通信，我们就必须使用路由器把这两个自治系统连接起来。</p><p>负责把自治系统连接在一起的路由器，我们就把它形象地称为：<strong>边界网关</strong>。它跟普通路由器的不同之处在于，它的路由表里拥有其他自治系统里的主机路由信息。</p><p>BGP在面对由成千上万个主机、无数个路由器，甚至是由多个公司、多个网络提供商、多个自治系统组成的复合自治系统有着天然的优势。</p><p>使用了 BGP 之后，你可以认为，在每个边界网关上都会运行着一个小程序，它们会将各自的路由表信息，通过 TCP 传输给其他的边界网关。而其他边界网关上的这个小程序，则会对收到的这些数据进行分析，然后将需要的信息添加到自己的路由表里。</p><p><strong>BGP，就是在大规模网络中实现节点路由信息共享的一种协议。</strong></p><p>BGP 的这个能力，正好可以取代 Flannel 维护主机上路由表的功能。而且，BGP 这种原生就是为大规模网络环境而实现的协议，其可靠性和可扩展性，远非 Flannel 自己的方案可比。</p><p>Calico 项目的架构由三个部分组成：</p><ol><li>Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。我已经在上一篇文章中，和你详细分享了 CNI 插件的工作原理，这里就不再赘述了。</li><li>Felix。它是一个 DaemonSet，负责在宿主机上插入路由规则（即：写入 Linux 内核的 FIB 转发信息库），以及维护 Calico 所需的网络设备等工作。</li><li>BIRD。它就是 BGP 的客户端，专门负责在集群里分发路由规则信息。</li></ol><p><strong>除了对路由信息的维护方式之外，Calico 项目与 Flannel 的 host-gw 模式的另一个不同之处，就是它不会在宿主机上创建任何网桥设备</strong>。</p><p>Calico 工作原理如图：</p><p>![](assets&#x2F;img&#x2F;Kubernetes&#x2F;Calico 工作原理.png)</p><p>其中的绿色实线标出的路径，就是一个 IP 包从 Node 1 上的 Container 1，到达 Node 2 上的 Container 4 的完整路径。</p><p>可以看到，Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备，然后把其中的一端放置在宿主机上（它的名字以 cali 前缀开头）。</p><p>由于 Calico 没有使用 CNI 的网桥模式，Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则，用于接收传入的 IP 包。</p><p>有了这样的 Veth Pair 设备之后，容器发出的 IP 包就会经过 Veth Pair 设备出现在宿主机上。然后，宿主机网络栈就会根据路由规则的下一跳 IP 地址，把它们转发给正确的网关。接下来的流程就跟 Flannel host-gw 模式完全一致了。</p><p>这里最核心的“下一跳”路由规则，就是由 Calico 的 Felix 进程负责维护的。这些路由规则信息，则是通过 BGP Client 也就是 BIRD 组件，使用 BGP 协议传输而来的。</p><p>Calico 项目实际上将集群里的所有节点，都当作是边界路由器来处理，它们一起组成了一个全连通的网络，互相之间通过 BGP 协议交换路由规则。这些节点，称为 BGP Peer。</p><p><strong>Calico 维护的网络在默认配置下，是一个被称为“Node-to-Node Mesh”的模式</strong>。这时候，每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以便交换路由信息。但是，随着节点数量 N 的增加，这些连接的数量就会以 N²的规模快速增长，从而给集群本身的网络带来巨大的压力。Node-to-Node Mesh 模式一般推荐用在少于 100 个节点的集群里。而在更大规模的集群中，你需要用到的是一个叫作 Route Reflector 的模式。在这种模式下，Calico 会指定一个或者几个专门的节点，来负责跟所有节点建立 BGP 连接从而学习到全局的路由规则。而其他节点，只需要跟这几个专门的节点交换路由信息，就可以获得整个集群的路由规则信息了。这些专门的节点，就是所谓的 Route Reflector 节点，它们实际上扮演了“中间代理”的角色，从而把 BGP 连接的规模控制在 N 的数量级上。</p><h5 id="soft-multi-tenancy"><a href="#soft-multi-tenancy" class="headerlink" title="soft multi-tenancy"></a>soft multi-tenancy</h5><p>Kubernetes 的网络模型只关注容器之间网络的“连通”，并不关心容器之间网络的“隔离”。这跟传统的 IaaS 层的网络方案，区别非常明显。</p><p>在 Kubernetes 里，网络隔离能力的定义，是依靠一种专门的 API 对象来描述的，即：NetworkPolicy。<strong>Kubernetes 里的 Pod 默认都是“允许所有”（Accept All）的</strong>，即：Pod 可以接收来自任何发送方的请求；或者，向任何接收方发送请求。而如果你要对这个情况作出限制，就必须通过 NetworkPolicy 对象来指定。</p><p>在YAML文件中如果把podSelector字段留空：</p><figure class="highlight plaintext"><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">spec:</span><br><span class="line"> podSelector: &#123;&#125;</span><br></pre></td></tr></table></figure><p>那么这个 NetworkPolicy 就会作用于当前 Namespace 下的所有 Pod。一旦 Pod 被 NetworkPolicy 选中，<strong>那么这个 Pod 就会进入“拒绝所有”（Deny All）的状态</strong>，即：这个 Pod 既不允许被外界访问，也不允许对外界发起访问。</p><p><strong>NetworkPolicy 定义的规则，其实就是“白名单”</strong></p><p>NetworkPolicy 指定的隔离规则如下所示：</p><ol><li>该隔离规则只对 default Namespace 下的，携带了 role&#x3D;db 标签的 Pod 有效。限制的请求类型包括 ingress（流入）和 egress（流出）。</li><li>Kubernetes 会拒绝任何访问被隔离 Pod 的请求，除非这个请求来自于“白名单”里的对象，并且访问的是被隔离 Pod 的指定端口。</li><li>default Namespace 里的，携带了 role&#x3D;fronted 标签的 Pod；</li><li>任何 Namespace 里的、携带了 project&#x3D;myproject 标签的 Pod；</li><li>任何源地址属于 172.17.0.0&#x2F;16 网段，且不属于 172.17.1.0&#x2F;24 网段的请求。</li><li>Kubernetes 会拒绝被隔离 Pod 对外发起任何请求，除非请求的目的地址属于 10.0.0.0&#x2F;24 网段，并且访问的是该网段地址的 5978 端口。</li></ol><p><strong>Kubernetes 网络插件对 Pod 进行隔离，其实是靠在宿主机上生成 NetworkPolicy 对应的 iptable 规则来实现的。</strong></p><p>iptables 是一个操作 Linux 内核 Netfilter 子系统的“界面”。顾名思义，Netfilter 子系统的作用，就是 Linux 内核里挡在“网卡”和“用户态进程”之间的一道“防火墙”。它们的关系，可以用如下的示意图来表示：</p><p><img src="/assets/img/Kubernetes/iptables.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/iptables.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>这幅示意图中，IP 包“一进一出”的两条路径上，有几个关键的“检查点”，它们正是 Netfilter 设置“防火墙”的地方。<strong>在 iptables 中，这些“检查点”被称为：链（Chain）</strong>。这是因为这些“检查点”对应的 iptables 规则，是按照定义顺序依次进行匹配的。这些“检查点”的具体工作原理，可以用如下所示的示意图进行描述：</p><p><img src="/assets/img/Kubernetes/Chain.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Chain.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>当一个 IP 包通过网卡进入主机之后，它就进入了 Netfilter 定义的流入路径（Input Path）里。在这个路径中，IP 包要经过路由表路由来决定下一步的去向。而在这次路由之前，Netfilter 设置了一个名叫 PREROUTING 的“检查点”。在 Linux 内核的实现里，所谓“检查点”实际上就是内核网络协议栈代码里的 Hook（比如，在执行路由判断的代码之前，内核会先调用 PREROUTING 的 Hook）。</p><p>经过路由之后，IP 包的去向分为了两种：</p><ul><li>第一种，继续在本机处理；</li><li>第二种，被转发到其他目的地。</li></ul><p><strong>IP 包的第一种去向</strong>： 包将继续向上层协议栈流动。在它进入传输层之前，Netfilter 会设置一个名叫 INPUT 的“检查点”。到这里，IP 包流入路径（Input Path）结束。</p><p>接下来，这个 IP 包通过传输层进入用户空间，交给用户进程处理。而处理完成后，用户进程会通过本机发出返回的 IP 包。这时候，这个 IP 包就进入了流出路径（Output Path）。</p><p>此时，IP 包首先还是会经过主机的路由表进行路由。路由结束后，Netfilter 就会设置一个名叫 OUTPUT 的“检查点”。然后，在 OUTPUT 之后，再设置一个名叫 POSTROUTING“检查点”。</p><p><strong>IP 包的第二种去向</strong>：IP 包不会进入传输层，而是会继续在网络层流动，从而进入到转发路径（Forward Path）。在转发路径中，Netfilter 会设置一个名叫 FORWARD 的“检查点”。</p><p>在 FORWARD“检查点”完成后，IP 包就会来到流出路径。而转发的 IP 包由于目的地已经确定，它就不会再经过路由，也自然不会经过 OUTPUT，而是会直接来到 POSTROUTING“检查点”。</p><p>POSTROUTING 的作用，其实就是上述两条路径，最终汇聚在一起的“最终检查点”。</p><blockquote><p>在有网桥参与的情况下，上述 Netfilter 设置“检查点”的流程，实际上也会出现在链路层（二层），并且会跟我在上面讲述的网络层（三层）的流程有交互。</p></blockquote><p>这些链路层的“检查点”对应的操作界面叫作 ebtables。所以，准确地说，数据包在 Linux Netfilter 子系统里完整的流动过程，其实应该如下所示</p><p><img src="/assets/img/Kubernetes/ebtables.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/ebtables.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>iptables 表的作用，就是在某个具体的“检查点”（比如 Output）上，按顺序执行几个不同的检查动作（比如，先执行 nat，再执行 filter）。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Linux 容器能看见“网络栈”，被隔离在它自己的 Network Namespace 当中。“网络栈”包括了：网卡（Network Interface）、回环设备（Loopback Device）、路由表（Routing Table）和 iptables 规则。对于一个进</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>CSI插件体系的设计原理</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/27/Kubernetes-12-CSI-%E6%8F%92%E4%BB%B6%E4%BD%93%E7%B3%BB%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/27/Kubernetes-12-CSI-%E6%8F%92%E4%BB%B6%E4%BD%93%E7%B3%BB%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/</id>
    <published>2025-11-27T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p>下图展示了Kubernetes 里通过存储插件管理容器持久化存储的原理：</p><p><img src="/assets/img/Kubernetes/CSIPVC.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/CSIPVC.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Kubernetes 内置的其他存储插件，它们实际上担任的角色，仅仅是 Volume 管理中的“Attach 阶段”和“Mount 阶段”的具体执行者。而像 Dynamic Provisioning 这样的功能，就不是存储插件的责任，而是 Kubernetes 本身存储管理功能的一部分。</p><p><strong>CSI 插件体系的设计思想，把 Provision 阶段，以及 Kubernetes 里的一部分存储管理功能，从主干代码里剥离出来，做成了几个单独的组件</strong>。这些组件会通过 Watch API 监听 Kubernetes 里与存储相关的事件变化，比如 PVC 的创建，来执行具体的存储管理动作。</p><p>而这些管理动作，比如“Attach 阶段”和“Mount 阶段”的具体操作，实际上就是通过调用 CSI 插件来完成的</p><p>下图展示了这种设计思路：</p><p><img src="/assets/img/Kubernetes/CSIProvision.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/CSIProvision.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>这套存储插件体系多了三个独立的外部组件（External Components），即：Driver Registrar、External Provisioner 和 External Attacher，对应的正是从 Kubernetes 项目里面剥离出来的那部分存储管理功能。</p><blockquote><p>External Components 虽然是外部组件，但依然由 Kubernetes 社区来开发和维护。</p></blockquote><p>图中最右侧的部分，是需要编写代码来实现的 CSI 插件。一个 CSI 插件只有一个二进制文件，但它会以 gRPC 的方式对外提供三个服务（gRPC Service），分别叫作：CSI Identity、CSI Controller 和 CSI Node。</p><p>图中 External Components有以下作用：</p><ul><li><strong>Driver Registrar 组件</strong>：<strong>负责将插件注册到 kubelet 里面</strong>，在具体实现上，Driver Registrar 需要请求 CSI 插件的 Identity 服务来获取插件信息。</li><li><strong>External Provisioner 组件</strong>：<strong>负责的正是 Provision 阶段</strong>。在具体实现上，External Provisioner 监听（Watch）了 APIServer 里的 PVC 对象。当一个 PVC 被创建时，它就会调用 CSI Controller 的 CreateVolume 方法，为你创建对应 PV。</li><li><strong>External Attacher 组件</strong>：<strong>负责的正是“Attach 阶段”</strong>。在具体实现上，它监听了 APIServer 里 VolumeAttachment 对象的变化。</li></ul><p>一旦出现了 VolumeAttachment 对象，External Attacher 就会调用 CSI Controller 服务的 ControllerPublish 方法，完成它所对应的 Volume 的 Attach 阶段。</p><p>Volume 的“Mount 阶段”，并不属于 External Components 的职责。当 kubelet 的 VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候，会通过 pkg&#x2F;volume&#x2F;csi 包，直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。</p><p>实际使用 CSI 插件的时候，我们会将这三个 External Components 作为 sidecar 容器和 CSI 插件放置在同一个 Pod 中。由于 External Components 对 CSI 插件的调用非常频繁，所以这种 sidecar 的部署方式非常高效。</p><p>CSI 插件的里三个服务：CSI Identity、CSI Controller 和 CSI Node有以下作用：</p><ul><li><strong>CSI Identity 服务，负责对外暴露这个插件本身的信息</strong></li><li><strong>CSI Controller 服务，定义的则是对 CSI Volume（对应 Kubernetes 里的 PV）的管理接口</strong></li><li><strong>CSI Volume 需要在宿主机上执行的操作，都定义在了 CSI Node 服务里面</strong></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;下图展示了Kubernetes 里通过存储插件管理容器持久化存储的原理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/img/Kubernetes/CSIPVC.png&quot; class=&quot;lazyload placeholder&quot; data-srcset=&quot;/asset</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>PV、PVC、StorageClass</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/26/Kubernetes-11-PV%E3%80%81PVC%E3%80%81StorageClass/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/26/Kubernetes-11-PV%E3%80%81PVC%E3%80%81StorageClass/</id>
    <published>2025-11-26T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<h5 id="Persistent-Volume（PV）"><a href="#Persistent-Volume（PV）" class="headerlink" title="Persistent Volume（PV）"></a>Persistent Volume（PV）</h5><p><strong>PV 描述的，是持久化存储数据卷</strong>。这个 API 对象主要定义的是一个持久化存储在宿主机上的目录，比如一个 NFS 的挂载目录。通常情况下，PV 对象是由运维人员事先创建在 Kubernetes 集群里待用的。</p><h5 id="Persistent-Volume-Claim（PVC）"><a href="#Persistent-Volume-Claim（PVC）" class="headerlink" title="Persistent Volume Claim（PVC）"></a>Persistent Volume Claim（PVC）</h5><p><strong>PVC 描述的，则是 Pod 所希望使用的持久化存储的属性</strong>。比如，Volume 存储的大小、可读写权限等等。PVC 对象通常由开发人员创建；或者以 PVC 模板的方式成为 StatefulSet 的一部分，然后由 StatefulSet 控制器负责创建带编号的 PVC</p><p>PVC 要真正被容器使用起来必须先和某个符合条件的 PV 进行绑定。这里要检查的条件，包括两部分：</p><ul><li>第一个条件，当然是 PV 和 PVC 的 spec 字段。比如，PV 的存储（storage）大小，就必须满足 PVC 的要求。</li><li>而第二个条件，则是 PV 和 PVC 的 storageClassName 字段必须一样。</li></ul><p>在成功地将 PVC 和 PV 进行绑定之后，Pod 就能够像使用 hostPath 等常规类型的 Volume 一样，在自己的 YAML 文件里声明使用这个 PVC 了</p><p><strong>PVC 和 PV 的设计，跟“面向对象”的思想完全一致。</strong></p><p>PVC 可以理解为持久化存储的“接口”，它提供了对某种持久化存储的描述，但不提供具体的实现；而这个持久化存储的实现部分则由 PV 负责完成。</p><p>当系统里并没有合适的 PV 跟它定义的 PVC 绑定</p><p>在 Kubernetes 中，存在着一个专门处理持久化存储的控制器，叫作 Volume Controller。这个 Volume Controller 维护着多个控制循环，其中有一个循环，扮演的就是撮合 PV 和 PVC 的“红娘”的角色。它的名字叫作 PersistentVolumeController。</p><p>PersistentVolumeController 会不断地查看当前每一个 PVC，是不是已经处于 Bound（已绑定）状态。如果不是，那它就会遍历所有的、可用的 PV，并尝试将其与这个“单身”的 PVC 进行绑定。这样，Kubernetes 就可以保证用户提交的每一个 PVC，只要有合适的 PV 出现，它就能够很快进入绑定状态，从而结束“单身”之旅。</p><p>而所谓将一个 PV 与 PVC 进行“绑定”，其实就是将这个 PV 对象的名字，填在了 PVC 对象的 spec.volumeName 字段上。所以，接下来 Kubernetes 只要获取到这个 PVC 对象，就一定能够找到它所绑定的 PV。</p><p>PV 对象的“持久化”Volume 的实现，往往依赖于一个远程存储服务，指的是容器在这个目录里写入的文件，都会保存在远程存储中，从而使得这个目录具备了“持久性”。<strong>这个准备“持久化”宿主机目录的过程，我们可以形象地称为“两阶段处理”。</strong></p><p>当一个 Pod 调度到一个节点上之后，kubelet 就要负责为这个 Pod 创建它的 Volume 目录。</p><p>接下来，kubelet 要做的操作就取决于你的 Volume 类型：如果 Volume 类型是远程块存储，那么 kubelet 就需要先调用远程磁盘服务的API，将它所提供的 Persistent Disk 挂载到 Pod 所在的宿主机上。这一步<strong>为虚拟机挂载远程磁盘的操作，对应的正是“两阶段处理”的第一阶段。在 Kubernetes 中，我们把这个阶段称为 Attach。</strong></p><p>Attach 阶段完成后，为了能够使用这个远程磁盘，kubelet 还要进行第二个操作，即：格式化这个磁盘设备，然后将它挂载到宿主机指定的挂载点上。这个<strong>将磁盘设备格式化并挂载到 Volume 宿主机目录的操作，对应的正是“两阶段处理”的第二个阶段，我们一般称为：Mount。</strong></p><p>Mount 阶段完成后，这个 Volume 的宿主机目录就是一个“持久化”的目录了，容器在它里面写入的内容，会保存在远程磁盘中。</p><p><strong>Kubernetes 定义和区分这两个阶段提供了两种不同的参数列表：</strong></p><ul><li>对于“第一阶段”（Attach），Kubernetes 提供的可用参数是 nodeName，即宿主机的名字。</li><li>而对于“第二阶段”（Mount），Kubernetes 提供的可用参数是 dir，即 Volume 的宿主机目录。</li></ul><p>经过了“两阶段处理”，我们就得到了一个“持久化”的 Volume 宿主机目录。</p><p>在 Kubernetes 中，上述<strong>关于 PV 的“两阶段处理”流程，是靠独立于 kubelet 主控制循环（Kubelet Sync Loop）之外的两个控制循环来实现的。</strong></p><p>第一阶段”的 Attach（以及 Dettach）操作，是由 Volume Controller 负责维护的，这个控制循环的名字叫作：<strong>AttachDetachController</strong>。而它的作用，就是不断地检查每一个 Pod 对应的 PV，和这个 Pod 所在宿主机之间挂载情况。从而决定，是否需要对这个 PV 进行 Attach（或者 Dettach）操作。</p><blockquote><p>作为一个 Kubernetes 内置的控制器，Volume Controller 自然是 kube-controller-manager 的一部分。所以，AttachDetachController 也一定是运行在 Master 节点上的。当然，Attach 操作只需要调用公有云或者具体存储项目的 API，并不需要在具体的宿主机上执行操作，所以这个设计没有任何问题。</p></blockquote><p>第二阶段”的 Mount（以及 Unmount）操作，必须发生在 Pod 对应的宿主机上，所以它必须是 kubelet 组件的一部分。这个控制循环的名字，叫作：<strong>VolumeManagerReconciler</strong>，它运行起来之后，是一个独立于 kubelet 主循环的 Goroutine。</p><p>通过这样将 Volume 的处理同 kubelet 的主循环解耦，Kubernetes 就避免了这些耗时的远程挂载操作拖慢 kubelet 的主控制循环，进而导致 Pod 的创建效率大幅下降的问题。实际上，<strong>kubelet 的一个主要设计原则，就是它的主控制循环绝对不可以被 block</strong>。</p><p>大规模的 Kubernetes 集群会有成千上万的PVC，Kubernetes提供了一套可以自动创建 PV 的机制：Dynamic Provisioning。Dynamic Provisioning 机制工作的核心，在于一个名叫 StorageClass 的 API 对象。<strong>StorageClass 对象的作用，其实就是创建 PV 的模板。</strong></p><p>StorageClass 对象会定义如下两个部分内容：</p><ul><li>第一，PV 的属性。比如，存储类型、Volume 的大小等等。</li><li>第二，创建这种 PV 需要用到的存储插件。比如，Ceph 等等。</li></ul><p>有了这样两个信息之后，Kubernetes 就能够根据用户提交的 PVC，找到一个对应的 StorageClass 了。然后，Kubernetes 就会调用该 StorageClass 声明的存储插件，创建出需要的 PV。</p><p>下图展示了PVC 和 PV 运行体系：</p><p><img src="/assets/img/Kubernetes/pv&pvc.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/pv&pvc.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>从图中我们可以看到，在这个体系中：</p><ul><li>PVC 描述的，是 Pod 想要使用的持久化存储的属性，比如存储的大小、读写权限等。</li><li>PV 描述的，则是一个具体的 Volume 的属性，比如 Volume 的类型、挂载目录、远程存储服务器地址等。</li><li>而 StorageClass 的作用，则是充当 PV 的模板。并且，只有同属于一个 StorageClass 的 PV 和 PVC，才可以绑定在一起。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h5 id=&quot;Persistent-Volume（PV）&quot;&gt;&lt;a href=&quot;#Persistent-Volume（PV）&quot; class=&quot;headerlink&quot; title=&quot;Persistent Volume（PV）&quot;&gt;&lt;/a&gt;Persistent Volume（PV）&lt;/</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>Operator工作原理</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/25/Kubernetes-10-Operator%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/25/Kubernetes-10-Operator%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/</id>
    <published>2025-11-25T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<p><strong>Operator 的工作原理，利用了 Kubernetes 的自定义 API 资源（CRD），来描述我们想要部署的“有状态应用”；然后在自定义控制器里，根据自定义 API 对象的变化，来完成具体的部署和运维工作。</strong></p><h5 id="Etcd-Operator"><a href="#Etcd-Operator" class="headerlink" title="Etcd Operator"></a>Etcd Operator</h5><p>使用 Helm Chart 部署 etcd 集群</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></pre></td><td class="code"><pre><span class="line"># 1. 添加 Bitnami Helm 仓库</span><br><span class="line">helm repo add bitnami https://charts.bitnami.com/bitnami</span><br><span class="line">helm repo update</span><br><span class="line"></span><br><span class="line"># 2. 创建命名空间</span><br><span class="line">kubectl create namespace etcd-system</span><br><span class="line"></span><br><span class="line"># 3. 部署 etcd 集群</span><br><span class="line">helm install etcd-cluster bitnami/etcd \</span><br><span class="line">  --namespace etcd-system \</span><br><span class="line">  --set replicaCount=3 \</span><br><span class="line">  --set auth.rbac.create=true \</span><br><span class="line">  --set persistence.enabled=true \</span><br><span class="line">  --set persistence.size=8Gi</span><br></pre></td></tr></table></figure><p>部署完成后实际上是在 Kubernetes 里添加了一个名叫 EtcdCluster 的自定义资源类型。而 Etcd Operator 本身，就是这个自定义资源类型对应的自定义控制器。</p><p><strong>Etcd Operator 部署 Etcd 集群，采用的是静态集群（Static）的方式</strong>。</p><p>静态集群的好处是，它不必依赖于一个额外的服务发现机制来组建集群，非常适合本地容器化部署。而它的难点，则在于你必须在部署的时候，就规划好这个集群的拓扑结构，并且能够知道这些节点固定的 IP 地址。</p><p>Etcd Operator集群具体的组建过程是逐个节点动态添加的方式，即：</p><p><strong>首先，Etcd Operator 会创建一个“种子节点”；</strong><br><strong>然后，Etcd Operator 会不断创建新的 Etcd 节点，然后将它们逐一加入到这个集群当中，直到集群的节点数等于 size。</strong></p><p>这就意味着，在生成不同角色的 Etcd Pod 时，Operator 需要能够区分种子节点与普通节点。</p><p>而这两种节点的不同之处，就在于一个名叫–initial-cluster-state 的启动参数：</p><ul><li>当这个参数值设为 new 时，就代表了该节点是种子节点。种子节点还必须通过–initial-cluster-token 声明一个独一无二的 Token。</li><li>而如果这个参数值设为 existing，那就是说明这个节点是一个普通节点，Etcd Operator 需要把它加入到已有集群里。</li></ul><p><strong>Etcd Operator 启动要做的第一件事</strong>（ c.initResource），是创建 EtcdCluster 对象所需要的 CRD</p><p><strong>接下来，Etcd Operator 会定义一个 EtcdCluster 对象的 Informer</strong>。</p><p>Etcd Operator 并没有用工作队列来协调 Informer 和控制循环。</p><p>在控制循环里执行的业务逻辑，往往是比较耗时间的。比如，创建一个真实的 Etcd 集群。而 Informer 的 WATCH 机制对 API 对象变化的响应，则非常迅速。所以，控制器里的业务逻辑就很可能会拖慢 Informer 的执行周期，甚至可能 Block 它。而要协调这样两个快、慢任务的一个典型解决方法，就是引入一个工作队列。</p><p>Etcd Operator 在业务逻辑的流程图如下：</p><p><img src="/assets/img/Kubernetes/EtcdOperator.jpg" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/EtcdOperator.jpg" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Etcd Operator 的特殊之处在于，它为每一个 EtcdCluster 对象，都启动了一个控制循环，“并发”地响应这些对象的变化。这种做法不仅可以简化 Etcd Operator 的代码实现，还有助于提高它的响应速度。</p><p>当YAML文件第一次被提交到 Kubernetes 之后，Etcd Operator 的 Informer，就会立刻“感知”到一个新的 EtcdCluster 对象被创建了出来。所以，EventHandler 里的“添加”事件会被触发。</p><p>而这个 Handler 要做的操作也很简单，即：在 Etcd Operator 内部创建一个对应的 Cluster 对象（cluster.New），比如流程图里的 Cluster1。</p><p>这个 Cluster 对象，就是一个 Etcd 集群在 Operator 内部的描述，所以它与真实的 Etcd 集群的生命周期是一致的。</p><p>一个 Cluster 对象需要具体负责的，其实有两个工作。</p><p>**第一个工作只在该 Cluster 对象第一次被创建的时候才会执行。这个工作，就是 Bootstrap，即：创建一个单节点的种子集群。**由于种子集群只有一个节点，所以这一步直接就会生成一个 Etcd 的 Pod 对象。这个 Pod 里有一个 InitContainer，负责检查 Pod 的 DNS 记录是否正常。如果检查通过，用户容器也就是 Etcd 容器就会启动起来。这个 Etcd 容器最重要的部分，是它的启动命令。启动中，Etcd Operator 只会使用 Pod 的 DNS 记录，而不是它的 IP 地址。因为在 Operator 生成上述启动命令的时候，Etcd 的 Pod 还没有被创建出来，它的 IP 地址自然也无从谈起。</p><p>这也就意味着，每个 Cluster 对象，都会事先创建一个与该 EtcdCluster 同名的 Headless Service。这样，Etcd Operator 在接下来的所有创建 Pod 的步骤里，就都可以使用 Pod 的 DNS 记录来代替它的 IP 地址了。</p><p><strong>Cluster 对象的第二个工作，则是启动该集群所对应的控制循环。</strong></p><p>这个控制循环每隔一定时间，就会执行一次下面的 Diff 流程。</p><p>首先，控制循环要获取到所有正在运行的、属于这个 Cluster 的 Pod 数量，也就是该 Etcd 集群的“实际状态”。</p><p>而这个 Etcd 集群的“期望状态”，正是用户在 EtcdCluster 对象里定义的 size。</p><p>所以接下来，控制循环会对比这两个状态的差异。</p><p>如果实际的 Pod 数量不够，那么控制循环就会执行一个添加成员节点的操作（即：上述流程图中的 addOneMember 方法）；反之，就执行删除成员节点的操作（即：上述流程图中的 removeOneMember 方法）。</p><p>以 addOneMember 方法为例，它执行的流程如下所示：</p><ol><li>生成一个新节点的 Pod 的名字，比如：example-etcd-cluster-v6v6s6stxd；</li><li>调用 Etcd Client，执行 etcdctl member add example-etcd-cluster-v6v6s6stxd 命令；</li><li>使用这个 Pod 名字，和已经存在的所有节点列表，组合成一个新的 initial-cluster 字段的值；</li><li>使用这个 initial-cluster 的值，生成这个 Pod 里 Etcd 容器的启动命令。</li></ol><p>当这个容器启动之后，一个新的 Etcd 成员节点就被加入到了集群当中。控制循环会重复这个过程，直到正在运行的 Pod 数量与 EtcdCluster 指定的 size 一致。</p><p>有了这样一个与 EtcdCluster 对象一一对应的控制循环之后，后续对这个 EtcdCluster 的任何修改，比如：修改 size 或者 Etcd 的 version，它们对应的更新事件都会由这个 Cluster 对象的控制循环进行处理。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;Operator 的工作原理，利用了 Kubernetes 的自定义 API 资源（CRD），来描述我们想要部署的“有状态应用”；然后在自定义控制器里，根据自定义 API 对象的变化，来完成具体的部署和运维工作。&lt;/strong&gt;&lt;/p&gt;
&lt;h5 id=&quot;E</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>RBAC</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/24/Kubernetes-9-RBAC/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/24/Kubernetes-9-RBAC/</id>
    <published>2025-11-24T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.383Z</updated>
    
    <content type="html"><![CDATA[<p>Kubernetes 中所有的 API 对象，都保存在 Etcd 里。可是，对这些 API 对象的操作，却一定都是通过访问 kube-apiserver 实现的。其中一个非常重要的原因，就是你需要 APIServer 来帮助你做授权工作</p><p><strong>在 Kubernetes 项目中，负责完成授权（Authorization）工作的机制，就是 RBAC</strong>：基于角色的访问控制（Role-Based Access Control）。</p><p>RBAC有三个基本概念：</p><ol><li>Role：角色，它其实是一组规则，定义了一组对 Kubernetes API 对象的操作权限。</li><li>Subject：被作用者，既可以是“人”，也可以是“机器”，也可以使你在 Kubernetes 里定义的“用户”。</li><li>RoleBinding：定义了“被作用者”和“角色”的绑定关系。</li></ol><h5 id="Role"><a href="#Role" class="headerlink" title="Role"></a>Role</h5><p>Role 本身就是一个 Kubernetes 的 API 对象，</p><h5 id="Subject"><a href="#Subject" class="headerlink" title="Subject"></a>Subject</h5><p>“被作用者”需要通过 RoleBinding 来实现。RoleBinding 对象里定义了一个 subjects 字段，即“被作用者”。它的类型是 User，即 Kubernetes 里的用户。</p><p>Kubernetes 里的“User”，也就是“用户”，只是一个授权系统里的逻辑概念。它需要通过外部认证服务，比如 Keystone，来提供。或者，你也可以直接给 APIServer 指定一个用户名、密码文件。那么 Kubernetes 的授权系统，就能够从这个文件里找到对应的“用户”了。</p><p>有一个 roleRef 字段，通过这个字段RoleBinding 对象就可以直接通过名字，来引用定义的 Role 对象，从而定义了“被作用者（Subject）”和“角色（Role）”之间的绑定关系。</p><p>Kubernetes的ServiceAccount 也可以作为权限分配的一个方式，并且更为常用</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Kubernetes 中所有的 API 对象，都保存在 Etcd 里。可是，对这些 API 对象的操作，却一定都是通过访问 kube-apiserver 实现的。其中一个非常重要的原因，就是你需要 APIServer 来帮助你做授权工作&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 K</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>Kubernetes编程范式</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/23/Kubernetes-8-Kubernetes%E7%BC%96%E7%A8%8B%E8%8C%83%E5%BC%8F/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/23/Kubernetes-8-Kubernetes%E7%BC%96%E7%A8%8B%E8%8C%83%E5%BC%8F/</id>
    <published>2025-11-23T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.383Z</updated>
    
    <content type="html"><![CDATA[<h5 id="声明式-API"><a href="#声明式-API" class="headerlink" title="声明式 API"></a>声明式 API</h5><p>在Kubernetes中kubectl apply是一个声明式 API，可以用它来代替 kubectl create 命令，修改YAML文件的kubectl replace是一个命令式API，kubectl replace 的执行过程，是使用新的 YAML 文件中的 API 对象，<strong>替换原有的 API 对象</strong>；同样也可以使用kubectl apply代替，kubectl apply相比于kubectl replace，则是执行了一个<strong>对原有 API 对象的 PATCH 操作</strong></p><p>kube-apiserver 在响应命令式请求（比如，kubectl replace）的时候，一次只能处理一个写请求，否则会有产生冲突的可能。而对于声明式请求（比如，kubectl apply），<strong>一次能处理多个写操作，并且具备 Merge 能力</strong>。</p><p><strong>Kubernetes“声明式 API”的独特之处：</strong></p><ul><li>首先，所谓“声明式”，指的就是我只需要提交一个定义好的 API 对象来“声明”，我所期望的状态是什么样子。</li><li>其次，“声明式 API”允许有多个 API 写端，以 PATCH 的方式对 API 对象进行修改，而无需关心本地原始 YAML 文件的内容。</li><li>最后，也是最重要的，有了上述两个能力，Kubernetes 项目才可以基于对 API 对象的增、删、改、查，在完全无需外界干预的情况下，完成对“实际状态”和“期望状态”的调谐（Reconcile）过程。</li></ul><p><strong>声明式 API，是 Kubernetes 项目编排能力“赖以生存”的核心所在</strong></p><h5 id="声明式-API-的设计"><a href="#声明式-API-的设计" class="headerlink" title="声明式 API 的设计"></a>声明式 API 的设计</h5><p>Kubernetes 项目中，一个 API 对象在 Etcd 里的完整资源路径，是由：Group（API 组）、Version（API 版本）和 Resource（API 资源类型）三个部分组成的。</p><p>下图展示了<strong>Kubernetes 里 API 对象的组织方式</strong>：</p><p><img src="/assets/img/Kubernetes/api-page.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/api-page.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Kubernetes对 Resource、Group 和 Version的解析分以下步骤：</p><ol><li><p><strong>Kubernetes 会匹配 API 对象的组。</strong>：需要明确的是，对于 Kubernetes 里的核心 API 对象，比如：Pod、Node 等，是不需要 Group 的（即：它们 Group 是“”）。所以，对于这些 API 对象来说，Kubernetes 会直接在 &#x2F;api 这个层级进行下一步的匹配过程。</p><p>而对于 CronJob 等非核心 API 对象来说，Kubernetes 就必须在 &#x2F;apis 这个层级里查找它对应的 Group，进而根据“batch”这个 Group 的名字，找到 &#x2F;apis&#x2F;batch。</p><p>不难发现，这些 API Group 的分类是以对象功能为依据的，比如 Job 和 CronJob 就都属于“batch” （离线业务）这个 Group。</p></li><li><p><strong>然后，Kubernetes 会进一步匹配到 API 对象的版本号。</strong>：在 Kubernetes 中，同一种 API 对象可以有多个版本，这正是 Kubernetes 进行 API 版本化管理的重要手段。这样，比如在 CronJob 的开发过程中，对于会影响到用户的变更就可以通过升级新版本来处理，从而保证了向后兼容。</p></li><li><p><strong>最后，Kubernetes 会匹配 API 对象的资源类型。</strong></p></li></ol><p>下图展示了APIServer创建CronJob 对象的过程：</p><p><img src="/assets/img/Kubernetes/create-cronjob.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/create-cronjob.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p><strong>首先</strong>，当我们发起了创建 CronJob 的 POST 请求之后，我们编写的 YAML 的信息就被提交给了 APIServer。</p><p>而 APIServer 的第一个功能，就是过滤这个请求，并完成一些前置性的工作，比如授权、超时处理、审计等。</p><p><strong>然后</strong>，请求会进入 MUX 和 Routes 流程。如果你编写过 Web Server 的话就会知道，MUX 和 Routes 是 APIServer 完成 URL 和 Handler 绑定的场所。而 APIServer 的 Handler 要做的事情，就是按照我刚刚介绍的匹配过程，找到对应的 CronJob 类型定义。</p><p><strong>接着</strong>，APIServer 最重要的职责就来了：根据这个 CronJob 类型定义，使用用户提交的 YAML 文件里的字段，创建一个 CronJob 对象。</p><p>在这个过程中，APIServer 会进行一个 Convert 工作，即：把用户提交的 YAML 文件，转换成一个叫作 Super Version 的对象，它正是该 API 资源类型所有版本的字段全集。这样用户提交的不同版本的 YAML 文件，就都可以用这个 Super Version 对象来进行处理了。</p><p><strong>接下来</strong>，APIServer 会先后进行 Admission() 和 Validation() 操作。 Admission Controller 和 Initializer，就都属于 Admission 的内容。</p><p>而 Validation，则负责验证这个对象里的各个字段是否合法。这个被验证过的 API 对象，都保存在了 APIServer 里一个叫作 Registry 的数据结构中。也就是说，只要一个 API 对象的定义能在 Registry 里查到，它就是一个有效的 Kubernetes API 对象。</p><p><strong>最后</strong>，APIServer 会把验证过的 API 对象转换成用户最初提交的版本，进行序列化操作，并调用 Etcd 的 API 把它保存起来。</p><p>由此可见，声明式 API 对于 Kubernetes 来说非常重要。所以，<strong>APIServer 这样一个在其他项目里“平淡无奇”的组件，却成了 Kubernetes 项目的重中之重</strong>。它不仅是 Google Borg 设计思想的集中体现，也是 Kubernetes 项目里唯一一个被 Google 公司和 RedHat 公司双重控制、其他势力根本无法参与其中的组件。</p><p>在APIServer 中，添加一个 Kubernetes 风格的 API 资源类型可以使用Kubernetes的API 插件机制：CRD</p><p>CRD 的全称是 Custom Resource Definition。顾名思义，它指的就是，允许用户在 Kubernetes 中添加一个跟 Pod、Node 类似的、新的 API 资源类型，即：自定义 API 资源。</p><h5 id="编写自定义控制器（Custom-Controller）"><a href="#编写自定义控制器（Custom-Controller）" class="headerlink" title="编写自定义控制器（Custom Controller）"></a>编写自定义控制器（Custom Controller）</h5><p><strong>基于声明式 API 的业务功能实现，往往需要通过控制器模式来“监视”API 对象的变化（比如，创建或者删除 Network），然后以此来决定实际要执行的具体工作。</strong></p><p>下图展示了一个自定义控制器的工作原理：</p><p><img src="/assets/img/Kubernetes/CustomController.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/CustomController.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>从这幅示意图的最左边看起，<strong>这个控制器要做的第一件事，是从 Kubernetes 的 APIServer 里获取它所关心的对象，也就是我定义的 Network 对象</strong>。</p><p>这个操作，依靠的是一个叫作 Informer（可以翻译为：通知器）的代码库完成的。Informer 与 API 对象是一一对应的，所以我传递给自定义控制器的，正是一个 Network 对象的 Informer（Network Informer）。</p><p>在创建这个 Informer 工厂的时候，需要给它传递一个 networkClient。事实上，Network Informer 正是使用这个 networkClient，跟 APIServer 建立了连接。不过，真正负责维护这个连接的，则是 Informer 所使用的 Reflector 包。更具体地说，Reflector 使用的是一种叫作<strong>ListAndWatch</strong>的方法，来“获取”并“监听”这些 Network 对象实例的变化。在 ListAndWatch 机制下，一旦 APIServer 端有新的 Network 实例被创建、删除或者更新，Reflector 都会收到“事件通知”。这时，该事件及它对应的 API 对象这个组合，就被称为增量（Delta），它会被放进一个 Delta FIFO Queue（即：增量先进先出队列）中。而另一方面，Informe 会不断地从这个 Delta FIFO Queue 里读取（Pop）增量。每拿到一个增量，Informer 就会判断这个增量里的事件类型，然后创建或者更新本地对象的缓存。这个缓存，在 Kubernetes 里一般被叫作 Store。</p><p>这个<strong>同步本地缓存的工作，是 Informer 的第一个职责，也是它最重要的职责。Informer 的第二个职责，则是根据这些事件的类型，触发事先注册好的 ResourceEventHandler</strong>。这些 Handler，需要在创建控制器的时候注册给它对应的 Informer</p><p><strong>Informer，其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client</strong>。它是自定义控制器跟 APIServer 进行数据同步的重要组件。更具体地说，Informer 通过一种叫作 ListAndWatch 的方法，把 APIServer 中的 API 对象缓存在了本地，并负责更新和维护这个缓存。其中，ListAndWatch 方法的含义是：首先，通过 APIServer 的 LIST API“获取”所有最新版本的 API 对象；然后，再通过 WATCH API 来“监听”所有这些 API 对象的变化。而通过监听到的事件变化，Informer 就可以实时地更新本地缓存，并且调用这些事件对应的 EventHandler 了。</p><p>在这个过程中，每经过 resyncPeriod 指定的时间，Informer 维护的本地缓存，都会使用最近一次 LIST 返回的结果强制更新一次，从而保证缓存的有效性。在 Kubernetes 中，这个缓存强制更新的操作就叫作：resync。这个定时 resync 操作，也会触发 Informer 注册的“更新”事件。但此时，这个“更新”事件对应的 Network 对象实际上并没有发生变化，即：新、旧两个 Network 对象的 ResourceVersion 是一样的。在这种情况下，Informer 就不需要对这个更新事件再做进一步的处理了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h5 id=&quot;声明式-API&quot;&gt;&lt;a href=&quot;#声明式-API&quot; class=&quot;headerlink&quot; title=&quot;声明式 API&quot;&gt;&lt;/a&gt;声明式 API&lt;/h5&gt;&lt;p&gt;在Kubernetes中kubectl apply是一个声明式 API，可以用它来代替 kubect</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>离线业务</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/22/Kubernetes-7-%E7%A6%BB%E7%BA%BF%E4%B8%9A%E5%8A%A1/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/22/Kubernetes-7-%E7%A6%BB%E7%BA%BF%E4%B8%9A%E5%8A%A1/</id>
    <published>2025-11-22T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.383Z</updated>
    
    <content type="html"><![CDATA[<h5 id="Job"><a href="#Job" class="headerlink" title="Job"></a>Job</h5><p>Job 对象离线计算的 Pod 永远都不应该被重启，否则它们会再重新计算一遍，所以我们需要在 Pod 模板中定义 restartPolicy&#x3D;Never 。Job Controller 重新创建 Pod 的间隔是呈指数增加的，即下一次重新创建 Pod 的动作会分别发生在 10 s、20 s、40 s …后。</p><p>Job Controller离线作业失败后有两种处理方式</p><ol><li><strong>当定义了 restartPolicy&#x3D;Never，那么离线作业失败后 Job Controller 就会不断地尝试创建一个新 Pod</strong></li><li><strong>当定义的 restartPolicy&#x3D;OnFailure，那么离线作业失败后，Job Controller 就不会去尝试创建新的 Pod。但是，它会不断地尝试重启 Pod 里的容器</strong></li></ol><p>在 Job 对象中，负责并行控制的参数有两个：</p><ol><li>spec.parallelism，它定义的是一个 Job 在任意时间最多可以启动多少个 Pod 同时运行；</li><li>spec.completions，它定义的是 Job 至少要完成的 Pod 数目，即 Job 的最小完成数</li></ol><p>Job Controller 控制的对象，直接就是 Pod。Job Controller 在控制循环中进行的调谐（Reconcile）操作，是根据实际在 Running 状态 Pod 的数目、已经成功退出的 Pod 的数目，以及 parallelism、completions 参数的值共同计算出在这个周期里，应该创建或者删除的 Pod 数目，然后调用 Kubernetes API 来执行这个操作。</p><p>Job Controller 实际上控制了，作业执行的<strong>并行度</strong>，以及总共需要完成的<strong>任务数</strong>这两个重要参数。而在实际使用时，你需要根据作业的特性，来决定并行度（parallelism）和任务数（completions）的合理取值。</p><p>有三种常用的、使用 Job 对象的方法：</p><ol><li><strong>外部管理器 +Job 模板</strong>：把 Job 的 YAML 文件定义为一个“模板”，然后用一个外部工具控制这些“模板”来生成 Job</li><li><strong>拥有固定任务数目的并行 Job</strong>：设置指定数目（spec.completions）个任务成功退出即可，无须关注执行时的并行度</li><li><strong>指定并行度（parallelism），但不设置固定的 completions 的值</strong>：任务数目的总数不固定，需要决定什么时候启动新 Pod，什么时候 Job 才算执行完成</li></ol><h5 id="CronJob"><a href="#CronJob" class="headerlink" title="CronJob"></a>CronJob</h5><p>CronJob是指定时任务。在YAML文件中，关键词是<strong>jobTemplate</strong>，这表明CronJob 是一个 Job 对象的控制器（Controller）</p><p>CronJob 与 Job 的关系，正如同 Deployment 与 Pod 的关系一样。CronJob 是一个专门用来管理 Job 对象的控制器。只不过，它创建和删除 Job 的依据，是 schedule 字段定义的、一个标准的<a href="https://en.wikipedia.org/wiki/Cron">Unix Cron</a>格式的表达式。</p><p>于定时任务的特殊性，很可能某个 Job 还没有执行完，另外一个新 Job 就产生了。这时候，你可以通过 spec.concurrencyPolicy 字段来定义具体的处理策略。比如：</p><ol><li>concurrencyPolicy&#x3D;Allow，这也是默认情况，这意味着这些 Job 可以同时存在；</li><li>concurrencyPolicy&#x3D;Forbid，这意味着不会创建新的 Pod，该创建周期被跳过；</li><li>concurrencyPolicy&#x3D;Replace，这意味着新产生的 Job 会替换旧的、没有执行完的 Job。</li></ol><p>如果某一次 Job 创建失败，这次创建就会被标记为“miss”。当在指定的时间窗口内，miss 的数目达到 100 时，那么 CronJob 会停止再创建这个 Job。这个时间窗口，可以由 spec.startingDeadlineSeconds 字段指定</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h5 id=&quot;Job&quot;&gt;&lt;a href=&quot;#Job&quot; class=&quot;headerlink&quot; title=&quot;Job&quot;&gt;&lt;/a&gt;Job&lt;/h5&gt;&lt;p&gt;Job 对象离线计算的 Pod 永远都不应该被重启，否则它们会再重新计算一遍，所以我们需要在 Pod 模板中定义 restartPo</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>DaemonSet</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/21/Kubernetes-6-DaemonSet/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/21/Kubernetes-6-DaemonSet/</id>
    <published>2025-11-21T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.383Z</updated>
    
    <content type="html"><![CDATA[<h5 id="DaemonSet"><a href="#DaemonSet" class="headerlink" title="DaemonSet"></a>DaemonSet</h5><p>DaemonSet 的主要作用，是让你在 Kubernetes 集群里，运行一个 Daemon Pod。这个 Pod 有如下三个特征：</p><ol><li>这个 Pod 运行在 Kubernetes 集群里的每一个节点（Node）上；</li><li>每个节点上只有一个这样的 Pod 实例；</li><li>当有新的节点加入 Kubernetes 集群后，该 Pod 会自动地在新节点上被创建出来；而当旧节点被删除后，它上面的 Pod 也相应地会被回收掉。</li></ol><p>但 Daemon Pod 的意义是非常重要的，像各种网络插件的 Agent 组件、各种存储插件的 Agent 组件、各种监控组件和日志组件，DaemonSet 开始运行的时机，很多时候比整个 Kubernetes 集群出现的时机都要早。DaemonSet 跟 Deployment 其实非常相似，只不过是没有 replicas 字段；</p><p><strong>DaemonSet 通过DaemonSet Controller保证每个 Node 上有且只有一个被管理的 Pod</strong>，DaemonSet Controller首先从 Etcd 里获取所有的 Node 列表，然后遍历所有的 Node。这时检查的结果有三种情况：</p><ol><li>没有这种 Pod，那么就意味着要在这个 Node 上创建这样一个 Pod；</li><li>有这种 Pod，但是数量大于 1，那就说明要把多余的 Pod 从这个 Node 上删除掉；</li><li>正好只有一个这种 Pod，那说明这个节点是正常的。</li></ol><p>Kubernetes中删除节点（Node）上多余的 Pod 非常简单，直接调用 Kubernetes API 就可以了。而在指定的 Node 上创建新 Pod需要使用nodeAffinity字段</p><p><strong>DaemonSet Controller 会在创建 Pod 的时候，自动在这个 Pod 的 API 对象里，加上这样一个 nodeAffinity 定义</strong>。其中，需要绑定的节点名字，正是当前正在遍历的这个 Node。</p><p>DaemonSet 并不需要修改用户提交的 YAML 文件里的 Pod 模板，而是在向 Kubernetes 发起请求之前，直接修改根据模板生成的 Pod 对象。此外，DaemonSet 还会给这个 Pod 自动加上另外一个与调度相关的字段，叫作 tolerations。这个字段意味着这个 Pod，会“容忍”（Toleration）某些 Node 的“污点”（Taint）。</p><p>在正常情况下，被标记了 unschedulable“污点”的 Node，是不会有任何 Pod 被调度上去的（effect: NoSchedule）。可是，DaemonSet 自动地给被管理的 Pod 加上了这个特殊的 Toleration，就使得这些 Pod 可以忽略这个限制，继而保证每个节点上都会被调度一个 Pod。当然，如果这个节点有故障的话，这个 Pod 可能会启动失败，而 DaemonSet 则会始终尝试下去，直到 Pod 启动成功</p><p><strong>DaemonSet 的“过人之处”，其实就是依靠 Toleration 实现的。通过一个 Toleration，调度器在调度这个 Pod 的时候，就会忽略当前节点上的“污点”，从而成功地将网络插件的 Agent 组件调度到这台机器上启动起来。</strong></p><p>这种机制，正是我们在部署 Kubernetes 集群的时候，能够先部署 Kubernetes 本身、再部署网络插件的根本原因：因为当时我们所创建的 Weave 的 YAML，实际上就是一个 DaemonSet。</p><p><strong>DaemonSet 其实是一个非常简单的控制器</strong>。在它的控制循环中，只需要遍历所有节点，然后根据节点上是否有被管理 Pod 的情况，来决定是否要创建或者删除一个 Pod。</p><p>只不过，在创建每个 Pod 的时候，DaemonSet 会自动给这个 Pod 加上一个 nodeAffinity，从而保证这个 Pod 只会在指定节点上启动。同时，它还会自动给这个 Pod 加上一个 Toleration，从而忽略节点的 unschedulable“污点”。</p><p>Kubernetes 集群不允许用户在 Master 节点部署 Pod。因为，Master 节点默认携带了一个叫作<code>node-role.kubernetes.io/master</code>的“污点”。所以，为了能在 Master 节点上部署 DaemonSet 的 Pod，我就必须让这个 Pod“容忍”这个“污点”。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h5 id=&quot;DaemonSet&quot;&gt;&lt;a href=&quot;#DaemonSet&quot; class=&quot;headerlink&quot; title=&quot;DaemonSet&quot;&gt;&lt;/a&gt;DaemonSet&lt;/h5&gt;&lt;p&gt;DaemonSet 的主要作用，是让你在 Kubernetes 集群里，运行一个 D</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>StatefulSet</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/19/Kubernetes-5-StatefulSet/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/19/Kubernetes-5-StatefulSet/</id>
    <published>2025-11-19T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.383Z</updated>
    
    <content type="html"><![CDATA[<center style="font-size:24px;font-weight:900;">StatefulSet</center><h5 id="拓扑状态"><a href="#拓扑状态" class="headerlink" title="拓扑状态"></a>拓扑状态</h5><p>Deployment有一个问题，Deployment会认为一个应用的所有 Pod，是完全一样的。所以，它们互相之间没有顺序，也无所谓运行在哪台宿主机上。需要的时候，Deployment 就可以通过 Pod 模板创建新的 Pod；不需要的时候，Deployment 就可以“杀掉”任意一个 Pod。在实际的场景中，并不是所有的应用都可以满足这样的要求。</p><p>数据存储类应用，它的多个实例，往往都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉，即便重建出来，实例与数据之间的对应关系也已经丢失，从而导致应用失败。这种实例之间有不对等关系，以及实例对外部数据有依赖关系的应用，就被称为“有状态应用”（Stateful Application）。</p><p>StatefulSet 的设计其实非常容易理解。它把真实世界里的应用状态，抽象为了两种情况：</p><ol><li><strong>拓扑状态</strong>。这种情况意味着，应用的多个实例之间不是完全对等的关系。这些应用实例，必须按照某些顺序启动，比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉，它们再次被创建出来时也必须严格按照这个顺序才行。并且，新创建出来的 Pod，必须和原来 Pod 的网络标识一样，这样原先的访问者才能使用同样的方法，访问到这个新 Pod。</li><li><strong>存储状态</strong>。这种情况意味着，应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说，Pod A 第一次读取到的数据，和隔了十分钟之后再次读取到的数据，应该是同一份，哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子，就是一个数据库应用的多个存储实例。</li></ol><p><strong>StatefulSet 的核心功能，就是通过某种方式记录这些状态，然后在 Pod 被重新创建时，能够为新 Pod 恢复这些状态。</strong></p><p>Service 是 Kubernetes 项目中用来将一组 Pod 暴露给外界访问的一种机制。用户只要能访问到这个 Service，它就能访问到某个具体的 Pod。这个Service有两种可以被访问的方式：</p><ol><li><strong>以 Service 的 VIP（Virtual IP，即：虚拟 IP）方式</strong>。比如：当我访问 10.0.23.1 这个 Service 的 IP 地址时，10.0.23.1 其实就是一个 VIP，它会把请求转发到该 Service 所代理的某一个 Pod 上。</li><li><strong>以 Service 的 DNS 方式</strong>。比如：这时候，只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录，就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。</li></ol><p>第二种 Service DNS 的方式下，具体可以分为两种处理方法：</p><ol><li>Normal Service：访问“my-svc.my-namespace.svc.cluster.local”解析到的，是 my-svc 这个 Service 的 VIP，后面的流程就跟 VIP 方式一致了。</li><li>Headless Service：访问“my-svc.my-namespace.svc.cluster.local”解析到的，直接就是 my-svc 代理的某一个 Pod 的 IP 地址。</li></ol><p><strong>这里的区别在于，Headless Service 不需要分配一个 VIP，而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。</strong></p><p>在Kubernetes里 Headless Service，其实是一个标准 Service 的 YAML 文件。只不过，它的 clusterIP 字段的值是：None，即：这个 Service，没有一个 VIP 作为“头”。这也就是 Headless 的含义。所以，这个 Service 被创建后并不会被分配一个 VIP，而是会以 DNS 记录的方式暴露出它所代理的 Pod。而它所代理的 Pod是 Label Selector 机制选择出来的。</p><p>通过 Headless Service 的方式，StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录，来作为它的访问入口。</p><p>StatefulSet 这个控制器的主要作用之一，就是使用 Pod 模板创建 Pod 的时候，对它们进行编号，并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致，需要新建或者删除 Pod 进行“调谐”的时候，它会严格按照这些 Pod 编号的顺序，逐一完成这些操作。StatefulSet 其实可以认为是对 Deployment 的改良。</p><p>对于“有状态应用”实例的访问，必须使用 DNS 记录或者 hostname 的方式，而绝不应该直接访问这些 Pod 的 IP 地址。</p><h5 id="存储状态"><a href="#存储状态" class="headerlink" title="存储状态"></a>存储状态</h5><p>这个机制，主要使用的是一个叫作 Persistent Volume Claim 的功能。</p><p>要在一个 Pod 里声明 Volume，只要在 Pod 里加上 spec.volumes 字段即可。然后，你就可以在这个字段里定义一个具体类型的 Volume 了，比如：hostPath。</p><p>当不知道有哪些 Volume 类型可以用，Kubernetes 项目引入了一组叫作 Persistent Volume Claim（PVC）和 Persistent Volume（PV）的 API 对象，大大降低了用户声明和使用持久化 Volume 的门槛。</p><p>有了 PVC 之后，一个开发人员想要使用一个 Volume，只需要简单的两步即可。</p><ol><li><strong>定义一个 PVC，声明想要的 Volume 的属性</strong></li><li><strong>在应用的 Pod 中，声明使用这个 PVC</strong></li></ol><p>Kubernetes 中 PVC 和 PV 的设计，<strong>实际上类似于“接口”和“实现”的思想</strong>。开发者只要知道并会使用“接口”，即：PVC；而运维人员则负责给“接口”绑定具体的实现，即：PV。这种解耦，就避免了因为向开发者暴露过多的存储系统细节而带来的隐患。</p><p>PVC、PV 的设计，也使得 StatefulSet 对存储状态的管理成为了可能。</p><p>StatefulSet里面有一个volumeClaimTemplates 字段，它跟 Deployment 里 Pod 模板（PodTemplate）的作用类似。表示被这个 StatefulSet 管理的 Pod，都会声明一个对应的 PVC；这个 PVC 的定义，就来自于 volumeClaimTemplates 这个模板字段。更重要的是，这个 PVC 的名字，会被分配一个与这个 Pod 完全一致的编号。</p><p>这个自动创建的 PVC，与 PV 绑定成功后，就会进入 Bound 状态，这就意味着这个 Pod 可以挂载并使用这个 PV 了。</p><p>当使用 kubectl delete 命令删除这个 Pod，这些 Volume 里的文件并不会丢失。在被删除之后，Pod 会被按照编号的顺序被重新创建出来。并且原先的 Pod 绑定的 PV，在这个 Pod 被重新创建之后，依然同新的同名的 Pod 绑定在了一起。</p><p>可以这么理解 StatefulSet 控制器恢复 Pod 的过程：</p><ol><li>当把一个 Pod，删除之后，这个 Pod 对应的 PVC 和 PV，并不会被删除，而这个 Volume 里已经写入的数据，也依然会保存在远程存储服务里。</li><li>此时，StatefulSet 控制器发现，Pod 消失了。所以，控制器就会重新创建一个新的、名字还是和原来同名的Pod 来，“纠正”这个不一致的情况。</li><li>在这个新的Pod 被创建出来之后，Kubernetes 为它查找对应的PVC 时，就会直接找到旧 Pod 遗留下来的同名的 PVC，进而找到跟这个 PVC 绑定在一起的 PV。</li><li>这样，新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume，并且获取到保存在 Volume 里的数据。</li></ol><p><strong>通过这种方式，Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。</strong></p><p>StatefulSet 的工作原理可以这样描述：</p><p><strong>首先，StatefulSet 的控制器直接管理的是 Pod</strong>。这是因为，StatefulSet 里的不同 Pod 实例，不再像 ReplicaSet 中那样都是完全一样的，而是有了细微区别的。比如，每个 Pod 的 hostname、名字等都是不同的、携带了编号的。而 StatefulSet 区分这些实例的方式，就是通过在 Pod 的名字里加上事先约定好的编号。</p><p><strong>其次，Kubernetes 通过 Headless Service，为这些有编号的 Pod，在 DNS 服务器中生成带有同样编号的 DNS 记录</strong>。只要 StatefulSet 能够保证这些 Pod 名字里的编号不变，那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变，而这条记录解析出来的 Pod 的 IP 地址，则会随着后端 Pod 的删除和再创建而自动更新。这当然是 Service 机制本身的能力，不需要 StatefulSet 操心。</p><p><strong>最后，StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC</strong>。这样，Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV，从而保证了每一个 Pod 都拥有一个独立的 Volume。</p><p>在这种情况下，即使 Pod 被删除，它所对应的 PVC 和 PV 依然会保留下来。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;center style=&quot;font-size:24px;font-weight:900;&quot;&gt;StatefulSet&lt;/center&gt;

&lt;h5 id=&quot;拓扑状态&quot;&gt;&lt;a href=&quot;#拓扑状态&quot; class=&quot;headerlink&quot; title=&quot;拓扑状态&quot;&gt;&lt;/a&gt;拓扑状态</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>编排</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/18/Kubernetes-4-%E7%BC%96%E6%8E%92/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/18/Kubernetes-4-%E7%BC%96%E6%8E%92/</id>
    <published>2025-11-18T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<center style="font-size:24px;font-weight:900;">编排</center><p>Kubernetes 操作Pod的逻辑，都由控制器（Controller）完成</p><h4 id="Deployment控制器对象"><a href="#Deployment控制器对象" class="headerlink" title="Deployment控制器对象"></a>Deployment控制器对象</h4><p>Deployment是一个定义多副本应用（即多个副本 Pod）的对象，Deployment 还负责在 Pod 定义发生变化时，对每个副本进行滚动更新（Rolling Update）。</p><p>使用一种 API 对象（Deployment）管理另一种 API 对象（Pod）的方法，在 Kubernetes 中，叫作“控制器”模式（controller pattern）。Deployment 扮演的是 Pod 的控制器的角色。</p><p>对于统一进行 Kubernetes 对象的创建和更新操作，推荐使用 kubectl apply 命令。这样的操作方法，是 Kubernetes“声明式 API”所推荐的使用方法。也就是说，作为用户，你不必关心当前的操作是创建，还是更新，你执行的命令始终是 kubectl apply，而 Kubernetes 则会根据 YAML 文件的内容变化，自动进行具体的处理。</p><p>通过容器镜像，能够保证应用本身在开发与部署环境里的一致性的话，Kubernetes 项目通过这些 YAML 文件，就保证了应用的“部署参数”在开发与部署环境中的一致性。</p><p><strong>而当应用本身发生变化时，开发人员和运维人员可以依靠容器镜像来进行同步；当应用部署参数发生变化时，这些 YAML 文件就是他们相互沟通和信任的媒介。</strong></p><p>以上，是 Kubernetes 发布应用的最基本操作。</p><p>Deployment对控制器模型的实现：</p><ol><li>Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod，然后统计它们的数量，这就是实际状态；</li><li>Deployment 对象的 Replicas 字段的值就是期望状态；</li><li>Deployment 控制器将两个状态做比较，然后根据比较结果，确定是创建 Pod，还是删除已有的 Pod。</li></ol><p>可以看到，一个 Kubernetes 对象的主要编排逻辑，实际上是在第三步的“对比”阶段完成的。</p><p>这个操作，通常被叫作调谐（Reconcile）。这个调谐的过程，则被称作“Reconcile Loop”（调谐循环）或者“Sync Loop”（同步循环）。而调谐的最终结果，往往都是对被控制对象的某种写操作。</p><h5 id="作业副本与水平扩展"><a href="#作业副本与水平扩展" class="headerlink" title="作业副本与水平扩展"></a>作业副本与水平扩展</h5><p>Deployment 看似简单，但实际上，它实现了 Kubernetes 项目中一个非常重要的功能：Pod 的“水平扩展 &#x2F; 收缩”（horizontal scaling out&#x2F;in）。这个功能，是从 PaaS 时代开始，一个平台级项目就必须具备的编排能力。</p><p>这个功能依赖的是 Kubernetes 项目中的一个非常重要的概念（API 对象）：ReplicaSet。</p><p><strong>一个 ReplicaSet 对象，其实就是由副本数目的定义和一个 Pod 模板组成的</strong>。</p><p>下图展示了一个定义了 replicas&#x3D;3 的 Deployment，与它的 ReplicaSet，以及 Pod 的关系，实际上是一种“层层控制”的关系。</p><p><img src="/assets/img/Kubernetes/Deployment.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Deployment.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>其中，ReplicaSet 负责通过“控制器模式”，保证系统中 Pod 的个数永远等于指定的个数（比如，3 个）。这也正是 Deployment 只允许容器的 restartPolicy&#x3D;Always 的主要原因：只有在容器能保证自己始终是 Running 状态的前提下，ReplicaSet 调整 Pod 的个数才有意义。</p><p>在此基础上，Deployment 同样通过“控制器模式”，来操作 ReplicaSet 的个数和属性，进而实现“水平扩展 &#x2F; 收缩”和“滚动更新”这两个编排动作。</p><p>其中，“水平扩展 &#x2F; 收缩”非常容易实现，Deployment Controller 只需要修改它所控制的 ReplicaSet 的 Pod 副本个数就可以了。</p><p>滚动更新可以描述为：<strong>将一个集群中正在运行的多个 Pod 版本，交替地逐一升级的过程。</strong></p><p>现在我们可以扩展一下 Deployment、ReplicaSet 和 Pod 的关系图：</p><p><img src="/assets/img/Kubernetes/Deployment-Pod.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Deployment-Pod.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Deployment 的控制器，实际上控制的是 ReplicaSet 的数目，以及每个 ReplicaSet 的属性。一个应用的版本，对应的正是一个 ReplicaSet；这个版本应用的 Pod 数量，则由 ReplicaSet 通过它自己的控制器（ReplicaSet Controller）来保证。通过这样的多个 ReplicaSet 对象，Kubernetes 项目就实现了对多个“应用版本”的描述。</p><p>Deployment 实际上是一个<strong>两层控制器</strong>。首先，它通过<strong>ReplicaSet 的个数</strong>来描述应用的版本；然后，它再通过<strong>ReplicaSet 的属性</strong>（比如 replicas 的值），来保证 Pod 的副本数量。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;center style=&quot;font-size:24px;font-weight:900;&quot;&gt;编排&lt;/center&gt;

&lt;p&gt;Kubernetes 操作Pod的逻辑，都由控制器（Controller）完成&lt;/p&gt;
&lt;h4 id=&quot;Deployment控制器对象&quot;&gt;&lt;a href</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>Pod</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/17/Kubernetes-3-Pod/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/17/Kubernetes-3-Pod/</id>
    <published>2025-11-17T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<center style="font-size:24px;font-weight:900;">Pod</center><p>Pod，是 Kubernetes 项目中最小的 API 对象。换一个更专业的说法，可以这样描述：Pod，是 Kubernetes 项目的原子调度单位。</p><p>容器，是云计算系统中的进程；容器镜像就是这个系统里的“.exe”安装包，而Kubernetes 是操作系统。在一个真正的操作系统里面，进程是以进程组的方式阻止在一起运行。在进程的树状图中，每个进程后面括号中的数字就是它的进程组ID（Process Group ID, PGID）。 Kubernetes所做的，就是将“进程组”的概念映射到了容器技术中，并使其成为了这个云计算“操作系统”里的“一等公民”。</p><p>Pod在 Kubernetes 项目里重要的意义：<strong>容器设计模式</strong>；Pod只是一个逻辑概念，Kubernetes 真正处理的，还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups，而并不存在一个所谓的 Pod 的边界或者隔离环境。</p><p>可以说Pod是一组共享了某些资源的容器。<strong>Pod 里的所有容器，共享的是同一个 Network Namespace，并且可以声明共享同一个 Volume。</strong></p><h4 id="Kubernetes-中的Pod"><a href="#Kubernetes-中的Pod" class="headerlink" title="Kubernetes 中的Pod"></a>Kubernetes 中的Pod</h4><p>在 Kubernetes 中，Volume 是属于 Pod 对象的一部分。在YAML的 template.spec 字段下。 Pod 中的容器，使用的是 volumeMounts 字段来声明自己要挂载哪个 Volume，并通过 mountPath 字段来定义容器内的 Volume 目录</p><p>在 Kubernetes 项目里，Pod 的实现需要使用一个中间容器，这个容器叫作 Infra 容器。Pod 中，Infra 容器永远都是第一个被创建的容器，而其他用户定义的容器，则通过 Join Network Namespace 的方式，与 Infra 容器关联在一起。如图所示Pod 里有两个用户容器 A 和 B，还有一个 Infra 容器。</p><p><img src="/assets/img/Kubernetes/pod.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/pod.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>在 Kubernetes 项目里，Infra 容器一定要占用极少的资源，所以它使用的是一个非常特殊的镜像，叫作：<code>k8s.gcr.io/pause</code>。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器，解压后的大小也只有 100~200 KB 左右。</p><p>对于 Pod 里的容器 A 和容器 B 来说：</p><ul><li>它们可以直接使用 localhost 进行通信；</li><li>它们看到的网络设备跟 Infra 容器看到的完全一样；</li><li>一个 Pod 只有一个 IP 地址，也就是这个 Pod 的 Network Namespace 对应的 IP 地址；</li><li>当然，其他的所有网络资源，都是一个 Pod 一份，并且被该 Pod 中的所有容器共享；</li><li>Pod 的生命周期只跟 Infra 容器一致，而与容器 A 和 B 无关。</li></ul><p>而对于同一个 Pod 里面的所有用户容器来说，它们的进出流量，也可以认为都是通过 Infra 容器完成的。这一点很重要，因为<strong>将来如果你要为 Kubernetes 开发一个网络插件时，应该重点考虑的是如何配置这个 Pod 的 Network Namespace，而不是每一个用户容器如何使用你的网络配置，这是没有意义的。</strong></p><p>而对于共享Volume ：Kubernetes 项目只要把所有 Volume 的定义都设计在 Pod 层级即可。这样，一个 Volume 对应的宿主机目录对于 Pod 来说就只有一个，Pod 里的容器只要声明挂载这个 Volume，就一定可以共享这个 Volume 对应的宿主机目录</p><p>Pod 这种容器的设计思想，实际上就是希望，当用户想在一个容器里跑多个功能并不相关的应用时，应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。</p><p>在 Pod 中，所有 Init Container 定义的容器，都会比 spec.containers 定义的用户容器先启动。并且，Init Container 容器会按顺序逐一启动，而直到它们都启动并且退出了，用户容器才会启动。</p><h4 id="Pod的基本概念"><a href="#Pod的基本概念" class="headerlink" title="Pod的基本概念"></a>Pod的基本概念</h4><p>Pod是是 Kubernetes 项目中的最小编排单位。将这个设计落实到 API 对象上，容器（Container）就成了 Pod 属性里的一个普通的字段。</p><p>凡是描述的是“机器”这个整体，比如配置“机器”的网卡（pod网络定义），比如配置“机器”的磁盘（pod存储定义），配置“机器”防火墙（Pod 安全定义），这些属性都属于 Pod 对象，也就是说<strong>凡是调度、网络、存储，以及安全相关的属性，基本上是 Pod 级别的。</strong></p><p>例如：</p><ul><li>**NodeSelector：**是一个供用户将 Pod 与 Node 进行绑定的字段</li><li>**NodeName：**一旦 Pod 的这个字段被赋值，Kubernetes 项目就会被认为这个 Pod 已经经过了调度，调度的结果就是赋值的节点名字。</li><li>**HostAliases：**定义了 Pod 的 hosts 文件（比如 &#x2F;etc&#x2F;hosts）里的内容</li></ul><blockquote><p><strong>凡是跟容器的 Linux Namespace 相关的属性，也一定是 Pod 级别的</strong>。</p><p><strong>凡是 Pod 中的容器要共享宿主机的 Namespace，也一定是 Pod 级别的定义</strong></p></blockquote><h5 id="Containers"><a href="#Containers" class="headerlink" title="Containers"></a>Containers</h5><p>Kubernetes 项目中对 Container 的定义，和 Docker 相比并没有什么太大区别。 Image（镜像）、Command（启动命令）、workingDir（容器的工作目录）、Ports（容器要开发的端口），以及 volumeMounts（容器要挂载的 Volume）都是构成 Kubernetes 项目中 Container 的主要字段。</p><p>有几个字段需要关注一下：</p><ul><li>**ImagePullPolicy：**定义了镜像拉取的策略。默认值是 Always，即每次创建 Pod 都重新拉取一次镜像。值被定义为 Never 或者 IfNotPresent时，则意味着 Pod 永远不会主动拉取这个镜像，或者只在宿主机上不存在这个镜像时才拉取。</li><li>**Lifecycle ：**定义的是 Container Lifecycle Hooks。在容器状态发生变化时触发一系列“钩子”。</li></ul><h5 id="Pod-对象在-Kubernetes-中的生命周期"><a href="#Pod-对象在-Kubernetes-中的生命周期" class="headerlink" title="Pod 对象在 Kubernetes 中的生命周期"></a>Pod 对象在 Kubernetes 中的生命周期</h5><p>Pod 生命周期的变化，主要体现在 Pod API 对象的<strong>Status 部分</strong>，这是它除了 Metadata 和 Spec 之外的第三个重要字段。其中，pod.status.phase，就是 Pod 的当前状态，它有如下几种可能的情况：</p><ol><li>Pending。这个状态意味着，Pod 的 YAML 文件已经提交给了 Kubernetes，API 对象已经被创建并保存在 Etcd 当中。但是，这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如，调度不成功。</li><li>Running。这个状态下，Pod 已经调度成功，跟一个具体的节点绑定。它包含的容器都已经创建成功，并且至少有一个正在运行中。</li><li>Succeeded。这个状态意味着，Pod 里的所有容器都正常运行完毕，并且已经退出了。这种情况在运行一次性任务时最为常见。</li><li>Failed。这个状态下，Pod 里至少有一个容器以不正常的状态（非 0 的返回码）退出。这个状态的出现，意味着你得想办法 Debug 这个容器的应用，比如查看 Pod 的 Events 和日志。</li><li>Unknown。这是一个异常状态，意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver，这很有可能是主从节点（Master 和 Kubelet）间的通信出现了问题。</li></ol><h5 id="Projected-Volume（投射数据卷）"><a href="#Projected-Volume（投射数据卷）" class="headerlink" title="Projected Volume（投射数据卷）"></a>Projected Volume（投射数据卷）</h5><p>在 Kubernetes 中，有几种特殊的 Volume，它们存在的意义不是为了存放容器里的数据，也不是用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用，是为容器提供预先定义好的数据。所以，从容器的角度来看，这些 Volume 里的信息就是仿佛是<strong>被 Kubernetes“投射”（Project）进入容器当中的</strong>。这正是 Projected Volume 的含义。</p><p>我使用的Kubernetes版本为1.34.1,当前版本支持的Projected Volume 有以下几种：</p><ul><li><a href="https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#secret"><code>secret</code></a></li><li><a href="https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#downwardapi"><code>downwardAPI</code></a></li><li><a href="https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#configmap"><code>configMap</code></a></li><li><a href="https://kubernetes.io/zh-cn/docs/concepts/storage/projected-volumes/#serviceaccounttoken"><code>serviceAccountToken</code></a></li><li><a href="https://kubernetes.io/zh-cn/docs/concepts/storage/projected-volumes/#clustertrustbundle"><code>clusterTrustBundle</code></a></li><li><a href="https://kubernetes.io/zh-cn/docs/concepts/storage/projected-volumes/#podcertificate"><code>podCertificate</code></a></li></ul><p>[<code>secret</code> ][<a href="https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#secret]%E5%8D%B7%E7%94%A8%E6%9D%A5%E7%BB%99">https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#secret]卷用来给</a> Pod 传递敏感信息，例如密码。你可以将 Secret 存储在 Kubernetes API 服务器上，然后以文件的形式挂载到 Pod 中，无需直接与 Kubernetes 耦合。 <code>secret</code> 卷由 tmpfs（基于 RAM 的文件系统）提供存储，因此它们永远不会被写入非易失性（持久化的）存储器。</p><p><a href="https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/configure-pod-configmap/"><code>configMap</code></a> 卷提供了向 Pod 注入配置数据的方法。 ConfigMap 对象中存储的数据可以被 <code>configMap</code> 类型的卷引用，然后被 Pod 中运行的容器化应用使用。</p><p>[<code>downwardAPI</code>][<a href="https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#downwardapi]">https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/#downwardapi]</a> 卷用于为应用提供 <a href="https://kubernetes.io/zh-cn/docs/concepts/workloads/pods/downward-api/">downward API</a> 数据。 在这类卷中，所公开的数据以纯文本格式的只读文件形式存在。</p><p>而作为用户，可以通过设置 restartPolicy，改变 Pod 的恢复策略。除了 Always，它还有 OnFailure 和 Never 两种情况：</p><ul><li>Always：在任何情况下，只要容器不在运行状态，就自动重启容器；</li><li>OnFailure: 只在容器 异常时才自动重启容器；</li><li>Never: 从来不重启容器。</li></ul><p>在实际使用时，需要根据应用运行的特性，合理设置这三种恢复策略。</p><p>estartPolicy 和 Pod 里容器的状态，以及 Pod 状态的对应关系可以描述为：</p><ol><li><strong>只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器（比如：Always），那么这个 Pod 就会保持 Running 状态，并进行容器重启</strong>。否则，Pod 就会进入 Failed 状态 。</li><li><strong>对于包含多个容器的 Pod，只有它里面所有的容器都进入异常状态后，Pod 才会进入 Failed 状态</strong>。在此之前，Pod 都是 Running 状态。此时，Pod 的 READY 字段会显示正常容器的个数。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;center style=&quot;font-size:24px;font-weight:900;&quot;&gt;Pod&lt;/center&gt;

&lt;p&gt;Pod，是 Kubernetes 项目中最小的 API 对象。换一个更专业的说法，可以这样描述：Pod，是 Kubernetes 项目的原子调度单位。</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>历史</title>
    <link href="https://github.com/setking/setking.github.io/2025/11/16/Kubernetes-2-%E5%8E%86%E5%8F%B2/"/>
    <id>https://github.com/setking/setking.github.io/2025/11/16/Kubernetes-2-%E5%8E%86%E5%8F%B2/</id>
    <published>2025-11-16T03:33:00.000Z</published>
    <updated>2026-03-10T09:35:58.382Z</updated>
    
    <content type="html"><![CDATA[<center style="font-size:24px;font-weight:900;">初识</center><p><strong>Kubernetes 项目从一开始就比较幸运地站上了一个他人难以企及的高度</strong>：在它的成长阶段，这个项目每一个核心特性的提出，几乎都脱胎于 Borg&#x2F;Omega 系统的设计与经验。更重要的是，这些特性在开源社区落地的过程中，又在整个社区的合力之下得到了极大的改进，修复了很多当年遗留在 Borg 体系中的缺陷和问题。</p><p>而Borg 系统，一直以来都被誉为 Google 公司内部最强大的“秘密武器”。Borg 要承担的责任，是承载 Google 公司整个基础设施的核心依赖。在 Google 公司已经公开发表的基础设施体系论文中，Borg 项目当仁不让地位居整个基础设施技术栈的最底层。</p><p>下图来源于Google Omega 论文的第一作者的博士毕业论文。它描绘了当时 Google 已经公开发表的整个基础设施栈：</p><p><img src="/assets/img/Kubernetes/google-omega.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/google-omega.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>在这个图里，可以找到 MapReduce、BigTable 等知名项目，也能看到 Borg 和它的继任者 Omega 位于整个技术栈的最底层。Kubernetes 项目在 Borg 体系的指导下，体现出了一种独有的“先进性”与“完备性”，而这些特质才是一个基础设施领域开源项目赖以生存的核心价值。</p><p>下图是Kubernetes架构图：</p><p><img src="/assets/img/Kubernetes/Kubernetes.png" class="lazyload placeholder" data-srcset="/assets/img/Kubernetes/Kubernetes.png" srcset="https://pic1.zhimg.com/v2-cd38920285d125be80b3eb504052c550_b.webp"></p><p>Kubernetes 项目的架构，跟它的原型项目 Borg 非常类似，都由 Master 和 Node 两种节点组成，而这两种角色分别对应着控制节点和计算节点。其中，控制节点，即 Master 节点，由三个紧密协作的独立组件组合而成，它们分别是负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler，以及负责容器编排的 kube-controller-manager。整个集群的持久化数据，则由 kube-apiserver 处理后保存在 Ectd 中。</p><p>而计算节点上最核心的部分，则是一个叫作 kubelet 的组件。</p><p><strong>在 Kubernetes 项目中，kubelet 主要负责同容器运行时（比如 Docker 项目）打交道</strong>。而这个交互所依赖的，是一个称作 CRI（Container Runtime Interface）的远程调用接口，这个接口定义了容器运行时的各项核心操作，比如：启动一个容器需要的所有参数。</p><p>Kubernetes 项目并不关心你部署的是什么容器运行时、使用的什么技术实现，只要你的这个容器运行时能够运行标准的容器镜像，它就可以通过实现 CRI 接入到 Kubernetes 项目当中。</p><p><strong>kubelet 还通过 gRPC 协议同一个叫作 Device Plugin 的插件进行交互</strong>。这个插件，是 Kubernetes 项目用来管理 GPU 等宿主机物理设备的主要组件，也是基于 Kubernetes 项目进行机器学习训练、高性能作业支持等工作必须关注的功能。</p><p><strong>kubelet 的另一个重要功能，则是调用网络插件和存储插件为容器配置网络和持久化存储</strong>。这两个插件与 kubelet 进行交互的接口，分别是 CNI（Container Networking Interface）和 CSI（Container Storage Interface）。</p><p>而 Kubernetes 项目要着重解决的问题是：<strong>运行在大规模集群中的各种任务之间，实际上存在着各种各样的关系。这些关系的处理，才是作业编排和管理系统最困难的地方。Kubernetes 项目最主要的设计思想是，从更宏观的角度，以统一的方式来定义任务之间的各种关系，并且为将来支持更多种类的关系留有余地。</strong></p><p>比如，Kubernetes 项目对容器间的“访问”进行了分类，首先总结出了一类非常常见的“紧密交互”的关系，即：这些应用之间需要非常频繁的交互和访问；又或者，它们会直接通过本地文件进行信息交换。</p><p>在常规环境下，这些应用往往会被直接部署在同一台机器上，通过 Localhost 通信，通过本地磁盘目录交换文件。而在 Kubernetes 项目中，这些容器则会被划分为一个“Pod”，Pod 里的容器共享同一个 Network Namespace、同一组数据卷，从而达到高效率交换信息的目的。</p><p>Pod 是 Kubernetes 项目中最基础的一个对象，源自于 Google Borg 论文中一个名叫 Alloc 的设计。</p><p>对于例如Web 应用与数据库之间的访问关系，Kubernetes 项目则提供了一种叫作“Service”的服务。像这样的两个应用，往往故意不部署在同一台机器上，这样即使 Web 应用所在的机器宕机了，数据库也完全不受影响。Kubernetes 项目给 Pod 绑定一个 Service 服务，而 Service 服务声明的 IP 地址等信息是“终生不变”的。这个<strong>Service 服务的主要作用，就是作为 Pod 的代理入口（Portal），从而代替 Pod 对外暴露一个固定的网络地址</strong>。</p><p>这样，对于 Web 应用的 Pod 来说，它需要关心的就是数据库 Pod 的 Service 信息。不难想象，Service 后端真正代理的 Pod 的 IP 地址、端口等信息的自动更新、维护，则是 Kubernetes 项目的职责。</p><p>Kubernetes 项目提供了一种叫作 Secret 的对象，它是一个保存在 Etcd 里的键值对数据。把 Credential 信息以 Secret 的方式存在 Etcd 里，Kubernetes 就会在你指定的 Pod（比如，Web 应用的 Pod）启动时，自动把 Secret 里的数据以 Volume 的方式挂载到容器里。这样， Web 应用就可以访问数据库了。</p><p><strong>除了应用与应用之间的关系外，应用运行的形态是影响“如何容器化这个应用”的第二个重要因素。</strong></p><p>Kubernetes 定义了新的、基于 Pod 改进后的对象。比如 Job，用来描述一次性运行的 Pod（比如，大数据任务）；再比如 DaemonSet，用来描述每个宿主机上必须且只能运行一个副本的守护进程服务；又比如 CronJob，则用于描述定时任务等等。</p><p>在 Kubernetes 项目中，推崇的使用方法是：</p><ul><li>首先，通过一个“编排对象”，比如 Pod、Job、CronJob 等，来描述你试图管理的应用；</li><li>然后，再为它定义一些“服务对象”，比如 Service、Secret、Horizontal Pod Autoscaler（自动水平扩展器）等。这些对象，会负责具体的平台级功能。</li></ul><p>**这种使用方法，就是所谓的“声明式 API”。这种 API 对应的“编排对象”和“服务对象”，都是 Kubernetes 项目中的 API 对象（API Object）。**这就是 Kubernetes 最核心的设计理念</p>]]></content>
    
    
      
      
    <summary type="html">&lt;center style=&quot;font-size:24px;font-weight:900;&quot;&gt;初识&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes 项目从一开始就比较幸运地站上了一个他人难以企及的高度&lt;/strong&gt;：在它的成长阶段，这个项目每一个核心特性的提</summary>
      
    
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="学习" scheme="https://github.com/setking/setking.github.io/tags/%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
</feed>
