<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>XcentIot's Wiki Blog</title>
        <link>https://tech.xcentiot.com/blog</link>
        <description>XcentIot's Wiki Blog</description>
        <lastBuildDate>Thu, 13 Mar 2025 14:32:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[上下文被取消,谁是罪魁祸首？]]></title>
            <link>https://tech.xcentiot.com/blog/gin</link>
            <guid>https://tech.xcentiot.com/blog/gin</guid>
            <pubDate>Thu, 13 Mar 2025 14:32:38 GMT</pubDate>
            <description><![CDATA[context canceled 谁是罪魁祸首？一次很有意义的问题定位]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>context canceled 谁是罪魁祸首？一次很有意义的问题定位</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="名词解释">名词解释<a href="https://tech.xcentiot.com/blog/gin#%E5%90%8D%E8%AF%8D%E8%A7%A3%E9%87%8A" class="hash-link" aria-label="Direct link to 名词解释" title="Direct link to 名词解释">​</a></h2>
<ul>
<li>context canceled：上下文取消</li>
<li>context：上下文</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="背景">背景<a href="https://tech.xcentiot.com/blog/gin#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="Direct link to 背景" title="Direct link to 背景">​</a></h2>
<p>gitea是一个开源的git服务，我们使用 gitea sdk 的时候，复用 gitea client 时，当服务第一次启动的时候，请求client 1 正常运行，
但是如果请求了client 2 ，就会报 context canceled 错误，由于是第一次遇到这个 context canceled 错误，因此我们开始了调查，
最后发现是因为 client 2 调用了sync.RWMutex Lock，并且强制替换了ctx 最终导致 context canceled 错误。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="定位开始">定位开始<a href="https://tech.xcentiot.com/blog/gin#%E5%AE%9A%E4%BD%8D%E5%BC%80%E5%A7%8B" class="hash-link" aria-label="Direct link to 定位开始" title="Direct link to 定位开始">​</a></h2>
<p>一开始我们怀疑是 gitea 的问题，但是其他相同的接口可以正常调用, 然后我们有怀疑是 gitea sdk 的问题，但是我们通过curl 调用 gitea
接口正常， 通过日志发现这个问题，在2月初都是正常的，但是过年后就出现了，必然是我们提交的代码导致的，因此我们继续向上排查，
看下是不是message-sever调用方式的问题，因为前端访问的接口就可以正常进行，因此我们把内部接口放开，通过api调用，系统重启后，
通过apidoc 调用接口，发现接口可以正常调用，但是一旦前端调用了client 2 ，就会报 context canceled 错误。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="定位遭遇战">定位遭遇战<a href="https://tech.xcentiot.com/blog/gin#%E5%AE%9A%E4%BD%8D%E9%81%AD%E9%81%87%E6%88%98" class="hash-link" aria-label="Direct link to 定位遭遇战" title="Direct link to 定位遭遇战">​</a></h2>
<p>通过上面的定位，我们初步把问题放到了git-access代码仓，因为git-access里面存在context.withTimeout的代码，因此我们怀疑是不是
context.withTimeout的代码超时导致的。</p>
<p>因此我去查看了网站的代码，并且试图从http源码中正向定位找到问题，但是没有找到问题的根源。</p>
<p>参考 <a href="https://learnku.com/articles/63884" target="_blank" rel="noopener noreferrer">https://learnku.com/articles/63884</a> 得到我们这篇文章，大概了解context canceled 的原因。</p>
<ol>
<li>withTimeout 会创建一个新的context，并且设置一个定时任务，会在timeout时间后，发送一个取消context的信号。</li>
<li>context 主动canceled 会触发所有的子context 取消。</li>
</ol>
<p>但是和我们的问题都不一致，因此我们尝试从问题日志去倒推。</p>
<p>不过我们日志仅仅保存30天，导致得到了30天之前的ok的错误结论。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="定位启发">定位启发<a href="https://tech.xcentiot.com/blog/gin#%E5%AE%9A%E4%BD%8D%E5%90%AF%E5%8F%91" class="hash-link" aria-label="Direct link to 定位启发" title="Direct link to 定位启发">​</a></h2>
<ol>
<li>日志的重要性，日志是我们定位问题的重要依据。</li>
<li>定位问题先正向分析，如果正向分析没有找到问题的根源，就可以尝试从问题日志去倒推。</li>
<li>通过对现象的再次分析，发现了一定client2被调用就会有问题，于是把火力集中在client2上。</li>
<li>在对client2调用的的代码中，发现了sync.RWMutex Lock，认为是lock的原因。</li>
<li>经过讨论分析，问题的本质是全局context 对象被强制替换了。</li>
</ol>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="追溯">追溯<a href="https://tech.xcentiot.com/blog/gin#%E8%BF%BD%E6%BA%AF" class="hash-link" aria-label="Direct link to 追溯" title="Direct link to 追溯">​</a></h2>
<p>为啥会有强制替换这个问题的原因呢？</p>
<p>因为在开发链路跟踪otelhttp的时候，需要把ctx 传递到下游，不然无法进行链路跟踪。</p>
<p>但是我们这里的ctx又是在init初始的时候创建的并且没有可以初始化的方法。</p>
<p>所有之前就把这个 context 强制替换了。</p>
<p>但是一定其他地方使用到之前的context的话，就会出现这个问题。</p>]]></content:encoded>
            <category>Gin</category>
        </item>
        <item>
            <title><![CDATA[后台自动提交代码任务]]></title>
            <link>https://tech.xcentiot.com/blog/golang</link>
            <guid>https://tech.xcentiot.com/blog/golang</guid>
            <pubDate>Sat, 22 Feb 2025 03:29:39 GMT</pubDate>
            <description><![CDATA[后台自动提交代码功能]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>后台自动提交代码功能</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="背景">背景<a href="https://tech.xcentiot.com/blog/golang#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="Direct link to 背景" title="Direct link to 背景">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="why">why?<a href="https://tech.xcentiot.com/blog/golang#why" class="hash-link" aria-label="Direct link to why?" title="Direct link to why?">​</a></h3>
<p>忘记打卡了，需要的凭证就是提交代码才有效，于是有了这个需求，希望自动定时每天8点50提交一份代码。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="需求分析">需求分析<a href="https://tech.xcentiot.com/blog/golang#%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90" class="hash-link" aria-label="Direct link to 需求分析" title="Direct link to 需求分析">​</a></h2>
<ol>
<li>写一个程序可以提交代码并且push</li>
<li>定时执行</li>
<li>后台启动</li>
</ol>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="具体实现">具体实现<a href="https://tech.xcentiot.com/blog/golang#%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 具体实现" title="Direct link to 具体实现">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="auto_commitgo">auto_commit.go<a href="https://tech.xcentiot.com/blog/golang#auto_commitgo" class="hash-link" aria-label="Direct link to auto_commit.go" title="Direct link to auto_commit.go">​</a></h3>
<div class="language-golang codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-golang codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">package main</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">import (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "fmt"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "os"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "os/exec"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "time"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func updateReadme() error {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 切换到 gwx_test 分支</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "checkout", "gwx_test").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("切换分支失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 追加内容到 hello.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> timestamp := time.Now().Format("2006-01-02 15:04:05")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> content := fmt.Sprintf("\nAuto update at %s", timestamp)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> f, err := os.OpenFile("hello.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("打开文件失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> defer f.Close()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if _, err := f.WriteString(content); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("写入文件失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 添加更改</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "add", ".").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("git add 失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 提交更改</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "commit", "-m", "Auto update time").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("git commit 失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 推送到远程</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "push", "upstream", "gwx_test").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("git push 失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> fmt.Printf("成功更新 at %s\n", timestamp)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> return nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func main() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> fmt.Println("自动提交任务已启动...")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 添加 recover 机制</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> defer func() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  if r := recover(); r != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   fmt.Printf("程序发生严重错误: %v\n", r)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   // 重新启动主循环</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   main()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> for {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  now := time.Now()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  next := time.Date(now.Year(), now.Month(), now.Day(), 8, 50, 0, 0, now.Location())</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  if now.After(next) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   next = next.Add(24 * time.Hour)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  timer := time.NewTimer(next.Sub(now))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fmt.Printf("等待下一次执行，将在 %s 运行\n", next.Format("2006-01-02 15:04:05"))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  &lt;-timer.C</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  // 使用 recover 包装任务执行</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  func() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   defer func() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if r := recover(); r != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">     fmt.Printf("任务执行出错: %v\n", r)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   }()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   if err := updateReadme(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fmt.Printf("执行出错: %v\n", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="编译为可执行文件">编译为可执行文件<a href="https://tech.xcentiot.com/blog/golang#%E7%BC%96%E8%AF%91%E4%B8%BA%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6" class="hash-link" aria-label="Direct link to 编译为可执行文件" title="Direct link to 编译为可执行文件">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">go build -o auto_commit.exe auto_commit.go</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="后台运行">后台运行<a href="https://tech.xcentiot.com/blog/golang#%E5%90%8E%E5%8F%B0%E8%BF%90%E8%A1%8C" class="hash-link" aria-label="Direct link to 后台运行" title="Direct link to 后台运行">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">@echo off</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">echo 开始执行自动提交任务 %date% %time% &gt;&gt; auto_commit_log.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start /b auto_commit.exe</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">echo 任务已启动 %date% %time% &gt;&gt; auto_commit_log.txt</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="遇到的问题">遇到的问题<a href="https://tech.xcentiot.com/blog/golang#%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98" class="hash-link" aria-label="Direct link to 遇到的问题" title="Direct link to 遇到的问题">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="没有使用定时触发器导致cpu卡顿">没有使用定时触发器，导致cpu卡顿<a href="https://tech.xcentiot.com/blog/golang#%E6%B2%A1%E6%9C%89%E4%BD%BF%E7%94%A8%E5%AE%9A%E6%97%B6%E8%A7%A6%E5%8F%91%E5%99%A8%E5%AF%BC%E8%87%B4cpu%E5%8D%A1%E9%A1%BF" class="hash-link" aria-label="Direct link to 没有使用定时触发器，导致cpu卡顿" title="Direct link to 没有使用定时触发器，导致cpu卡顿">​</a></h3>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">1. 使用 time.NewTimer 创建定时器</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2. 使用 &lt;-timer.C 等待定时器触发，而不是持续循环</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3. 移除了 time.Sleep</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- CPU 使用率会大幅降低</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- 程序仍然会在指定时间执行任务</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- 定时器触发前程序会处于休眠状态</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- 系统资源占用更少使用 time.Timer 来替代持续循环</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>]]></content:encoded>
            <category>Golang</category>
        </item>
        <item>
            <title><![CDATA[深入浅出 K8s 设备插件技术（Device Plugin）]]></title>
            <link>https://tech.xcentiot.com/blog/k8s</link>
            <guid>https://tech.xcentiot.com/blog/k8s</guid>
            <pubDate>Tue, 11 Feb 2025 01:41:50 GMT</pubDate>
            <description><![CDATA[Kubernetes 提供了 Device Plugin 机制，用于向 kubelet 上报硬件信息并配置容器资源。本文以 NVIDIA GPU Plugin 为例，通俗易懂 并 深入浅出 地剖析注册、ListAndWatch、Allocate 及 kubelet 管理流程，介绍常见问题和配置要点。]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Kubernetes 提供了 Device Plugin 机制，用于向 kubelet 上报硬件信息并配置容器资源。本文以 NVIDIA GPU Plugin 为例，通俗易懂 并 深入浅出 地剖析注册、ListAndWatch、Allocate 及 kubelet 管理流程，介绍常见问题和配置要点。</p>
</blockquote>
<p>先用一张原理概览图把 Device Plugin 和 kubelet 之间的交互勾勒出来，让大家感受下插件技术的整体架构（示例以 NVIDIA 插件为例）：</p>
<p><img decoding="async" loading="lazy" src="https://gitee.com/gongme/blog-image/raw/master/img/20250210165547.png" alt="device-plugin" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="概念对齐">概念对齐<a href="https://tech.xcentiot.com/blog/k8s#%E6%A6%82%E5%BF%B5%E5%AF%B9%E9%BD%90" class="hash-link" aria-label="Direct link to 概念对齐" title="Direct link to 概念对齐">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="kubelet集群每个节点资源的管家">kubelet：集群每个节点资源的“管家”<a href="https://tech.xcentiot.com/blog/k8s#kubelet%E9%9B%86%E7%BE%A4%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E8%B5%84%E6%BA%90%E7%9A%84%E7%AE%A1%E5%AE%B6" class="hash-link" aria-label="Direct link to kubelet：集群每个节点资源的“管家”" title="Direct link to kubelet：集群每个节点资源的“管家”">​</a></h3>
<p>Kubernetes 集群中每个节点上的“管家”，负责管理本节点的容器生命周期（如启动、停止容器），并与 Kubernetes 控制平面通信。Device Plugin 机制扩展了 kubelet 的能力，使其能够管理 GPU 等第三方硬件。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="kubelet-与-device-plugin-之间的通信sock-文件socket">kubelet 与 Device Plugin 之间的通信：sock 文件（Socket）<a href="https://tech.xcentiot.com/blog/k8s#kubelet-%E4%B8%8E-device-plugin-%E4%B9%8B%E9%97%B4%E7%9A%84%E9%80%9A%E4%BF%A1sock-%E6%96%87%E4%BB%B6socket" class="hash-link" aria-label="Direct link to kubelet 与 Device Plugin 之间的通信：sock 文件（Socket）" title="Direct link to kubelet 与 Device Plugin 之间的通信：sock 文件（Socket）">​</a></h3>
<p>在 Linux 中，常见的 “.sock 文件” 实际上是一种 Unix 域套接字（Unix Domain Socket）。它类似于网络通信中的 TCP/UDP Socket，但只在本机进程之间进行高效数据交换，不会走网络接口。 在本文中，你会看到 kubelet.sock、nvidia.sock 等文件路径，意味着 kubelet 与 Device Plugin 的所有请求和响应都是通过这些 Socket 文件来传递的。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="grpc远程过程调用remote-procedure-call框架">gRPC：远程过程调用（Remote Procedure Call）框架<a href="https://tech.xcentiot.com/blog/k8s#grpc%E8%BF%9C%E7%A8%8B%E8%BF%87%E7%A8%8B%E8%B0%83%E7%94%A8remote-procedure-call%E6%A1%86%E6%9E%B6" class="hash-link" aria-label="Direct link to gRPC：远程过程调用（Remote Procedure Call）框架" title="Direct link to gRPC：远程过程调用（Remote Procedure Call）框架">​</a></h3>
<p>当我们说 ListAndWatch、Allocate 等 “RPC 接口” 时，指的是依赖 gRPC 协议来调用。gRPC 让不同进程可以像调用本地函数一样，在 Socket 之间 发送请求、获取结果。 可以简单理解为：“kubelet” 会通知 “插件” 要求列出设备、分配设备，双方都要实现 gRPC 的“请求-响应”流程。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="daemonset在每个节点上运行一个副本">DaemonSet：在每个节点上运行一个副本<a href="https://tech.xcentiot.com/blog/k8s#daemonset%E5%9C%A8%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E4%B8%8A%E8%BF%90%E8%A1%8C%E4%B8%80%E4%B8%AA%E5%89%AF%E6%9C%AC" class="hash-link" aria-label="Direct link to DaemonSet：在每个节点上运行一个副本" title="Direct link to DaemonSet：在每个节点上运行一个副本">​</a></h3>
<p>这是 NVIDIA 官方提供的一套 C 语言库，封装了查询 GPU 信息、监控健康状态等方法。我们常用它获取 GPU UUID、温度、功耗等数据。 许多 NVIDIA GPU Device Plugin 都会依赖 NVML 来确定有哪些 GPU 可用、是否健康。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="container-runtime-与-nvidia_visible_devices">Container Runtime 与 NVIDIA_VISIBLE_DEVICES<a href="https://tech.xcentiot.com/blog/k8s#container-runtime-%E4%B8%8E-nvidia_visible_devices" class="hash-link" aria-label="Direct link to Container Runtime 与 NVIDIA_VISIBLE_DEVICES" title="Direct link to Container Runtime 与 NVIDIA_VISIBLE_DEVICES">​</a></h3>
<p>DaemonSet 是 Kubernetes 的一种高级调度机制，可以让某个 Pod 在 集群里每台节点 都有一份副本在运行。 之所以用 DaemonSet 来部署 GPU Device Plugin，是因为我们希望 每个节点上都要启动一个插件，在新增节点的同时自动启动新插件实例，帮助 kubelet 管理当前节点的 GPU。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="1-背景为什么需要-gpu-device-plugin">1. 背景：为什么需要 GPU Device Plugin<a href="https://tech.xcentiot.com/blog/k8s#1-%E8%83%8C%E6%99%AF%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-gpu-device-plugin" class="hash-link" aria-label="Direct link to 1. 背景：为什么需要 GPU Device Plugin" title="Direct link to 1. 背景：为什么需要 GPU Device Plugin">​</a></h2>
<p>在 Kubernetes 中，如果我们想让某些容器使用 GPU（或 FPGA、NIC 等特殊硬件），就需要想办法让 kubelet “认识” 这些硬件设备，并且在调度和容器启动时正确地让容器访问到设备。</p>
<ol>
<li>
<p>传统方法：在 Kubernetes 1.8 之前，往往需要修改 kubelet 源码来适配具体厂商的硬件，这样一来就会导致：</p>
<ul>
<li>Kubernetes 需要内置太多厂商的硬件支持，难以维护；</li>
<li>新硬件出来后，想接入 Kubernetes 就必须修改 kubelet，周期长不灵活。</li>
</ul>
</li>
<li>
<p>Device Plugin 机制：从 Kubernetes 1.8 开始，官方提供了可插拔的 Device Plugin 框架。厂商可以自己实现一个 Device Plugin —— 一个自定义的守护进程（daemon），通过 gRPC 与 kubelet 通信，把硬件信息“上报”给 kubelet。kubelet 不必自带厂商逻辑，只管和这个 Device Plugin 标准对接即可。</p>
</li>
</ol>
<p>因此，NVIDIA 的 GPU Device Plugin（即 nvidia/k8s-device-plugin）就是这么一个“外部插件”。它以 DaemonSet 的方式部署到集群每个节点上，让节点上的 GPU 被 kubelet 发现、上报给 Kubernetes。接下来用户只需要在 Pod spec 里声明 resources.limits.nvidia.com/gpu: 1 就可以使用对应节点上的 GPU 了。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="2-device-plugin-的核心流程概览">2. Device Plugin 的核心流程概览<a href="https://tech.xcentiot.com/blog/k8s#2-device-plugin-%E7%9A%84%E6%A0%B8%E5%BF%83%E6%B5%81%E7%A8%8B%E6%A6%82%E8%A7%88" class="hash-link" aria-label="Direct link to 2. Device Plugin 的核心流程概览" title="Direct link to 2. Device Plugin 的核心流程概览">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="21-整体流程">2.1 整体流程<a href="https://tech.xcentiot.com/blog/k8s#21-%E6%95%B4%E4%BD%93%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 2.1 整体流程" title="Direct link to 2.1 整体流程">​</a></h3>
<ol>
<li>kubelet 启动时，会在 /var/lib/kubelet/device-plugins/kubelet.sock 上监听 gRPC 服务，用于 “接受插件的注册”；</li>
<li>NVIDIA Device Plugin 启动时，会跟 kubelet 的 kubelet.sock 进行 gRPC 注册（Register()）；</li>
<li>注册成功后，kubelet 就会通过 Device Plugin 内部自己开的 socket（比如 /var/lib/kubelet/device-plugins/nvidia.sock），去调用 ListAndWatch 了解 GPU 列表，并在有容器需要 GPU 时调用 Allocate 协商如何设置容器环境变量、挂载、等信息。</li>
</ol>
<p>对照代码，这个流程最关键的函数有两类：</p>
<ul>
<li>Device Plugin 端：Register(...)（把自己注册给 kubelet）、ListAndWatch(...)（向 kubelet 报告设备列表）、Allocate(...)（提供给 kubelet），等等。</li>
<li>kubelet 端：内部有一个 Device Manager，对应 ManagerImpl.Register(...)、ListAndWatch(...) 回调处理，最终将资源更新到 Node Status。</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="3-nvidia-device-plugin-的关键实现">3. NVIDIA Device Plugin 的关键实现<a href="https://tech.xcentiot.com/blog/k8s#3-nvidia-device-plugin-%E7%9A%84%E5%85%B3%E9%94%AE%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 3. NVIDIA Device Plugin 的关键实现" title="Direct link to 3. NVIDIA Device Plugin 的关键实现">​</a></h2>
<p>下面以 nvidia/k8s-device-plugin 为例，结合其 主要函数 以及 简化的核心代码 来看一下每一步是怎么做的。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="31-代码组织结构概览">3.1 代码组织结构概览<a href="https://tech.xcentiot.com/blog/k8s#31-%E4%BB%A3%E7%A0%81%E7%BB%84%E7%BB%87%E7%BB%93%E6%9E%84%E6%A6%82%E8%A7%88" class="hash-link" aria-label="Direct link to 3.1 代码组织结构概览" title="Direct link to 3.1 代码组织结构概览">​</a></h3>
<p>常见版本中，代码结构大概如下（只列一些主要文件）：</p>
<p>.
├── main.go                 # 插件入口
├── nvidia_device_plugin.go # 实现 DevicePlugin 接口的核心逻辑
├── nvml                    # NVIDIA 提供的NVML库封装，用于获取GPU信息
└── ...
main.go 大概就是初始化、启动一个 NvidiaDevicePlugin，并在发现 kubelet 重启时，会重新注册。
nvidia_device_plugin.go 里实现了对 Device Plugin API 的几个接口（ListAndWatch(), Allocate(), ...）。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="32-插件如何启动并注册serve--start--register">3.2 插件如何启动并注册（Serve → Start → Register）<a href="https://tech.xcentiot.com/blog/k8s#32-%E6%8F%92%E4%BB%B6%E5%A6%82%E4%BD%95%E5%90%AF%E5%8A%A8%E5%B9%B6%E6%B3%A8%E5%86%8Cserve--start--register" class="hash-link" aria-label="Direct link to 3.2 插件如何启动并注册（Serve → Start → Register）" title="Direct link to 3.2 插件如何启动并注册（Serve → Start → Register）">​</a></h3>
<p>代码示意（简化）：</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">// main.go 中最核心的逻辑片段:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func main() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 1. 初始化 NVML 以获取 GPU 信息</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err := nvml.Init(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        log.Fatal("Failed to init NVML")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 2. 监听文件变化(Watch /var/lib/kubelet/device-plugins/kubelet.sock)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    //    如果 kubelet.sock 重新被创建，说明 kubelet 重启，需要重新注册</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    watcher, _ := newFSWatcher(pluginapi.DevicePluginPath)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    var devicePlugin *NvidiaDevicePlugin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    restart := true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    for {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        if restart {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            // 每次要“重启”时，都先停止旧的，然后重新New、Serve</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            if devicePlugin != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                devicePlugin.Stop()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            devicePlugin = NewNvidiaDevicePlugin()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            err := devicePlugin.Serve()  // &lt;-- 核心</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            restart = false</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // 监听文件事件或系统信号，一旦发现kubelet.sock被删除/新建，就要重启</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        select {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        case event := &lt;-watcher.Events:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            if event.Name == pluginapi.KubeletSocket &amp;&amp; event.Op&amp;fsnotify.Create == fsnotify.Create {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                restart = true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="321-serve">3.2.1 Serve()<a href="https://tech.xcentiot.com/blog/k8s#321-serve" class="hash-link" aria-label="Direct link to 3.2.1 Serve()" title="Direct link to 3.2.1 Serve()">​</a></h4>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">func (m *NvidiaDevicePlugin) Serve() error {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    err := m.Start() </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        return err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 1. 启动自身 gRPC server(监听 nvidia.sock)，并准备好ListAndWatch等实现</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 2. ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // Register 向kubelet注册：</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    err = m.Register(pluginapi.KubeletSocket, "nvidia.com/gpu")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // 若注册失败，停止自己的server</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        m.Stop()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        return err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    log.Printf("Registered nvidia device plugin with Kubelet")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    return nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>重点：Serve() 函数里会先 Start()（负责在 /var/lib/kubelet/device-plugins/nvidia.sock 开启 gRPC），再 Register()（把 Endpoint=/var/lib/kubelet/device-plugins/nvidia.sock、ResourceName=nvidia.com/gpu 上报给 kubelet）。</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="322-start">3.2.2 Start()<a href="https://tech.xcentiot.com/blog/k8s#322-start" class="hash-link" aria-label="Direct link to 3.2.2 Start()" title="Direct link to 3.2.2 Start()">​</a></h4>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func (m *NvidiaDevicePlugin) Start() error {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 1. 先清理旧的 socket 文件</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    err := m.cleanup()  </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        return err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 2. 在 nvidia.sock 上启动 gRPC Server</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    sock, err := net.Listen("unix", m.socket) </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err != nil { return err }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    m.server = grpc.NewServer()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    pluginapi.RegisterDevicePluginServer(m.server, m) // 注册我们的Server实现</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    go m.server.Serve(sock)                           // 开启服务</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 3. 尝试拨号，确保server真的起来了</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    conn, err := dial(m.socket, 5*time.Second)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err != nil { return err }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    conn.Close()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 4. 启动GPU健康检查goroutine(见后文healthcheck)，用于后续ListAndWatch上报</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    go m.healthcheck()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    return nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li>
<p>这里 pluginapi.RegisterDevicePluginServer(m.server, m) 就把 ListAndWatch, Allocate 等接口都“挂载”到 grpc 上了；</p>
</li>
<li>
<p>healthcheck() 用来检测 GPU 是否出现 nvmlEventTypeXidCriticalError，一旦检测到某块 GPU 异常，就会在后面 ListAndWatch 中汇报给 kubelet。</p>
</li>
</ul>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="323-register">3.2.3 Register()<a href="https://tech.xcentiot.com/blog/k8s#323-register" class="hash-link" aria-label="Direct link to 3.2.3 Register()" title="Direct link to 3.2.3 Register()">​</a></h4>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">func (m *NvidiaDevicePlugin) Register(kubeletEndpoint, resourceName string) error {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 1. 连接到 /var/lib/kubelet/device-plugins/kubelet.sock</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    conn, err := dial(kubeletEndpoint, 5*time.Second)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        return err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    defer conn.Close()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 2. 构造 RegisterRequest</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    client := pluginapi.NewRegistrationClient(conn)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    req := &amp;pluginapi.RegisterRequest{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Version:      pluginapi.Version,         // v1beta1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Endpoint:     path.Base(m.socket),       // "nvidia.sock"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ResourceName: resourceName,              // "nvidia.com/gpu"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 3. 调用kubelet的 Registration.Register()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    _, err = client.Register(context.Background(), req)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    return err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>kubelet 端会在 kubelet.sock 里监听一个 Register(RegisterRequest) 的 gRPC。收到此请求后，kubelet 就记下 ResourceName: nvidia.com/gpu，以及 Endpoint: nvidia.sock。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="33-kubelet-对接插件listandwatch">3.3 kubelet 对接插件：ListAndWatch<a href="https://tech.xcentiot.com/blog/k8s#33-kubelet-%E5%AF%B9%E6%8E%A5%E6%8F%92%E4%BB%B6listandwatch" class="hash-link" aria-label="Direct link to 3.3 kubelet 对接插件：ListAndWatch" title="Direct link to 3.3 kubelet 对接插件：ListAndWatch">​</a></h3>
<p>一旦注册成功，kubelet 会反过来拨号 /var/lib/kubelet/device-plugins/nvidia.sock，调用 ListAndWatch() 来获取设备列表，并持续接收设备健康状态更新。</p>
<p>在 NVIDIA 插件中，ListAndWatch() 代码大概如下（简化）：</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func (m *NvidiaDevicePlugin) ListAndWatch(_ *pluginapi.Empty,</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    s pluginapi.DevicePlugin_ListAndWatchServer) error {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 1. 第一次调用时，立刻把当前所有GPU列表发送给kubelet</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    s.Send(&amp;pluginapi.ListAndWatchResponse{Devices: m.devs})</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 2. 然后死循环，监控health管道</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    for {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        select {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        case &lt;-m.stop:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            return nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        case d := &lt;-m.health:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            // 若某块GPU出现异常，就更新它的Health=Unhealthy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            d.Health = pluginapi.Unhealthy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            // 把最新的 m.devs（带Unhealthy的）再发给kubelet</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            s.Send(&amp;pluginapi.ListAndWatchResponse{Devices: m.devs})</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li>m.devs 里保存了 GPU 的 ID（通常是 GPU UUID）以及其 Health （默认 Healthy）；</li>
<li>当检测到某块 GPU 出了故障，就把它标记为 Unhealthy，再刷新发给 kubelet，kubelet 就会减少可用 GPU 数量，并更新 Node Status；</li>
<li>kubelet 这边 有个对应的“长连”处理，会反复接收 ListAndWatchResponse，更新其本地缓存。</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="34-容器启动allocate">3.4 容器启动：Allocate<a href="https://tech.xcentiot.com/blog/k8s#34-%E5%AE%B9%E5%99%A8%E5%90%AF%E5%8A%A8allocate" class="hash-link" aria-label="Direct link to 3.4 容器启动：Allocate" title="Direct link to 3.4 容器启动：Allocate">​</a></h3>
<p>当用户创建一个 Pod，需要 nvidia.com/gpu: 1 之类的资源时，调度器把该 Pod 分配给节点后，kubelet 在 创建容器 的流程里会调用 Device Plugin 的 Allocate() 来获取容器运行时需要的一些配置信息，比如 环境变量 / Mount / Device 映射等等。</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func (m *NvidiaDevicePlugin) Allocate(ctx context.Context, </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    responses := pluginapi.AllocateResponse{}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    for _, req := range reqs.ContainerRequests {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // 1. 拿到Container请求使用的 GPU ID 列表</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        devIDs := req.DevicesIDs</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // 2. 构造 ContainerAllocateResponse</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        resp := &amp;pluginapi.ContainerAllocateResponse{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            Envs: map[string]string{</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                // NVIDIA_VISIBLE_DEVICES 是nvidia-container-runtime识别的变量，</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                // 指定容器可见的GPU</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                "NVIDIA_VISIBLE_DEVICES": strings.Join(devIDs, ","),</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            },</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // 3. 这里也可添加 Mounts, Devices, 等信息(若需要访问 /dev/nvidiaX )</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // resp.Devices = ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // resp.Mounts = ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        responses.ContainerResponses = append(responses.ContainerResponses, resp)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    return &amp;responses, nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li>可以看到，NVIDIA 的做法比较简单，主要就是把 DevicesIDs 打包到 NVIDIA_VISIBLE_DEVICES 环境变量，依靠 nvidia-container-runtime 的机制让容器只看见指定 GPU；</li>
<li>有些其他 Device Plugin，会在 resp.Devices 字段里声明要挂载 /dev/specialX 到容器里，或者设置更多 Annotations 等。这都取决于硬件的实际接入方式。</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="35-小结nvidia-device-plugin-的基本逻辑">3.5 小结：NVIDIA Device Plugin 的基本逻辑<a href="https://tech.xcentiot.com/blog/k8s#35-%E5%B0%8F%E7%BB%93nvidia-device-plugin-%E7%9A%84%E5%9F%BA%E6%9C%AC%E9%80%BB%E8%BE%91" class="hash-link" aria-label="Direct link to 3.5 小结：NVIDIA Device Plugin 的基本逻辑" title="Direct link to 3.5 小结：NVIDIA Device Plugin 的基本逻辑">​</a></h3>
<ol>
<li>启动阶段：</li>
</ol>
<ul>
<li>监听 /var/lib/kubelet/device-plugins/kubelet.sock 重建事件，如果 kubelet 重启，会重新向 kubelet 注册；</li>
<li>Serve：自己在 nvidia.sock 开 gRPC 服务，实现 ListAndWatch, Allocate 等。</li>
</ul>
<ol start="2">
<li>ListAndWatch 阶段：</li>
</ol>
<ul>
<li>一开始就把可用 GPU 全部上报（Healthy）；</li>
<li>后续如果发现 GPU 故障，就把该 GPU 标为 Unhealthy 并再次上报。</li>
</ul>
<ol start="3">
<li>Allocate 阶段：</li>
</ol>
<ul>
<li>Pod/container 要使用 GPU 时，kubelet 调用本方法；</li>
<li>Plugin 返回对容器的改动（通常是设置 NVIDIA_VISIBLE_DEVICES 环境变量，加上 /dev/nvidia* 访问权限），让容器能正确访问到对应 GPU。</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="4-深入k8s内部-设备上报与调度协同原理">4. 深入K8s内部 设备上报与调度协同原理<a href="https://tech.xcentiot.com/blog/k8s#4-%E6%B7%B1%E5%85%A5k8s%E5%86%85%E9%83%A8-%E8%AE%BE%E5%A4%87%E4%B8%8A%E6%8A%A5%E4%B8%8E%E8%B0%83%E5%BA%A6%E5%8D%8F%E5%90%8C%E5%8E%9F%E7%90%86" class="hash-link" aria-label="Direct link to 4. 深入K8s内部 设备上报与调度协同原理" title="Direct link to 4. 深入K8s内部 设备上报与调度协同原理">​</a></h3>
<p>在前文中，我们已经重点介绍了：Device Plugin 如何通过 ListAndWatch 和 Allocate 告诉 kubelet 自己有多少设备、分配设备时要怎么配置容器。本节我们再补充一下和 调度器 的配合机制，从而串联起一个 Pod 从“请求 GPU 资源”到“成功使用 GPU 设备”的完整流程。</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="41-kubelet-对不同设备的统一抽象">4.1 kubelet 对不同设备的统一抽象<a href="https://tech.xcentiot.com/blog/k8s#41-kubelet-%E5%AF%B9%E4%B8%8D%E5%90%8C%E8%AE%BE%E5%A4%87%E7%9A%84%E7%BB%9F%E4%B8%80%E6%8A%BD%E8%B1%A1" class="hash-link" aria-label="Direct link to 4.1 kubelet 对不同设备的统一抽象" title="Direct link to 4.1 kubelet 对不同设备的统一抽象">​</a></h4>
<p>首先，kubelet 内部有一个 Device Manager（位于 pkg/kubelet/cm/devicemanager/）。它的核心思路是：</p>
<ol>
<li>不管什么品牌或类型的硬件（GPU、FPGA ...），只要 Device Plugin 使用了 Register() 报告 ResourceName，kubelet 便将这类硬件视为 统一的“可分配资源”；</li>
<li>kubelet 会把这些资源更新到 Node Status 中——比如 nvidia.com/gpu: 4，然后在调度时，Scheduler 就会用通用的 资源匹配逻辑 来判断 “哪个节点满足 nvidia.com/gpu &gt;= 1” 等；</li>
<li>在容器启动阶段，kubelet 会再次调用 Device Manager 的 Allocate（其实是去调用具体的 Plugin 的 Allocate），由插件告诉它如何“挂载”或“配置”真正的硬件。</li>
</ol>
<p>为了让你清楚 kubelet 的这种 “统一抽象” 是怎么实现的，可以看一下 kubelet 源码中 ManagerImpl 的几个关键数据结构（简化）：</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">type ManagerImpl struct {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    allDevices      map[string]map[string]pluginapi.Device</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    healthyDevices  map[string]sets.String</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    unhealthyDevices map[string]sets.String</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li>allDevices：保存了所有插件上报的设备，map[ResourceName] -&gt; map[DeviceID] -&gt; Device信息；</li>
<li>healthyDevices：记录每个 ResourceName 下，哪些设备ID是 Healthy 状态；</li>
<li>unhealthyDevices：记录每个 ResourceName 下，哪些设备ID是 Unhealthy 状态。</li>
</ul>
<p>当 plugin 通过 ListAndWatch 告知 kubelet 设备列表时，kubelet 会调用下面这样的方法进行更新：</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func (m *ManagerImpl) PluginListAndWatchReceiver(resourceName string, resp *pluginapi.ListAndWatchResponse) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    var devices []pluginapi.Device</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    for _, d := range resp.Devices {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        devices = append(devices, *d)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    m.genericDeviceUpdateCallback(resourceName, devices)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func (m *ManagerImpl) genericDeviceUpdateCallback(resourceName string, devices []pluginapi.Device) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 遍历该插件发来的所有设备</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    for _, dev := range devices {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        m.allDevices[resourceName][dev.ID] = dev</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        if dev.Health == pluginapi.Healthy {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            m.healthyDevices[resourceName].Insert(dev.ID)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        } else {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            m.unhealthyDevices[resourceName].Insert(dev.ID)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>然后 kubelet 会在更新 Node Status 的阶段，把 healthyDevices 的数量当作该节点的可用容量，比如 nvidia.com/gpu: 4。这就是为什么 scheduler 能看到这个节点有 4 个 GPU 可以用——因为 kubelet 做了“抽象”，把真实的硬件数量记到 Node.Status.Allocatable 里。</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="42-调度原理统一的资源-filter">4.2 调度原理：统一的资源 Filter<a href="https://tech.xcentiot.com/blog/k8s#42-%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86%E7%BB%9F%E4%B8%80%E7%9A%84%E8%B5%84%E6%BA%90-filter" class="hash-link" aria-label="Direct link to 4.2 调度原理：统一的资源 Filter" title="Direct link to 4.2 调度原理：统一的资源 Filter">​</a></h4>
<p>当一个 Pod 需要 GPU 资源时（例如 nvidia.com/gpu: 1），调度器并不需要有什么特殊的 GPU 逻辑，它只是在 Filter 阶段检查：</p>
<ul>
<li>Node 上分配出去的 nvidia.com/gpu 有多少？</li>
<li>Node 上的可用剩余是否 &gt;= 1？</li>
</ul>
<p>如果满足，就认为 “在资源层面” 通过了。这部分可参考调度器的源码（示例）：</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func (f *Fit) Filter(ctx context.Context, cycleState *framework.CycleState, </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                     pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 1. 先汇总 podRequest: Pod 需要的 CPU/Memory/... 以及 Extended Resource (如 nvidia.com/gpu)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    podRequest := calculatePodResourceRequest(pod)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    // 2. 比较 nodeInfo 上的可用资源 vs. podRequest，如果不足，就返回失败</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    insufficientRes := fitsRequest(podRequest, nodeInfo, ...)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if len(insufficientRes) &gt; 0 {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        // 代表某些资源不足</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        return framework.NewStatus(framework.Unschedulable, "...")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    return nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func fitsRequest(podRequest *preFilterState, nodeInfo *framework.NodeInfo, ...) []InsufficientResource {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    for rName, rQuant := range podRequest.ScalarResources {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        if rQuant &gt; (nodeInfo.Allocatable.ScalarResources[rName] - nodeInfo.Requested.ScalarResources[rName]) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            // 资源不足</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            insufficientResources = append(insufficientResources, InsufficientResource{ ResourceName: rName, ... })</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li>当 rName 是 "nvidia.com/gpu" 时，这里就去比对 “Node 上 GPU 的 Allocatable - 已分配量” 是否够。</li>
<li>若够，就留在候选节点里。调度器然后可能再用其它 Plugin（如 优先级、亲和性 等）来选择最优节点。</li>
</ul>
<p>由此可见，在调度阶段，插件对接 的 GPU 资源会跟 CPU/Memory 一样被当作“资源”来 Filter，没有特殊专门的 GPU 调度逻辑。</p>
<p>4.3 调度成功后 kubelet 的 Allocate 调用</p>
<p>当调度器最终确定 某节点 能满足 Pod 需求，Pod 就被 绑定 到该节点上。
然后，该节点上的 kubelet 启动容器前，会在 “Device Manager → Plugin” 调用 Allocate()，让 plugin 返回如何设置容器。NVIDIA plugin 这里，就会返回 NVIDIA_VISIBLE_DEVICES 环境变量，或 /dev/nvidia* 等特定挂载，让最终的容器只看到被分配到的 GPU。</p>
<p>简化示意（对照前文）：</p>
<ol>
<li>Scheduler：判断 “nvidia.com/gpu” 是否够用 → 选中节点X；</li>
<li>kubelet(节点X)：接收到调度结果 → 创建容器 → 调用 Device Manager → 进而调用插件的 Allocate()；</li>
<li>plugin 返回 GPU ID 对应的挂载配置/环境变量 → kubelet 设置到容器 → 容器启动后就能使用那几块 GPU。</li>
</ol>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="44-小结device-manager-的基本逻辑">4.4 小结：Device Manager 的基本逻辑<a href="https://tech.xcentiot.com/blog/k8s#44-%E5%B0%8F%E7%BB%93device-manager-%E7%9A%84%E5%9F%BA%E6%9C%AC%E9%80%BB%E8%BE%91" class="hash-link" aria-label="Direct link to 4.4 小结：Device Manager 的基本逻辑" title="Direct link to 4.4 小结：Device Manager 的基本逻辑">​</a></h4>
<ol>
<li>kubelet 启动时 会初始化一个 DeviceManager（在 /var/lib/kubelet/device-plugins/kubelet.sock 开启一个 gRPC server），等着外部的 Device Plugin 来 “Register()”。</li>
<li>插件注册后：kubelet 使用一个内部的 endpointImpl 去拨号插件的 socket（例如 nvidia.sock），调用 ListAndWatch()，拿到 GPU 列表。</li>
<li>更新 Node 状态：kubelet 把 GPU 数量更新到 Node Status 中，比如 nvidia.com/gpu = 4。</li>
<li>容器创建时：kubelet 要检查 Pod 的资源需求，调用 Allocate() 获取容器启动参数（环境变量 / 设备挂载 / ...），最终把这些注入到容器 runtime。</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="5-nvidia-设备插件关键点与常见问题">5. NVIDIA 设备插件关键点与常见问题<a href="https://tech.xcentiot.com/blog/k8s#5-nvidia-%E8%AE%BE%E5%A4%87%E6%8F%92%E4%BB%B6%E5%85%B3%E9%94%AE%E7%82%B9%E4%B8%8E%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98" class="hash-link" aria-label="Direct link to 5. NVIDIA 设备插件关键点与常见问题" title="Direct link to 5. NVIDIA 设备插件关键点与常见问题">​</a></h3>
<ol>
<li>必须要把 Docker 的默认 runtime 设置为 nvidia</li>
</ol>
<ul>
<li>在 /etc/docker/daemon.json 里，"default-runtime": "nvidia", 并配置好 nvidia-container-runtime，这样容器启动时才可以识别并正确加载 GPU 驱动；</li>
</ul>
<ol start="2">
<li>
<p>如果容器加了特权（privileged: true），可能导致 NVIDIA_VISIBLE_DEVICES 失效，容器会看到所有 GPU，这就违背了 Kubernetes “整卡分配” 的隔离逻辑；</p>
</li>
<li>
<p>NVIDIA 版本兼容</p>
</li>
</ol>
<ul>
<li>官方推荐 NVIDIA drivers &gt;= 384.81，nvidia-docker &gt;= 2.0，并且 Kubernetes 版本 &gt;= 1.10 等；</li>
</ul>
<ol start="4">
<li>虚拟化 GPU</li>
</ol>
<ul>
<li>如果只想 “假装” 有更多 GPU 给调度用（但实际上共享同一张卡），就需要在 ListAndWatch() 阶段把一张 GPU 虚拟成多张 ID，然后在 Allocate() 里做对应的合并映射。业界也有更复杂的 GPU 虚拟化方案（比如 vGPU）；</li>
</ul>
<ol start="5">
<li>PodResources 接口</li>
</ol>
<ul>
<li>对于监控 GPU 使用，你可以访问 kubelet 提供的 /var/lib/kubelet/pod-resources/kubelet.sock 的 List gRPC 来查询哪些 Pod 正在使用哪些 GPU 设备，这个属于 “PodResources API”。</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="6-结语">6. 结语<a href="https://tech.xcentiot.com/blog/k8s#6-%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="Direct link to 6. 结语" title="Direct link to 6. 结语">​</a></h3>
<p>通过以上分析过程，我们可以看到 GPU Device Plugin 的总体原理相对清晰：</p>
<ol>
<li>插件负责：</li>
</ol>
<ul>
<li>获取真实硬件信息（如调用 NVML 取 GPU UUID）</li>
<li>在本机开一个 gRPC 服务，提供 ListAndWatch, Allocate 等接口</li>
<li>向 kubelet 注册自己</li>
<li>持续检测硬件健康，并向 kubelet 实时汇报</li>
</ul>
<ol start="2">
<li>kubelet 负责：</li>
</ol>
<ul>
<li>接收注册请求、与插件建立长连</li>
<li>把设备信息更新到 Node Status</li>
<li>在容器启动时调用 Allocate() 让插件告诉它怎么配置容器</li>
<li>确保集群调度、资源分配符合“整卡”模型（或厂商自定义模型）</li>
<li>因为 Device Plugin 框架将 “硬件逻辑” 和 “kubelet 核心” 解耦，厂商可以各自实现插件，大大降低了 Kubernetes 自身的复杂度。NVIDIA/k8s-device-plugin 即是其中的一个典型实现，也是目前使用最广泛的 GPU Plugin。</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="参考链接">参考链接<a href="https://tech.xcentiot.com/blog/k8s#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5" class="hash-link" aria-label="Direct link to 参考链接" title="Direct link to 参考链接">​</a></h3>
<ul>
<li>NVIDIA/k8s-device-plugin 源码仓库：<a href="https://github.com/NVIDIA/k8s-device-plugin" target="_blank" rel="noopener noreferrer">https://github.com/NVIDIA/k8s-device-plugin</a></li>
<li>Kubernetes 官方文档: <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/" target="_blank" rel="noopener noreferrer">https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/</a></li>
<li>NVIDIA 容器文档（nvidia-container-runtime）：<a href="https://github.com/NVIDIA/nvidia-container-runtime" target="_blank" rel="noopener noreferrer">https://github.com/NVIDIA/nvidia-container-runtime</a></li>
<li>NVML API 文档：<a href="https://docs.nvidia.com/deploy/nvml-api/" target="_blank" rel="noopener noreferrer">https://docs.nvidia.com/deploy/nvml-api/</a></li>
</ul>]]></content:encoded>
            <category>k8s</category>
        </item>
        <item>
            <title><![CDATA[失业难以避免，重构人生规划]]></title>
            <link>https://tech.xcentiot.com/blog/book</link>
            <guid>https://tech.xcentiot.com/blog/book</guid>
            <pubDate>Sun, 26 Jan 2025 02:07:26 GMT</pubDate>
            <description><![CDATA[目标，成长，人脉]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="目标成长人脉">目标，成长，人脉<a href="https://tech.xcentiot.com/blog/book#%E7%9B%AE%E6%A0%87%E6%88%90%E9%95%BF%E4%BA%BA%E8%84%89" class="hash-link" aria-label="Direct link to 目标，成长，人脉" title="Direct link to 目标，成长，人脉">​</a></h2>
<p>最近读到了一段话，引起了我的思考。</p>
<p>"工作对于人生至关重要。它提供了收入，身份和意义，失业的创伤仅次于亲人的死亡。"</p>
<p>我完全同意这段话。因为我父母的人生就是这样，上了一辈子的班，如果把工作的部分去掉，就没有其他值得一说的东西了。</p>
<p>对于许多人来说，人生就是围绕着工作。上学是为了找工作，毕业是为了上班，等到退休不上班，就已经是晚年了。万一失业，生活顿时就崩溃了，就像上面的引言说的，收入、身份、意义瞬间都没了。</p>
<p>我今天想说的是，这种"工作型人生"已经过时了。要是现在你还把上班当作人生的主轴，是一件很危险的事情，因为失业的风险越来越大。 时代变了，稳定的工作越来越少，失业会是家常便饭，人生已经没法依附在某一份具体的工作上了。</p>
<p>由于经济全球化和技术进步，行业的兴衰正变得越来越快。往往几年时间，新事物就彻底取代了旧事物，比如网络淘汰了报纸杂志，电动车淘汰汽油车，自动驾驶以后会淘汰司机。新行业瞬间崛起，又瞬间衰落，共享单车、P2P 网贷、长租公寓等等都是如此。</p>
<p>这些变化都是预想不到的，根本没法提前准备。上学的时候，我们学习内燃机，怎么可能想到有生之年这个东西会过时，汽车会不用汽油。在技术浪潮的面前，很多行业一转眼都没了，从业人员都得下岗再就业，当代社会的失业可能性比以前大得多。</p>
<p>所以，已经没法围绕工作而规划人生了，因为工作靠不住了，人生不能押宝在某一份具体的工作上面，风险太大。现在是互联网 + 智能手机 + 云计算 + 5G，未来会变成什么样，没人知道。</p>
<p>我的意思是，我们对人生的预设应该是，一生中会多次改变工作，服务过多家公司，干过各种事情，中间很可能有失业期。 不能像以前那种，以某一份具体的工作或职业作为预设来规划人生。</p>
<p>人生是一次旅行，不同的工作或公司只是我们沿途投宿过的村庄。它们不是人生的主轴，你不要误以为那是可以定居的地方，大多数人没有这种幸运。 不要再关注能不能找一个好工作/好公司（让你定居），人生的主轴应该是关注能不能实现下面三个目标。</p>
<ul>
<li>目的：你的人生想做到什么。</li>
<li>成长：怎么保证自己的技能不断在成长。</li>
<li>人脉关系：你能联系和动员的社会网络有多大。</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="文档">文档<a href="https://tech.xcentiot.com/blog/book#%E6%96%87%E6%A1%A3" class="hash-link" aria-label="Direct link to 文档" title="Direct link to 文档">​</a></h2>
<ul>
<li>企业最关心的（或者说唯一在乎的）事情，就是增加收入、降低成本。</li>
<li>企业实际上需要的不是程序员，而是能够帮助它们增加收入、降低成本的人。</li>
<li>企业雇佣你，是为了让你帮它们完成某个可以增加收入、降低成本的项目，而不是为了让你追求个人的软件成就。</li>
</ul>
<p>我自己的体会是，写文档对一个人的锻炼是很大的。
（1）可以熟悉项目；
（2）可以提高表达能力；
（3）可以提高技术能力，因为只有理解了代码才能表达出来，文档越清晰，就代表思路越清晰；
（4）可以提高个人影响力，文档写得越好，看的人就越多。久而久之，你就会成为项目的代言人，别人有问题就会来找你。</p>
<p>总之，新人要重视写文档，主动为整个团队写文档，对个人成长有帮助，对团队也会有帮助。
不停地写，大量地写，现在就开始写。
长期中，你会意识到这给自己带来的巨大价值。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="技术写作的首要诀窍">技术写作的首要诀窍<a href="https://tech.xcentiot.com/blog/book#%E6%8A%80%E6%9C%AF%E5%86%99%E4%BD%9C%E7%9A%84%E9%A6%96%E8%A6%81%E8%AF%80%E7%AA%8D" class="hash-link" aria-label="Direct link to 技术写作的首要诀窍" title="Direct link to 技术写作的首要诀窍">​</a></h2>
<p>把问题说清楚的关键，在于你的思想是否清楚</p>
<p>文章采用单线结构。</p>
<p>只要采用了单线结构，技术文章就不会太差。即使内容是难懂的，至少结构是清晰的，一环扣一环，读者能知道自己卡在哪一环上。只要克服了这个难点，就能继续往前走，不会有陷入迷宫、找不到方向、如坠五里雾中。</p>
<p>有一篇老外的文章，使用图形表示文章结构，非常形象。</p>
<p>单线结构，按照箭头，一步步推进。</p>
<p>有的问题比较复杂，涉及多个因素，可能是星状结构。</p>
<p>也可能是"层次结构"。</p>
<p>这时，建议把这个问题拆分成多篇文章，每篇文章保持单线结构，坚持做到一篇文章只讲一点，而且争取把这一点讲透。</p>
<p>但是，单线结构说起来容易，做起来难。难就难在，人类的思想不是单线的，而是多线的，甚至是非线性。</p>
<p>你要把混乱而跳跃的思维，整理成单线结构，表达出来，让他人理解，谈何容易，通常都需要反复推敲和提炼。</p>
<p>举例来说，我们的思维可能是下图这样，好多点各自发散，连接在一起。</p>
<p>这种乱麻一团的思维并不可怕，可怕的是，你把文章也写成这样的结构，而不是耐心地梳理出一条线索。</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="编写一个最小的-64-位-hello-world">编写一个最小的 64 位 Hello World<a href="https://tech.xcentiot.com/blog/book#%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E6%9C%80%E5%B0%8F%E7%9A%84-64-%E4%BD%8D-hello-world" class="hash-link" aria-label="Direct link to 编写一个最小的 64 位 Hello World" title="Direct link to 编写一个最小的 64 位 Hello World">​</a></h3>
<p>参考一直这篇技术文章，是发散结构还是单线结构</p>
<p><a href="https://cjting.me/2020/12/10/tiny-x64-helloworld/" target="_blank" rel="noopener noreferrer">最小Hello World</a></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="一些思考">一些思考<a href="https://tech.xcentiot.com/blog/book#%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83" class="hash-link" aria-label="Direct link to 一些思考" title="Direct link to 一些思考">​</a></h3>
<ul>
<li>支付</li>
<li>社交</li>
<li>good ui</li>
<li>开源代码，写blog</li>
</ul>]]></content:encoded>
            <category>think</category>
        </item>
        <item>
            <title><![CDATA[三甲医院的小物件有大作用]]></title>
            <link>https://tech.xcentiot.com/blog/live</link>
            <guid>https://tech.xcentiot.com/blog/live</guid>
            <pubDate>Sun, 26 Jan 2025 02:07:26 GMT</pubDate>
            <description><![CDATA[总览]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="总览">总览<a href="https://tech.xcentiot.com/blog/live#%E6%80%BB%E8%A7%88" class="hash-link" aria-label="Direct link to 总览" title="Direct link to 总览">​</a></h2>
<blockquote>
<p>协和硅霜、中医院的红纱条、北医三院的 鼻炎三号……很多医院都有一些包装简单、价格便宜 、疗效却非常好的“小物件”，这些“小物件”虽小，但 年头长，名声大，对于小儿、老人、妇女的一些皮 肤、鼻炎、咳嗽、咽炎等却是个“宝贝”，好处就是简 便验廉，很多患者甚至不远千里来到医院“求购”。这 些“小物件”的规范名字应是“内部制剂”。本期，我们 搜集了国内较为知名的十余种内部制剂，感兴趣的 读者不妨留意并备不时之需。
名词解释：内部制剂： 临床用的药品（成药） 一般分两类：一类是经国家药监部门批准上市销售 的药品，批准文号统一为“国药准字 ××××”；还有一 类为医疗机构根据本单位临床需要经批准而配制、 自用的固定处方制剂，简称为医疗机构制剂或医院 制剂，如“京药制字 ××××”，但只能凭处方在本医疗 机构使用。</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="具体">具体<a href="https://tech.xcentiot.com/blog/live#%E5%85%B7%E4%BD%93" class="hash-link" aria-label="Direct link to 具体" title="Direct link to 具体">​</a></h2>
<p>■皮肤病</p>
<p>北京中医医院：<strong>红纱条</strong></p>
<p>适应症：治疗感染性疮面、顽固难愈性皮肤溃 疡，如痈、疽、<strong>糖尿病足</strong>坏疽、褥疮等</p>
<p>健康时报记者 郑帆影</p>
<p>红纱条在皮肤溃疡界可是个 “明星”，北京中医 医院赴四川汶川抗震救灾带上了“红纱条”， 使许 多伤口溃烂感染的伤员避免了截肢的厄运。</p>
<p>北京中医院中医外科主任董建勋介绍，红纱条 的年纪和北京中医医院差不多大，1956年建院就有 了，安全使用大概57年了。红纱条是小名，大名叫 朱红膏纱条，是把能活血生肌的朱红膏涂抹在纱条 上，激发气血运行，刺激创面肉芽组织生长，激活 生长缓慢或不生长的创面。截止到2008年，就有近 1 0万人次的临床应用，充分证实疗效高安全性好。</p>
<p>需要注意的是，使用 “红纱条”初期，疮面常出现 脓性分泌物增多，并不是感染加重，而是疮面修复 过程启动的标志之一。</p>
<p>北京协和医院：<strong>精心硅霜</strong></p>
<p>适应症：皮肤干裂、瘙痒</p>
<p>健康时报记者 吴润果</p>
<p>上世纪70年代，我国西医皮肤科开创者、北京协 和医院李洪迥教授和药剂科陈兰英教授合作研究， 针对北方秋冬空气干燥寒冷，皮肤易冻、裂、干燥 ，以及老年性皮肤干燥瘙痒症等，率先将军工专用 产品二甲基硅油开发为民用产品，并与多种制剂精 心调配而研制成“协和硅霜 ”。</p>
<p>“协和硅霜 ”刚开始的时候只是作为院内制剂， 只在药剂科小规模生产，供医院内部人员以及一些 皮肤科患者使用，因为在保护皮肤，防裂、防冻、 防燥方面表现良好，渐渐供不应求。1992年，北京 协和医院产业与外资合作成立了北京协和精细化学 制品有限公司，专门生产销售协和硅霜。后来，由 于“协和”商标遭到抢注，产品更名为 “精心硅霜 ”，继 续在市场上销售。</p>
<p>虽已商业化生产，但目前精心硅霜的销售也仍 基本限于协和医院内的小卖部及网站直销。经过几 次提价， 60克装的价格已经涨至 19元一支，相对同 类化妆品，仍然性价比实足，每到入冬季节，总有 不少人会专程到协和医院去购买几支给家人备着。</p>
<p>从军用产品，到院内制剂，再走向商业化生产 ，精心硅霜也算是有些传奇色彩了。</p>
<p>■呼吸道疾病</p>
<p>北京大学第三医院：<strong>鼻炎三号</strong></p>
<p>适应症：慢性鼻炎、<strong>过敏性鼻炎</strong></p>
<p>健康时报记者 郑帆影</p>
<p>感冒后鼻炎犯了，鼻塞难受，北京大学第三医 院药剂科副主任药师张晓乐会自己去耳鼻喉科开瓶 鼻炎三号，大约吃一周左右，慢性鼻炎就能缓解， 这个场景不仅仅在药剂科，据张晓乐医生观察，该 院的许多医生都用此药治鼻炎，医生自己用，它的 疗效和安全性不言而喻了吧。</p>
<p>张晓乐医生介绍，鼻炎三号是曾用名，作为院 内制剂审批的正式名称是复方苍耳子片，由散风除 湿通窍的苍耳子和善通鼻窍的辛夷组成，听名字就 知道是标准的纯中药制剂，是上世纪80年代研制的 ，当时北医三院共研制了十多种院内制剂，到目前 流传下来六七种，鼻炎三号就是其中之一，仅6元多 一瓶，70片大约吃 5天左右，一般吃一两瓶就能见效 ，在医保报销范围。</p>
<p>复旦大学附属眼耳鼻喉科医院：<strong>玄柏爽声颗粒</strong></p>
<p>适用症：<strong>喉炎</strong>、声带红肿、早期声带小结</p>
<p>健康时报驻上海记者 尹 薇</p>
<p>玄柏爽声颗粒在沪上文艺界内名声很广。尤其 是不能离开嗓子的歌唱、戏曲界专业人士，谁喉咙 有个肿痛，声带查出不太严重的小结，都会想起去 这家医院配点“爽声颗粒 ”，让声音爽一爽。</p>
<p>该院制剂针对咽喉红肿、声音嘶哑不能出声有很 好效果，一般以30天为一个疗程，是清热滋阴、开 音利咽的利器。由于复旦大学附属眼耳鼻喉科医院 是专科医院，院内制剂规模较小，故而一般委托中 药专门机构加工，仅供院内患者使用，因此有一定 配额计划，为此还曾出现因为病人临时增多而供不 应求的场面。</p>
<p>该院耳鼻喉科邵俊主任医师介绍，玄柏爽声颗 粒始于上世纪70年代。因疗效好而沿用至今。顾名 思义，这个配方是由玄参、黄柏等组成的纯中药制 剂，凝结着中西智慧结晶的该制剂对于治疗声带水 肿和早期声带小结都有比较好的疗效。</p>
<p>值得提醒的是，有外地患者也想慕名网上求购 ，但该药虽属纯中药颗粒，毕竟是处方药物，需要 前来就医后遵医嘱使用。</p>
<p>黑龙江中医药大学：<strong>牛黄利咽丸</strong></p>
<p>适应症：急用于<strong>口疮</strong>、喉痹、喉蛾、扁桃体炎</p>
<p>健康时报记者 李桂兰</p>
<p>三十年前，为了鼓励医院里的老中医们拿出自 己的好方子用于临床，黑龙江中医药大学附属第一 医院喉科老主任王圣云拿出了自己家里的祖传秘方“ 牛黄利咽丸 ”（原名：牛黄利咽丹），这种以牛黄为 主要药物，配以黄连、桂枝、大黄等8味中药组成的 丹丸。住院子里的医学院的学生们也都会在逢年过 节买点这个药回家，以备不时之需。</p>
<p>药剂科主任严斌介绍说，这个药丸具有泻火解 毒、清热化痰、活血止痛、消肿利咽之功效，尤其 对上呼吸道感染所至的扁桃体炎症有独到治疗效果 ，对疼痛的效果最为明显。如果您不小心患上了急 性咽炎，或慢性咽炎处于急性发作时，含上这样一 粒牛黄利咽丸就能酣然入睡了。严斌主任提醒说， 口中含服效果最好。</p>
<p>这个药需要在黑龙江中医药大学的几大附属医 院的门诊或病房找医生根据具体病情依据处方来购 买，不支持邮购。价格：13.3元/盒，10粒/盒。</p>
<p>杭州市中医院：<strong>蝉贝合剂</strong></p>
<p>适应症：<strong>咽痒</strong>、干咳无痰等慢性咳嗽</p>
<p>健康时报记者 李桂兰</p>
<p>说起杭州市中医院的 “蝉贝合剂 ”，那可是医院 有名的老中医唐福安传下来的，专门针对慢性咳嗽 的一味药，用了三四十年，一到秋冬天就供不应求 。</p>
<p>有些人咳嗽能咳上两个月，有的中老年人甚至 好几年都在咳嗽，杭州市中医院药剂科陈红梅主任 介绍说，医院里的有不少医生、护士自己或者家人 出现喉咙痒，还干咳的时候，就会想到买点这个药 ，不舒服的时候吃点，好了也就不吃了。</p>
<p>蝉贝合剂是在国家级名老中医临床验方的基础上 进行剂型改革而成的，由蝉蜕、浙贝母、桔梗、黛 蛤散、前胡等11味中药材提取纯化而成，具有清热 宣肺、化痰利咽的功效。</p>
<p>如果人体比较虚的话，蝉贝合剂就不能用。中 药房吴敏副主任说，除了“蝉贝合剂 ”，杭州市中医 院治疗慢性咳嗽比较有名的还有“润咽合剂 ”和“热咳 清合剂”，都是医院自创方剂。润咽合剂用于急慢性 咽炎引起的咽痛、咽干及咳嗽痰少等症，热咳清合 剂用于外感发热，咳嗽。</p>
<p>需要到门诊找医生处方购买，不支持邮购。价 格：64.5元/瓶，500毫升/瓶。</p>
<p>■消化系统疾病</p>
<p>解放军总医院：<strong>木苏丸</strong></p>
<p>适用症：<strong>慢性肝病</strong>、肝硬化及其并发症</p>
<p>健康时报记者 白轶南</p>
<p>由解放军总医院消化科程留芳教授研制的木苏 丸，不仅在院内被广泛用于临床治疗，在院外也广 受好评，到处有人打听去哪能买到木苏丸。</p>
<p>在中药制剂中，只有木苏丸是国内专门用于治 疗慢性肝病、肝硬化失代偿期及其并发症的，说具 体点就是用于治疗肝硬化患者食欲、腹胀腹水、乏 力、肝区不适，以及改善肝区疼痛等不适。</p>
<p>木苏丸的研制归结为程留芳教授多年的临床经 验，是程教授在不断观察、总结患者的各类不适， 治疗前后差异等的基础上总结研制出来的。与传统 中药不同的是，它的治疗效果可以通过西医的具体 数据表现出来，比如蛋白数量、出凝血时间等，降 低并发症。</p>
<p>在对木苏丸的临床疗效观察中，有数千名肝硬 化患者接受治疗和观察，入选肝硬化患者186例，病 因多为乙型病毒性肝炎肝硬化120例，酒精性肝硬化 16例，其中肝硬化合并肝癌 50例。经过治疗前和治 疗三个月后的对照观察，入组186例中175例自觉腹 胀、乏力症状明显减轻，食量增加，肛门排气增多 ，牙龈出血明显减少，发热、腹痛、腹泻频次明显 减少。少数患者服药初期偶有腹部不适，继续服用 可自行减轻或消失。</p>
<p>目前，木苏丸售价是一盒 236.5元，内装有 30袋 ，一次1袋，一天3次，一般三个月为一个疗程 (9盒) 。</p>
<p>广东省中医院：<strong>疏肝降脂片</strong></p>
<p>适应症：高血脂症、肥胖病、<strong>脂肪肝</strong></p>
<p>健康时报记者 董 蕊</p>
<p>一位挺着大肚子的小伙子，服了几个月的疏肝降 脂片，肚子小了不少，结婚时穿上礼服，标准的“靓 仔新郎官”！这是在广东省中医院药剂厂工作 20多年 的谢文健药师的朋友的经历。现在，来广东省中医 院跟医生要求开疏肝降脂片的，很多都是重量级的 胖子。“在我们这边，这才是正宗的纯中药 ‘减肥药’啦 ！”谢文健介绍。</p>
<p>现在，胖子越来越多。怎么胖起来的？广东省 中医院肝病专科的萧焕明医生说，很大一部分人是 因为肝气郁滞，脾失健运，痰瘀互结，代谢功能下 降。其中肝脏更是人体脂质代谢的“集中地”，肝失 疏泄、脾失健运，很容易引起脂肪肝、高血脂症。 疏肝降脂片正是将能疏肝凉肝的柴胡、郁金，养肝 血的首乌以及能清热祛湿的茵陈、消积滞的山楂等 相配伍，促进肝脏功能正常，适用于血脂偏高的肥 胖症。同时，疏肝降脂片还能避免引起转氨酶升高 ，长期服用也无不良反应。不少患者反映服用了疏 肝降脂片后，大便更通畅、身体更轻松，精神头更 足。</p>
<p>该制剂为 20多年前由广东省中医院原业务副院 长、名中医张壮战与内科副教授王雪玲共同供方、 生产，每年产销三万多瓶。</p>
<p>青岛第五人民医院：<strong>消石利胆合剂</strong></p>
<p>适应症：<strong>胆囊炎</strong>（急性尤佳）、胆囊结石、肝内 胆管结石、胆总管结石</p>
<p>健康时报记者 董 蕊</p>
<p>消石利胆合剂只有 “5岁”的药龄，其历史渊源却 可以追溯到40多年前。早在 1968年，山东省著名老 中医、卫生部首届中医药传承特别贡献奖的获得者 姚开炳教授就开始研究、试验消石利胆的中药方，1 972年，利胆排石片开始生产，并在逐渐改进、完善 的过程中，最终演化为现在的消石利胆合剂——古 方加新药，该制剂不仅受到业内专家一致认可，也 成了当地百姓口口相传的好药、管用药，不少外地 患者打来电话，要求开方开药。</p>
<p>目前仍在医院出诊的姚老告诉记者，消石利胆 合剂非常对症于急慢性胆囊炎，亦能溶石、化石、 排石。近十年来，胆囊结石的患者有增多的趋势， 但其实90%的患者不必手术，对症的中医调理能够 达到溶石排石的效果。一般说来，服用消石利胆合 剂（根据个人体质，可能还会配合其它中药方/制剂 ）20多天，再查一次 B超，就会发现患者的结石小很 多。不过，姚开炳教授强调，该药剂偏苦寒，吃上 两个月左右就需停下一段时间，用其它药剂调养一 下肠胃再继续。</p>
<p>重庆西南医院：<strong>胃炎颗粒、胃痛灵</strong></p>
<p>适应症：<strong>慢性胃炎</strong>、胃痛</p>
<p>健康时报记者 吴润果</p>
<p>问问重庆主城区的老胃病患者，不少人都知道 三军医大西南医院的胃炎颗粒和胃痛灵这对院内中 成药制剂。作为慢性胃病的辅助用药，一般的慢性 胃炎吃点胃炎颗粒，胃痛得有些难受的时候就吃片 胃痛灵，经常是一吃就灵，而且价格还非常便宜， 一盒十来块钱。</p>
<p>与很多院内制剂都是临床科室与药剂科联合研 发的不同，胃炎颗粒和胃痛灵是西南医院消化科独 立研制的。据科室医生回忆介绍，慢性胃病不易根 治，还容易反复发作，患者西药吃多了又担心副作 用，当年消化科的几位教授就联合研制了胃炎颗粒 和胃痛灵这两种中成药，一个针对慢性症状，一个 针对急症，都是纯中药，无副作用。消化科研制出 来后，就送去药剂科做试验，确定疗效以及没有副 作用后，再逐步在院内临床推广。</p>
<p>现在胃炎颗粒和胃痛灵已经成了西南医院最有 名的院内制剂之一，可要说这个药的详细研发过程 ，就连现在的五十多岁的消化科主任都摇摇头，表 示这药用了有二三十年了，当年研发的教授都早已 退休，而自己那时大概还在读大学呢。</p>
<p>现在胃炎颗粒和胃痛灵已经成了西南医院最有 名的院内制剂之一，虽然已有三十年上下的“高龄” ，胃炎颗粒和胃痛灵这两兄弟仍在西南医院内广泛 使用，除了来消化科看病的患者，就连院内绝大多 数的员工都用过。</p>
<p>■风湿关节病</p>
<p>中国中医科学院西苑医院：<strong>四妙消痹颗粒</strong></p>
<p>适用症：<strong>缓解类风湿关节红肿热痛</strong></p>
<p>健康时报记者 白轶南</p>
<p>凡是到中国中医科学院西苑医院来看类风湿关 节炎的患者，都对“四妙消痹颗粒 ”不陌生。</p>
<p>这款院内制剂已经在西苑医院临床应用了近 30 年，在此之前，绝大多数治疗类风湿的中成药都是 温热性质的，因为传统中医认为类风湿是风、寒、 湿引起的，所以治疗上需祛除风寒。</p>
<p>然而，西苑医院风湿病科房定亚教授在 1984年 开创性地提出用清热解毒的方法治疗类风湿的观点 ，并主持研发“四妙消痹颗粒 ”，打破了传统的治疗 方法，当时便引起了业界广泛关注，先后获得了中 国中医科学院和中华中医药学会的科学技术进步奖 。</p>
<p>房定亚教授了解到，类风湿除了 “滑膜炎”之外，“ 血管炎”同样是一种基本的病理改变，而在中医方剂 的“四妙勇安汤 ”，就是专门以清热解毒来治疗血管 炎的。于是房教授在这个古方基础上，结合现代免 疫学研究成果，研发了四妙消痹颗粒。</p>
<p>临床试验证实总体疗效在 80%以上，主治关节 疼痛、红热、肿胀、活动受限、晨起僵硬等。这款院 内制剂只能在西苑医院买到，售价是每盒（10袋）2 4元。</p>
<p>■骨科跌打损伤</p>
<p>佛山市中医院：<strong>伤科黄水</strong></p>
<p>适应症：闭合性、开放性的跌打损伤，创伤感 染等</p>
<p>健康时报记者 杜文明</p>
<p>因黄水姓 “黄”，颜色是浅黄色的，几乎每个来 看骨伤科的患者都使用过水剂的黄水和油剂的黄油 ，故骨伤科被笑称为“一片黄”。本地市民喜欢在家 里备瓶黄水“看门口”，外地患者也托人到佛山市中 医院开药寄回去。广东省内多地多次发现了不少伪 冒仿造的伤科黄水出售，药监部门不得不联合医院 药务科多次到外地“扫黄”。</p>
<p>据制剂中心刘东文主任药剂师介绍，伤科黄水 以黄连、栀子为主要成分，除了用于骨伤科领域的 一切跌打损伤，软组织及骨骼损伤，包括闭合性、 开放性创伤、创伤感染等外，还广泛用于烧伤烫伤 、糖尿病足等。</p>
<p>伤科黄水从研发至今已有 50多年的历史，使用 病例超过数百万例。刘东文介绍，经过临床药理观 察，伤科黄水具有明显的促进肉芽组织生长和抗感 染的作用，还有一定的镇痛作用，能清热解毒、去 腐生肌。</p>
<p>■儿童用药</p>
<p>北京儿童医院：<strong>青紫合剂和复幼合剂</strong></p>
<p>适应症：分别治疗儿童过敏性紫癜和儿童性早 熟</p>
<p>健康时报记者 余易安</p>
<p>“外地家长一开就是好几十瓶。 ”北京儿童医院 药剂科中药制剂室组长高艳梅介绍说，该院具有特 色的院内制剂是青紫合剂和复幼合剂，分别治疗儿 童过敏性紫癜和儿童性早熟。</p>
<p>青紫合剂源于儿童医院京城著名 “小儿王”王鹏 飞先生70年代创立的 “青紫汤”。复幼合剂由中医小 儿妇科专业医师经过长期临床经验总结研制而成。</p>
<p>“市场上没有同类药品，患儿又需长期服用，所 以需求量很大。”高艳梅说，以青紫合剂为例，生产 一批流程需要8天，一批仅 3000瓶，而2012年12月 的门诊需求量显示，一个月就销售了一万八千多瓶 ，供不应求。</p>
<p>高艳梅提醒，院内制剂 “仅限本医疗机构使用 ”， 因此患儿需经医院专科医生诊疗开具处方后方可使 用。因货源紧张，目前，这两种药门诊均已无货， 请先打电话确认有药再来。</p>
<p>首都儿科研究所：<strong>肤乐霜</strong></p>
<p>适用证：<strong>婴幼儿湿疹</strong></p>
<p>健康时报记者 文 雯</p>
<p>打开淘宝搜一下首儿肤乐霜，就能知道首都儿科研 究所院内制剂肤乐霜的受欢迎程度，几乎是两岁以 下宝宝的必备用品。</p>
<p>湿疹是婴幼儿最常见的皮肤病之一。婴幼儿皮 肤薄，冬天干燥，是高发期，肤乐霜从1989年就开 始应用，是治疗儿童湿疹等皮肤病的王牌药物之一 。</p>
<p>儿研所皮肤科主任刘晓雁介绍，肤乐霜属于纯 中药制剂，不含任何激素，可清热燥湿，祛风解毒 。它没有刺激性，故在口周、眼周等皮肤娇嫩的地 方也可以使用，除皮肤科，五官科、眼科等科室也 会用到。</p>
<p>湿疹较轻的可以单独局部使用肤乐霜，而比较 严重的则一般会配合儿童适用的中效、弱效的激素 类药物，而且用药时间一般不超过两周。</p>
<p>肤乐霜作为治疗湿疹的明星药品，凭医生处方 购买（而且不可邮购），39.5元/支。另外，医院从 未在网络上或以其他方式进行销售。</p>
<p>天津市儿童医院：<strong>复方氯霉素软膏、氯霉素软 膏</strong></p>
<p>适应症：<strong>亚急性及慢性湿疹</strong></p>
<p>健康时报记者 杜文明</p>
<p>“第一次听到 ‘湿疹特效 ’时，我还不知道这是什 么，后来才知道就是我们院的‘复方氯霉素软膏、氯 霉素软膏’。”天津市儿童医院皮肤科主治医师廉佳说 。这种天津儿童医院的院内制剂因为治疗儿童湿疹 效果特别好，被患者取名叫“湿疹特效 ”。</p>
<p>天津市儿童医院药剂科主任林宏介绍，复方氯 霉素软膏和氯霉素软膏主要用于治疗儿童急、慢性 湿疹，销量很好，每天都有上百人来医院排队购买 。</p>
<p>天津市儿童医院皮肤科主治医师廉佳介绍，两 种药膏偏重略有不同，氯霉素乳膏需要和尤卓尔混 合后治疗湿疹，副作用较小。复方氯霉素是直接外 用治疗湿疹的，副作用较大，患儿父母最好不要随 便自行给孩子用药。</p>
<p>林宏主任表示，这种儿童医院的院内制剂的确 很火，每天都有很多人排队。为此医院专设有1块钱 挂号处，就是专门为慕“药”而来的人开放的。</p>
<p>上海新华医院：<strong>蓝白药膏</strong></p>
<p>适用症：<strong>婴儿湿疹、糜烂性湿疹、接触性皮炎</strong></p>
<p>健康时报驻上海记者 尹 薇</p>
<p>新华医院大名鼎鼎的儿童湿疹油膏分两小盒， 一个蓝盖，一个白盖，故而被亲切称呼为“蓝白药膏 ” 。它们价廉物美，加起来也才五六元钱，却被人网 上几十倍价格倒卖，还有人挂了几百元的特需门诊 专为买此药。</p>
<p>蓝白药膏诞生于上世纪 80年代，由新华医院一 位经验丰富的老专家研制，并在现任皮肤科主任姚 志荣教授手中更加发扬光大。“白膏”（醋酸地塞米松 软膏）主要负责消除过敏反应，“蓝膏”负责收敛滋润 ，并非任何时候都要同时使用，要根据病情情况， 在医嘱下有序使用。</p>
<p>制剂室叶齐药师提醒，尽管 “蓝白药膏 ”价格低 廉，不良反应也比较小，最近几年通过新妈妈们的 口口相传和网络上经验交流一下子热起来了，但毕 竟含有微量糖皮质激素，因此还是建议宝宝先到医 院就诊后，在医生指导下使用，不要在院外自行购 买及随意使用。</p>
<p>南京市中医院：<strong>健脾益肺口服液</strong></p>
<p>适用症：<strong>小儿咳嗽</strong></p>
<p>健康时报记者 马晓慧</p>
<p>每到感冒高发季节，南京市中医院儿科总会有 家长来打听健脾益肺口服液的事儿。这个口服液在 院内已经使用了近三十年，用于表虚不固，咳喘， 自汗恶风，脾虚便溏，或体虚易感风邪者。可以起 到益气，固表，止汗，健脾益肺的作用。</p>
<p>南京市中医院儿科唐为红主任医师介绍，此制 剂适合小儿反复呼吸道感染，反复上感、扁桃体炎 、支气管炎、肺炎等。</p>
<p>一般婴幼儿免疫系统发育还未成熟，新陈代谢 较快，能量需求量大，但胃肠消化吸收功能又比较 弱，因此这个年龄段的孩子容易会出现感冒或腹泻 。</p>
<p>加之 3周岁后孩子上幼儿园，彼此之间易于交叉 感染，生病可能更频繁。如果您孩子反复感冒，不 妨试试服用健脾益肺口服液。</p>
<p>需要该制剂的患者可以先挂医院的儿科号，凭 医生处方在院内购买。每瓶价格不到30元。</p>]]></content:encoded>
            <category>live</category>
        </item>
        <item>
            <title><![CDATA[golang scafflod]]></title>
            <link>https://tech.xcentiot.com/blog/golang</link>
            <guid>https://tech.xcentiot.com/blog/golang</guid>
            <pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[why need it?]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>后台自动提交代码功能</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="背景">背景<a href="https://tech.xcentiot.com/blog/golang#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="Direct link to 背景" title="Direct link to 背景">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="why">why?<a href="https://tech.xcentiot.com/blog/golang#why" class="hash-link" aria-label="Direct link to why?" title="Direct link to why?">​</a></h3>
<p>忘记打卡了，需要的凭证就是提交代码才有效，于是有了这个需求，希望自动定时每天8点50提交一份代码。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="需求分析">需求分析<a href="https://tech.xcentiot.com/blog/golang#%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90" class="hash-link" aria-label="Direct link to 需求分析" title="Direct link to 需求分析">​</a></h2>
<ol>
<li>写一个程序可以提交代码并且push</li>
<li>定时执行</li>
<li>后台启动</li>
</ol>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="具体实现">具体实现<a href="https://tech.xcentiot.com/blog/golang#%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 具体实现" title="Direct link to 具体实现">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="auto_commitgo">auto_commit.go<a href="https://tech.xcentiot.com/blog/golang#auto_commitgo" class="hash-link" aria-label="Direct link to auto_commit.go" title="Direct link to auto_commit.go">​</a></h3>
<div class="language-golang codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-golang codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">package main</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">import (</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "fmt"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "os"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "os/exec"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> "time"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func updateReadme() error {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 切换到 gwx_test 分支</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "checkout", "gwx_test").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("切换分支失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 追加内容到 hello.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> timestamp := time.Now().Format("2006-01-02 15:04:05")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> content := fmt.Sprintf("\nAuto update at %s", timestamp)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> f, err := os.OpenFile("hello.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("打开文件失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> defer f.Close()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if _, err := f.WriteString(content); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("写入文件失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 添加更改</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "add", ".").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("git add 失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 提交更改</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "commit", "-m", "Auto update time").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("git commit 失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 推送到远程</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> if err := exec.Command("git", "push", "upstream", "gwx_test").Run(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  return fmt.Errorf("git push 失败: %v", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> fmt.Printf("成功更新 at %s\n", timestamp)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> return nil</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">func main() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> fmt.Println("自动提交任务已启动...")</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> // 添加 recover 机制</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> defer func() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  if r := recover(); r != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   fmt.Printf("程序发生严重错误: %v\n", r)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   // 重新启动主循环</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   main()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> for {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  now := time.Now()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  next := time.Date(now.Year(), now.Month(), now.Day(), 8, 50, 0, 0, now.Location())</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  if now.After(next) {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   next = next.Add(24 * time.Hour)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  timer := time.NewTimer(next.Sub(now))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fmt.Printf("等待下一次执行，将在 %s 运行\n", next.Format("2006-01-02 15:04:05"))</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  &lt;-timer.C</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  // 使用 recover 包装任务执行</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  func() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   defer func() {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    if r := recover(); r != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">     fmt.Printf("任务执行出错: %v\n", r)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   }()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   if err := updateReadme(); err != nil {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fmt.Printf("执行出错: %v\n", err)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  }()</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="编译为可执行文件">编译为可执行文件<a href="https://tech.xcentiot.com/blog/golang#%E7%BC%96%E8%AF%91%E4%B8%BA%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6" class="hash-link" aria-label="Direct link to 编译为可执行文件" title="Direct link to 编译为可执行文件">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">go build -o auto_commit.exe auto_commit.go</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="后台运行">后台运行<a href="https://tech.xcentiot.com/blog/golang#%E5%90%8E%E5%8F%B0%E8%BF%90%E8%A1%8C" class="hash-link" aria-label="Direct link to 后台运行" title="Direct link to 后台运行">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">@echo off</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">echo 开始执行自动提交任务 %date% %time% &gt;&gt; auto_commit_log.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start /b auto_commit.exe</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">echo 任务已启动 %date% %time% &gt;&gt; auto_commit_log.txt</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="遇到的问题">遇到的问题<a href="https://tech.xcentiot.com/blog/golang#%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98" class="hash-link" aria-label="Direct link to 遇到的问题" title="Direct link to 遇到的问题">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="没有使用定时触发器导致cpu卡顿">没有使用定时触发器，导致cpu卡顿<a href="https://tech.xcentiot.com/blog/golang#%E6%B2%A1%E6%9C%89%E4%BD%BF%E7%94%A8%E5%AE%9A%E6%97%B6%E8%A7%A6%E5%8F%91%E5%99%A8%E5%AF%BC%E8%87%B4cpu%E5%8D%A1%E9%A1%BF" class="hash-link" aria-label="Direct link to 没有使用定时触发器，导致cpu卡顿" title="Direct link to 没有使用定时触发器，导致cpu卡顿">​</a></h3>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">1. 使用 time.NewTimer 创建定时器</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2. 使用 &lt;-timer.C 等待定时器触发，而不是持续循环</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3. 移除了 time.Sleep</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- CPU 使用率会大幅降低</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- 程序仍然会在指定时间执行任务</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- 定时器触发前程序会处于休眠状态</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">- 系统资源占用更少使用 time.Timer 来替代持续循环</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copy code to clipboard" title="Copy" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>]]></content:encoded>
            <category>Golang</category>
            <category>Gin</category>
            <category>DDD</category>
        </item>
        <item>
            <title><![CDATA[Welcome]]></title>
            <link>https://tech.xcentiot.com/blog/welcome</link>
            <guid>https://tech.xcentiot.com/blog/welcome</guid>
            <pubDate>Sat, 11 Jan 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[欢迎来到我们的公开技术博客社区]]></description>
            <content:encoded><![CDATA[<p>欢迎来到我们的公开技术博客社区</p>
<p>在这里你可以获得:</p>
<ol>
<li>我们的最新研究思考方向。</li>
<li>参与开源代码的分享和讨论。</li>
<li>生活的一些思考，并且你也可以提交pr给我们，我们一起共建社区</li>
</ol>]]></content:encoded>
            <category>hello</category>
        </item>
    </channel>
</rss>