<?xml version="1.0" encoding="utf-8"?>
<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>roc</title>
        <link>https://imroc.cc/blog</link>
        <description>roc 云原生 Blog</description>
        <lastBuildDate>Mon, 15 Aug 2022 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-CN</language>
        <copyright>Copyright 2024 roc | All Right Reserved | &lt;a href="http://beian.miit.gov.cn/"&gt;蜀ICP备2021009081号-1&lt;/a&gt;</copyright>
        <item>
            <title><![CDATA[重磅: 利用 req 统一为 Golang 所有 HTTP 请求集成链路追踪]]></title>
            <link>https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger</link>
            <guid>https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger</guid>
            <pubDate>Mon, 15 Aug 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[概述]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="概述">概述<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E6%A6%82%E8%BF%B0" class="hash-link" aria-label="概述的直接链接" title="概述的直接链接">​</a></h2>
<p>为了增强 Golang 程序的可观测性，方便定位问题，我们往往会在代码中集成链路追踪 (tracing) 的能力，<a href="https://www.jaegertracing.io/" target="_blank" rel="noopener noreferrer">Jaeger</a> 是当今比较主流的选择，而 tracing 相关的 API 如今都抽象到了 <a href="https://opentelemetry.io/docs/instrumentation/go/getting-started/" target="_blank" rel="noopener noreferrer">OpenTelemetry</a> 项目中，涵盖各种实现，也包括 Jaeger 在内。</p>
<p>利用 req 强大的中间件能力，可以轻松为我们涉及 HTTP 调用的代码统一集成链路追踪的能力，且能以最少的代码量进行扩展。</p>
<p>本文将给出一个可运行的程序示例：输入一个 GitHub 用户名，展示用户的简短介绍，包含名字、网站地址以及该用户下的最火开源项目与 star 数量，期间涉及的函数与 API 调用链路追踪信息均上报至 Jaeger，进行可视化展示。</p>
<p>主要包含以下特点：</p>
<ul>
<li>内置一个基于 req 封装的 GitHub SDK。</li>
<li>SDK 中利用 req 的 <code>RequestMiddleware</code> 与 <code>ResponseMiddleware</code>，统一处理 API 异常，对接 API 的实现函数无需关心错误处理。</li>
<li>SDK 支持传入 OpenTelemetry 的 Tracer 来开启链路追踪，利用 req 的 Client 中间件能力，在请求前创建 trace span，并记录请求与响应的详细信息到 span 中(URL、Method、请求头、请求体、响应状态码、响应头、响应体等)，在响应结束后自动终止 span。</li>
<li>在调用 SDK 的上层函数也使用 trace，层层传递，在 Jaeger UI 上可查看完整且非常详细的调用链路详情。</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="初始化项目">初始化项目<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E5%88%9D%E5%A7%8B%E5%8C%96%E9%A1%B9%E7%9B%AE" class="hash-link" aria-label="初始化项目的直接链接" title="初始化项目的直接链接">​</a></h2>
<p>首先创建一个目录，使用 <code>go mod init</code> 初始化工程:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">go mod init opentelemetry-jaeger-tracing</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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="封装支持-tracing-的-github-sdk">封装支持 Tracing 的 GitHub SDK<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E5%B0%81%E8%A3%85%E6%94%AF%E6%8C%81-tracing-%E7%9A%84-github-sdk" class="hash-link" aria-label="封装支持 Tracing 的 GitHub SDK的直接链接" title="封装支持 Tracing 的 GitHub SDK的直接链接">​</a></h2>
<p>在项目根目录下面创建一个名为 <code>github</code> 的目录，作为内置的 GitHub SDK 的 package，在里面创建源文件 <code>github.go</code>，写入代码:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">package</span><span class="token plain"> github</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"context"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"fmt"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"github.com/imroc/req/v3"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/attribute"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/codes"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/trace"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"strconv"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"strings"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// Client is the go client for GitHub API.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">type</span><span class="token plain"> Client </span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Client</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// APIError represents the error message that GitHub API returns.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// GitHub API doc: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">type</span><span class="token plain"> APIError </span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  Message          </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"message"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  DocumentationUrl </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"documentation_url,omitempty"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  Errors           </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    Resource </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"resource"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    Field    </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"field"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    Code     </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"code"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"errors,omitempty"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// Error convert APIError to a human readable error and return.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">e </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">APIError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  msg </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Sprintf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"API error: %s"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Message</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> e</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">DocumentationUrl </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">""</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Sprintf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"%s (see doc %s)"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> msg</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">DocumentationUrl</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">len</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">e</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Errors</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">0</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> msg</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  errs </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token keyword" style="color:#C586C0">for</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">_</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">range</span><span class="token plain"> e</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Errors </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    errs </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">append</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">errs</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Sprintf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"resource:%s field:%s code:%s"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Resource</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Field</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Code</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Sprintf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"%s (%s)"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> msg</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> strings</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Join</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">errs</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">" | "</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// NewClient create a GitHub client.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">NewClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  c </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">C</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// All GitHub API requests need this header.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token function" style="color:rgb(220, 220, 170)">SetCommonHeader</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"Accept"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"application/vnd.github.v3+json"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// All GitHub API requests use the same base URL.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token function" style="color:rgb(220, 220, 170)">SetBaseURL</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"https://api.github.com"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// EnableDump at the request level in request middleware which dump content into</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// memory (not print to stdout), we can record dump content only when unexpected</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// exception occurs, it is helpful to troubleshoot problems in production.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token function" style="color:rgb(220, 220, 170)">OnBeforeRequest</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">func</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">c </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> r </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Request</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> r</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">RetryAttempt </span><span class="token operator" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">0</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"> </span><span class="token comment" style="color:rgb(106, 153, 85)">// Ignore on retry, no need to repeat EnableDump.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      r</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">EnableDump</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// Unmarshal response body into an APIError struct when status &gt;= 400.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token function" style="color:rgb(220, 220, 170)">SetCommonError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;</span><span class="token plain">APIError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token comment" style="color:rgb(106, 153, 85)">// Handle common exceptions in response middleware.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token function" style="color:rgb(220, 220, 170)">OnAfterResponse</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">func</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">client </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Response</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"> </span><span class="token comment" style="color:rgb(106, 153, 85)">// There is an underlying error, e.g. network error or unmarshal error(SetResult or SetError was invoked before).</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> dump </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Dump</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"> dump </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">""</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"> </span><span class="token comment" style="color:rgb(106, 153, 85)">// Append dump content to original underlying error to help troubleshoot.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">          resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Errorf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"%s\nraw content:\n%s"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Dump</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token comment" style="color:rgb(106, 153, 85)">// Skip the following logic if there is an underlying error.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> ok </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">APIError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"> ok </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"> </span><span class="token comment" style="color:rgb(106, 153, 85)">// Server returns an error message.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token comment" style="color:rgb(106, 153, 85)">// Convert it to human-readable go error.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token comment" style="color:rgb(106, 153, 85)">// Corner case: neither an error response nor a success response,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token comment" style="color:rgb(106, 153, 85)">// dump content to help troubleshoot.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">!</span><span class="token plain">resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">IsSuccess</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Errorf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"bad response, raw content:\n%s"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Dump</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>使用 <code>Client</code> 结构体作为 GitHub 的客户端，也是 SDK 的核心结构体，内置了一个 <code>*req.Client</code>。</li>
<li>分别使用 <code>SetCommonHeader</code> 与 <code>SetBaseURL</code> 为 GitHub 所有 API 请求设置统一的 <code>Accept</code> 请求头与 URL 前缀。</li>
<li>GitHub API 响应的错误格式是统一的，使用 <code>SetCommonError</code> 告知 req 如果响应了错误(状态码大于等于400），则自动将响应体 Unmarshal 到 <code>APIError</code> 结构体的对象中。</li>
<li><code>APIError</code> 结构体实现了 go 的 error 接口，将 API 层面的错误信息转换成可读的字符串。</li>
<li>在 <code>OnAfterResponse</code> 中设置 <code>ResponseMiddleware</code>，检测到 API 响应错误时，将其写入到 <code>resp.Err</code>，自动会将其作为 go error 抛给上层的调用方。</li>
<li>在 <code>OnBeforeRequest</code> 中设置 <code>RequestMiddleware</code>，为所有请求开启请求级别的 dump (暂存到内存，不打印出来)，若遇到底层错误(如超时、dns 解析失败、Unmarshal 失败)，或者收到未知的状态码(小于200)，在 <code>ResponseMiddleware</code> 中尽可能将有助于定位问题的信息(dump 内容)记录到 error，写入 <code>resp.Err</code> 以便抛给上层的调用方。</li>
</ul>
<p>下面为 <code>Client</code> 增加 Tracing 的能力:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">type</span><span class="token plain"> apiNameType </span><span class="token builtin" style="color:rgb(86, 156, 214)">int</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">const</span><span class="token plain"> apiNameKey apiNameType </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">iota</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// SetTracer set the tracer of opentelemetry.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">c </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">SetTracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">tracer trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Tracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WrapRoundTripFunc</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">func</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">rt req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">RoundTripper</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">RoundTripFunc </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">func</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">req </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Request</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">resp </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Response</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            ctx </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            apiName</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> ok </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Value</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">apiNameKey</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">!</span><span class="token plain">ok </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                apiName </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">URL</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Path</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token boolean" style="color:#569CD6">_</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> span </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> tracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Start</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> apiName</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token keyword" style="color:#C586C0">defer</span><span class="token plain"> span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">End</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.url"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">URL</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.method"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Method</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.req.header"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">HeaderToString</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">len</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Body</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">0</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                    attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.req.body"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Body</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                </span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> rt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">RoundTrip</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">req</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">RecordError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetStatus</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">codes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Response </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                    attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Int</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.status_code"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">StatusCode</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                    attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.resp.header"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">HeaderToString</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                    attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"http.resp.body"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                </span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>在 <code>Client.SetTracer</code> 中传入 OpenTelemetry 的 Tracer 来开启 Tracing 能力。</li>
<li>调用 <code>Client</code> 中内置的 <code>*req.Client</code> 的 <code>WrapRoundTripFunc</code> 添加 Client 中间件，确保将 <code>rt.RoundTrip(req)</code> 返回的 resp 和 err 最终返回给上层。该行代码之前是发起请求前，可记录请求信息，之后是收到响应后，可记录响应信息。</li>
<li>在中间件实现函数里，为每个请求创建一个 trace span，从 context 中获取 API 名称作为 span 名称，如果 context 中有 parant span，当前 span 也会自动成为其 child span。</li>
<li>使用 <code>defer span.End()</code> 确保在响应结束后再结束 span，以便 tracing 能够正确统计耗时。</li>
<li>将请求与响应的详细信息全都记录到 span 中，如 URL、Method、请求头、请求体、响应状态码、响应头、响应体等。</li>
<li>如果检测到 error，也记录到 span 中并设置 span 的 error 状态。</li>
</ul>
<p>下面开始对接 GitHub API，第一个实现的是获取 GitHub 用户信息的 API，方法命名为 <code>GetUserProfile</code>:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">withAPIName</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> name </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Context </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> ctx </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		ctx </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Background</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WithValue</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> apiNameKey</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">type</span><span class="token plain"> UserProfile </span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	Name </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"name"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	Blog </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"blog"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// GetUserProfile returns the user profile for the specified user.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// Github API doc: https://docs.github.com/en/rest/users/users#get-a-user</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">c </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">GetUserProfile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">user </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">UserProfile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Get</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"/users/{username}"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token function" style="color:rgb(220, 220, 170)">SetPathParam</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"username"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token function" style="color:rgb(220, 220, 170)">SetResult</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token function" style="color:rgb(220, 220, 170)">Do</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">withAPIName</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"GetUserProfile"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>链路追踪的 span 都是通过 context 传递，每个方法的第一个参数都用 context。</li>
<li>利用链式调用构造 Request，<code>Get</code> 表示创建一个 <code>GET</code> 请求，并传入 API 路径(之前 <code>Client.SetCommonBaseURL</code> 已设置所有请求的 URL 前缀，这里就可以省略前缀只写路径)， 路径中还有 <code>username</code> 路径参数 (REST 风格 API)，使用 <code>SetPathParam</code> 传入。</li>
<li>响应体格式是 <code>UserProfile</code> 结构体，直接将返回参数中的空指针变量的地址传入 <code>SetResult</code>，表示如果没有异常，自动创建一个该结构体类型的对象，并让指针变量指向该结构体，这样都不需要自己事先初始化结构体，减少代码量。</li>
<li>利用公共函数 <code>withAPIName</code> 将 API 名称放入 context，然后调用 <code>Do</code> 发起请求时，将 context 传进去，以便让 Client 中间件能够获取到 API 名称并自动将其作为 span 名称。</li>
<li><code>Do</code> 会返回 <code>*req.Response</code>，任何情况它都不为 nil，如果请求过程中返回了 error，会记录到其 <code>Err</code> 字段，将其赋值给返回参数的 <code>err</code> 以便 error 能够层层传递上去。</li>
</ul>
<p>下面再来增加一个获取指定用户代码仓库列表的 API <code>ListUserRepo</code>:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">type</span><span class="token plain"> Repo </span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    Name </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"name"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    Star </span><span class="token builtin" style="color:rgb(86, 156, 214)">int</span><span class="token plain">    </span><span class="token string" style="color:rgb(206, 145, 120)">`json:"stargazers_count"`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// ListUserRepo returns a list of public repositories for the specified user</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// Github API doc: https://docs.github.com/en/rest/repos/repos#list-repositories-for-a-user</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">c </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">ListUserRepo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> page </span><span class="token builtin" style="color:rgb(86, 156, 214)">int</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">repos </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Get</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"/users/{username}/repos"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token function" style="color:rgb(220, 220, 170)">SetPathParam</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"username"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token function" style="color:rgb(220, 220, 170)">SetQueryParamsAnyType</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">map</span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token plain">any</span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token string" style="color:rgb(206, 145, 120)">"type"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain">      </span><span class="token string" style="color:rgb(206, 145, 120)">"owner"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token string" style="color:rgb(206, 145, 120)">"page"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain">      page</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token string" style="color:rgb(206, 145, 120)">"per_page"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain">  </span><span class="token string" style="color:rgb(206, 145, 120)">"100"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token string" style="color:rgb(206, 145, 120)">"sort"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain">      </span><span class="token string" style="color:rgb(206, 145, 120)">"updated"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token string" style="color:rgb(206, 145, 120)">"direction"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"desc"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token function" style="color:rgb(220, 220, 170)">SetResult</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;</span><span class="token plain">repos</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token function" style="color:rgb(220, 220, 170)">Do</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">withAPIName</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"ListUserRepo"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Err</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>该 API 支持分页，需要传入 username 和  page。</li>
<li>page 是整数类型，需要将其入查询参数，使用 <code>SetQueryParamsAnyType</code> 传入所有查询参数，无需提前转成字符串。</li>
<li>其余与上一个 API 实现类似。</li>
</ul>
<p>可以看到，后续我们每次对接新的 API 都变得非常轻松，因为利用了 req 的中间件能力，对异常与链路追踪都进行了统一处理，对接 API 时，只需传入 API 必要的参数与响应体结构类型即可，没有一点多余的代码，非常直观和简洁。</p>
<p>好了，作为示例我们就只对接这两个 API 就够了，我们还可以再为 Client 增加一些实用的小功能:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)">// LoginWithToken login with GitHub personal access token.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// GitHub API doc: https://docs.github.com/en/rest/overview/other-authentication-methods#authenticating-for-saml-sso</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">c </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">LoginWithToken</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">token </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetCommonHeader</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"Authorization"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"token "</span><span class="token operator" style="color:rgb(212, 212, 212)">+</span><span class="token plain">token</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> c</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// SetDebug enable debug if set to true, disable debug if set to false.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">c </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">SetDebug</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">enable </span><span class="token builtin" style="color:rgb(86, 156, 214)">bool</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">Client </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> enable </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">EnableDebugLog</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">EnableDumpAll</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">else</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">DisableDebugLog</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		c</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">DisableDumpAll</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> c</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>如果是匿名用户调用 GitHub API，会有限频，可以使用 token 来避免被限频，增加 <code>LoginWithToken</code> 以支持为所有请求带上认证的 <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" target="_blank" rel="noopener noreferrer">personal access token</a>。</li>
<li>增加 <code>SetDebug</code> 以支持 debug 能力，开启 debug 时，将打印 req 的 debug 日志以及原始的请求与响应内容。</li>
</ul>
<p>至此，我们的 GitHub SDK 封装完成。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="程序示例">程序示例<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E7%A8%8B%E5%BA%8F%E7%A4%BA%E4%BE%8B" class="hash-link" aria-label="程序示例的直接链接" title="程序示例的直接链接">​</a></h2>
<p>下面，正式开始写可运行的示例程序。</p>
<p>在项目根目录下创建 <code>main.go</code>:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">package</span><span class="token plain"> main</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"context"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"fmt"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/attribute"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/codes"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/exporters/jaeger"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/sdk/resource"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/sdk/trace"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	semconv </span><span class="token string" style="color:rgb(206, 145, 120)">"go.opentelemetry.io/otel/semconv/v1.12.0"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"log"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"opentelemetry-jaeger-tracing/github"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token string" style="color:rgb(206, 145, 120)">"os"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">const</span><span class="token plain"> serviceName </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"github-query"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">var</span><span class="token plain"> githubClient </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">github</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Client</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>定义 <code>serviceName</code> 作为本服务的标识 (通常每个程序都是一个服务，上报 tracing 数据时，需标识服务名)，这里就定义为 <code>github-query</code>。</li>
<li>本示例程序需要调用 GitHub API 进行查询，使用前面我们封装的 GitHub SDK 作为 client，这里定义一个全局 <code>githubClient</code> 变量，内部函数直接使用该 client 进行调用。</li>
</ul>
<p>使用 OpenTelemetry 进行链路追踪，需要创建一个 <code>TracerProvider</code>，这里我们定义 <code>traceProvider</code> 函数来创建包含 Jaeger 实现的 <code>TracerProvider</code>:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">traceProvider</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">TracerProvider</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token comment" style="color:rgb(106, 153, 85)">// Create the Jaeger exporter</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	ep </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> os</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Getenv</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"JAEGER_ENDPOINT"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> ep </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">""</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		ep </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"http://localhost:14268/api/traces"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	exp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> jaeger</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">New</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">jaeger</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WithCollectorEndpoint</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">jaeger</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WithEndpoint</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ep</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token comment" style="color:rgb(106, 153, 85)">// Record information about this application in a Resource.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	res</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">_</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> resource</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Merge</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		resource</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Default</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		resource</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">NewWithAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			semconv</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">SchemaURL</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			semconv</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">ServiceNameKey</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">serviceName</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			semconv</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">ServiceVersionKey</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"v0.1.0"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"environment"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"test"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token comment" style="color:rgb(106, 153, 85)">// Create the TraceProvider.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	tp </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">NewTracerProvider</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token comment" style="color:rgb(106, 153, 85)">// Always be sure to batch in production.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WithBatcher</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">exp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token comment" style="color:rgb(106, 153, 85)">// Record information about this application in a Resource.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WithResource</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">res</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">WithSampler</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">trace</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">AlwaysSample</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> tp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>使用 <code>JAEGER_ENDPOINT</code> 自定义 Jaeger 地址，默认使用本地测试的地址。</li>
<li>传入 <code>serviceName</code> 以便在 tracing 数据对本服务进行标识。</li>
</ul>
<p>下面来写查询用户信息的主要函数 <code>QueryUser</code>:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)">// QueryUser queries information for specified GitHub user, and display a</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token comment" style="color:rgb(106, 153, 85)">// brief introduction which includes name, blog, and the most popular repo.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">QueryUser</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">username </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> span </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> otel</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Tracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"query"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Start</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Background</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"QueryUser"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">defer</span><span class="token plain"> span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">End</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"query.username"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	profile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> githubClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">GetUserProfile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">RecordError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetStatus</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">codes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"query.name"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> profile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"result.blog"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> profile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Blog</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">findMostPopularRepo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">RecordError</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetStatus</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">codes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetAttributes</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">String</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"popular.repo.name"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		attribute</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Int</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"popular.repo.star"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Star</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Printf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"The most popular repo of %s (%s) is %s, with %d stars\n"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> profile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> profile</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Blog</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Star</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">findMostPopularRepo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">repo </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">github</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token builtin" style="color:rgb(86, 156, 214)">error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> span </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> otel</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Tracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"query"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Start</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"findMostPopularRepo"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">defer</span><span class="token plain"> span</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">End</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">for</span><span class="token plain"> page </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">1</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"> page</span><span class="token operator" style="color:rgb(212, 212, 212)">++</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">var</span><span class="token plain"> repos </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">github</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Repo</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		repos</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> githubClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">ListUserRepo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> page</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">len</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">repos</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">0</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			</span><span class="token keyword" style="color:#C586C0">break</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> repo </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			repo </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> repos</span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token number" style="color:#B5CEA8">0</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">for</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">_</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> rp </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">range</span><span class="token plain"> repos</span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token number" style="color:#B5CEA8">1</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> rp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Star </span><span class="token operator" style="color:rgb(212, 212, 212)">&gt;=</span><span class="token plain"> repo</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Star </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">				repo </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> rp</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">len</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">repos</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">100</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">			</span><span class="token keyword" style="color:#C586C0">continue</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">break</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> repo </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Errorf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"no repo found for %s"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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><code>QueryUser</code> 需传入一个 username，以便查询指定 GitHub 用户的信息。</li>
<li>在函数开头创建一个名为 <code>QueryUser</code> 的 root span，作为链路追踪的初始 span。</li>
<li>在 span 中记录查询相关信息，包含查询的 username 以及查询到的昵称、blog 地址(使用 GetUserProfile 接口)，也包含该用户最火的开源项目及其 star 数量(使用 ListUserRepo 接口并进行计算对比得出)。</li>
<li>在函数末尾打印最终查询到的信息到控制台。</li>
<li>其中计算用户最火开源项目及其 star 数量由单独的 <code>findMostPopularRepo</code> 函数来实现，该函数也有对应的 span。</li>
</ul>
<p>主要的实现函数准备就绪，现在我们来写 main 函数:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">func</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">main</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    tp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">traceProvider</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token function" style="color:rgb(220, 220, 170)">panic</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    otel</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetTracerProvider</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">tp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    githubClient </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> github</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">NewClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> os</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Getenv</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"DEBUG"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"on"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        githubClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetDebug</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token boolean" style="color:#569CD6">true</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> token </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> os</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Getenv</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"GITHUB_TOKEN"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"> token </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">""</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        githubClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">LoginWithToken</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">token</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    githubClient</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">SetTracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">otel</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Tracer</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"github"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    sigs </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">make</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">chan</span><span class="token plain"> os</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Signal</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">1</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    signal</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Notify</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sigs</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> syscall</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">SIGTERM</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> syscall</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">SIGINT</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">go</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">func</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        sig </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&lt;-</span><span class="token plain">sigs</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Printf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"Caught %s, shutting down\n"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> sig</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> tp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Shutdown</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">context</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Background</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            log</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Fatal</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        os</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Exit</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token number" style="color:#B5CEA8">0</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token keyword" style="color:#C586C0">for</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">var</span><span class="token plain"> name </span><span class="token builtin" style="color:rgb(86, 156, 214)">string</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Printf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"Please give a github username: "</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token boolean" style="color:#569CD6">_</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Fscanf</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">os</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">Stdin</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"%s\n"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            </span><span class="token function" style="color:rgb(220, 220, 170)">panic</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        err </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">QueryUser</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            fmt</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Println</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Error</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>调用 <code>traceProvider()</code> 创建一个 <code>TraceProvider</code>，并使用 <code>otel.SetTracerProvider(tp)</code> 设置到全局共享，以便前面其它函数调用 <code>otel.Tracer(xx)</code> 能够使用此 provider 来创建与获取 tracer。</li>
<li>调用 <code>github.NewClient()</code> 为全局的 <code>githubClient</code> 进行初始化。</li>
<li>判断环境变量，如果 <code>DEBUG=on</code> 则开启 Debug，如果提供 <code>GITHUB_TOKEN</code> 则将其设置给所有请求。</li>
<li>使用 <code>githubClient.SetTracer(otel.Tracer("github"))</code> 来为 GitHub 的 Client 启用 Tracing 能力，用名为 <code>gihtub</code> 的 tracer 标识 SDK 中产生的 tracing 信息。</li>
<li>处理 <code>SIGTERM</code> 和 <code>SIGTNT</code> 信号以实现优雅终止，在程序退出前关闭 <code>TraceProvider</code>，确保 trace 数据上报完再退出 (如果程序不是常驻运行，可以在 main 函数中用 defer 语句关闭 <code>TraceProvider</code>)。</li>
<li>主体是一个 for 死循环: 获取用户输入的 username，然后调用 <code>QueryUser</code> 查询并展示用户信息。</li>
</ul>
<p>大功告成，下面我们来运行一下看看效果。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="运行与效果">运行与效果<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E8%BF%90%E8%A1%8C%E4%B8%8E%E6%95%88%E6%9E%9C" class="hash-link" aria-label="运行与效果的直接链接" title="运行与效果的直接链接">​</a></h2>
<p>首先按照 Jaeger 官方文档 <a href="https://www.jaegertracing.io/docs/getting-started/" target="_blank" rel="noopener noreferrer">Getting Started</a> 在本地启动一个 Jaeger。</p>
<p>然后在项目根目录运行 <code>go run .</code> 运行程序，输入一个 GitHub 用户名（如 <code>spf13</code>），不出意外的话，会自动展示该用户的简短介绍:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ go run .</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Please give a github username: spf13</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">The most popular repo of Steve Francia (http://spf13.com) is cobra, with 28044 stars</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>然后使用浏览器进入 Jaeger UI 界面（<a href="http://127.0.0.1:16686/%EF%BC%89%E6%9D%A5%E6%9F%A5%E7%9C%8B" target="_blank" rel="noopener noreferrer">http://127.0.0.1:16686/）来查看</a> Tracing 详情:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111746.png" alt="" class="img_ev3q"></p>
<p>可以清晰的看到函数调用链路与耗时信息:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">QueryUser (3.27s)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">   |</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">   |----&gt; GetUserProfile (1.1s)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">   |----&gt; findMostPopularRepo (2.16s)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                  |</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                  |----&gt; ListUserRepo (1.17s)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                  |----&gt; ListUserRepo (453.24ms)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>
<blockquote>
<p><code>ListUserRepo</code> 调用两次是因为分页查询用户 repo 时一页没查询完，分成了两次查询。</p>
</blockquote>
<p>点进 <code>QueryUser</code> 的 span 详情，可以看到我们在函数内记录的查询与结果信息:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111755.png" alt="" class="img_ev3q"></p>
<p>再点进 <code>GetUserProfile</code> 这个 SDK 产生的 span 详情，可以看到我们在中间件统一记录的 URL、Method、请求头、响应状态码、响应头、响应体等信息全都在这里，非常详细:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111804.png" alt="" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111811.png" alt="" class="img_ev3q"></p>
<p>不断输入其它 username 测试，经过多次后可能会因 GitHub 的 API 限频导致异常:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ go run .</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Please give a github username: spf13</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">API error: API rate limit exceeded for 43.132.98.44. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.) (see doc https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>检查下 Jaeger UI，可以看到很详细很显眼的错误信息：</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111824.png" alt="" class="img_ev3q"></p>
<p>此时，你可以将你的 GitHub 账号 <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" target="_blank" rel="noopener noreferrer">personal access token</a> 写到环境变量来避免被限频:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">export GITHUB_TOKEN=*******</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>尝试输入一个不存在的用户:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ go run .</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Please give a github username: kjtlejkdglfjsadhfajfsa</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">API error: Not Found (see doc https://docs.github.com/rest/reference/users#get-a-user)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>检查下 Jaeger UI，同样的也可以看到详细的错误信息:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111838.png" alt="" class="img_ev3q"></p>
<p>如果断开公网测试，可能会报 dns 解析失败的错:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ </span><span class="token keyword" style="color:#C586C0">go</span><span class="token plain"> run </span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Please give a github username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> imroc</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Get </span><span class="token string" style="color:rgb(206, 145, 120)">"https://api.github.com/users/imroc"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> dial tcp</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> lookup api</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">github</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">com</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> no such host</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111853.png" alt="" class="img_ev3q"></p>
<p>或者连接超时的错:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ </span><span class="token keyword" style="color:#C586C0">go</span><span class="token plain"> run </span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Please give a github username</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> spf13</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Get </span><span class="token string" style="color:rgb(206, 145, 120)">"https://api.github.com/users/spf13"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> dial tcp </span><span class="token number" style="color:#B5CEA8">20.205</span><span class="token number" style="color:#B5CEA8">.243</span><span class="token number" style="color:#B5CEA8">.168</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token number" style="color:#B5CEA8">443</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> connect</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> operation timed out</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926111904.png" alt="" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="完整代码">完整代码<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81" class="hash-link" aria-label="完整代码的直接链接" title="完整代码的直接链接">​</a></h2>
<p>本文涉及的完整代码已放入 req 官方 examples 下的 <a href="https://github.com/imroc/req/tree/master/examples/opentelemetry-jaeger-tracing" target="_blank" rel="noopener noreferrer">opentelemetry-jaeger-tracing</a> 目录。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="总结">总结<a href="https://imroc.cc/blog/2022/08/15/integrate-req-with-opentelemetry-and-jaeger#%E6%80%BB%E7%BB%93" class="hash-link" aria-label="总结的直接链接" title="总结的直接链接">​</a></h2>
<p>如果业务程序中需要调用其它服务的 API，我们可以利用 req 强大的中间件能力，统一处理所有请求的异常，统一记录所有请求详细信息到 Tracing 系统，写出健壮、可观测性强且极易扩展的 SDK 与业务代码。</p>]]></content:encoded>
            <category>req</category>
        </item>
        <item>
            <title><![CDATA[Kubernetes 疑难杂症排查分享：神秘的溢出与丢包]]></title>
            <link>https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop</link>
            <guid>https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop</guid>
            <pubDate>Sun, 12 Jan 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[上一篇 Kubernetes 疑难杂症排查分享: 诡异的 No route to host 不小心又爆火，这次继续带来干货，看之前请提前泡好茶，避免口干。]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>上一篇 <a href="https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host">Kubernetes 疑难杂症排查分享: 诡异的 No route to host</a> 不小心又爆火，这次继续带来干货，看之前请提前泡好茶，避免口干。</p>
</blockquote>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="问题描述">问题描述<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E9%97%AE%E9%A2%98%E6%8F%8F%E8%BF%B0" class="hash-link" aria-label="问题描述的直接链接" title="问题描述的直接链接">​</a></h2>
<p>有用户反馈大量图片加载不出来。</p>
<p>图片下载走的 k8s ingress，这个 ingress 路径对应后端 service 是一个代理静态图片文件的 nginx deployment，这个 deployment 只有一个副本，静态文件存储在 nfs 上，nginx 通过挂载 nfs 来读取静态文件来提供图片下载服务，所以调用链是：client --&gt; k8s ingress --&gt; nginx --&gt; nfs。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="猜测">猜测<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E7%8C%9C%E6%B5%8B" class="hash-link" aria-label="猜测的直接链接" title="猜测的直接链接">​</a></h2>
<p>猜测: ingress 图片下载路径对应的后端服务出问题了。</p>
<p>验证：在 k8s 集群直接 curl nginx 的 pod ip，发现不通，果然是后端服务的问题！</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="抓包">抓包<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E6%8A%93%E5%8C%85" class="hash-link" aria-label="抓包的直接链接" title="抓包的直接链接">​</a></h2>
<p>继续抓包测试观察，登上 nginx pod 所在节点，进入容器的 netns 中：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain"># 拿到 pod 中 nginx 的容器 id</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ kubectl describe pod tcpbench-6484d4b457-847gl | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">49b4135534dae77ce5151c6c7db4d528f05b69b0c6f8b9dd037ec4e7043c113e</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"># 通过容器 id 拿到 nginx 进程 pid</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ docker inspect -f {{.State.Pid}} 49b4135534dae77ce5151c6c7db4d528f05b69b0c6f8b9dd037ec4e7043c113e</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">3985</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"># 进入 nginx 进程所在的 netns</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ nsenter -n -t 3985</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"># 查看容器 netns 中的网卡信息，确认下</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ ip a</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    inet 127.0.0.1/8 scope host lo</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">       valid_lft forever preferred_lft forever</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">3: eth0@if11: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    link/ether 56:04:c7:28:b0:3c brd ff:ff:ff:ff:ff:ff link-netnsid 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    inet 172.26.0.8/26 scope global eth0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">       valid_lft forever preferred_lft forever</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>使用 tcpdump 指定端口 24568 抓容器 netns 中 eth0 网卡的包:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">tcpdump -i eth0 -nnnn -ttt port 24568</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>在其它节点准备使用 nc 指定源端口为 24568 向容器发包：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">nc -u 24568 172.16.1.21 80</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>观察抓包结果：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:00.000000 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000206334 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:01.032218 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000207366 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:02.011962 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000209378 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:04.127943 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000213506 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:08.192056 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000221698 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:16.127983 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000237826 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">00:00:33.791988 IP 10.0.0.3.24568 &gt; 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000271618 ecr 0,nop,wscale 9], length 0</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>SYN 包到容器内网卡了，但容器没回 ACK，像是报文到达容器内的网卡后就被丢了。看样子跟防火墙应该也没什么关系，也检查了容器 netns 内的 iptables 规则，是空的，没问题。</p>
<p>排除是 iptables 规则问题，在容器 netns 中使用 <code>netstat -s</code> 检查下是否有丢包统计:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ netstat -s | grep -E 'overflow|drop'</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    12178939 times the listen queue of a socket overflowed</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    12247395 SYNs to LISTEN sockets dropped</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>果然有丢包，为了理解这里的丢包统计，我深入研究了一下，下面插播一些相关知识。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="syn-queue-与-accept-queue">syn queue 与 accept queue<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#syn-queue-%E4%B8%8E-accept-queue" class="hash-link" aria-label="syn queue 与 accept queue的直接链接" title="syn queue 与 accept queue的直接链接">​</a></h2>
<p>Linux 进程监听端口时，内核会给它对应的 socket 分配两个队列：</p>
<ul>
<li>syn queue: 半连接队列。server 收到 SYN 后，连接会先进入 <code>SYN_RCVD</code> 状态，并放入 syn queue，此队列的包对应还没有完全建立好的连接（TCP 三次握手还没完成）。</li>
<li>accept queue: 全连接队列。当 TCP 三次握手完成之后，连接会进入 <code>ESTABELISHED</code> 状态并从 syn queue 移到 accept queue，等待被进程调用 <code>accept()</code> 系统调用 "拿走"。</li>
</ul>
<blockquote>
<p>注意：这两个队列的连接都还没有真正被应用层接收到，当进程调用 <code>accept()</code> 后，连接才会被应用层处理，具体到我们这个问题的场景就是 nginx 处理 HTTP 请求。</p>
</blockquote>
<p>为了更好理解，可以看下这张 TCP 连接建立过程的示意图：</p>
<p><img decoding="async" loading="lazy" src="https://imroc.cc/assets/blog/troubleshooting-k8s-network/backlog.png" alt="" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="listen-与-accept">listen 与 accept<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#listen-%E4%B8%8E-accept" class="hash-link" aria-label="listen 与 accept的直接链接" title="listen 与 accept的直接链接">​</a></h2>
<p>不管使用什么语言和框架，在写 server 端应用时，它们的底层在监听端口时最终都会调用 <code>listen()</code> 系统调用，处理新请求时都会先调用 <code>accept()</code> 系统调用来获取新的连接，然后再处理请求，只是有各自不同的封装而已，以 go 语言为例：</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)">// 调用 listen 监听端口</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">l</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> net</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Listen</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"tcp"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">":80"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token function" style="color:rgb(220, 220, 170)">panic</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">for</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token comment" style="color:rgb(106, 153, 85)">// 不断调用 accept 获取新连接，如果 accept queue 为空就一直阻塞</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	conn</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">:=</span><span class="token plain"> l</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Accept</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> err </span><span class="token operator" style="color:rgb(212, 212, 212)">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#569CD6">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		log</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token function" style="color:rgb(220, 220, 170)">Println</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token string" style="color:rgb(206, 145, 120)">"accept error:"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">continue</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token comment" style="color:rgb(106, 153, 85)">// 每来一个新连接意味着一个新请求，启动协程处理请求</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">go</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">handle</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">conn</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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="linux-的-backlog">Linux 的 backlog<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#linux-%E7%9A%84-backlog" class="hash-link" aria-label="Linux 的 backlog的直接链接" title="Linux 的 backlog的直接链接">​</a></h2>
<p>内核既然给监听端口的 socket 分配了 syn queue 与 accept queue 两个队列，那它们有大小限制吗？可以无限往里面塞数据吗？当然不行！ 资源是有限的，尤其是在内核态，所以需要限制一下这两个队列的大小。那么它们的大小是如何确定的呢？我们先来看下 listen 这个系统调用:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">int listen(int sockfd, int backlog)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>可以看到，能够传入一个整数类型的 <code>backlog</code> 参数，我们再通过 <code>man listen</code> 看下解释：</p>
<p><code>The behavior of the backlog argument on TCP sockets changed with Linux 2.2.  Now it specifies the queue length for completely established sockets waiting to  be  accepted,  instead  of  the  number  of  incomplete  connection requests.   The  maximum  length  of  the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog.  When syncookies are enabled there is no logical maximum length and this setting is ignored.  See tcp(7) for more information. </code></p>
<p><code>If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128.  In kernels before 2.4.25, this limit  was  a  hard  coded value, SOMAXCONN, with the value 128.</code></p>
<p>继续深挖了一下源码，结合这里的解释提炼一下：</p>
<ul>
<li>listen 的 backlog 参数同时指定了 socket 的 syn queue 与 accept queue 大小。</li>
<li>accept queue 最大不能超过 <code>net.core.somaxconn</code> 的值，即:<!-- -->
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">max accept queue size = min(backlog, net.core.somaxconn)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>
</li>
<li>如果启用了 syncookies (net.ipv4.tcp_syncookies=1)，当 syn queue 满了，server 还是可以继续接收 <code>SYN</code> 包并回复 <code>SYN+ACK</code> 给 client，只是不会存入 syn queue 了。因为会利用一套巧妙的 syncookies 算法机制生成隐藏信息写入响应的 <code>SYN+ACK</code> 包中，等 client 回 <code>ACK</code> 时，server 再利用 syncookies 算法校验报文，校验通过后三次握手就顺利完成了。所以如果启用了 syncookies，syn queue 的逻辑大小是没有限制的，</li>
<li>syncookies 通常都是启用了的，所以一般不用担心 syn queue 满了导致丢包。syncookies 是为了防止 SYN Flood 攻击 (一种常见的 DDoS 方式)，攻击原理就是 client 不断发 SYN 包但不回最后的 ACK，填满 server 的 syn queue 从而无法建立新连接，导致 server 拒绝服务。</li>
<li>如果 syncookies 没有启用，syn queue 的大小就有限制，除了跟 accept queue 一样受 <code>net.core.somaxconn</code> 大小限制之外，还会受到 <code>net.ipv4.tcp_max_syn_backlog</code> 的限制，即:<!-- -->
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">max syn queue size = min(backlog, net.core.somaxconn, net.ipv4.tcp_max_syn_backlog)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>
</li>
</ul>
<p>4.3 及其之前版本的内核，syn queue 的大小计算方式跟现在新版内核这里还不一样，详细请参考 commit <a href="https://github.com/torvalds/linux/commit/ef547f2ac16bd9d77a780a0e7c70857e69e8f23f#diff-56ecfd3cd70d57cde321f395f0d8d743L43" target="_blank" rel="noopener noreferrer">ef547f2ac16b</a></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="队列溢出">队列溢出<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E9%98%9F%E5%88%97%E6%BA%A2%E5%87%BA" class="hash-link" aria-label="队列溢出的直接链接" title="队列溢出的直接链接">​</a></h2>
<p>毫无疑问，在队列大小有限制的情况下，如果队列满了，再有新连接过来肯定就有问题。</p>
<p>翻下 linux 源码，看下处理 SYN 包的部分，在 <code>net/ipv4/tcp_input.c</code> 的 <code>tcp_conn_request</code> 函数:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">net</span><span class="token operator" style="color:rgb(212, 212, 212)">-&gt;</span><span class="token plain">ipv4</span><span class="token punctuation" style="color:rgb(212, 212, 212)">.</span><span class="token plain">sysctl_tcp_syncookies </span><span class="token operator" style="color:rgb(212, 212, 212)">==</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">2</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">||</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">     </span><span class="token function" style="color:rgb(220, 220, 170)">inet_csk_reqsk_queue_is_full</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">!</span><span class="token plain">isn</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	want_cookie </span><span class="token operator" style="color:rgb(212, 212, 212)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">tcp_syn_flood_action</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> rsk_ops</span><span class="token operator" style="color:rgb(212, 212, 212)">-&gt;</span><span class="token plain">slab_name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">!</span><span class="token plain">want_cookie</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">		</span><span class="token keyword" style="color:#C586C0">goto</span><span class="token plain"> drop</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">sk_acceptq_is_full</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token function" style="color:rgb(220, 220, 170)">NET_INC_STATS</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">sock_net</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> LINUX_MIB_LISTENOVERFLOWS</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token keyword" style="color:#C586C0">goto</span><span class="token plain"> drop</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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><code>goto drop</code> 最终会走到 <code>tcp_listendrop</code> 函数，实际上就是将 <code>ListenDrops</code> 计数器 +1:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token keyword" style="color:#C586C0">static</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">inline</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">void</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">tcp_listendrop</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">const</span><span class="token plain"> </span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(78, 201, 176)">sock</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token function" style="color:rgb(220, 220, 170)">atomic_inc</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token keyword" style="color:#C586C0">struct</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(78, 201, 176)">sock</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">*</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token operator" style="color:rgb(212, 212, 212)">-&gt;</span><span class="token plain">sk_drops</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">	</span><span class="token function" style="color:rgb(220, 220, 170)">__NET_INC_STATS</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">sock_net</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> LINUX_MIB_LISTENDROPS</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>大致可以看出来，对于 SYN 包：</p>
<ul>
<li>如果 syn queue 满了并且没有开启 syncookies 就丢包，并将 <code>ListenDrops</code> 计数器 +1。</li>
<li>如果 accept queue 满了也会丢包，并将 <code>ListenOverflows</code> 和 <code>ListenDrops</code> 计数器 +1。</li>
</ul>
<p>而我们前面排查问题通过 <code>netstat -s</code> 看到的丢包统计，其实就是对应的 <code>ListenOverflows</code> 和 <code>ListenDrops</code> 这两个计数器。</p>
<p>除了用 <code>netstat -s</code>，还可以使用 <code>nstat -az</code> 直接看系统内各个计数器的值:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ nstat -az | grep -E 'TcpExtListenOverflows|TcpExtListenDrops'</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">TcpExtListenOverflows           12178939              0.0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">TcpExtListenDrops               12247395              0.0</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>另外，对于低版本内核，当 accept queue 满了，并不会完全丢弃 SYN 包，而是对 SYN 限速。把内核源码切到 3.10 版本，看 <code>net/ipv4/tcp_ipv4.c</code> 中 <code>tcp_v4_conn_request</code> 函数:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)">/* Accept backlog is full. If we have already queued enough</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)"> * of warm entries in syn queue, drop request. It is better than</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)"> * clogging syn queue with openreqs with exponentially increasing</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)"> * timeout.</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)"> */</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token keyword" style="color:#C586C0">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">sk_acceptq_is_full</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:rgb(220, 220, 170)">inet_csk_reqsk_queue_young</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">1</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token function" style="color:rgb(220, 220, 170)">NET_INC_STATS_BH</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token function" style="color:rgb(220, 220, 170)">sock_net</span><span class="token punctuation" style="color:rgb(212, 212, 212)">(</span><span class="token plain">sk</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> LINUX_MIB_LISTENOVERFLOWS</span><span class="token punctuation" style="color:rgb(212, 212, 212)">)</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token keyword" style="color:#C586C0">goto</span><span class="token plain"> drop</span><span class="token punctuation" style="color:rgb(212, 212, 212)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token punctuation" style="color:rgb(212, 212, 212)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>其中 <code>inet_csk_reqsk_queue_young(sk) &gt; 1</code> 的条件实际就是用于限速，仿佛在对 client 说: 哥们，你慢点！我的 accept queue 都满了，即便咱们握手成功，连接也可能放不进去呀。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="回到问题上来">回到问题上来<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E5%9B%9E%E5%88%B0%E9%97%AE%E9%A2%98%E4%B8%8A%E6%9D%A5" class="hash-link" aria-label="回到问题上来的直接链接" title="回到问题上来的直接链接">​</a></h2>
<p>总结之前观察到两个现象：</p>
<ul>
<li>容器内抓包发现收到 client 的 SYN，但 nginx 没回包。</li>
<li>通过 <code>netstat -s</code> 发现有溢出和丢包的统计 (<code>ListenOverflows</code> 与 <code>ListenDrops</code>)。</li>
</ul>
<p>根据之前的分析，我们可以推测是 syn queue 或 accept queue 满了。</p>
<p>先检查下 syncookies 配置:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ cat /proc/sys/net/ipv4/tcp_syncookies</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">1</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>确认启用了 <code>syncookies</code>，所以 syn queue 大小没有限制，不会因为 syn queue 满而丢包，并且即便没开启 <code>syncookies</code>，syn queue 有大小限制，队列满了也不会使 <code>ListenOverflows</code> 计数器 +1。</p>
<p>从计数器结果来看，<code>ListenOverflows</code> 和 <code>ListenDrops</code> 的值差别不大，所以推测很有可能是 accept queue 满了，因为当 accept queue 满了会丢 SYN 包，并且同时将 <code>ListenOverflows</code> 与 <code>ListenDrops</code> 计数器分别 +1。</p>
<p>如何验证 accept queue 满了呢？可以在容器的 netns 中执行 <code>ss -lnt</code> 看下:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ ss -lnt</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">State      Recv-Q Send-Q Local Address:Port                Peer Address:Port</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">LISTEN     129    128                *:80                             *:*</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>通过这条命令我们可以看到当前 netns 中监听 tcp 80 端口的 socket，<code>Send-Q</code> 为 128，<code>Recv-Q</code> 为 129。</p>
<p>什么意思呢？通过调研得知：</p>
<ul>
<li>对于 <code>LISTEN</code> 状态，<code>Send-Q</code> 表示 accept queue 的最大限制大小，<code>Recv-Q</code> 表示其实际大小。</li>
<li>对于 <code>ESTABELISHED</code> 状态，<code>Send-Q</code> 和 <code>Recv-Q</code> 分别表示发送和接收数据包的 buffer。</li>
</ul>
<p>所以，看这里输出结果可以得知 accept queue 满了，当 <code>Recv-Q</code> 的值比 <code>Send-Q</code> 大 1 时表明 accept queue 溢出了，如果再收到 SYN 包就会丢弃掉。</p>
<p>导致 accept queue 满的原因一般都是因为进程调用 <code>accept()</code> 太慢了，导致大量连接不能被及时 "拿走"。</p>
<p>那么什么情况下进程调用 <code>accept()</code> 会很慢呢？猜测可能是进程连接负载高，处理不过来。</p>
<p>而负载高不仅可能是 CPU 繁忙导致，还可能是 IO 慢导致，当文件 IO 慢时就会有很多 IO WAIT，在 IO WAIT 时虽然 CPU 不怎么干活，但也会占据 CPU 时间片，影响 CPU 干其它活。</p>
<p>最终进一步定位发现是 nginx pod 挂载的 nfs 服务对应的 nfs server 负载较高，导致 IO 延时较大，从而使 nginx 调用 <code>accept()</code> 变慢，accept queue 溢出，使得大量代理静态图片文件的请求被丢弃，也就导致很多图片加载不出来。</p>
<p>虽然根因不是 k8s 导致的问题，但也从中挖出一些在高并发场景下值得优化的点，请继续往下看。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="somaxconn-的默认值很小">somaxconn 的默认值很小<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#somaxconn-%E7%9A%84%E9%BB%98%E8%AE%A4%E5%80%BC%E5%BE%88%E5%B0%8F" class="hash-link" aria-label="somaxconn 的默认值很小的直接链接" title="somaxconn 的默认值很小的直接链接">​</a></h2>
<p>我们再看下之前 <code>ss -lnt</code> 的输出:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ ss -lnt</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">State      Recv-Q Send-Q Local Address:Port                Peer Address:Port</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">LISTEN     129    128                *:80                             *:*</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>仔细一看，<code>Send-Q</code> 表示 accept queue 最大的大小，才 128 ？也太小了吧！</p>
<p>根据前面的介绍我们知道，accept queue 的最大大小会受 <code>net.core.somaxconn</code> 内核参数的限制，我们看下 pod 所在节点上这个内核参数的大小:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ cat /proc/sys/net/core/somaxconn</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">32768</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>是 32768，挺大的，为什么这里 accept queue 最大大小就只有 128 了呢？</p>
<p><code>net.core.somaxconn</code> 这个内核参数是 namespace 隔离了的，我们在容器 netns 中再确认了下：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ cat /proc/sys/net/core/somaxconn</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">128</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>为什么只有 128？看下 stackoverflow <a href="https://stackoverflow.com/questions/26177059/refresh-net-core-somaxcomm-or-any-sysctl-property-for-docker-containers/26197875#26197875" target="_blank" rel="noopener noreferrer">这里</a> 的讨论:</p>
<p><code>The "net/core" subsys is registered per network namespace. And the initial value for somaxconn is set to 128.</code></p>
<p>原来新建的 netns 中 somaxconn 默认就为 128，在 <code>include/linux/socket.h</code> 中可以看到这个常量的定义:</p>
<div class="language-c codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-c codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token comment" style="color:rgb(106, 153, 85)">/* Maximum queue length specifiable by listen.  */</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token macro property directive-hash" style="color:#9CDCFE">#</span><span class="token macro property directive keyword" style="color:#C586C0">define</span><span class="token macro property" style="color:#9CDCFE"> </span><span class="token macro property macro-name" style="color:#9CDCFE">SOMAXCONN</span><span class="token macro property" style="color:#9CDCFE">	</span><span class="token macro property expression number" style="color:#B5CEA8">128</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>很多人在使用 k8s 时都没太在意这个参数，为什么大家平常在较高并发下也没发现有问题呢？</p>
<p>因为通常进程 <code>accept()</code> 都是很快的，所以一般 accept queue 基本都没什么积压的数据，也就不会溢出导致丢包了。</p>
<p>对于并发量很高的应用，还是建议将 somaxconn 调高。虽然可以进入容器 netns 后使用 <code>sysctl -w net.core.somaxconn=1024</code> 或 <code>echo 1024 &gt; /proc/sys/net/core/somaxconn</code> 临时调整，但调整的意义不大，因为容器内的进程一般在启动的时候才会调用 <code>listen()</code>，然后 accept queue 的大小就被决定了，并且不再改变。</p>
<p>下面介绍几种调整方式:</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="方式一-使用-k8s-sysctls-特性直接给-pod-指定内核参数">方式一: 使用 k8s sysctls 特性直接给 pod 指定内核参数<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E6%96%B9%E5%BC%8F%E4%B8%80-%E4%BD%BF%E7%94%A8-k8s-sysctls-%E7%89%B9%E6%80%A7%E7%9B%B4%E6%8E%A5%E7%BB%99-pod-%E6%8C%87%E5%AE%9A%E5%86%85%E6%A0%B8%E5%8F%82%E6%95%B0" class="hash-link" aria-label="方式一: 使用 k8s sysctls 特性直接给 pod 指定内核参数的直接链接" title="方式一: 使用 k8s sysctls 特性直接给 pod 指定内核参数的直接链接">​</a></h3>
<p>示例 yaml:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token key atrule" style="color:#569CD6">apiVersion</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> v1</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">kind</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> Pod</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">metadata</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> sysctl</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">example</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">spec</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">securityContext</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">sysctls</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> net.core.somaxconn</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token key atrule" style="color:#569CD6">value</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"8096"</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>有些参数是 <code>unsafe</code> 类型的，不同环境不一样，我的环境里是可以直接设置 pod 的 <code>net.core.somaxconn</code> 这个 sysctl 的。如果你的环境不行，请参考官方文档 <a href="https://kubernetes-io-vnext-staging.netlify.com/docs/tasks/administer-cluster/sysctl-cluster/#enabling-unsafe-sysctls" target="_blank" rel="noopener noreferrer">Using sysctls in a Kubernetes Cluster</a> 启用 <code>unsafe</code> 类型的 sysctl。</p>
<blockquote>
<p>注：此特性在 k8s v1.12 beta，默认开启。</p>
</blockquote>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="方式二-使用-initcontainers-设置内核参数">方式二: 使用 initContainers 设置内核参数<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E6%96%B9%E5%BC%8F%E4%BA%8C-%E4%BD%BF%E7%94%A8-initcontainers-%E8%AE%BE%E7%BD%AE%E5%86%85%E6%A0%B8%E5%8F%82%E6%95%B0" class="hash-link" aria-label="方式二: 使用 initContainers 设置��内核参数的直接链接" title="方式二: 使用 initContainers 设置内核参数的直接链接">​</a></h3>
<p>示例 yaml:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token key atrule" style="color:#569CD6">apiVersion</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> v1</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">kind</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> Pod</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">metadata</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> sysctl</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">example</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">init</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">spec</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">initContainers</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#569CD6">image</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> busybox</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">command</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> sh</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">c</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> echo 1024 </span><span class="token punctuation" style="color:rgb(212, 212, 212)">&gt;</span><span class="token plain"> /proc/sys/net/core/somaxconn</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">imagePullPolicy</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> Always</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> setsysctl</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">securityContext</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token key atrule" style="color:#569CD6">privileged</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#569CD6">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">Containers</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">...</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>
<blockquote>
<p>注: init container 需要 privileged 权限。</p>
</blockquote>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="方式三-安装-tuning-cni-插件统一设置-sysctl">方式三: 安装 tuning CNI 插件统一设置 sysctl<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E6%96%B9%E5%BC%8F%E4%B8%89-%E5%AE%89%E8%A3%85-tuning-cni-%E6%8F%92%E4%BB%B6%E7%BB%9F%E4%B8%80%E8%AE%BE%E7%BD%AE-sysctl" class="hash-link" aria-label="方式三: 安装 tuning CNI 插件统一设置 sysctl的直接链接" title="方式三: 安装 tuning CNI 插件统一设置 sysctl的直接链接">​</a></h3>
<p>tuning plugin 地址: <a href="https://github.com/containernetworking/plugins/tree/master/plugins/meta/tuning" target="_blank" rel="noopener noreferrer">https://github.com/containernetworking/plugins/tree/master/plugins/meta/tuning</a></p>
<p>CNI 配置示例:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">{</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  "name": "mytuning",</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  "type": "tuning",</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  "sysctl": {</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">          "net.core.somaxconn": "1024"</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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="nginx-的-backlog">nginx 的 backlog<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#nginx-%E7%9A%84-backlog" class="hash-link" aria-label="nginx 的 backlog的直接链接" title="nginx 的 backlog的直接链接">​</a></h2>
<p>我们使用方式一尝试给 nginx pod 的 somaxconn 调高到 8096 后观察:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ ss -lnt</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">State      Recv-Q Send-Q Local Address:Port                Peer Address:Port</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">LISTEN     512    511                *:80                             *:*</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>WTF? 还是溢出了，而且调高了 somaxconn 之后虽然 accept queue 的最大大小 (<code>Send-Q</code>) 变大了，但跟 8096 还差很远呀！</p>
<p>在经过一番研究，发现 nginx 在 <code>listen()</code> 时并没有读取 somaxconn 作为 backlog 默认值传入，它有自己的默认值，也支持在配置里改。通过 <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html" target="_blank" rel="noopener noreferrer">ngx_http_core_module</a> 的官方文档我们可以看到它在 linux 下的默认值就是 511:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">backlog=number</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">   sets the backlog parameter in the listen() call that limits the maximum length for the queue of pending connections. By default, backlog is set to -1 on FreeBSD, DragonFly BSD, and macOS, and to 511 on other platforms.</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>配置示例:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">listen  80  default  backlog=1024;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>所以，在容器中使用 nginx 来支撑高并发的业务时，记得要同时调整下 <code>net.core.somaxconn</code> 内核参数和 <code>nginx.conf</code> 中的 backlog 配置。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="参考资料">参考资料<a href="https://imroc.cc/blog/2020/01/12/kubernetes-overflow-and-drop#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" class="hash-link" aria-label="参考资料的直接链接" title="参考资料的直接链接">​</a></h2>
<ul>
<li>Using sysctls in a Kubernetes Cluster: <a href="https://kubernetes-io-vnext-staging.netlify.com/docs/tasks/administer-cluster/sysctl-cluster/" target="_blank" rel="noopener noreferrer">https://kubernetes-io-vnext-staging.netlify.com/docs/tasks/administer-cluster/sysctl-cluster/</a></li>
<li>SYN packet handling in the wild: <a href="https://blog.cloudflare.com/syn-packet-handling-in-the-wild/" target="_blank" rel="noopener noreferrer">https://blog.cloudflare.com/syn-packet-handling-in-the-wild/</a></li>
</ul>]]></content:encoded>
            <category>kubernetes</category>
            <category>network</category>
            <category>troubleshooting</category>
        </item>
        <item>
            <title><![CDATA[Kubernetes 疑难杂症排查分享: 诡异的 No route to host]]></title>
            <link>https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host</link>
            <guid>https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host</guid>
            <pubDate>Sun, 15 Dec 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[之前发过一篇干货满满的爆火文章 Kubernetes 网络疑难杂症排查分享，包含多个疑难杂症的排查案例分享，信息量巨大。这次我又带来了续集，只讲一个案例，但信息量也不小，Are you ready ?]]></description>
            <content:encoded><![CDATA[<p>之前发过一篇干货满满的爆火文章 <a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network">Kubernetes 网络疑难杂症排查分享</a>，包含多个疑难杂症的排查案例分享，信息量巨大。这次我又带来了续集，只讲一个案例，但信息量也不小，Are you ready ?</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="问题反馈">问题反馈<a href="https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host#%E9%97%AE%E9%A2%98%E5%8F%8D%E9%A6%88" class="hash-link" aria-label="问题反馈的直接链接" title="问题反馈的直接链接">​</a></h2>
<p>有用户反馈 Deployment 滚动更新的时候，业务日志偶尔会报 "No route to host" 的错误。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="分析">分析<a href="https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host#%E5%88%86%E6%9E%90" class="hash-link" aria-label="分析的直接链接" title="分析的直接链接">​</a></h2>
<p>之前没遇到滚动更新会报 "No route to host" 的问题，我们先看下滚动更新导致连接异常有哪些常见的报错:</p>
<ul>
<li>
<p><code>Connection reset by peer</code>: 连接被重置。通常是连接建立过，但 server 端发现 client 发的包不对劲就返回 RST，应用层就报错连接被重置。比如在 server 滚动更新过程中，client 给 server 发的请求还没完全结束，或者本身是一个类似 grpc 的多路复用长连接，当 server 对应的旧 Pod 删除(没有做优雅结束，停止时没有关闭连接)，新 Pod 很快创建启动并且刚好有跟之前旧 Pod 一样的 IP，这时 kube-proxy 也没感知到这个 IP 其实已经被删除然后又被重建了，针对这个 IP 的规则就不会更新，旧的连接依然发往这个 IP，但旧 Pod 已经不在了，后面继续发包时依然转发给这个 Pod IP，最终会被转发到这个有相同 IP 的新 Pod 上，而新 Pod 收到此包时检查报文发现不对劲，就返回 RST 给 client 告知将连接重置。针对这种情况，建议应用自身处理好优雅结束：Pod 进入 Terminating 状态后会发送 <code>SIGTERM</code> 信号给业务进程，业务进程的代码需处理这个信号，在进程退出前关闭所有连接。</p>
</li>
<li>
<p><code>Connection refused</code>: 连接被拒绝。通常是连接还没建立，client 正在发 SYN 包请求建立连接，但到了 server 之后发现端口没监听，内核就返回 RST 包，然后应用层就报错连接被拒绝。比如在 server 滚动更新过程中，旧的 Pod 中的进程很快就停止了(网卡还未完全销毁)，但 client 所在节点的 iptables/ipvs 规则还没更新，包就可能会被转发到了这个停止的 Pod (由于 k8s 的 controller 模式，从 Pod 删除到 service 的 endpoint 更新，再到 kube-proxy watch 到更新并更新 节点上的 iptables/ipvs 规则，这个过程是异步的，中间存在一点时间差，所以有可能存在 Pod 中的进程已经没有监听，但 iptables/ipvs 规则还没更新的情况)。针对这种情况，建议给容器加一个 preStop，在真正销毁 Pod 之前等待一段时间，留时间给 kube-proxy 更新转发规则，更新完之后就不会再有新连接往这个旧 Pod 转发了，preStop 示例:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token key atrule" style="color:#569CD6">lifecycle</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">preStop</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">exec</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token key atrule" style="color:#569CD6">command</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> /bin/bash</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">c</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> sleep 30</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>另外，还可能是新的 Pod 启动比较慢，虽然状态已经 Ready，但实际上可能端口还没监听，新的请求被转发到这个还没完全启动的 Pod 就会报错连接被拒绝。针对这种情况，建议给容器加就绪检查 (readinessProbe)，让容器真正启动完之后才将其状态置为 Ready，然后 kube-proxy 才会更新转发规则，这样就能保证新的请求只被转发到完全启动的 Pod，readinessProbe 示例:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token key atrule" style="color:#569CD6">readinessProbe</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">httpGet</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">path</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> /healthz</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">port</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">80</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">httpHeaders</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> X</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">Custom</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">Header</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token key atrule" style="color:#569CD6">value</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> Awesome</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">initialDelaySeconds</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">15</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">timeoutSeconds</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">1</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>
</li>
<li>
<p><code>Connection timed out</code>: 连接超时。通常是连接还没建立，client 发 SYN 请求建立连接一直等到超时时间都没有收到 ACK，然后就报错连接超时。这个可能场景跟前面 <code>Connection refused</code> 可能的场景类似，不同点在于端口有监听，但进程无法正常响应了: 转发规则还没更新，旧 Pod 的进程正在停止过程中，虽然端口有监听，但已经不响应了；或者转发规则更新了，新 Pod 端口也监听了，但还没有真正就绪，还没有能力处理新请求。针对这些情况的建议跟前面一样：加 preStop 和 readinessProbe。</p>
</li>
</ul>
<p>下面我们来继续分析下滚动更新时发生 <code>No route to host</code> 的可能情况。</p>
<p>这个报错很明显，IP 无法路由，通常是将报文发到了一个已经彻底销毁的 Pod (网卡已经不在)。不可能发到一个网卡还没创建好的 Pod，因为即便不加存活检查，也是要等到 Pod 网络初始化完后才可能 Ready，然后 kube-proxy 才会更新转发规则。</p>
<p>什么情况下会转发到一个已经彻底销毁的 Pod？ 借鉴前面几种滚动更新的报错分析，我们推测应该是 Pod 很快销毁了但转发规则还没更新，从而新的请求被转发了这个已经销毁的 Pod，最终报文到达这个 Pod 所在 PodCIDR 的 Node 上时，Node 发现本机已经没有这个 IP 的容器，然后 Node 就返回 ICMP 包告知 client 这个 IP 不可达，client 收到 ICMP 后，应用层就会报错 "No route to host"。</p>
<p>所以根据我们的分析，关键点在于 Pod 销毁太快，转发规则还没来得及更新，导致后来的请求被转发到已销毁的 Pod。针对这种情况，我们可以给容器加一个 preStop，留时间给 kube-proxy 更新转发规则来解决，参考 《Kubernetes实践指南》中的部分章节: <a href="https://k8s.imroc.cc/best-practice/high-availability-deployment-of-applications#smooth-update-using-prestophook-and-readinessprobe" target="_blank" rel="noopener noreferrer">https://k8s.imroc.cc/best-practice/high-availability-deployment-of-applications#smooth-update-using-prestophook-and-readinessprobe</a></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="问题没有解决">问题没有解决<a href="https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host#%E9%97%AE%E9%A2%98%E6%B2%A1%E6%9C%89%E8%A7%A3%E5%86%B3" class="hash-link" aria-label="问题没有解决的直接链接" title="问题没有解决的直接链接">​</a></h2>
<p>我们自己没有复现用户的 "No route to host" 的问题，可能是复现条件比较苛刻，最后将我们上面理论上的分析结论作为解决方案给到了用户。</p>
<p>但用户尝试加了 preStop 之后，问题依然存在，服务滚动更新时偶尔还是会出现 "No route to host"。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="深入分析">深入分析<a href="https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host#%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90" class="hash-link" aria-label="深入分析的直接链接" title="深入分析的直接链接">​</a></h2>
<p>为了弄清楚根本原因，我们请求用户协助搭建了一个可以复现问题的测试环境，最终这个问题在测试环境中可以稳定复现。</p>
<p>仔细观察，实际是部署两个服务：ServiceA 和 ServiceB。使用 ab 压测工具去压测 ServiceA （短连接），然后 ServiceA 会通过 RPC 调用 ServiceB (短连接)，滚动更新的是 ServiceB，报错发生在 ServiceA 调用 ServiceB 这条链路。</p>
<p>在 ServiceB 滚动更新期间，新的 Pod Ready 了之后会被添加到 IPVS 规则的 RS 列表，但旧的 Pod 不会立即被踢掉，而是将新的 Pod 权重置为1，旧的置为 0，通过在 client 所在节点查看 IPVS 规则可以看出来:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">root@VM-0-3-ubuntu:~# ipvsadm -ln -t 172.16.255.241:80</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">Prot LocalAddress:Port Scheduler Flags</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  -&gt; RemoteAddress:Port           Forward Weight ActiveConn InActConn</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">TCP  172.16.255.241:80 rr</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  -&gt; 172.16.8.106:80              Masq    0      5          14048</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  -&gt; 172.16.8.107:80              Masq    1      2          243</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>为什么不立即踢掉旧的 Pod 呢？因为要支持优雅结束，让存量的连接处理完，等存量连接全部结束了再踢掉它(ActiveConn+InactiveConn=0)，这个逻辑可以通过这里的代码确认：<a href="https://github.com/kubernetes/kubernetes/blob/v1.17.0/pkg/proxy/ipvs/graceful_termination.go#L170" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/kubernetes/blob/v1.17.0/pkg/proxy/ipvs/graceful_termination.go#L170</a></p>
<p>然后再通过 <code>ipvsadm -lnc | grep 172.16.8.106</code> 发现旧 Pod 上的连接大多是 <code>TIME_WAIT</code> 状态，这个也容易理解：因为 ServiceA 作为 client 发起短连接请求调用 ServiceB，调用完成就会关闭连接，TCP 三次挥手后进入 <code>TIME_WAIT</code> 状态，等待 2*MSL (2 分钟) 的时长再清理连接。</p>
<p>经过上面的分析，看起来都是符合预期的，那为什么还会出现 "No route to host" 呢？难道权重被置为 0 之后还有新连接往这个旧 Pod 转发？我们来抓包看下：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">root@VM-0-3-ubuntu:~# tcpdump -i eth0 host 172.16.8.106 -n -tttt</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">tcpdump: verbose output suppressed, use -v or -vv for full protocol decode</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">2019-12-13 11:49:47.319093 IP 10.0.0.3.36708 &gt; 172.16.8.106.80: Flags [S], seq 3988339656, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">2019-12-13 11:49:47.319133 IP 10.0.0.3.36706 &gt; 172.16.8.106.80: Flags [S], seq 109196945, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">2019-12-13 11:49:47.319144 IP 10.0.0.3.36704 &gt; 172.16.8.106.80: Flags [S], seq 1838682063, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">2019-12-13 11:49:47.319153 IP 10.0.0.3.36702 &gt; 172.16.8.106.80: Flags [S], seq 1591982963, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>果然是！即使权重为 0，仍然会尝试发 SYN 包跟这个旧 Pod 建立连接，但永远无法收到 ACK，因为旧 Pod 已经销毁了。为什么会这样呢？难道是 IPVS 内核模块的调度算法有问题？尝试去看了下 linux 内核源码，并没有发现哪个调度策略的实现函数会将新连接调度到权重为 0 的 rs 上。</p>
<p>这就奇怪了，可能不是调度算法的问题？继续尝试看更多的代码，主要是 <code>net/netfilter/ipvs/ip_vs_core.c</code> 中的 <code>ip_vs_in</code> 函数，也就是 IPVS 模块处理报文的主要入口，发现它会先在本地连接转发表看这个包是否已经有对应的连接了（匹配五元组），如果有就说明它不是新连接也就不会调度，直接发给这个连接对应的之前已经调度过的 rs (也不会判断权重)；如果没匹配到说明这个包是新的连接，就会走到调度这里 (rr, wrr 等调度策略)，这个逻辑看起来也没问题。</p>
<p>那为什么会转发到权重为 0 的 rs ？难道是匹配连接这里出问题了？新的连接匹配到了旧的连接？我开始做实验验证这个猜想，修改一下这里的逻辑：检查匹配到的连接对应的 rs 如果权重为 0，则重新调度。然后重新编译和加载 IPVS 内核模块，再重新压测一下，发现问题解决了！没有报 "No route to host" 了。</p>
<p>虽然通过改内核源码解决了，但我知道这不是一个好的解决方案，它会导致 IPVS 不支持连接的优雅结束，因为不再转发包给权重为 0 的 rs，存量的连接就会立即中断。</p>
<p>继续陷入深思......</p>
<p>这个实验只是证明了猜想：新连接匹配到了旧连接。那为什么会这样呢？难道新连接报文的五元组跟旧连接的相同了？</p>
<p>经过一番思考，发现这个是有可能的。因为 ServiceA 作为 client 请求 ServiceB，不同请求的源 IP 始终是相同的，关键点在于源端口是否可能相同。由于 ServiceA 向 ServiceB 发起大量短连接，ServiceA 所在节点就会有大量 <code>TIME_WAIT</code> 状态的连接，需要等 2 分钟 (2*MSL) 才会清理，而由于连接量太大，每次发起的连接都会占用一个源端口，当源端口不够用了，就会重用 <code>TIME_WAIT</code> 状态连接的源端口，这个时候当报文进入 IPVS 模块，检测到它的五元组跟本地连接转发表中的某个连接一致(<code>TIME_WAIT</code> 状态)，就以为它是一个存量连接，然后直接将报文转发给这个连接之前对应的 rs 上，然而这个 rs 对应的 Pod 早已销毁，所以抓包看到的现象是将 SYN 发给了旧 Pod，并且无法收到 ACK，伴随着返回 ICMP 告知这个 IP 不可达，也被应用解释为 "No route to host"。</p>
<p>后来无意间又发现一个还在 open 状态的 issue，虽然还没提到 "No route to host" 关键字，但讨论的跟我们这个其实是同一个问题。我也参与了讨论，有兴趣的同学可以看下：<a href="https://github.com/kubernetes/kubernetes/issues/81775" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/kubernetes/issues/81775</a></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="总结">总结<a href="https://imroc.cc/blog/2019/12/15/kubernetes-no-route-to-host#%E6%80%BB%E7%BB%93" class="hash-link" aria-label="总结的直接链接" title="总结的直接链接">​</a></h2>
<p>这个问题通常发生的场景就是类似于我们测试环境这种：ServiceA 对外提供服务，当外部发起请求，ServiceA 会通过 rpc 或 http 调用 ServiceB，如果外部请求量变大，ServiceA 调用 ServiceB 的量也会跟着变大，大到一定程度，ServiceA 所在节点源端口不够用，复用 <code>TIME_WAIT</code> 状态连接的源端口，导致五元组跟 IPVS 里连接转发表中的 <code>TIME_WAIT</code> 连接相同，IPVS 就认为这是一个存量连接的报文，就不判断权重直接转发给之前的 rs，导致转发到已销毁的 Pod，从而发生 "No route to host"。</p>
<p>如何规避？集群规模小可以使用 iptables 模式，如果需要使用 ipvs 模式，可以增加 ServiceA 的副本，并且配置反亲和性 (podAntiAffinity)，让 ServiceA 的 Pod 部署到不同节点，分摊流量，避免流量集中到某一个节点，导致调用 ServiceB 时源端口复用。</p>
<p>如何彻底解决？暂时还没有一个完美的方案。</p>
<p>Issue 85517 讨论让 kube-proxy 支持自定义配置几种连接状态的超时时间，但这对 <code>TIME_WAIT</code> 状态无效。</p>
<p>Issue 81308 讨论 IVPS 的优雅结束是否不考虑不活跃的连接 (包括 <code>TIME_WAIT</code> 状态的连接)，也就是只考虑活跃连接，当活跃连接数为 0 之后立即踢掉 rs。这个确实可以更快的踢掉 rs，但无法让优雅结束做到那么优雅了，并且有人测试了，即便是不考虑不活跃连接，当请求量很大，还是不能很快踢掉 rs，因为源端口复用还是会导致不断有新的连接占用旧的连接，在较新的内核版本，<code>SYN_RECV</code> 状态也被视为活跃连接，所以活跃连接数还是不会很快降到 0。</p>
<p>这个问题的终极解决方案该走向何方，我们拭目以待，感兴趣的同学可以持续关注 issue 81775 并参与讨论。想学习更多 K8S 知识，可以关注本人的开源书《Kubernetes实践指南》: <a href="https://k8s.imroc.cc/" target="_blank" rel="noopener noreferrer">https://k8s.imroc.cc</a></p>]]></content:encoded>
            <category>kubernetes</category>
            <category>network</category>
            <category>troubleshooting</category>
        </item>
        <item>
            <title><![CDATA[k8s v1.17 新特性预告: 拓扑感知服务路由]]></title>
            <link>https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing</link>
            <guid>https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing</guid>
            <pubDate>Tue, 26 Nov 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[今天给大家介绍下我参与开发的一个 k8s v1.17 新特性: 拓扑感知服务路由。]]></description>
            <content:encoded><![CDATA[<p>今天给大家介绍下我参与开发的一个 k8s v1.17 新特性: 拓扑感知服务路由。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="名词解释">名词解释<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E5%90%8D%E8%AF%8D%E8%A7%A3%E9%87%8A" class="hash-link" aria-label="名词解释的直接链接" title="名词解释的直接链接">​</a></h2>
<ul>
<li>拓扑域: 表示在集群中的某一类 "地方"，比如某节点、某机架、某可用区或某地域等，这些都可以作为某种拓扑域。</li>
<li>endpoint: k8s 某个服务的某个 ip+port，通常是 pod 的 ip+port。</li>
<li>service: k8s 的 service 资源(服务)，关联一组 endpoint ，访问 service 会被转发到关联的某个 endpoint 上。</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="背景">背景<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="背景的直接链接" title="背景的直接链接">​</a></h2>
<p>拓扑感知服务路由，此特性最初由杜军大佬提出并设计。为什么要设计此特性呢？想象一下，k8s 集群节点分布在不同的地方，service 对应的 endpoints 分布在不同节点，传统转发策略会对所有 endpoint 做负载均衡，通常会等概率转发，当访问 service 时，流量就可能被分散打到这些不同的地方。虽然 service 转发做了负载均衡，但如果 endpoint 距离比较远，流量转发过去网络时延就相对比较高，会影响网络性能，在某些情况下甚至还可能会付出额外的流量费用。要是如能实现 service 就近转发 endpoint，是不是就可以实现降低网络时延，提升网络性能了呢？是的！这也正是该特性所提出的目的和意义。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="k8s-亲和性">k8s 亲和性<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#k8s-%E4%BA%B2%E5%92%8C%E6%80%A7" class="hash-link" aria-label="k8s 亲和性的直接链接" title="k8s 亲和性的直接链接">​</a></h2>
<p>service 的就近转发实际就是一种网络的亲和性，倾向于转发到离自己比较近的 endpoint。在此特性之前，已经在调度和存储方面有一些亲和性的设计与实现:</p>
<ul>
<li>节点亲和性 (Node Affinity): 让 Pod 被调度到符合一些期望条件的 Node 上，比如限制调度到某一可用区，或者要求节点支持 GPU，这算是调度亲和，调度结果取决于节点属性。</li>
<li>Pod 亲和性与反亲和性 (Pod Affinity/AntiAffinity): 让一组 Pod 调度到同一拓扑域的节点上，或者打散到不同拓扑域的节点， 这也算是调度亲和，调度结果取决于其它 Pod。</li>
<li>数据卷拓扑感知调度 (Volume Topology-aware Scheduling): 让 Pod 只被调度到符合其绑定的存储所在拓扑域的节点上，这算是调度与存储的亲和，调度结果取决于存储的拓扑域。</li>
<li>本地数据卷 (Local Persistent Volume): 让 Pod 使用本地数据卷，比如高性能 SSD，在某些需要高 IOPS 低时延的场景很有用，它还会保证 Pod 始终被调度到同一节点，数据就不会不丢失，这也算是调度与存储的亲和，调度结果取决于存储所在节点。</li>
<li>数据卷拓扑感知动态创建 (Topology-Aware Volume Dynamic Provisioning): 先调度 Pod，再根据 Pod 所在节点的拓扑域来创建存储，这算是存储与调度的亲和，存储的创建取决于调度的结果。</li>
</ul>
<p>而 k8s 目前在网络方面还没有亲和性能力，拓扑感知服务路由这个新特性恰好可以补齐这个的空缺，此特性使得 service 可以实现就近转发而不是所有 endpoint 等概率转发。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="如何实现">如何实现<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="如何实现的直接链接" title="如何实现的直接链接">​</a></h2>
<p>我们知道，service 转发主要是 node 上的 kube-proxy 进程通过 watch apiserver 获取 service 对应的 endpoint，再写入 iptables 或 ipvs 规则来实现的; 对于 headless service，主要是通过 kube-dns 或 coredns 动态解析到不同 endpoint ip 来实现的。实现 service 就近转发的关键点就在于如何将流量转发到跟当前节点在同一拓扑域的 endpoint 上，也就是会进行一次 endpoint 筛选，选出一部分符合当前节点拓扑域的 endpoint 进行转发。</p>
<p>那么如何判断 endpoint 跟当前节点是否在同一拓扑域里呢？只要能获取到 endpoint 的拓扑信息，用它跟当前节点拓扑对比下就可以知道了。那又如何获取 endpoint 的拓扑信息呢？答案是通过 endpoint 所在节点的 label，我们可以使用 node label 来描述拓扑域。</p>
<p>通常在节点初始化的时候，controller-manager 就会为节点打上许多 label，比如 <code>kubernetes.io/hostname</code> 表示节点的 hostname 来区分节点；另外，在云厂商提供的 k8s 服务，或者使用 cloud-controller-manager 的自建集群，通常还会给节点打上 <code>failure-domain.beta.kubernetes.io/zone</code> 和 <code>failure-domain.beta.kubernetes.io/region</code> 以区分节点所在可用区和所在地域，但自 v1.17 开始将会改名成 <code>topology.kubernetes.io/zone</code> 和 <code>topology.kubernetes.io/region</code>，参见 <a href="https://github.com/kubernetes/kubernetes/pull/81431" target="_blank" rel="noopener noreferrer">PR #81431</a>。</p>
<p>如何根据 endpoint 查到它所在节点的这些 label 呢？答案是通过 <code>Endpoint Slice</code>，该特性在 v1.16 发布了 alpha，在 v1.17 将会进入 beta，它相当于 Endpoint API 增强版，通过将 endpoint 做数据分片来解决大规模 endpoint 的性能问题，并且可以携带更多的信息，包括 endpoint 所在节点的拓扑信息，拓扑感知服务路由特性会通过 <code>Endpoint Slice</code> 获取这些拓扑信息实现 endpoint 筛选 (过滤出在同一拓扑域的 endpoint)，然后再转换为 iptables 或 ipvs 规则写入节点以实现拓扑感知的路由转发。</p>
<p>细心的你可能已经发现，之前每个节点上转发 service 的 iptables/ipvs 规则基本是一样的，但启用了拓扑感知服务路由特性之后，每个节点上的转发规则就可能不一样了，因为不同节点的拓扑信息不一样，导致过滤出的 endpoint 就不一样，也正是因为这样，service 转发变得不再等概率，灵活的就近转发才得以实现。</p>
<p>当前还不支持 headless service 的拓扑路由，计划在 beta 阶段支持。由于 headless service 不是通过 kube-proxy 生成转发规则，而是通过 dns 动态解析实现的，所以需要改 kube-dns/coredns 来支持这个特性。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="前提条件">前提条件<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6" class="hash-link" aria-label="前提条件的直接链接" title="前提条件的直接链接">​</a></h2>
<p>启用当前 alpha 实现的拓扑感知服务路由特性需要满足以下前提条件:</p>
<ul>
<li>集群版本在 v1.17 及其以上。</li>
<li>Kube-proxy 以 iptables 或 IPVS 模式运行 (alpha 阶段暂时只实现了这两种模式)。</li>
<li>启用了 <a href="https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/" target="_blank" rel="noopener noreferrer">Endpoint Slices</a> (此特性虽然在 v1.17 进入 beta，但没有默认开启)。</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="如何启用此特性">如何启用此特性<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E5%A6%82%E4%BD%95%E5%90%AF%E7%94%A8%E6%AD%A4%E7%89%B9%E6%80%A7" class="hash-link" aria-label="如何启用此特性的直接链接" title="如何启用此特性的直接链接">​</a></h2>
<p>给所有 k8s 组件打开 <code>ServiceTopology</code> 和 <code>EndpointSlice</code> 这两个 feature:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">--feature-gates="ServiceTopology=true,EndpointSlice=true"</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8" class="hash-link" aria-label="如何使用的直接链接" title="如何使用的直接链接">​</a></h2>
<p>在 Service spec 里加上 <code>topologyKeys</code> 字段，表示该 Service 优先顺序选用的拓扑域列表，对应节点标签的 key；当访问此 Service 时，会找是否有 endpoint 有对应 topology key 的拓扑信息并且 value 跟当前节点也一样，如果是，那就选定此 topology key 作为当前转发的拓扑域，并且筛选出其余所有在这个拓扑域的 endpoint 来进行转发；如果没有找到任何 endpoint 在当前 topology key 对应拓扑域，就会尝试第二个 topology key，依此类推；如果遍历完所有 topology key 也没有匹配到 endpoint 就会拒绝转发，就像此 service 没有后端 endpoint 一样。</p>
<p>有一个特殊的 topology key "<code>*</code>"，它可以匹配所有 endpoint，如果 <code>topologyKeys</code> 包含了 <code>*</code>，它必须在列表末尾，通常是在没有匹配到合适的拓扑域来实现就近转发时，就打消就近转发的念头，可以转发到任意 endpoint 上。</p>
<p>当前 topology key 支持以下可能的值（未来会增加更多）:</p>
<ul>
<li><code>kubernetes.io/hostname</code>: 节点的 hostname，通常将它放列表中第一个，表示如果本机有 endpoint 就直接转发到本机的 endpoint。</li>
<li><code>topology.kubernetes.io/zone</code>: 节点所在的可用区，通常将它放在 <code>kubernetes.io/hostname</code> 后面，表示如果本机没有对应 endpoint，就转发到当前可用区其它节点上的 endpoint（部分云厂商跨可用区通信会收取额外的流量费用）。</li>
<li><code>topology.kubernetes.io/region</code>: 表示节点所在的地域，表示转发到当前地域的 endpoint，这个用的应该会比较少，因为通常集群所有节点都只会在同一个地域，如果节点跨地域了，节点之间通信延时将会很高。</li>
<li><code>*</code>: 忽略拓扑域，匹配所有 endpoint，相当于一个保底策略，避免丢包，只能放在列表末尾。</li>
</ul>
<p>除此之外，还有以下约束:</p>
<ul>
<li><code>topologyKeys</code> 与 <code>externalTrafficPolicy=Local</code> 不兼容，是互斥的，如果 <code>externalTrafficPolicy</code> 为 <code>Local</code>，就不能定义 <code>topologyKeys</code>，反之亦然。</li>
<li>topology key 必须是合法的 label 格式，并且最多定义 16 个 key。</li>
</ul>
<p>这里给出一个简单的 Service 示例:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token key atrule" style="color:#569CD6">apiVersion</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> v1</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">kind</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> Service</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">metadata</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> nginx</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain"></span><span class="token key atrule" style="color:#569CD6">spec</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">type</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> ClusterIP</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">ports</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> http</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">port</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">80</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">protocol</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> TCP</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">targetPort</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token number" style="color:#B5CEA8">80</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">selector</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">app</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> nginx</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">  </span><span class="token key atrule" style="color:#569CD6">topologyKeys</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(212, 212, 212)">[</span><span class="token string" style="color:rgb(206, 145, 120)">"kubernetes.io/hostname"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"topology.kubernetes.io/zone"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(206, 145, 120)">"*"</span><span class="token punctuation" style="color:rgb(212, 212, 212)">]</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>解释: 当访问 nginx 服务时，首先看本机是否有这个服务的 endpoint，如果有就直接本机路由过去；如果没有，就看是否有 endpoint 位于当前节点所在可用区，如果有，就转发过去，如果还是没有，就转发给任意 endpoint。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926123543.png" alt="" class="img_ev3q"></p>
<p>上图就是其中一次转发的例子：Pod 访问 nginx 这个 service 时，发现本机没有 endpoint，就找当前可用区的，找到了就转发过去，也就不会考虑转发给另一可用区的 endpoint。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="背后小故事">背后小故事<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E8%83%8C%E5%90%8E%E5%B0%8F%E6%95%85%E4%BA%8B" class="hash-link" aria-label="背后小故事的直接链接" title="背后小故事的直接链接">​</a></h2>
<p>此特性的 KEP Proposal 最终被认可（合并）时的设计与当前最终的代码实现已经有一些差别，实现方案历经一变再变，但同时也推动了其它特性的发展，我来讲下这其中的故事。</p>
<p>一开始设计是在 alpha 时，让 kube-proxy 直接暴力 watch node，每个节点都有一份全局的 node 的缓存，通过 endpoint 的 <code>nodeName</code> 字段找到对应的 node 缓存，再查 node 包含的 label 就可以知道该 endpoint 的拓扑域了，但在集群节点数量多的情况下，kube-proxy 将会消耗大量资源，不过优点是实现上很简单，可以作为 alpha 阶段的实现，beta 时再从 watch node 切换到 watch 一个新设计的 PodLocator API，作为拓扑信息存储的中介，避免 watch 庞大的 node。</p>
<p>实际上一开始我也是按照 watch node 的方式，花了九牛二虎之力终于实现了这个特性，后来 v1.15 时 k8s 又支持了 metadata-only watch，参见 <a href="https://github.com/kubernetes/kubernetes/pull/71548" target="_blank" rel="noopener noreferrer">PR 71548</a>，利用此特性可以仅仅 watch node 的 metadata，而不用 watch 整个 node，可以极大减小传输和缓存的数据量，然后我就将实现切成了 watch node metadata; 即便如此，metadata 还是会更新比较频繁，主要是 <code>resourceVersion</code> 会经常变 (kubelet 经常上报 node 状态)，所以虽然 watch node metadata 比 watch node 要好，但也还是可能会造成大量不必要的网络流量，但作为 alpha 实现是可以接受的。</p>
<p>可惜在 v1.16 code freeze 之前没能将此特性合进去，只因有一点小细节还没讨论清楚。 实际在实现 watch node 方案期间，Endpoint Slice 特性就提出来了，在这个特性讨论的阶段，我们就想到了可以利用它来携带拓扑信息，以便让拓扑感知服务路由这个特性后续可以直接利用 Endpoint Slice 来获取拓扑信息，也就可以替代之前设计的 PodLocator API，但由于它还处于很早期阶段，并且代码还未合并进去，所以 alpha 阶段先不考虑 watch Endpint Slice。后来，Endpoint Slice 特性在 v1.16 发布了 alpha。</p>
<p>由于 v1.16 没能将拓扑感知服务路由特性合进去，在 v1.17 周期开始后，有更多时间来讨论小细节，并且 Endpoint Slice 代码已经合并，我就干脆直接又将实现从 watch node metadata 切成了 watch Endpint Slice，在 alpha 阶段就做了打算在 beta 阶段做的事情，终于，此特性实现代码最终合进了主干。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="结尾">结尾<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E7%BB%93%E5%B0%BE" class="hash-link" aria-label="结尾的直接链接" title="结尾的直接链接">​</a></h2>
<p>拓扑感知服务路由可以实现 service 就近转发，减少网络延时，进一步提升 k8s 的网络性能，此特性将于 k8s v1.17 发布 alpha，时间是 12 月上旬，让我们一起期待吧！k8s 网络是块难啃的硬骨头，感兴趣的同学可以看下杜军的新书 <a href="https://item.jd.com/12724298.html" target="_blank" rel="noopener noreferrer">《Kubernetes 网络权威指南》</a>，整理巩固一下 k8s 的网络知识。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="参考资料">参考资料<a href="https://imroc.cc/blog/2019/11/26/k8s117-topology-service-routing#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" class="hash-link" aria-label="参考资料的直接链接" title="参考资料的直接链接">​</a></h2>
<ul>
<li>KEP: EndpintSlice - <a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/20190603-EndpointSlice-API.md" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/20190603-EndpointSlice-API.md</a></li>
<li>Proposal: Volume Topology-aware Scheduling - <a href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/volume-topology-scheduling.md" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/volume-topology-scheduling.md</a></li>
<li>PR: Service Topology implementation for Kubernetes - <a href="https://github.com/kubernetes/kubernetes/pull/72046" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/kubernetes/pull/72046</a></li>
<li>Proposal: Inter-pod topological affinity and anti-affinity - <a href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/scheduling/podaffinity.md" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/community/blob/master/contributors/design-proposals/scheduling/podaffinity.md</a></li>
<li>Topology-Aware Volume Provisioning in Kubernetes - <a href="https://kubernetes.io/blog/2018/10/11/topology-aware-volume-provisioning-in-kubernetes/" target="_blank" rel="noopener noreferrer">https://kubernetes.io/blog/2018/10/11/topology-aware-volume-provisioning-in-kubernetes/</a></li>
<li>Kubernetes 1.14: Local Persistent Volumes GA - <a href="https://kubernetes.io/blog/2019/04/04/kubernetes-1.14-local-persistent-volumes-ga/" target="_blank" rel="noopener noreferrer">https://kubernetes.io/blog/2019/04/04/kubernetes-1.14-local-persistent-volumes-ga/</a></li>
<li>KubeCon 演讲: 面向 k8s 的拓扑感知服务路由即将推出! - <a href="https://v.qq.com/x/page/t0893nn9zqa.html" target="_blank" rel="noopener noreferrer">https://v.qq.com/x/page/t0893nn9zqa.html</a></li>
<li>拓扑感知服务路由官方文档(等v1.17发布后才能看到) - <a href="https://kubernetes.io/docs/concepts/services-networking/service-topology/" target="_blank" rel="noopener noreferrer">https://kubernetes.io/docs/concepts/services-networking/service-topology/</a></li>
<li>KEP: Topology-aware service routing - <a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/20181024-service-topology.md" target="_blank" rel="noopener noreferrer">https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/20181024-service-topology.md</a> (此文档后续会更新，因为实现跟设计已经不一样了)</li>
</ul>]]></content:encoded>
            <category>kubernetes</category>
        </item>
        <item>
            <title><![CDATA[Kubernetes 网络疑难杂症排查分享]]></title>
            <link>https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network</link>
            <guid>https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network</guid>
            <pubDate>Mon, 12 Aug 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[大家好，我是 roc，来自腾讯云容器服务(TKE)团队，经常帮助用户解决各种 K8S 的疑难杂症，积累了比较丰富的经验，本文分享几个比较复杂的网络方面的问题排查和解决思路，深入分析并展开相关知识，信息量巨大，相关经验不足的同学可能需要细细品味才能消化，我建议收藏本文反复研读，当完全看懂后我相信你的功底会更加扎实，解决问题的能力会大大提升。]]></description>
            <content:encoded><![CDATA[<p>大家好，我是 roc，来自腾讯云容器服务(TKE)团队，经常帮助用户解决各种 K8S 的疑难杂症，积累了比较丰富的经验，本文分享几个比较复杂的网络方面的问题排查和解决思路，深入分析并展开相关知识，信息量巨大，相关经验不足的同学可能需要细细品味才能消化，我建议收藏本文反复研读，当完全看懂后我相信你的功底会更加扎实，解决问题的能力会大大提升。</p>
<blockquote>
<p>本文发现的问题是在使用 TKE 时遇到的，不同厂商的网络环境可能不一样，文中会对不同的问题的网络环境进行说明</p>
</blockquote>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114244.png" alt="" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="跨-vpc-访问-nodeport-经常超时">跨 VPC 访问 NodePort 经常超时<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#%E8%B7%A8-vpc-%E8%AE%BF%E9%97%AE-nodeport-%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6" class="hash-link" aria-label="跨 VPC 访问 NodePort 经常超时的直接链接" title="跨 VPC 访问 NodePort 经常超时的直接链接">​</a></h2>
<p>现象: 从 VPC a 访问 VPC b 的 TKE 集群的某个节点的 NodePort，有时候正常，有时候会卡住直到超时。</p>
<p>原因怎么查？</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114252.png" alt="" class="img_ev3q"></p>
<p>当然是先抓包看看啦，抓 server 端 NodePort 的包，发现异常时 server 能收到 SYN，但没响应 ACK:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114304.png" alt="" class="img_ev3q"></p>
<p>反复执行 <code>netstat -s | grep LISTEN</code> 发现 SYN 被丢弃数量不断增加:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114318.png" alt="" class="img_ev3q"></p>
<p>分析：</p>
<ul>
<li>两个VPC之间使用对等连接打通的，CVM 之间通信应该就跟在一个内网一样可以互通。</li>
<li>为什么同一 VPC 下访问没问题，跨 VPC 有问题? 两者访问的区别是什么?</li>
</ul>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114329.png" alt="" class="img_ev3q"></p>
<p>再仔细看下 client 所在环境，发现 client 是 VPC a 的 TKE 集群节点，捋一下:</p>
<ul>
<li>client 在 VPC a 的 TKE 集群的节点</li>
<li>server 在 VPC b 的 TKE 集群的节点</li>
</ul>
<p>因为 TKE 集群中有个叫 <code>ip-masq-agent</code> 的 daemonset，它会给 node 写 iptables 规则，默认 SNAT 目的 IP 是 VPC 之外的报文，所以 client 访问 server 会做 SNAT，也就是这里跨 VPC 相比同 VPC 访问 NodePort 多了一次 SNAT，如果是因为多了一次 SNAT 导致的这个问题，直觉告诉我这个应该跟内核参数有关，因为是 server 收到包没回包，所以应该是 server 所在 node 的内核参数问题，对比这个 node 和 普通 TKE node 的默认内核参数，发现这个 node <code>net.ipv4.tcp_tw_recycle = 1</code>，这个参数默认是关闭的，跟用户沟通后发现这个内核参数确实在做压测的时候调整过。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114339.png" alt="" class="img_ev3q"></p>
<p>解释一下，TCP 主动关闭连接的一方在发送最后一个 ACK 会进入 <code>TIME_AWAIT</code> 状态，再等待 2 个 MSL 时间后才会关闭(因为如果 server 没收到 client 第四次挥手确认报文，server 会重发第三次挥手 FIN 报文，所以 client 需要停留 2 MSL的时长来处理可能会重复收到的报文段；同时等待 2 MSL 也可以让由于网络不通畅产生的滞留报文失效，避免新建立的连接收到之前旧连接的报文)，了解更详细的过程请参考 TCP 四次挥手。</p>
<p>参数 <code>tcp_tw_recycle</code> 用于快速回收 <code>TIME_AWAIT</code> 连接，通常在增加连接并发能力的场景会开启，比如发起大量短连接，快速回收可避免  <code>tw_buckets</code> 资源耗尽导致无法建立新连接 (<code>time wait bucket table overflow</code>)</p>
<p>查得 <code>tcp_tw_recycle</code> 有个坑，在 RFC1323 有段描述:</p>
<p><code>An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender’s timestamp clock. Such an extension is not part of the proposal of this RFC.</code></p>
<p>大概意思是说 TCP 有一种行为，可以缓存每个连接最新的时间戳，后续请求中如果时间戳小于缓存的时间戳，即视为无效，相应的数据包会被丢弃。</p>
<p>Linux 是否启用这种行为取决于 <code>tcp_timestamps</code> 和 <code>tcp_tw_recycle</code>，因为 <code>tcp_timestamps</code> 缺省开启，所以当 <code>tcp_tw_recycle</code> 被开启后，实际上这种行为就被激活了，当客户端或服务端以 <code>NAT</code> 方式构建的时候就可能出现问题。</p>
<p>当多个客户端通过 NAT 方式联网并与服务端交互时，服务端看到的是同一个 IP，也就是说对服务端而言这些客户端实际上等同于一个，可惜由于这些客户端的时间戳可能存在差异，于是乎从服务端的视角看，便可能出现时间戳错乱的现象，进而直接导致时间戳小的数据包被丢弃。如果发生了此类问题，具体的表现通常是是客户端明明发送的 SYN，但服务端就是不响应 ACK。</p>
<p>回到我们的问题上，client 所在节点上可能也会有其它 pod 访问到 server 所在节点，而它们都被 SNAT 成了 client 所在节点的 NODE IP，但时间戳存在差异，server 就会看到时间戳错乱，因为开启了 <code>tcp_tw_recycle</code> 和 <code>tcp_timestamps</code> 激活了上述行为，就丢掉了比缓存时间戳小的报文，导致部分 SYN 被丢弃，这也解释了为什么之前我们抓包发现异常时 server 收到了 SYN，但没有响应 ACK，进而说明为什么 client 的请求部分会卡住直到超时。</p>
<p>由于 <code>tcp_tw_recycle</code> 坑太多，在内核 4.12 之后已移除: <a href="https://github.com/torvalds/linux/commit/4396e46187ca5070219b81773c4e65088dac50cc" target="_blank" rel="noopener noreferrer">remove tcp_tw_recycle</a></p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114352.png" alt="" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="lb-压测-cps-低">LB 压测 CPS 低<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#lb-%E5%8E%8B%E6%B5%8B-cps-%E4%BD%8E" class="hash-link" aria-label="LB 压测 CPS 低的直接链接" title="LB 压测 CPS 低的直接链接">​</a></h2>
<p>现象: LoadBalancer 类型的 Service，直接压测 NodePort CPS 比较高，但如果压测 LB CPS 就很低。</p>
<p>环境说明: 用户使用的黑石TKE，不是公有云TKE，黑石的机器是物理机，LB的实现也跟公有云不一样，但 LoadBalancer 类型的 Service 的实现同样也是 LB 绑定各节点的 NodePort，报文发到 LB 后转到节点的 NodePort， 然后再路由到对应 pod，而测试在公有云 TKE 环境下没有这个问题。</p>
<p>client 抓包: 大量SYN重传。</p>
<p>server 抓包: 抓 NodePort 的包，发现当 client SYN 重传时 server 能收到 SYN 包但没有响应。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114252.png" alt="" class="img_ev3q"></p>
<p>又是 SYN 收到但没响应，难道又是开启 <code>tcp_tw_recycle</code> 导致的？检查节点的内核参数发现并没有开启，除了这个原因，还会有什么情况能导致被丢弃？</p>
<p><code>conntrack -S</code> 看到 <code>insert_failed</code> 数量在不断增加，也就是 conntrack 在插入很多新连接的时候失败了，为什么会插入失败？什么情况下会插入失败？</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114735.png" alt="" class="img_ev3q"></p>
<p>挖内核源码: netfilter conntrack 模块为每个连接创建 conntrack 表项时，表项的创建和最终插入之间还有一段逻辑，没有加锁，是一种乐观锁的过程。conntrack 表项并发刚创建时五元组不冲突的话可以创建成功，但中间经过 NAT 转换之后五元组就可能变成相同，第一个可以插入成功，后面的就会插入失败，因为已经有相同的表项存在。比如一个 SYN 已经做了 NAT 但是还没到最终插入的时候，另一个 SYN 也在做 NAT，因为之前那个 SYN 还没插入，这个 SYN 做 NAT 的时候就认为这个五元组没有被占用，那么它 NAT 之后的五元组就可能跟那个还没插入的包相同。</p>
<p>在我们这个问题里实际就是 netfilter 做 SNAT 时源端口选举冲突了，黑石 LB 会做 SNAT，SNAT 时使用了 16 个不同 IP 做源，但是短时间内源 Port 却是集中一致的，并发两个 SYN a 和SYN b，被 LB SNAT 后源 IP 不同但源 Port 很可能相同，这里就假设两个报文被 LB SNAT 之后它们源 IP 不同源 Port 相同，报文同时到了节点的 NodePort 会再次做 SNAT 再转发到对应的 Pod，当报文到了 NodePort 时，这时它们五元组不冲突，netfilter 为它们分别创建了 conntrack 表项，SYN a 被节点 SNAT 时默认行为是 从 port_range 范围的当前源 Port 作为起始位置开始循环遍历，选举出没有被占用的作为源 Port，因为这两个 SYN 源 Port 相同，所以它们源 Port 选举的起始位置相同，当 SYN a 选出源 Port 但还没将 conntrack 表项插入时，netfilter 认为这个 Port 没被占用就很可能给 SYN b 也选了相同的源 Port，这时他们五元组就相同了，当 SYN a 的 conntrack 表项插入后再插入 SYN b 的 conntrack 表项时，发现已经有相同的记录就将 SYN b 的 conntrack 表项丢弃了。</p>
<p>解决方法探索: 不使用源端口选举，在 iptables 的 MASQUERADE 规则如果加 <code>--random-fully</code> 这个 flag 可以让端口选举完全随机，基本上能避免绝大多数的冲突，但也无法完全杜绝。最终决定开发 LB 直接绑 Pod IP，不基于 NodePort，从而避免 netfilter 的 SNAT 源端口冲突问题。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114743.png" alt="" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="dns-解析偶尔-5s-延时">DNS 解析偶尔 5S 延时<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#dns-%E8%A7%A3%E6%9E%90%E5%81%B6%E5%B0%94-5s-%E5%BB%B6%E6%97%B6" class="hash-link" aria-label="DNS 解析偶尔 5S 延时的直接链接" title="DNS 解析偶尔 5S 延时的直接链接">​</a></h2>
<p>网上一搜，是已知问题，仔细分析，实际跟之前黑石 TKE 压测 LB CPS 低的根因是同一个，都是因为 netfilter conntrack 模块的设计问题，只不过之前发生在 SNAT，这个发生在 DNAT，这里用我的语言来总结下原因:</p>
<p>DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录，跟 DNS Server 通信自然会先 connect (建立fd)，后面请求报文使用这个 fd 来发送，由于 UDP 是无状态协议， connect 时并不会发包，也就不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包，send 时各自发的包它们源 Port 相同(因为用的同一个socket发送)，当并发发包时，两个包都还没有被插入 conntrack 表项，所以 netfilter 会为它们分别创建 conntrack 表项，而集群内请求 kube-dns 或 coredns 都是访问的CLUSTER-IP，报文最终会被 DNAT 成一个 endpoint 的 POD IP，当两个包恰好又被 DNAT 成同一个 POD IP时，它们的五元组就相同了，在最终插入的时候后面那个包就会被丢掉，如果 dns 的 pod 副本只有一个实例的情况就很容易发生(始终被DNAT成同一个POD IP)，现象就是 dns 请求超时，client 默认策略是等待 5s 自动重试，如果重试成功，我们看到的现象就是 dns 请求有 5s 的延时。</p>
<p>参考 weave works 工程师总结的文章: <a href="https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts" target="_blank" rel="noopener noreferrer">Racy conntrack and DNS lookup timeouts</a></p>
<p>解决方案一: 使用 TCP 发送 DNS 请求</p>
<p>如果使用 TCP 发 DNS 请求，connect 时就会发包建立连接并插入 conntrack 表项，而后并发的 A 和 AAAA 记录的请求在 send 时都使用 connect 建立好的这个 fd，由于 connect 时 conntrack 表项已经建立，所以 send 时不会再建立，也就不存在并发创建 conntrack 表项，避免了冲突。</p>
<p><code>resolv.conf</code> 可以加 <code>options use-vc</code> 强制 glibc 使用 TCP 协议发送 DNS query。下面是这个 <code>man resolv.conf</code>中关于这个选项的说明:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">use-vc (since glibc 2.14)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     Sets RES_USEVC in _res.options.  This option forces the</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     use of TCP for DNS resolutions.</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>解决方案二: 避免相同五元组 DNS 请求的并发</p>
<p><code>resolv.conf</code> 还有另外两个相关的参数：</p>
<ul>
<li>single-request-reopen (since glibc 2.9): A 和 AAAA 请求使用不同的 socket 来发送，这样它们的源 Port 就不同，五元组也就不同，避免了使用同一个 conntrack 表项。</li>
<li>single-request (since glibc 2.10): A 和 AAAA 请求改成串行，没有并发，从而也避免了冲突。</li>
</ul>
<p><code>man resolv.conf</code> 中解释如下:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">single-request-reopen (since glibc 2.9)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     Sets RES_SNGLKUPREOP in _res.options.  The resolver</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     uses the same socket for the A and AAAA requests.  Some</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     hardware mistakenly sends back only one reply.  When</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     that happens the client system will sit and wait for</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     the second reply.  Turning this option on changes this</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     behavior so that if two requests from the same port are</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     not handled correctly it will close the socket and open</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     a new one before sending the second request.</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">single-request (since glibc 2.10)</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     Sets RES_SNGLKUP in _res.options.  By default, glibc</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     performs IPv4 and IPv6 lookups in parallel since</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     version 2.9.  Some appliance DNS servers cannot handle</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     these queries properly and make the requests time out.</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     This option disables the behavior and makes glibc</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     perform the IPv6 and IPv4 requests sequentially (at the</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">                     cost of some slowdown of the resolving process).</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>要给容器的 <code>resolv.conf</code> 加上 options 参数，最方便的是直接在 Pod Spec 里面的 dnsConfig 加 (k8s v1.9 及以上才支持)</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114803.png" alt="" class="img_ev3q"></p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">    </span><span class="token key atrule" style="color:#569CD6">spec</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">      </span><span class="token key atrule" style="color:#569CD6">dnsConfig</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">        </span><span class="token key atrule" style="color:#569CD6">options</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">          </span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#569CD6">name</span><span class="token punctuation" style="color:rgb(212, 212, 212)">:</span><span class="token plain"> single</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">request</span><span class="token punctuation" style="color:rgb(212, 212, 212)">-</span><span class="token plain">reopen</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>加 options 还有其它一些方法:</p>
<ul>
<li>在容器的 <code>ENTRYPOINT</code> 或者 <code>CMD</code> 脚本中，执行 <code>/bin/echo 'options single-request-reopen' &gt;&gt; /etc/resolv.conf</code></li>
<li>在 postStart hook 里加:</li>
</ul>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">        lifecycle:</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">          postStart:</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">            exec:</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">              command:</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">              - /bin/sh</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">              - -c </span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">              - "/bin/echo 'options single-request-reopen' &gt;&gt; /etc/resolv.conf"</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>使用 <a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#mutatingadmissionwebhook-beta-in-1-9" target="_blank" rel="noopener noreferrer">MutatingAdmissionWebhook</a>，这是 1.9 引入的 Controller，用于对一个指定的资源的操作之前，对这个资源进行变更。 istio 的自动 sidecar 注入就是用这个功能来实现的，我们也可以通过 <code>MutatingAdmissionWebhook</code> 来自动给所有 Pod 注入 <code>resolv.conf</code> 文件，不过需要一定的开发量。</li>
</ul>
<p>解决方案三: 使用本地 DNS 缓存</p>
<p>仔细观察可以看到前面两种方案是 glibc 支持的，而基于 alpine 的镜像底层库是 musl libc 不是 glibc，所以即使加了这些 options 也没用，这种情况可以考虑使用本地 DNS 缓存来解决，容器的 DNS 请求都发往本地的 DNS 缓存服务(dnsmasq, nscd, coredns等)，不需要走 DNAT，也不会发生 conntrack 冲突。另外还有个好处，就是避免 DNS 服务成为性能瓶颈。</p>
<p>使用本地DNS缓存有两种方式：</p>
<ul>
<li>每个容器自带一个 DNS 缓存服务</li>
<li>每个节点运行一个 DNS 缓存服务，所有容器都把本节点的 DNS 缓存作为自己的 nameserver</li>
</ul>
<p>从资源效率的角度来考虑的话，推荐后一种方式。</p>
<p>官方也意识到了这个问题比较常见，给出了 coredns 以 cache 模式作为 daemonset 部署的解决方案: <a href="https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/" target="_blank" rel="noopener noreferrer">https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/</a></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="pod-访问另一个集群的-apiserver-有延时">Pod 访问另一个集群的 apiserver 有延时<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#pod-%E8%AE%BF%E9%97%AE%E5%8F%A6%E4%B8%80%E4%B8%AA%E9%9B%86%E7%BE%A4%E7%9A%84-apiserver-%E6%9C%89%E5%BB%B6%E6%97%B6" class="hash-link" aria-label="Pod 访问另一个集群的 apiserver 有延时的直接链接" title="Pod 访问另一个集群的 apiserver 有延时的直接链接">​</a></h2>
<p>现象：集群 a 的 Pod 内通过 kubectl 访问集群 b 的内网地址，偶尔出现延时的情况，但直接在宿主机上用同样的方法却没有这个问题。</p>
<p>提炼环境和现象精髓:</p>
<ol>
<li>在 pod 内将另一个集群 apiserver 的 ip 写到了 hosts，因为 TKE apiserver 开启内网集群外内网访问创建的内网 LB 暂时没有支持自动绑内网 DNS 域名解析，所以集群外的内网访问 apiserver 需要加 hosts</li>
<li>pod 内执行 kubectl 访问另一个集群偶尔延迟 5s，有时甚至10s</li>
</ol>
<p>观察到 5s 延时，感觉跟之前 conntrack 的丢包导致 dns 解析 5s 延时有关，但是加了 hosts 呀，怎么还去解析域名？</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114252.png" alt="" class="img_ev3q"></p>
<p>进入 pod netns 抓包: 执行 kubectl 时确实有 dns 解析，并且发生延时的时候 dns 请求没有响应然后做了重试。</p>
<p>看起来延时应该就是之前已知 conntrack 丢包导致 dns 5s 超时重试导致的。但是为什么会去解析域名? 明明配了 hosts 啊，正常情况应该是优先查找 hosts，没找到才去请求 dns 呀，有什么配置可以控制查找顺序?</p>
<p>搜了一下发现: <code>/etc/nsswitch.conf</code> 可以控制，但看有问题的 pod 里没有这个文件。然后观察到有问题的 pod 用的 alpine 镜像，试试其它镜像后发现只有基于 alpine 的镜像才会有这个问题。</p>
<p>再一搜发现: musl libc 并不会使用 <code>/etc/nsswitch.conf</code> ，也就是说 alpine 镜像并没有实现用这个文件控制域名查找优先顺序，瞥了一眼 musl libc 的 <code>gethostbyname</code> 和 <code>getaddrinfo</code> 的实现，看起来也没有读这个文件来控制查找顺序，写死了先查 hosts，没找到再查 dns。</p>
<p>这么说，那还是该先查 hosts 再查 dns 呀，为什么这里抓包看到是先查的 dns? (如果是先查 hosts 就能命中查询，不会再发起dns请求)</p>
<p>访问 apiserver 的 client 是 kubectl，用 go 写的，会不会是 go 程序解析域名时压根没调底层 c 库的 <code>gethostbyname</code> 或 <code>getaddrinfo</code>?</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114903.png" alt="" class="img_ev3q"></p>
<p>搜一下发现果然是这样: go runtime 用 go 实现了 glibc 的 <code>getaddrinfo</code> 的行为来解析域名，减少了 c 库调用 (应该是考虑到减少 cgo 调用带来的的性能损耗)</p>
<p>issue: <a href="https://github.com/golang/go/issues/18518" target="_blank" rel="noopener noreferrer">net: replicate DNS resolution behaviour of getaddrinfo(glibc) in the go dns resolver</a></p>
<p>翻源码验证下:</p>
<p>Unix 系的 OS 下，除了 openbsd， go runtime 会读取 <code>/etc/nsswitch.conf</code> (<code>net/conf.go</code>):</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114914.png" alt="" class="img_ev3q"></p>
<p><code>hostLookupOrder</code> 函数决定域名解析顺序的策略，Linux 下，如果没有 <code>nsswitch.conf</code> 文件就 dns 比 hosts 文件优先 (<code>net/conf.go</code>):</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114923.png" alt="" class="img_ev3q"></p>
<p>可以看到 <code>hostLookupDNSFiles</code> 的意思是 dns first (<code>net/dnsclient_unix.go</code>):</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114933.png" alt="" class="img_ev3q"></p>
<p>所以虽然 alpine 用的 musl libc 不是 glibc，但 go 程序解析域名还是一样走的 glibc 的逻辑，而 alpine 没有 <code>/etc/nsswitch.conf</code> 文件，也就解释了为什么 kubectl 访问 apiserver 先做 dns 解析，没解析到再查的 hosts，导致每次访问都去请求 dns，恰好又碰到 conntrack 那个丢包问题导致 dns 5s 延时，在用户这里表现就是 pod 内用 kubectl 访问 apiserver 偶尔出现 5s 延时，有时出现 10s 是因为重试的那次 dns 请求刚好也遇到 conntrack 丢包导致延时又叠加了 5s 。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114942.png" alt="" class="img_ev3q"></p>
<p>解决方案:</p>
<ol>
<li>换基础镜像，不用 alpine</li>
<li>挂载 <code>nsswitch.conf</code> 文件 (可以用 hostPath)</li>
</ol>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="dns-解析异常">DNS 解析异常<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#dns-%E8%A7%A3%E6%9E%90%E5%BC%82%E5%B8%B8" class="hash-link" aria-label="DNS 解析异常的直接链接" title="DNS 解析异常的直接链接">​</a></h2>
<p>现象: 有个用户反馈域名解析有时有问题，看报错是解析超时。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114252.png" alt="" class="img_ev3q"></p>
<p>第一反应当然是看 coredns 的 log:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">[ERROR] 2 loginspub.xxxxmobile-inc.net. </span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">A: unreachable backend: read udp 172.16.0.230:43742-&gt;10.225.30.181:53: i/o timeout</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>这是上游 DNS 解析异常了，因为解析外部域名 coredns 默认会请求上游 DNS 来查询，这里的上游 DNS 默认是 coredns pod 所在宿主机的 <code>resolv.conf</code> 里面的 nameserver (coredns pod 的 dnsPolicy 为 "Default"，也就是会将宿主机里的 <code>resolv.conf</code> 里的 nameserver 加到容器里的 <code>resolv.conf</code>, coredns 默认配置 <code>proxy . /etc/resolv.conf</code>, 意思是非 service 域名会使用 coredns 容器中 <code>resolv.conf</code> 文件里的 nameserver 来解析)</p>
<p>确认了下，超时的上游 DNS 10.225.30.181&nbsp;并不是期望的 nameserver，VPC 默认 DNS 应该是 180 开头的。看了 coredns 所在节点的 <code>resolv.conf</code>，发现确实多出了这个非期望的 nameserver，跟用户确认了下，这个 DNS 不是用户自己加上去的，添加节点时这个 nameserver 本身就在 <code>resolv.conf</code> 中。</p>
<p>根据内部同学反馈， 10.225.30.181 是广州一台年久失修将被撤裁的 DNS，物理网络，没有 VIP，撤掉就没有了，所以如果 coredns 用到了这台 DNS 解析时就可能 timeout。后面我们自己测试，某些 VPC 的集群确实会有这个 nameserver，奇了怪了，哪里冒出来的？</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115020.png" alt="" class="img_ev3q"></p>
<p>又试了下直接创建 CVM，不加进 TKE 节点发现没有这个 nameserver，只要一加进 TKE 节点就有了 !!!</p>
<p>看起来是 TKE 的问题，将 CVM 添加到 TKE 集群会自动重装系统，初始化并加进集群成为 K8S 的 node，确认了初始化过程并不会写 <code>resolv.conf</code>，会不会是 TKE 的 OS 镜像问题？尝试搜一下除了 <code>/etc/resolv.conf</code> 之外哪里还有这个 nameserver 的 IP，最后发现 <code>/etc/resolvconf/resolv.conf.d/base</code> 这里面有。</p>
<p>看下 <code>/etc/resolvconf/resolv.conf.d/base</code> 的作用：Ubuntu 的 <code>/etc/resolv.conf</code> 是动态生成的，每次重启都会将 <code>/etc/resolvconf/resolv.conf.d/base</code>  里面的内容加到 <code>/etc/resolv.conf</code> 里。</p>
<p>经确认: 这个文件确实是 TKE 的 Ubuntu OS 镜像里自带的，可能发布 OS 镜像时不小心加进去的。</p>
<p>那为什么有些 VPC 的集群的节点 <code>/etc/resolv.conf</code> 里面没那个 IP 呢？它们的 OS 镜像里也都有那个文件那个 IP 呀。</p>
<p>请教其它部门同学发现:</p>
<ul>
<li>非 dhcp 子机，cvm 的 cloud-init 会覆盖 <code>/etc/resolv.conf</code> 来设置 dns</li>
<li>dhcp 子机，cloud-init 不会设置，而是通过 dhcp 动态下发</li>
<li>2018 年 4 月 之后创建的 VPC 就都是 dhcp 类型了的，比较新的 VPC 都是 dhcp 类型的</li>
</ul>
<p>真相大白：<code>/etc/resolv.conf</code> 一开始内容都包含 <code>/etc/resolvconf/resolv.conf.d/base</code> 的内容，也就是都有那个不期望的 nameserver，但老的 VPC 由于不是 dhcp 类型，所以 cloud-init 会覆盖 <code>/etc/resolv.conf</code>，抹掉了不被期望的 nameserver，而新创建的 VPC 都是 dhcp 类型，cloud-init 不会覆盖 <code>/etc/resolv.conf</code>，导致不被期望的 nameserver 残留在了 <code>/etc/resolv.conf</code>，而 coredns pod 的 dnsPolicy 为 “Default”，也就是会将宿主机的 <code>/etc/resolv.conf</code> 中的 nameserver 加到容器里，coredns 解析集群外的域名默认使用这些 nameserver 来解析，当用到那个将被撤裁的 nameserver 就可能 timeout。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114942.png" alt="" class="img_ev3q"></p>
<p>临时解决: 删掉 <code>/etc/resolvconf/resolv.conf.d/base</code>  重启</p>
<p>长期解决: 我们重新制作 TKE Ubuntu OS 镜像然后发布更新</p>
<p>这下应该没问题了吧，But, 用户反馈还是会偶尔解析有问题，但现象不一样了，这次并不是 dns timeout。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115130.png" alt="" class="img_ev3q"></p>
<p>用脚本跑测试仔细分析现象:</p>
<ul>
<li>请求 <code>loginspub.xxxxmobile-inc.net</code> 时，偶尔提示域名无法解析</li>
<li>请求 <code>accounts.google.com</code> 时，偶尔提示连接失败</li>
</ul>
<p>进入 dns 解析偶尔异常的容器的 netns 抓包:</p>
<ul>
<li>dns 请求会并发请求 A 和 AAAA 记录</li>
<li>测试脚本发请求打印序号，抓包然后 wireshark 分析对比异常时请求序号偏移量，找到异常时的 dns 请求报文，发现异常时 A 和 AAAA 记录的请求 id 冲突，并且 AAAA 响应先返回</li>
</ul>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115141.png" alt="" class="img_ev3q"></p>
<p>正常情况下id不会冲突，这里冲突了也就能解释这个 dns 解析异常的现象了:</p>
<ul>
<li><code>loginspub.xxxxmobile-inc.net</code> 没有 AAAA (ipv6) 记录，它的响应先返回告知 client 不存在此记录，由于请求 id 跟 A 记录请求冲突，后面 A 记录响应返回了 client 发现 id 重复就忽略了，然后认为这个域名无法解析</li>
<li><code>accounts.google.com</code> 有 AAAA 记录，响应先返回了，client 就拿这个记录去尝试请求，但当前容器环境不支持 ipv6，所以会连接失败</li>
</ul>
<p>那为什么 dns 请求 id 会冲突?</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115251.png" alt="" class="img_ev3q"></p>
<p>继续观察发现: 其它节点上的 pod 不会复现这个问题，有问题这个节点上也不是所有 pod 都有这个问题，只有基于 alpine 镜像的容器才有这个问题，在此节点新起一个测试的 <code>alpine:latest</code> 的容器也一样有这个问题。</p>
<p>为什么 alpine 镜像的容器在这个节点上有问题在其它节点上没问题？ 为什么其他镜像的容器都没问题？它们跟 alpine 的区别是什么？</p>
<p>发现一点区别: alpine 使用的底层 c 库是 musl libc，其它镜像基本都是 glibc</p>
<p>翻 musl libc 源码, 构造 dns 请求时，请求 id 的生成没加锁，而且跟当前时间戳有关:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115152.png" alt="" class="img_ev3q"></p>
<p>看注释，作者应该认为这样id基本不会冲突，事实证明，绝大多数情况确实不会冲突，我在网上搜了很久没有搜到任何关于 musl libc 的 dns 请求 id 冲突的情况。这个看起来取决于硬件，可能在某种类型硬件的机器上运行，短时间内生成的 id 就可能冲突。我尝试跟用户在相同地域的集群，添加相同配置相同机型的节点，也复现了这个问题，但后来删除再添加时又不能复现了，看起来后面新建的 cvm 又跑在了另一种硬件的母机上了。</p>
<p>OK，能解释通了，再底层的细节就不清楚了，我们来看下解决方案:</p>
<ul>
<li>换基础镜像 (不用alpine)</li>
<li>完全静态编译业务程序(不依赖底层c库)，比如go语言程序编译时可以关闭 cgo (CGO_ENABLED=0)，并告诉链接器要静态链接 (<code>go build</code> 后面加 <code>-ldflags '-d'</code>)，但这需要语言和编译工具支持才可以</li>
</ul>
<p>最终建议用户基础镜像换成另一个比较小的镜像: <code>debian:stretch-slim</code>。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="pod-偶尔存活检查失败">Pod 偶尔存活检查失败<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#pod-%E5%81%B6%E5%B0%94%E5%AD%98%E6%B4%BB%E6%A3%80%E6%9F%A5%E5%A4%B1%E8%B4%A5" class="hash-link" aria-label="Pod 偶尔存活检查失败的直接链接" title="Pod 偶尔存活检查失败的直接链接">​</a></h2>
<p>现象: Pod 偶尔会存活检查失败，导致 Pod 重启，业务偶尔连接异常。</p>
<p>之前从未遇到这种情况，在自己测试环境尝试复现也没有成功，只有在用户这个环境才可以复现。这个用户环境流量较大，感觉跟连接数或并发量有关。</p>
<p>用户反馈说在友商的环境里没这个问题。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926114252.png" alt="" class="img_ev3q"></p>
<p>对比友商的内核参数发现有些区别，尝试将节点内核参数改成跟友商的一样，发现问题没有复现了。</p>
<p>再对比分析下内核参数差异，最后发现是 backlog 太小导致的，节点的 <code>net.ipv4.tcp_max_syn_backlog</code> 默认是 1024，如果短时间内并发新建 TCP 连接太多，SYN 队列就可能溢出，导致部分新连接无法建立。</p>
<p>解释一下:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115324.png" alt="" class="img_ev3q"></p>
<p>TCP 连接建立会经过三次握手，server 收到 SYN 后会将连接加入 SYN 队列，当收到最后一个 ACK 后连接建立，这时会将连接从 SYN 队列中移动到 ACCEPT 队列。在 SYN 队列中的连接都是没有建立完全的连接，处于半连接状态。如果 SYN 队列比较小，而短时间内并发新建的连接比较多，同时处于半连接状态的连接就多，SYN 队列就可能溢出，<code>tcp_max_syn_backlog</code> 可以控制 SYN 队列大小，用户节点的 backlog 大小默认是 1024，改成 8096 后就可以解决问题。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="访问-externaltrafficpolicy-为-local-的-service-对应-lb-有时超时">访问 externalTrafficPolicy 为 Local 的 Service 对应 LB 有时超时<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#%E8%AE%BF%E9%97%AE-externaltrafficpolicy-%E4%B8%BA-local-%E7%9A%84-service-%E5%AF%B9%E5%BA%94-lb-%E6%9C%89%E6%97%B6%E8%B6%85%E6%97%B6" class="hash-link" aria-label="访问 externalTrafficPolicy 为 Local 的 Service 对应 LB 有时超时的直接链接" title="访问 externalTrafficPolicy 为 Local 的 Service 对应 LB 有时超时的直接链接">​</a></h2>
<p>现象：用户在 TKE 创建了公网 LoadBalancer 类型的 Service，externalTrafficPolicy 设为了 Local，访问这个 Service 对应的公网 LB 有时会超时。</p>
<p>externalTrafficPolicy 为 Local 的 Service 用于在四层获取客户端真实源 IP，官方参考文档：<a href="https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-type-loadbalancer" target="_blank" rel="noopener noreferrer">Source IP for Services with Type=LoadBalancer</a></p>
<p>TKE 的 LoadBalancer 类型 Service 实现是使用 CLB 绑定所有节点对应 Service 的 NodePort，CLB 不做 SNAT，报文转发到 NodePort 时源 IP 还是真实的客户端 IP，如果 NodePort 对应 Service 的 externalTrafficPolicy 不是 Local 的就会做 SNAT，到 pod 时就看不到客户端真实源 IP 了，但如果是 Local 的话就不做 SNAT，如果本机 node 有这个 Service 的 endpoint 就转到对应 pod，如果没有就直接丢掉，因为如果转到其它 node 上的 pod 就必须要做 SNAT，不然无法回包，而 SNAT 之后就无法获取真实源 IP 了。</p>
<p>LB 会对绑定节点的 NodePort 做健康检查探测，检查 LB 的健康检查状态: 发现这个 NodePort 的所有节点都不健康 !!!</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115346.png" alt="" class="img_ev3q"></p>
<p>那么问题来了:</p>
<ol>
<li>为什么会全不健康，这个 Service 有对应的 pod 实例，有些节点上是有 endpoint 的，为什么它们也不健康?</li>
<li>LB 健康检查全不健康，但是为什么有时还是可以访问后端服务?</li>
</ol>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115352.png" alt="" class="img_ev3q"></p>
<p>跟 LB 的同学确认: 如果后端 rs 全不健康会激活 LB 的全死全活逻辑，也就是所有后端 rs 都可以转发。</p>
<p>那么有 endpoint 的 node 也是不健康这个怎么解释?</p>
<p>在有 endpoint 的 node 上抓 NodePort 的包: 发现很多来自 LB 的 SYN，但是没有响应 ACK。</p>
<p>看起来报文在哪被丢了，继续抓下 cbr0 看下: 发现没有来自 LB 的包，说明报文在 cbr0 之前被丢了。</p>
<p>再观察用户集群环境信息:</p>
<ol>
<li>k8s 版本1.12</li>
<li>启用了 ipvs</li>
<li>只有 local 的 service 才有异常</li>
</ol>
<p>尝试新建一个 1.12 启用 ipvs 和一个没启用 ipvs 的测试集群。也都创建 Local 的 LoadBalancer Service，发现启用 ipvs 的测试集群复现了那个问题，没启用 ipvs 的集群没这个问题。</p>
<p>再尝试创建 1.10 的集群，也启用 ipvs，发现没这个问题。</p>
<p>看起来跟集群版本和是否启用 ipvs 有关。</p>
<p>1.12 对比 1.10 启用 ipvs 的集群: 1.12 的会将 LB 的 <code>EXTERNAL-IP</code> 绑到 <code>kube-ipvs0</code> 上，而 1.10 的不会:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ ip a show kube-ipvs0 | grep -A2 170.106.134.124</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">    inet 170.106.134.124/32 brd 170.106.134.124 scope global kube-ipvs0</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">       valid_lft forever preferred_lft forever</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>170.106.134.124 是 LB 的公网 IP</li>
<li>1.12 启用 ipvs 的集群将 LB 的公网 IP 绑到了 <code>kube-ipvs0</code> 网卡上</li>
</ul>
<p><code>kube-ipvs0</code> 是一个 dummy interface，实际不会接收报文，可以看到它的网卡状态是 DOWN，主要用于绑 ipvs 规则的 VIP，因为 ipvs 主要工作在 netfilter 的 INPUT 链，报文通过 PREROUTING 链之后需要决定下一步该进入 INPUT 还是 FORWARD 链，如果是本机 IP 就会进入 INPUT，如果不是就会进入 FORWARD 转发到其它机器。所以 k8s 利用 <code>kube-ipvs0</code> 这个网卡将 service 相关的 VIP 绑在上面以便让报文进入 INPUT 进而被 ipvs 转发。</p>
<p>当 IP 被绑到 <code>kube-ipvs0</code> 上，内核会自动将上面的 IP 写入 local 路由:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">$ ip route show table local | grep 170.106.134.124</span><br></span><span class="token-line" style="color:#D4D4D4"><span class="token plain">local 170.106.134.124 dev kube-ipvs0  proto kernel  scope host  src 170.106.134.124</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>内核认为在 local 路由里的 IP 是本机 IP，而 linux 默认有个行为: 忽略任何来自非回环网卡并且源 IP 是本机 IP 的报文。而 LB 的探测报文源 IP 就是 LB IP，也就是 Service 的 <code>EXTERNAL-IP</code> 猜想就是因为这个 IP 被绑到 <code>kube-ipvs0</code>，自动加进 local 路由导致内核直接忽略了 LB 的探测报文。</p>
<p>带着猜想做实现， 试一下将 LB IP 从 local 路由中删除:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#D4D4D4;--prism-background-color:#212121"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#D4D4D4;background-color:#212121"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#D4D4D4"><span class="token plain">ip route del table local local 170.106.134.124 dev kube-ipvs0  proto kernel  scope host  src 170.106.134.124</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" 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>发现这个 node 的在 LB 的健康检查的状态变成健康了! 看来就是因为这个 LB IP 被绑到 <code>kube-ipvs0</code> 导致内核忽略了来自 LB 的探测报文，然后 LB 收不到回包认为不健康。</p>
<p>那为什么其它厂商没反馈这个问题？应该是 LB 的实现问题，腾讯云的公网 CLB 的健康探测报文源 IP 就是 LB 的公网 IP，而大多数厂商的 LB 探测报文源 IP 是保留 IP 并非 LB 自身的 VIP。</p>
<p>如何解决呢? 发现一个内核参数:  <a href="https://github.com/torvalds/linux/commit/8153a10c08f1312af563bb92532002e46d3f504a" target="_blank" rel="noopener noreferrer">accept_local</a> 可以让 linux 接收源 IP 是本机 IP 的报文。</p>
<p>试了开启这个参数，确实在 cbr0 收到来自 LB 的探测报文了，说明报文能被 pod 收到，但抓 eth0 还是没有给 LB 回包。</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115408.png" alt="" class="img_ev3q"></p>
<p>为什么没有回包? 分析下五元组，要给 LB 回包，那么 <code>目的IP:目的Port</code> 必须是探测报文的 <code>源IP:源Port</code>，所以目的 IP 就是 LB IP，由于容器不在主 netns，发包经过 veth pair 到 cbr0 之后需要再经过 netfilter 处理，报文进入 PREROUTING 链然后发现目的 IP 是本机 IP，进入 INPUT 链，所以报文就出不去了。再分析下进入 INPUT 后会怎样，因为目的 Port 跟 LB 探测报文源 Port 相同，是一个随机端口，不在 Service 的端口列表，所以没有对应的 IPVS 规则，IPVS 也就不会转发它，而 <code>kube-ipvs0</code> 上虽然绑了这个 IP，但它是一个 dummy interface，不会收包，所以报文最后又被忽略了。</p>
<p>再看看为什么 1.12 启用 ipvs 会绑 <code>EXTERNAL-IP</code> 到 <code>kube-ipvs0</code>，翻翻 k8s 的 kube-proxy 支持 ipvs 的 <a href="https://github.com/kubernetes/enhancements/blob/baca87088480254b26d0fdeb26303d7c51a20fbd/keps/sig-network/0011-ipvs-proxier.md#support-loadbalancer-service" target="_blank" rel="noopener noreferrer">proposal</a>，发现有个地方说法有点漏洞:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115428.png" alt="" class="img_ev3q"></p>
<p>LB 类型 Service 的 status 里有 ingress IP，实际就是 <code>kubectl get service</code> 看到的 <code>EXTERNAL-IP</code>，这里说不会绑定这个 IP 到 kube-ipvs0，但后面又说会给它创建 ipvs 规则，既然没有绑到 <code>kube-ipvs0</code>，那么这个 IP 的报文根本不会进入 INPUT 被 ipvs 模块转发，创建的 ipvs 规则也是没用的。</p>
<p>后来找到作者私聊，思考了下，发现设计上确实有这个问题。</p>
<p>看了下 1.10 确实也是这么实现的，但是为什么 1.12 又绑了这个 IP 呢? 调研后发现是因为 <a href="https://github.com/kubernetes/kubernetes/issues/59976" target="_blank" rel="noopener noreferrer">#59976</a>  这个 issue 发现一个问题，后来引入 <a href="https://github.com/kubernetes/kubernetes/pull/63066" target="_blank" rel="noopener noreferrer">#63066</a> 这个 PR 修复的，而这个 PR 的行为就是让 LB IP 绑到 <code>kube-ipvs0</code>，这个提交影响 1.11 及其之后的版本。</p>
<p><a href="https://github.com/kubernetes/kubernetes/issues/59976" target="_blank" rel="noopener noreferrer">#59976</a> 的问题是因为没绑 LB IP到 <code>kube-ipvs0</code> 上，在自建集群使用 <code>MetalLB</code> 来实现 LoadBalancer 类型的 Service，而有些网络环境下，pod 是无法直接访问 LB 的，导致 pod 访问 LB IP 时访问不了，而如果将 LB IP 绑到 <code>kube-ipvs0</code> 上就可以通过 ipvs 转发到 LB 类型 Service 对应的 pod 去， 而不需要真正经过 LB，所以引入了 <a href="https://github.com/kubernetes/kubernetes/pull/63066" target="_blank" rel="noopener noreferrer">#63066</a> 这个PR。</p>
<p>临时方案: 将 <a href="https://github.com/kubernetes/kubernetes/pull/63066" target="_blank" rel="noopener noreferrer">#63066</a> 这个 PR 的更改回滚下，重新编译 kube-proxy，提供升级脚本升级存量 kube-proxy。</p>
<p>如果是让 LB 健康检查探测支持用保留 IP 而不是自身的公网 IP ，也是可以解决，但需要跨团队合作，而且如果多个厂商都遇到这个问题，每家都需要为解决这个问题而做开发调整，代价较高，所以长期方案需要跟社区沟通一起推进，所以我提了 issue，将问题描述的很清楚: <a href="https://github.com/kubernetes/kubernetes/issues/79783" target="_blank" rel="noopener noreferrer">#79783</a></p>
<p>小思考: 为什么 CLB 可以不做 SNAT ? 回包目的 IP 就是真实客户端 IP，但客户端是直接跟 LB IP 建立的连接，如果回包不经过 LB 是不可能发送成功的呀。</p>
<p>是因为 CLB 的实现是在母机上通过隧道跟 CVM 互联的，多了一层封装，回包始终会经过 LB。</p>
<p>就是因为 CLB 不做 SNAT，正常来自客户端的报文是可以发送到 nodeport，但健康检查探测报文由于源 IP 是 LB IP 被绑到 <code>kube-ipvs0</code> 导致被忽略，也就解释了为什么健康检查失败，但通过LB能访问后端服务，只是有时会超时。那么如果要做 SNAT 的 LB 岂不是更糟糕，所有报文都变成 LB IP，所有报文都会被忽略?</p>
<p>我提的 issue 有回复指出，AWS 的 LB 会做 SNAT，但它们不将 LB 的 IP 写到 Service 的 Status 里，只写了 hostname，所以也不会绑 LB IP 到 <code>kube-ipvs0</code>:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115443.png" alt="" class="img_ev3q"></p>
<p>但是只写 hostname 也得 LB 支持自动绑域名解析，并且个人觉得只写 hostname 很别扭，通过 <code>kubectl get svc</code> 或者其它 k8s 管理系统无法直接获取 LB IP，这不是一个好的解决方法。</p>
<p>我提了 <a href="https://github.com/kubernetes/kubernetes/pull/79976" target="_blank" rel="noopener noreferrer">#79976</a> 这个 PR 可以解决问题: 给 kube-proxy 加 <code>--exclude-external-ip</code> 这个 flag 控制是否为 LB IP
创建 ipvs 规则和绑定 <code>kube-ipvs0</code>。</p>
<p>但有人担心增加 kube-proxy flag 会增加 kube-proxy 的调试复杂度，看能否在 iptables 层面解决:
<img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115512.png" alt="" class="img_ev3q"></p>
<p>仔细一想，确实可行，打算有空实现下，重新提个 PR:</p>
<p><img decoding="async" loading="lazy" src="https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F26%2F20230926115522.png" alt="" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="结语">结语<a href="https://imroc.cc/blog/2019/08/12/troubleshooting-with-kubernetes-network#%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="结语的直接链接" title="结语的直接链接">​</a></h2>
<p>至此，我们一起完成了一段奇妙的问题排查之旅，信息量很大并且比较复杂，有些没看懂很正常，但我希望你可以收藏起来反复阅读，一起在技术的道路上打怪升级。</p>]]></content:encoded>
            <category>kubernetes</category>
            <category>network</category>
            <category>troubleshooting</category>
        </item>
    </channel>
</rss>