<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; DNS</title>
	<atom:link href="https://blog.gmem.cc/tag/dns/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>记录一次KeyDB缓慢的定位过程</title>
		<link>https://blog.gmem.cc/debugging-slow-keydb</link>
		<comments>https://blog.gmem.cc/debugging-slow-keydb#comments</comments>
		<pubDate>Thu, 28 Jan 2021 07:04:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35755</guid>
		<description><![CDATA[<p>环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点： [crayon-69e29aa70e95b956263074/] KeyDB配置 KeyDB通过StatefulSet管理，一共有三个实例：  [crayon-69e29aa70e961486212586/] 这三个实例： 由于反亲和设置，会在每个节点上各运行一个实例 启用Active - Active（--active-replica）模式的多主（--multi-master）复制 ：每个实例都是另外两个的Slave，每个实例都支持读写 故障描述 触发条件 出现一个节点宕机的情况，就可能出现此故障。经过一段时间以后，会出现GET/PUT或者任何其它请求处理缓慢的情况。 故障特征 此故障有两个明显的特征： 故障出现前需要等待的时间，随机性很强，有时甚至测试了数小时都没有发现请求缓慢的情况。常常发生的情况是，宕机后剩下的两个实例，一个很快出现缓慢问题，另外一个却还能运行较长时间 请求处理延缓的时长不定，有时候没有明显延缓，有时候长达10+秒。而且一次缓慢请求后，可以跟着10多次正常速度处理的请求。这个特征提示故障和某种周期性的、长时间占用的锁有关。在锁被释放的间隙，请求可以被快速处理 故障分析 <a class="read-more" href="https://blog.gmem.cc/debugging-slow-keydb">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/debugging-slow-keydb">记录一次KeyDB缓慢的定位过程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">环境说明</span></div>
<div class="blog_h2"><span class="graybg">运行环境</span></div>
<p>这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点：</p>
<pre class="crayon-plain-tag"># kubectl get node -o wide
NAME              STATUS   VERSION   INTERNAL-IP      OS-IMAGE                KERNEL-VERSION              CONTAINER-RUNTIME
192.168.104.51    Ready    v1.18.3   192.168.104.51   CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9
192.168.104.72    Ready    v1.18.3   192.168.104.72   CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9
192.168.104.108   Ready    v1.18.3   192.168.104.108  CentOS Linux 7 (Core)   3.10.0-862.3.2.el7.x86_64   docker://19.3.9</pre>
<div class="blog_h2"><span class="graybg">KeyDB配置</span></div>
<p><a href="/keydb-study-note">KeyDB</a>通过StatefulSet管理，一共有三个实例： </p>
<pre class="crayon-plain-tag"># kubectl -n default get pod -o wide -l app.kubernetes.io/name=keydb
NAME             READY   STATUS    RESTARTS     IP             NODE            
keydb-0   1/1     Running   0            172.29.2.63     192.168.104.108 
keydb-1   1/1     Running   0            172.29.1.69     192.168.104.72  
keydb-2   1/1     Running   0            172.29.1.121    192.168.104.51</pre>
<p>这三个实例：</p>
<ol>
<li>由于反亲和设置，会在每个节点上各运行一个实例</li>
<li>启用Active - Active（--active-replica）模式的多主（--multi-master）复制 ：每个实例都是另外两个的Slave，每个实例都支持读写</li>
</ol>
<div class="blog_h1"><span class="graybg">故障描述</span></div>
<div class="blog_h2"><span class="graybg">触发条件</span></div>
<p>出现一个节点宕机的情况，就可能出现此故障。经过一段时间以后，会出现<span style="background-color: #c0c0c0;">GET/PUT或者任何其它请求处理缓慢的情况</span>。</p>
<div class="blog_h2"><span class="graybg">故障特征</span></div>
<p>此故障有两个明显的特征：</p>
<ol>
<li>故障出现前需要等待的时间，随机性很强，有时甚至测试了数小时都没有发现请求缓慢的情况。常常发生的情况是，宕机后剩下的两个实例，一个很快出现缓慢问题，另外一个却还能运行较长时间</li>
<li>请求处理延缓的时长不定，有时候没有明显延缓，有时候长达10+秒。而且一次缓慢请求后，可以跟着10多次正常速度处理的请求。这个特征提示故障和某种<span style="background-color: #c0c0c0;">周期性的、长时间占用的锁</span>有关。在锁被释放的间隙，请求可以被快速处理</li>
</ol>
<div class="blog_h1"><span class="graybg">故障分析</span></div>
<div class="blog_h2"><span class="graybg">触发故障</span></div>
<p>我们将节点192.168.104.108强制关闭，这样实例keydb-0无法访问，另外两个节点无法和它进行Replication。</p>
<p>分别登录另外两个节点，监控GET/SET操作的性能：</p>
<pre class="crayon-plain-tag">kubectl -n default exec -it keydb-1 -- bash -c  \
  'while true; do key=keydb-1-$(date +%s); keydb-cli set $key $key-val; keydb-cli get $key; done'

kubectl -n default exec -it keydb-2 -- bash -c \
 'while true; do key=keydb-2-$(date +%s); keydb-cli set $key $key-val; keydb-cli get $key; done'</pre>
<p>监控Replication相关信息：</p>
<pre class="crayon-plain-tag">watch -- kubectl -n default exec -i keydb-1 -- keydb-cli info replication

watch -- kubectl -n default exec -i keydb-2 -- keydb-cli info replication</pre>
<p>监控KeyDB日志： </p>
<pre class="crayon-plain-tag">kubectl -n default logs  keydb-1 -f

kubectl -n default logs  keydb-2 -f</pre>
<p>经过一段时间，keydb-1请求处理随机延缓的情况出现：</p>
<pre class="crayon-plain-tag">127.0.0.1:6379&gt; set hello world
OK
(1.24s)
127.0.0.1:6379&gt; set hello world
OK
(8.96s)
127.0.0.1:6379&gt; get hello
"world"
(5.99s)
127.0.0.1:6379&gt; get hello
"world"
(9.44s) </pre>
<p>此时keydb-2仍然正常运行，请求处理速度正常</p>
<div class="blog_h2"><span class="graybg">缓慢查询</span></div>
<p>获取keydb-1的慢查询，没有发现有价值的信息。而且延缓的时间没有计算在内：</p>
<pre class="crayon-plain-tag">127.0.0.1:6379&gt; slowlog get 10                                         
1) 1) (integer) 7                                                      
   2) (integer) 1611833042                                             
   3) (integer) 14431               # 最慢的查询才耗时14ms                                            
   4) 1) "set"                                                         
      2) "keydb-1-1611833042"                                          
      3) "keydb-1-1611833042-val"                                      
   5) "127.0.0.1:38488"                                                
   6) ""                                                               
2) 1) (integer) 6                                                      
   2) (integer) 1611831322                                             
   3) (integer) 14486                                                  
   4) 1) "get"                                                         
      2) "keydb-1-1611831312"                                          
   5) "127.0.0.1:51680"                                                
   6) ""  </pre>
<div class="blog_h2"><span class="graybg">日志分析</span></div>
<p>部署KeyDB已经设置<pre class="crayon-plain-tag">--loglevel debug</pre>，以获得尽可能详尽的日志。</p>
<p>由于正在运行不间断执行SET/GET操作的脚本，因此日志量很大而刷屏，但是每隔一段时间就会出现卡顿。下面是keydb-1的日志片段：</p>
<pre class="crayon-plain-tag">7:11:S 28 Jan 2021 08:57:51.233 - Client closed connection
7:11:S 28 Jan 2021 08:57:51.251 - Accepted 127.0.0.1:44224
7:11:S 28 Jan 2021 08:57:51.252 - Client closed connection
7:12:S 28 Jan 2021 08:57:51.276 - Accepted 127.0.0.1:44226
7:11:S 28 Jan 2021 08:57:51.277 - Client closed connection
# 这一行日志之后，卡顿了10s。没有任何日志输出
7:11:S 28 Jan 2021 08:57:51.279 * Connecting to MASTER keydb-0.keydb:6379
7:11:S 28 Jan 2021 08:58:01.290 * Unable to connect to MASTER: Resource temporarily unavailable
7:11:S 28 Jan 2021 08:58:01.290 - Accepted 127.0.0.1:44228
7:11:S 28 Jan 2021 08:58:01.290 - Accepted 127.0.0.1:44264</pre>
<p>从日志信息上可以看到，卡顿前keydb-1正在尝试连接到已经宕机的keydb-0，这个连接尝试被阻塞10秒后报<pre class="crayon-plain-tag">EAGAIN</pre>错误。</p>
<p>阻塞期间SET/GET请求得不到处理，猜测原因包括：</p>
<ol>
<li>连接keydb-0的时候，占用了某种全局的锁，SET/GET请求也需要持有该锁</li>
<li>连接keydb-0、处理SET/GET请求，由同一线程负责</li>
</ol>
<p>第2种猜测应该不大可能，因为KeyDB宣称的优势之一就是，支持多线程处理请求。并且我们设置了参数<pre class="crayon-plain-tag">--server-threads 2</pre>，也就是有两个线程用于处理请求。</p>
<p>EAGAIN这个报错也没有参考价值，因为目前不卡顿的实例keydb-2输出的日志是一样的，只是没有任何卡顿：</p>
<pre class="crayon-plain-tag">7:11:S 28 Jan 2021 08:19:22.624 * Connecting to MASTER keydb-0.keydb:6379
# 仅仅耗时5ms即检测到连接失败
7:11:S 28 Jan 2021 08:19:22.629 * Unable to connect to MASTER: Resource temporarily unavailable</pre>
<div class="blog_h2"><span class="graybg">源码分析</span></div>
<div class="blog_h3"><span class="graybg">复制定时任务</span></div>
<p>我们使用的KeyDB版本是5.3.3，尝试用关键字“Connecting to MASTER”搜索，发现只有一个匹配，位于<pre class="crayon-plain-tag">replicationCron</pre>函数中。从函数名称上就可以看到，它是和复制（Replication）有关的定时任务。</p>
<p>KeyDB启动时会调用<pre class="crayon-plain-tag">initServer</pre>进行初始化，后者会在事件循环中每1ms调度一次<pre class="crayon-plain-tag">serverCron</pre>。serverCron负责后台任务的总体调度，它的一个职责就是，每1s调度一次replicationCron函数。</p>
<p>下面看一下replicationCron的源码：</p>
<pre class="crayon-plain-tag">/* Replication cron function, called 1 time per second. */
void replicationCron(void) {
    static long long replication_cron_loops = 0;
    serverAssert(GlobalLocksAcquired());
    listIter liMaster;
    listNode *lnMaster;
    listRewind(g_pserver-&gt;masters, &amp;liMaster);
    // 遍历当前实例的每一个Master
    while ((lnMaster = listNext(&amp;liMaster)))
    {
        redisMaster *mi = (redisMaster*)listNodeValue(lnMaster);
        std::unique_lock&lt;decltype(mi-&gt;master-&gt;lock)&gt; ulock;
        // 获得              Master的 客户端的 锁
        if (mi-&gt;master != nullptr)
            ulock = decltype(ulock)(mi-&gt;master-&gt;lock);

        /* Non blocking connection timeout? */
        // 如果当前复制状态为：正在连接到Master
        // 或者复制状态处于握手阶段（包含多个状态）且超时了
        if (mi-&gt;masterhost &amp;&amp;
            (mi-&gt;repl_state == REPL_STATE_CONNECTING ||
            slaveIsInHandshakeState(mi)) &amp;&amp;
            (time(NULL)-mi-&gt;repl_transfer_lastio) &gt; g_pserver-&gt;repl_timeout)
        {
            // 那么取消握手 —— 取消进行中的非阻塞连接尝试，或者取消进行中的RDB传输
            serverLog(LL_WARNING,"Timeout connecting to the MASTER...");
            cancelReplicationHandshake(mi);
        }

        /* Bulk transfer I/O timeout? */
        // 如果当前正在接收来自Master的RDB文件且超时了
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;repl_state == REPL_STATE_TRANSFER &amp;&amp;
            (time(NULL)-mi-&gt;repl_transfer_lastio) &gt; g_pserver-&gt;repl_timeout)
        {
            serverLog(LL_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in keydb.conf to a larger value.");
            // 那么取消握手
            cancelReplicationHandshake(mi);
        }

        /* Timed out master when we are an already connected replica? */
        // 如果当前复制状态为：已连接。而且超时之前没有活动（正常情况下有心跳维持）
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;master &amp;&amp; mi-&gt;repl_state == REPL_STATE_CONNECTED &amp;&amp;
            (time(NULL)-mi-&gt;master-&gt;lastinteraction) &gt; g_pserver-&gt;repl_timeout)
        {
            // 那么释放掉客户端资源
            serverLog(LL_WARNING,"MASTER timeout: no data nor PING received...");
            if (FCorrectThread(mi-&gt;master))
                freeClient(mi-&gt;master);
            else
                freeClientAsync(mi-&gt;master);
        }

        /* Check if we should connect to a MASTER */
        // 上面几个分支都不会匹配我们的场景，因为keydb-0已经宕机，因此
        // 状态必然是REPL_STATE_CONNECT
        if (mi-&gt;repl_state == REPL_STATE_CONNECT) {
            // 这一行就是卡顿前的日志
            serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
                mi-&gt;masterhost, mi-&gt;masterport);
            // 发起连接
            if (connectWithMaster(mi) == C_OK) {
                serverLog(LL_NOTICE,"MASTER &lt;-&gt; REPLICA sync started");
            }
        }

        // 每秒钟发送心跳给Master
        if (mi-&gt;masterhost &amp;&amp; mi-&gt;master &amp;&amp;
            !(mi-&gt;master-&gt;flags &amp; CLIENT_PRE_PSYNC))
            replicationSendAck(mi);
    }

    // 后面处理和本实例的Slave有关的逻辑，例如发送心跳。和我们的场景无关，略...
}</pre>
<p>很明显，卡顿是因为调用<pre class="crayon-plain-tag">connectWithMaster</pre>导致的。从代码注释也可以看到，KeyDB期望这个连接操作是非阻塞的，但是不知道为何，在我们的场景中严重的阻塞了。</p>
<p>进一步查看connectWithMaster的代码：</p>
<pre class="crayon-plain-tag">int connectWithMaster(redisMaster *mi) {
    int fd;

    fd = anetTcpNonBlockBestEffortBindConnect(NULL,
        mi-&gt;masterhost,mi-&gt;masterport,NET_FIRST_BIND_ADDR);
    if (fd == -1) {
        int sev = g_pserver-&gt;enable_multimaster ? LL_NOTICE : LL_WARNING;
        // 这一行是卡顿10s后的日志，因此阻塞发生在anetTcpNonBlockBestEffortBindConnect函数中
        serverLog(sev,"Unable to connect to MASTER: %s", strerror(errno));
        return C_ERR;
    }
    // ...
}

int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port,
                                         char *source_addr)
{
    return anetTcpGenericConnect(err,addr,port,source_addr,
            // 非阻塞 + BestEffort绑定
            ANET_CONNECT_NONBLOCK|ANET_CONNECT_BE_BINDING);
}


static int anetTcpGenericConnect(char *err, char *addr, int port,
                                 char *source_addr, int flags)
{
    int s = ANET_ERR, rv;
    char portstr[6];  /* strlen("65535") + 1; */
    struct addrinfo hints, *servinfo, *bservinfo, *p, *b;

    snprintf(portstr,sizeof(portstr),"%d",port);
    memset(&amp;hints,0,sizeof(hints));
    // 不指定地址族，这会触发getaddrinfo同时进行A/AAAA查询
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    // 根据Master的主机名查找得到IP地址信息（addrinfo）列表
    if ((rv = getaddrinfo(addr,portstr,&amp;hints,&amp;servinfo)) != 0) {
        anetSetError(err, "%s", gai_strerror(rv));
        return ANET_ERR;
    }
    // 遍历Master的IP地址列表
    for (p = servinfo; p != NULL; p = p-&gt;ai_next) {
        // 创建套接字，如果socket/connect调用失败，则尝试下一个
        if ((s = socket(p-&gt;ai_family,p-&gt;ai_socktype,p-&gt;ai_protocol)) == -1)
            continue;
        // 设置套接字选项SO_REUSEADDR
        if (anetSetReuseAddr(err,s) == ANET_ERR) 
            goto error;
        // 设置套接字选项 SO_REUSEPORT
        if (flags &amp; ANET_CONNECT_REUSEPORT &amp;&amp; anetSetReusePort(err, s) != ANET_OK)
            goto error;
        // 调用fcntl设置 O_NONBLOCK
        if (flags &amp; ANET_CONNECT_NONBLOCK &amp;&amp; anetNonBlock(err,s) != ANET_OK)
            goto error;
        if (source_addr) {
            int bound = 0;
            /* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */
            // 解析源地址
            if ((rv = getaddrinfo(source_addr, NULL, &amp;hints, &amp;bservinfo)) != 0)
            {
                anetSetError(err, "%s", gai_strerror(rv));
                goto error;
            }
            for (b = bservinfo; b != NULL; b = b-&gt;ai_next) {
                // 绑定到第一个源地址
                if (bind(s,b-&gt;ai_addr,b-&gt;ai_addrlen) != -1) {
                    bound = 1;
                    break;
                }
            }
            freeaddrinfo(bservinfo);
            if (!bound) {
                // 绑定源地址失败，跳转到Best Effort绑定
                anetSetError(err, "bind: %s", strerror(errno));
                goto error;
            }
        }
        // 发起连接
        if (connect(s,p-&gt;ai_addr,p-&gt;ai_addrlen) == -1) {
            // 我们的场景下套接字是非阻塞的，因此这里会立即返回EINPROGRESS，属于预期行为
            if (errno == EINPROGRESS &amp;&amp; flags &amp; ANET_CONNECT_NONBLOCK)
                goto end;
            // 其它错误均认为失败，尝试连接下一个Master地址
            close(s);
            s = ANET_ERR;
            continue;
        }

        goto end;
    }
    if (p == NULL)
        anetSetError(err, "creating socket: %s", strerror(errno));

error:
    if (s != ANET_ERR) {
        close(s);
        s = ANET_ERR;
    }

end:
    freeaddrinfo(servinfo);

    // 上面指定源地址，绑定失败时跳转到此处。尝试不指定源地址来连接
    if (s == ANET_ERR &amp;&amp; source_addr &amp;&amp; (flags &amp; ANET_CONNECT_BE_BINDING)) {
        return anetTcpGenericConnect(err,addr,port,NULL,flags);
    } else {
        return s;
    }
}</pre>
<p>尽管可以确定connectWithMaster调用的anetTcpGenericConnect就是发生阻塞的地方，但是从代码上看不出什么问题，就是简单的socket、bind，外加一个非阻塞的connect操作。</p>
<div class="blog_h3"><span class="graybg">请求处理逻辑</span></div>
<p>从现象上我们已经看到了，复制定时器卡顿的时候，请求处理也无法进行。通过代码分析，也明确了卡顿期间，复制定时器持有Master的客户端的锁。</p>
<p>那么，关于请求处理（线程？）会和复制定时器产生锁争用的猜测是否正确呢？</p>
<div class="blog_h2"><span class="graybg">单步跟踪</span></div>
<p>为了精确定位阻塞的代码，我们使用GDB进行单步跟踪：</p>
<pre class="crayon-plain-tag">#              需要特权模式，否则无法加载符号表
docker run -it --rm --name gdb --privileged --net=host --pid=host --entrypoint gdb docker.gmem.cc/debug

(gdb) attach 449
(gdb) break replication.cpp:3084
# 连续执行s，以step into anet.c
(gdb) s
# 连续执行n命令
(gdb) n
# 卡顿后，查看变量
# 解析的地址
(gdb) p addr
$2 = 0x7f2f31411281 "keydb-0.keydb"
# getaddrinfo的返回值
(gdb) p rv
$3 = -3</pre>
<p>进入anetTcpGenericConnect后，逐行执行，多次测试，均在<pre class="crayon-plain-tag">anet.c</pre>的291行出现卡顿：</p>
<pre class="crayon-plain-tag">if ((rv = getaddrinfo(addr,portstr,&amp;hints,&amp;servinfo)) != 0) {
    anetSetError(err, "%s", gai_strerror(rv));
    return ANET_ERR;
}</pre>
<p>也就是说，调用getaddrinfo函数耗时可能长达数秒。这是来自glibc的标准函数，用于将主机名解析为IP地址。</p>
<p>调试过程中发现此函数的返回值是-3，我们的场景中，需要解析的地址是keydb-0.keydb，卡顿时函数的返回值是-3，<pre class="crayon-plain-tag">man getaddrinfo</pre>可以了解到此返回值的意义：</p>
<p style="padding-left: 30px;">EAI_AGAIN  The name server returned a temporary failure indication. Try again later.</p>
<p>乍看起来，好像是<a href="/tcp-ip-study-note#dns">DNS</a>服务器，也就是K8S的<a href="/coredns-study-note">CoreDNS</a>存在问题。但无法解释此时keydb-2.keydb没有受到影响？</p>
<div class="blog_h2"><span class="graybg">检查CoreDNS</span></div>
<p>为了确认CoreDNS是否存在问题，我们分别在宿主机上、两个实例的网络命名空间中进行验证：</p>
<pre class="crayon-plain-tag"># nslookup keydb-0.keydb.default.svc.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.default.svc.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb.svc.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.svc.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb.cluster.local 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb.cluster.local: NXDOMAIN

# nslookup keydb-0.keydb 10.96.0.10
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find keydb-0.keydb: SERVFAIL</pre>
<p>反复测试循环测试，没有任何解析缓慢的现象。此外，查看CoreDNS的日志，我们也发现了来自keydb-1.keydb和keydb-2.keydb的查询请求，请求都是通过UDP协议发送的，处理耗时都是亚毫秒级别。</p>
<p>也就是说，从KeyDB实例所在宿主机/命名空间到CoreDNS的网络链路、CoreDNS服务器自身，都没有问题。</p>
<p>这就让人头疼了……难道问题出在getaddrinfo函数内部？或者在单步跟踪时判断错误，问题和DNS无关？为了确认，我们在CoreDNS上动了点手脚，强制将keydb-0.keydb解析到一个不存在的IP地址：</p>
<pre class="crayon-plain-tag">.:53 {
    # ...
    hosts {
        192.168.144.51  keydb-1.keydb
    }
    # ...
}</pre>
<p>结果很快，卡顿的问题就消失了。所以，我们更加怀疑问题出在getaddrinfo函数上了。</p>
<div class="blog_h2"><span class="graybg">调试getaddrinfo</span></div>
<p>查看文件/etc/lsb-release，可以看到KeyDB镜像是基于Ubuntu 18.04.4 LTS构建的，使用的libc6版本是2.27-3ubuntu1。</p>
<p>在launchpad.net找到了它的<a href="http://launchpadlibrarian.net/365856914/libc6-dbg_2.27-3ubuntu1_amd64.deb">调试文件</a>和<a href="http://launchpadlibrarian.net/365856911/glibc-source_2.27-3ubuntu1_all.deb">源码</a>。下载deb包，解压后复制到GDB容器，然后设置一下调试文件目录，就可以step into到glibc的代码进行跟踪了：</p>
<pre class="crayon-plain-tag">ar x libc6-dbg_2.27-3ubuntu1_amd64.deb
tar -xf data.tar.xz 

# 拷贝到我们正在运行GDB的容器
docker cp usr gdb:/root

# 修改调试文件搜索目录
(gdb) set debug-file-directory /root/usr/lib/debug
# 打断点，下面是缓慢的执行路径
(gdb) b anet.c:291
(gdb) b getaddrinfo.c:342
(gdb) b getaddrinfo.c:786  
# (gdb) print fct4
# $2 = (nss_gethostbyname4_r) 0x7f32f97e9a70 &lt;_nss_dns_gethostbyname4_r&gt;
(gdb) b dns-host.c:317
(gdb) b res_query.c:336
(gdb) b res_query.c:495                       # invoke __res_context_querydomain
(gdb) b res_query.c:601                       # invoke __res_context_query    
(gdb) b res_query.c:216                       # invoke __res_context_send
(gdb) b res_send.c:1066 if buflen==45         # send_dg</pre>
<p>通过调试，我们发现getaddrinfo会依次对4个名字进行DNS查询：</p>
<p style="padding-left: 30px;">keydb-0.keydb.default.svc.cluster.local. <br />keydb-0.keydb.svc.cluster.local. <br />keydb-0.keydb.cluster.local.<br />keydb-0.keydb.</p>
<p>CoreDNS的日志显示，所有请求都快速的处理完毕：</p>
<pre class="crayon-plain-tag">4242 "A IN keydb-0.keydb.default.svc.cluster.local. udp 68 false 512" NXDOMAIN qr,aa,rd 161 0.000215337s
38046 "AAAA IN keydb-0.keydb.default.svc.cluster.local. udp 68 false 512" NXDOMAIN qr,aa,rd 161 0.000203934s

23194 "A IN keydb-0.keydb.svc.cluster.local. udp 63 false 512" NXDOMAIN qr,aa,rd 156 0.000301011s
23722 "AAAA IN keydb-0.keydb.svc.cluster.local. udp 63 false 512" NXDOMAIN qr,aa,rd 156 0.000125386s

36552 "A IN keydb-0.keydb.cluster.local. udp 59 false 512" NXDOMAIN qr,aa,rd 152 0.000281247s
217 "AAAA IN keydb-0.keydb.cluster.local. udp 59 false 512" NXDOMAIN qr,aa,rd 152 0.000150689s

6776 "A IN keydb-0.keydb. udp 45 false 512" NOERROR - 0 0.000196686s
6776 "A IN keydb-0.keydb. udp 45 false 512" NOERROR - 0 0.000157011s </pre>
<p>最后一个名字，也就是传递给getaddrinfo的原始请求keydb-0.keydb.的处理过程有以下值得注意的点：</p>
<ol>
<li>从GDB角度来看，<span style="background-color: #c0c0c0;">卡顿就是在解析该名字时出现</span></li>
<li>从CoreDNS日志上看，没有AAAA请求。由于KeyDB<span style="background-color: #c0c0c0;">指定了AF_UNSPEC，getaddrinfo会同时发送并等待A/AAAA应答</span>。可能<span style="background-color: #c0c0c0;">因为某种原因，该名字的AAAA解析过程没有完成，导致getaddrinfo一直等待到超时</span>。作为对比，没有卡顿的keydb-2的A/AAAA查询处理过程都是正常的</li>
<li>其它名字是一次A请求，一次AAAA请求。该名字却是两次A请求，而且，<span style="background-color: #c0c0c0;">第一次A请求日志出现了数秒后，第二次日志才出现</span>。有可能第二次是getaddrinfo没有收到应答而进行的重试</li>
<li>前三个名字分别的错误码是NXDOMAIN，该名字的错误码却是<a href="/tcp-ip-study-note#dns-rtnmsg">NOERROR</a>。通过nslookup/dig查询，错误码却是SERVFAIL，难道是CoreDNS日志有BUG？尽管如此，是否不同的错误码影响了getaddrinfo的行为</li>
</ol>
<div class="blog_h2"><span class="graybg">抓包分析</span></div>
<p>glibc的代码是优化过（<a href="https://stackoverflow.com/questions/30089652/glibc-optimizations-required">也必须优化</a>）的，GDB跟踪起来相当耗时，因此我们打算换一个角度来定位问题。基于上一节的分析，我们相信实例keydb-1.keydb在发送DNS请求的时候存在超时或丢包的情况，可以抓包来证实：</p>
<pre class="crayon-plain-tag"># 进入keydb-1.keydb的网络命名空间
nsenter -t 449 --net
# 抓包
tcpdump -i any -vv -nn udp port 53</pre>
<p>抓包的结果如下： </p>
<p style="padding-left: 30px;"><span style="background-color: #c0c0c0;">对 keydb-0.keydb.default.svc.cluster.local.  的A请求</span><br /><em> 172.29.1.69.42083 &gt; 10.96.0.10.53: [bad udp cksum 0xb829 -&gt; 0x95aa!] 22719+ A? keydb-0.keydb.default.svc.cluster.local. (68)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.42083: [bad udp cksum 0xb886 -&gt; 0x3f57!] 22719 NXDomain*- q: A? keydb-0.keydb.default.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (161)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb.default.svc.cluster.local.  的AAAA请求，注意，仍然使用之前的UDP套接字</span><br /> 172.29.1.69.42083 &gt; 10.96.0.10.53: [bad udp cksum 0xb829 -&gt; 0x4d76!] 41176+ AAAA? keydb-0.keydb.default.svc.cluster.local. (68)<br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.42083: [bad udp cksum 0xb886 -&gt; 0xf722!] 41176 NXDomain*- q: AAAA? keydb-0.keydb.default.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (161)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.svc.cluster.local. 的A请求，注意，这里使用了新的UDP套接字</span><br /> <em>172.29.1.69.45508 &gt; 10.96.0.10.53: [bad udp cksum 0xb824 -&gt; 0x3b5e!] 45156+ A? keydb-0.keydb.svc.cluster.local. (63)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.45508: [bad udp cksum 0xb881 -&gt; 0x21ce!] 45156 NXDomain*- q: A? keydb-0.keydb.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (156)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb.svc.cluster.local. 的AAAA请求</span><br /> 172.29.1.69.45508 &gt; 10.96.0.10.53: [bad udp cksum 0xb824 -&gt; 0x8a4e!] 18036+ AAAA? keydb-0.keydb.svc.cluster.local. (63)<br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.45508: [bad udp cksum 0xb881 -&gt; 0x70be!] 18036 NXDomain*- q: AAAA? keydb-0.keydb.svc.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (156)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.cluster.local. 的A请求</span><br /><em>172.29.1.69.48243 &gt; 10.96.0.10.53: [bad udp cksum 0xb820 -&gt; 0x5054!] 2718+ A? keydb-0.keydb.cluster.local. (59)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain</span><br /> <em>10.96.0.10.53 &gt; 172.29.1.69.48243: [bad udp cksum 0xb87d -&gt; 0x36c4!] 2718 NXDomain*- q: A? keydb-0.keydb.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (152)</em><br /><span style="background-color: #c0c0c0;">对 keydb-0.keydb.cluster.local. 的AAAA请求</span><br /> <em>172.29.1.69.48243 &gt; 10.96.0.10.53: [bad udp cksum 0xb820 -&gt; 0x5147!] 61098+ AAAA? keydb-0.keydb.cluster.local. (59)</em><br /><span style="background-color: #c0c0c0;">CoreDNS应答NXDomain，这里开始，我们保留时间戳那一行日志</span><br /><em>14:42:15.028168 IP (tos 0x0, ttl 63, id 44636, offset 0, flags [DF], proto UDP (17), length 180)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.48243: [bad udp cksum 0xb87d -&gt; 0x37b7!] 61098 NXDomain*- q: AAAA? keydb-0.keydb.cluster.local. 0/1/0 ns: cluster.local. SOA ns.dns.cluster.local. hostmaster.cluster.local. 1612073759 7200 1800 86400 30 (152)</em><br /><span style="background-color: #c0c0c0;">对keydb-0.keydb的A请求，这里还没有出现卡顿</span><br /><em>14:42:15.028328 IP (tos 0x0, ttl 64, id 30583, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 172.29.1.69.47652 &gt; 10.96.0.10.53: [bad udp cksum 0xb812 -&gt; 0x181e!] 26682+ A? keydb-0.keydb. (45)</em><br /><span style="background-color: #c0c0c0;">很快接收到CoreDNS的ServFail应答，抓包和我们nslookup/dig的错误码一致，CoreDNS日志显示的应该不正常</span><br /><span style="background-color: #c0c0c0;">猜测“有可能第二次是getaddrinfo没有收到应答而进行的重试”被排除，至少说没收到应答不是网络层面的原因</span><br /><em>14:42:15.028651 IP (tos 0x0, ttl 63, id 44637, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.47652: [bad udp cksum 0xb812 -&gt; 0x981b!] 26682 ServFail- q: A? keydb-0.keydb. 0/0/0 (45)</em><br /><span style="background-color: #c0c0c0;">再一次对keydb-0.keydb.的A请求，<strong>注意时间戳，刚好5秒之后</strong>，这是默认DNS请求超时。<strong>还是使用之前的套接字</strong></span><br /><em>14:42:20.029271 IP (tos 0x0, ttl 64, id 33006, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 172.29.1.69.47652 &gt; 10.96.0.10.53: [bad udp cksum 0xb812 -&gt; 0x181e!] 26682+ A? keydb-0.keydb. (45)</em><br /><span style="background-color: #c0c0c0;">很快接收到CoreDNS的ServFail应答</span><br /><em>14:42:20.029812 IP (tos 0x0, ttl 63, id 46397, offset 0, flags [DF], proto UDP (17), length 73)</em><br /><em> 10.96.0.10.53 &gt; 172.29.1.69.47652: [bad udp cksum 0xb812 -&gt; 0x981b!] 26682 ServFail- q: A? keydb-0.keydb. 0/0/0 (45)</em></p>
<p>通过上述分析我们可以相信，keydb-1.keydb容器到CoreDNS之间的DNS通信是没有问题的。但是，getaddrinfo似乎没有收到keydb-0.keydb的第一次应答，并且在超时（5s）之后进行重试</p>
<div class="blog_h2"><span class="graybg">Conntrack竞态条件</span></div>
<p>tcpdump和应用程序之间，还有个netfilter框架。回想起之前阅读过的文章：<a href="/dns-problems-on-k8s">Kubernetes上和DNS相关的问题</a>，conntrack相关的竞态条件可能导致DNS查询5秒超时。遗憾的是，这里的故障和此竞态条件无关：</p>
<ol>
<li>通过<pre class="crayon-plain-tag">conntrack -S</pre>看到的<pre class="crayon-plain-tag">insert_failed</pre>是0</li>
<li>故障一旦出现，就每次都会超时5s，没有竞态条件的随机性</li>
<li>如果是conntrack竞态条件导致，无法解释为什么前面3个名字解析正常，也无法解释为什么CoreDNS中配置一个静态解析故障就消失</li>
</ol>
<div class="blog_h1"><span class="graybg">深入理解</span></div>
<div class="blog_h2"><span class="graybg">getaddrinfo</span></div>
<p>在IPv4中，我们使用<pre class="crayon-plain-tag">gethostbyname</pre>实现主机名到地址的解析。<pre class="crayon-plain-tag">getaddrinfo</pre>也用于地址解析，而且它是协议无关的，既可用于IPv4也可用于IPv6。它的原型如下：</p>
<pre class="crayon-plain-tag">int getaddrinfo(const char* hostname,  // 主机名，可以使用IP地址或者DNS名称
                const char* service,   // 服务名，可以使用端口号或者/etc/services中的服务名
                const struct addrinfo* hints, // 可以NULL，或者一个addrinfo，提示调用者想得到的信息类型
                struct addrinfo** res);  // 解析得到的addrinfo，地址的链表</pre>
<p>此函数返回的是套接字地址信息的链表，地址信息存储在下面的addrinfo结构中。参数<pre class="crayon-plain-tag">hints</pre>会影响getaddrinfo的行为，提示信息同样存放在addrinfo结构中：</p>
<pre class="crayon-plain-tag">struct addrinfo
{
  // 额外的提示标记
  int ai_flags;	
  // 提示需要查询哪些地址族，默认AF_UNSPEC，这意味着同时查询IPv4和IPv6地址
  // 也就是同时发起A/AAAA查询
  int ai_family;
  // 提示偏好的套接字类型，例如SOCK_STREAM|SOCK_DGRAM，默认可以返回任何套接字类型
  int ai_socktype;
  // 提示返回的套接字地址的协议类型
  int ai_protocol;

  // 套接字地址
  socklen_t ai_addrlen;
  struct sockaddr *ai_addr;
  // ...
  // 指向链表的下一条目
  struct addrinfo *ai_next;
};</pre>
<p>很多软件调用getaddrinfo的时候，都会指定AF_UNSPEC（或者不提供hints，效果一样），例如KeyDB。但是，很多运行环境根本没有IPv6支持，这就凭白的给DNS服务器增加了负担。这也是在K8S中查看CoreDNS日志，总是会发现很多AAAA记录的原因。</p>
<div class="blog_h3"><span class="graybg">解析流程概览</span></div>
<p>KeyDB 5.3.3使用的glibc版本是2.27。函数getaddrinfo过于冗长，这里就不贴出来了，大概梳理一下：</p>
<ol>
<li>如果可能，它会通过/var/run/nscd/socket访问DNS缓存服务，我们没有这个服务</li>
<li>初始化NSS的hosts数据库，如果没有在文件中配置，则默认使用<pre class="crayon-plain-tag">hosts: dns [!UNAVAIL=return] files</pre>，我们的环境下配置是<pre class="crayon-plain-tag">hosts: files dns</pre></li>
<li>通过<a href="/linux-faq#nss">NSS</a>进行名字查询，实际上是调用<pre class="crayon-plain-tag">gethostbyname4_r</pre>函数：
<ol>
<li>查找files源，调用<pre class="crayon-plain-tag">_nss_files_gethostbyname4_r</pre>函数，也就是打开/etc/hosts查找。K8S容器中，/etc/hosts中仅仅存在当前Pod的条目，因此files源不会匹配</li>
<li>查找dns源，调用<pre class="crayon-plain-tag">_nss_dns_gethostbyname4_r</pre>函数：
<ol>
<li>读取/etc/resolv.conf构建<pre class="crayon-plain-tag">resolv_context</pre>。我们的环境下，配置文件内容为：<br />
<pre class="crayon-plain-tag">nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5</pre>
</li>
<li>
<p>调用<pre class="crayon-plain-tag">__res_context_search</pre>， 执行DNS查找逻辑。它会<span style="background-color: #c0c0c0;">将上面的search domain作为域名后缀，产生多个名字，逐个尝试。每个名字查询失败时都会重试，重试时尽可能选择不同的DNS服务器。可能同时发起A/AAA查询</span></p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">DNS搜索逻辑</span></div>
<p>__res_context_search()首先会计算一下，待查找名字中的dot的数量。如果<span style="background-color: #c0c0c0;">名字以dot结尾，或者dot数量大于等于ndots</span>，则直接调用<pre class="crayon-plain-tag">__res_context_querydomain</pre>向DNS服务器发请求，该函数会同时发起A/AAAA查询。</p>
<p>否则，它会根据/etc/resolv.conf中的search domain列表，给待查找的名字加后缀，然后多次向DNS服务器发请求。我们的环境下，待查找名字为keydb-0.keydb，getaddrinfo函数会依次尝试：</p>
<p style="padding-left: 30px;">keydb-0.keydb.default.svc.cluster.local. <br />keydb-0.keydb.svc.cluster.local. <br />keydb-0.keydb.cluster.local.<br />keydb-0.keydb.</p>
<p>需要注意：</p>
<ol>
<li>向DNS服务器发请求，仍然是由__res_context_querydomain()负责</li>
<li>一旦查找成功，就立即返回不再尝试其它search domain</li>
<li><span style="background-color: #c0c0c0;">不加修饰的原始名字，会放在最后尝试</span></li>
</ol>
<p>在K8S中，*.cluster.local一般都由CoreDNS自身负责，处理速度会很快。至于keydb-0.keydb.的处理速度，如果为CoreDNS配置了上游DNS，则处理速度依赖于外部环境。</p>
<p>__res_context_querydomain仅仅是在domain参数不为空的时候，将name和domain连接起来，然后调用<pre class="crayon-plain-tag">__res_context_query</pre>函数。</p>
<div class="blog_h3"><span class="graybg">DNS查询过程</span></div>
<p>__res_context_query负责和DNS服务器的交互，完成<span style="background-color: #c0c0c0;">单个名字的DNS查</span>询。它会调用<pre class="crayon-plain-tag">__res_context_mkquery</pre><span style="background-color: #c0c0c0;">构建一个查询请求（对应DNS报文），然后发送</span>，然后等待应答。这是一个阻塞的过程，KeyDB在期望非阻塞的代码路径下调用getaddrinfo且没有任何缓存机制，同时还加了锁，我觉得是不妥的。这导致DNS缓慢/不可用会极大的影响KeyDB的服务质量。</p>
<p>发送DNS请求的代码在<pre class="crayon-plain-tag">__res_context_send</pre>中，<span style="background-color: #c0c0c0;">重试逻辑</span>发生在该函数中，我们的环境下重试次数为2，这解释了两次keydb-0.keydb. A查询：</p>
<pre class="crayon-plain-tag">//                  重试次数，statp-&gt;retry为2
for (try = 0; try &lt; statp-&gt;retry; try++) {
    // 如果有多个DNS服务器，重试时会轮询它们
    for (unsigned ns_shift = 0; ns_shift &lt; statp-&gt;nscount; ns_shift++)
    {
    unsigned int ns = ns_shift + ns_offset;
    if (ns &gt;= statp-&gt;nscount)
        ns -= statp-&gt;nscount;

    same_ns:
    if (__glibc_unlikely (v_circuit)) {
        // ...
    } else {
        // 使用UDP方式发送请求
        n = send_dg(statp, buf, buflen, buf2, buflen2,
                &amp;ans, &amp;anssiz, &amp;terrno,
                ns, &amp;v_circuit, &amp;gotsomewhere, ansp,
                ansp2, nansp2, resplen2, ansp2_malloced);
        if (n &lt; 0)
            return (-1);
        if (n == 0 &amp;&amp; (buf2 == NULL || *resplen2 == 0))
            // 如果有多个DNS服务器的时候，会尝试下一个
            goto next_ns;
        // ...
    }
    return (resplen);
next_ns: ;
   } /*foreach ns*/
} /*foreach retry*/</pre>
<p>通常情况下，都是通过UDP协议进行DNS查询的，因此会调用<pre class="crayon-plain-tag">send_dg</pre>函数。在我们的场景中，两次尝试均5秒超时（尽管抓包显示应答报文很快就收到），__res_context_send设置错误码ETIMEDOUT，返回-1：</p>
<pre class="crayon-plain-tag">__res_iclose(statp, false);
if (!v_circuit) {
    if (!gotsomewhere)
        __set_errno (ECONNREFUSED);	/* no nameservers found */
    else
        __set_errno (ETIMEDOUT);	/* no answer obtained */
} else
    __set_errno (terrno);
return (-1);</pre>
<p>而它的调用者__res_context_query则在返回值是-1的时候，设置错误码TRY_AGAIN，这就是我们从KeyDB日志上看到报错The name server returned a temporary failure indication的原因：</p>
<pre class="crayon-plain-tag">if (n &lt; 0) {
    RES_SET_H_ERRNO(statp, TRY_AGAIN);
    return (n);
}</pre>
<div class="blog_h3"><span class="graybg">缓慢之源</span></div>
<p>缓慢的根源是send_dg函数，它阻塞了5秒。该函数的原型如下：</p>
<pre class="crayon-plain-tag">// 如果没有错误，返回第一个应答的字节数
// 对于可恢复错误，返回0；对于不可恢复错误，返回负数
static int send_dg(
    // 各种选项、DNS服务器列表、指向DNS服务器的套接字（文件描述符）
    res_state statp,
    // 查询请求1的缓冲区 和 长度
	const u_char *buf, int buflen, 
    // 查询请求2的缓冲区 和 长度
    const u_char *buf2, int buflen2,
    // 收到的第1个应答   和 最大长度
	u_char **ansp,      int *anssizp,
    // 出现错误时，将errno设置到此字段
	int *terrno, 
    // 使用的DNS服务器的序号
    int ns, 
    // 如果由于UDP数据报的限制而导致截断，则v_circuit设置为1，提示调用者使用TCP方式重试
    int *v_circuit, 
    // 提示访问DNS服务器时，是拒绝服务还是超时。如果是超时则设置为1
    int *gotsomewhere,
    // 提示遇到超长应答的时候，是否重新分配缓冲区
    u_char **anscp,
    // 收到的第2个应答 和 最大长度
	u_char **ansp2, int *anssizp2, 
    // 第2个应答的实际长度 是否为第2个应答重新分配了缓冲区
    int *resplen2, int *ansp2_malloced);</pre>
<p>该函数会向指定序号的DNS服务器发送DNS查询。它同时支持IPv4/IPv6查询，你可以传递两个查询请求，分别放在buf和buf2参数中。<span style="background-color: #c0c0c0;">如果提供了两个查询请求，默认使用并行方式发送查询</span>。设置选项<pre class="crayon-plain-tag">RES_SINGLKUP</pre>可以强制串行发送；设置选项<pre class="crayon-plain-tag">RES_SNGLKUPREOP</pre>可以<span style="background-color: #c0c0c0;">强制串行发送，同时总是关闭并重新打开套接字</span>，这样可以和某些行为异常的DNS服务器一起工作。</p>
<p>由于请求可以并行发送，因此应答到达的顺序是不确定的。<span style="background-color: #c0c0c0;">先收到的</span>应答会存放在ansp中，入参最大长度anssizp。入参anscp用于提示，应答过长的时候的处理方式：</p>
<ol>
<li>如果anscp不为空：则自动分配新的缓冲区，并且ansp、anscp都被修改为指向该缓冲区</li>
<li>如果anscp为空：则过长的部分被截断，DNS包头的TC字段被设置为1</li>
</ol>
<p>glibc的2.27-3ubuntu1版本中send_dg的完整实现如下：</p>
<pre class="crayon-plain-tag">static int
send_dg(res_state statp,
	const u_char *buf, int buflen, const u_char *buf2, int buflen2,
	u_char **ansp, int *anssizp,
	int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp,
	u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced)
{
	const HEADER *hp = (HEADER *) buf;
	const HEADER *hp2 = (HEADER *) buf2;
	struct timespec now, timeout, finish;
	struct pollfd pfd[1];
	int ptimeout;
	struct sockaddr_in6 from;
	int resplen = 0;
	int n;

	/*
	 * Compute time for the total operation.
	 */
	int seconds = (statp-&gt;retrans &lt;&lt; ns); // 0. 计算超时
	if (ns &gt; 0)
		seconds /= statp-&gt;nscount;
	if (seconds &lt;= 0)
		seconds = 1;
	bool single_request_reopen = (statp-&gt;options &amp; RES_SNGLKUPREOP) != 0; // 0. 确定是否并行请求
	bool single_request = (((statp-&gt;options &amp; RES_SNGLKUP) != 0)
			       | single_request_reopen);
	int save_gotsomewhere = *gotsomewhere;

	int retval;
 retry_reopen: // tx1. 如果套接字没有创建，则创建， SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC，非阻塞
	retval = reopen (statp, terrno, ns); // tx1. 然后调用一下connect操作，不发数据
	if (retval &lt;= 0)
	  {
	    if (resplen2 != NULL)
	      *resplen2 = 0;
	    return retval;
	  }
 retry:
	evNowTime(&amp;now);
	evConsTime(&amp;timeout, seconds, 0);
	evAddTime(&amp;finish, &amp;now, &amp;timeout);
	int need_recompute = 0;
	int nwritten = 0;
	int recvresp1 = 0;  // 用于标记请求1的应答是否接收到
	/* Skip the second response if there is no second query.
	   To do that we mark the second response as received.  */
	int recvresp2 = buf2 == NULL; // 用于标记请求2的应答是否接收到，如果buf2为空则立即标记为1
	pfd[0].fd = EXT(statp).nssocks[ns];
	pfd[0].events = POLLOUT; // tx2. 准备监听可写事件
 wait:
	if (need_recompute) {
	recompute_resend:
		evNowTime(&amp;now);
		if (evCmpTime(finish, now) &lt;= 0) {
		poll_err_out:
			return close_and_return_error (statp, resplen2);
		}
		evSubTime(&amp;timeout, &amp;finish, &amp;now);
		need_recompute = 0;
	}
	/* Convert struct timespec in milliseconds.  */
	ptimeout = timeout.tv_sec * 1000 + timeout.tv_nsec / 1000000;

	n = 0;
	if (nwritten == 0)
	  n = __poll (pfd, 1, 0); // tx2. 等待套接字可写
	if (__glibc_unlikely (n == 0))       {
		n = __poll (pfd, 1, ptimeout); // rx1. 等待套接字可读，5秒超时
		need_recompute = 1;
	}
	if (n == 0) {
		if (resplen &gt; 1 &amp;&amp; (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)))
		  { // 处理某些DNS服务器不支持处理并行请求的场景
		    /* There are quite a few broken name servers out
		       there which don't handle two outstanding
		       requests from the same source.  There are also
		       broken firewall settings.  If we time out after
		       having received one answer switch to the mode
		       where we send the second request only once we
		       have received the first answer.  */
		    if (!single_request)
		      {
			statp-&gt;options |= RES_SNGLKUP; // 这里永久改变为串行发送请求。statp是线程本地变量，
			single_request = true;         // KeyDB复制定时任务总是在同一线程中运行
			*gotsomewhere = save_gotsomewhere;
			goto retry;
		      }
		    else if (!single_request_reopen)
		      {
			statp-&gt;options |= RES_SNGLKUPREOP;
			single_request_reopen = true;
			*gotsomewhere = save_gotsomewhere;
			__res_iclose (statp, false);
			goto retry_reopen;
		      }

		    *resplen2 = 1;
		    return resplen;
		  }

		*gotsomewhere = 1;
		if (resplen2 != NULL)
		  *resplen2 = 0;
		return 0;
	}
	if (n &lt; 0) {
		if (errno == EINTR)
			goto recompute_resend;

		goto poll_err_out;
	}
	__set_errno (0);
	if (pfd[0].revents &amp; POLLOUT) { // tx3. 监听到可写事件
#ifndef __ASSUME_SENDMMSG
		static int have_sendmmsg;
#else
# define have_sendmmsg 1
#endif
		if (have_sendmmsg &gt;= 0 &amp;&amp; nwritten == 0 &amp;&amp; buf2 != NULL // 查询请求2不为空
		    &amp;&amp; !single_request) // 且允许并行发送
		  {
		    struct iovec iov[2];
		    struct mmsghdr reqs[2];
		    reqs[0].msg_hdr.msg_name = NULL;
		    reqs[0].msg_hdr.msg_namelen = 0;
		    reqs[0].msg_hdr.msg_iov = &amp;iov[0];
		    reqs[0].msg_hdr.msg_iovlen = 1;
		    iov[0].iov_base = (void *) buf;
		    iov[0].iov_len = buflen;
		    reqs[0].msg_hdr.msg_control = NULL;
		    reqs[0].msg_hdr.msg_controllen = 0;

		    reqs[1].msg_hdr.msg_name = NULL;
		    reqs[1].msg_hdr.msg_namelen = 0;
		    reqs[1].msg_hdr.msg_iov = &amp;iov[1];
		    reqs[1].msg_hdr.msg_iovlen = 1;
		    iov[1].iov_base = (void *) buf2;
		    iov[1].iov_len = buflen2;
		    reqs[1].msg_hdr.msg_control = NULL;
		    reqs[1].msg_hdr.msg_controllen = 0;
            // 发送消息，注意这里同时发送2个查询请求，返回值是实际发送的数量
		    int ndg = __sendmmsg (pfd[0].fd, reqs, 2, MSG_NOSIGNAL);
		    if (__glibc_likely (ndg == 2))
		      {
			if (reqs[0].msg_len != buflen
			    || reqs[1].msg_len != buflen2)
			  goto fail_sendmmsg;

			pfd[0].events = POLLIN;
			nwritten += 2;
		      }
		    else if (ndg == 1 &amp;&amp; reqs[0].msg_len == buflen)
		      goto just_one;
		    else if (ndg &lt; 0 &amp;&amp; (errno == EINTR || errno == EAGAIN))
		      goto recompute_resend;
		    else
		      {
#ifndef __ASSUME_SENDMMSG
			if (__glibc_unlikely (have_sendmmsg == 0))
			  {
			    if (ndg &lt; 0 &amp;&amp; errno == ENOSYS)
			      {
				have_sendmmsg = -1;
				goto try_send;
			      }
			    have_sendmmsg = 1;
			  }
#endif

		      fail_sendmmsg:
			return close_and_return_error (statp, resplen2);
		      }
		  }
		else
		  { // 不支持并行发送
		    ssize_t sr;
#ifndef __ASSUME_SENDMMSG
		  try_send:
#endif
		    if (nwritten != 0)
		      sr = send (pfd[0].fd, buf2, buflen2, MSG_NOSIGNAL);
		    else
		      sr = send (pfd[0].fd, buf, buflen, MSG_NOSIGNAL); // tx4. 发送查询请求1

		    if (sr != (nwritten != 0 ? buflen2 : buflen)) { // 发送长度和缓冲区长度不匹配
		      if (errno == EINTR || errno == EAGAIN) // 如果原因是EINTR或EAGAIN，则尝试重发
			goto recompute_resend;
		      return close_and_return_error (statp, resplen2);
		    }
		  just_one:
		    if (nwritten != 0 || buf2 == NULL || single_request)
		      pfd[0].events = POLLIN;  // 串行模式下，后续只需监听可读时间
		    else
		      pfd[0].events = POLLIN | POLLOUT; // 并行发送，如果实际仅发送1个消息，跳转到这里。后续需要继续写入发送失败的那个消息
		    ++nwritten;
		  }
		goto wait; // tx4. 发送完毕，回到上面的wait分支等待应答
	} else if (pfd[0].revents &amp; POLLIN) { // rx2. 监听到套接字可读
		int *thisanssizp; // 本次读数据到哪个缓冲
		u_char **thisansp;
		int *thisresplenp;

		if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) {
			/* We have not received any responses
			   yet or we only have one response to
			   receive.  */
			thisanssizp = anssizp;
			thisansp = anscp ?: ansp;
			assert (anscp != NULL || ansp2 == NULL);
			thisresplenp = &amp;resplen;
		} else {
			thisanssizp = anssizp2;
			thisansp = ansp2;
			thisresplenp = resplen2;
		}

		if (*thisanssizp &lt; MAXPACKET
		    /* If the current buffer is not the the static
		       user-supplied buffer then we can reallocate
		       it.  */
		    &amp;&amp; (thisansp != NULL &amp;&amp; thisansp != ansp)
#ifdef FIONREAD
		    /* Is the size too small?  */
		    &amp;&amp; (ioctl (pfd[0].fd, FIONREAD, thisresplenp) &lt; 0
			|| *thisanssizp &lt; *thisresplenp)
#endif
                    ) {
			/* Always allocate MAXPACKET, callers expect
			   this specific size.  */
			u_char *newp = malloc (MAXPACKET);
			if (newp != NULL) {
				*thisanssizp = MAXPACKET;
				*thisansp = newp;
				if (thisansp == ansp2)
				  *ansp2_malloced = 1;
			}
		}
		/* We could end up with truncation if anscp was NULL
		   (not allowed to change caller's buffer) and the
		   response buffer size is too small.  This isn't a
		   reliable way to detect truncation because the ioctl
		   may be an inaccurate report of the UDP message size.
		   Therefore we use this only to issue debug output.
		   To do truncation accurately with UDP we need
		   MSG_TRUNC which is only available on Linux.  We
		   can abstract out the Linux-specific feature in the
		   future to detect truncation.  */
		HEADER *anhp = (HEADER *) *thisansp;
		socklen_t fromlen = sizeof(struct sockaddr_in6);
		assert (sizeof(from) &lt;= fromlen);
		*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp, // rx3. 读取应答
					 *thisanssizp, 0,
					(struct sockaddr *)&amp;from, &amp;fromlen);
		if (__glibc_unlikely (*thisresplenp &lt;= 0))       {
			if (errno == EINTR || errno == EAGAIN) {
				need_recompute = 1;
				goto wait;  // 如果EINTR|EAGAIN则重新等待
			}
			return close_and_return_error (statp, resplen2);
		}
		*gotsomewhere = 1;
		if (__glibc_unlikely (*thisresplenp &lt; HFIXEDSZ))       { // 消息比报文头长度还小，错误
			/*
			 * Undersized message.
			 */
			*terrno = EMSGSIZE;
			return close_and_return_error (statp, resplen2);
		}
		if ((recvresp1 || hp-&gt;id != anhp-&gt;id)
		    &amp;&amp; (recvresp2 || hp2-&gt;id != anhp-&gt;id)) { // 查询标识符不匹配，可能服务器缓慢，返回之前查询的应答
			/*
			 * response from old query, ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (!(statp-&gt;options &amp; RES_INSECURE1) &amp;&amp; // 安全性检查type1
		    !res_ourserver_p(statp, &amp;from)) {
			/*
			 * response from wrong server? ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (!(statp-&gt;options &amp; RES_INSECURE2) // 安全性检查type2
		    &amp;&amp; (recvresp1 || !res_queriesmatch(buf, buf + buflen,
						       *thisansp,
						       *thisansp
						       + *thisanssizp))
		    &amp;&amp; (recvresp2 || !res_queriesmatch(buf2, buf2 + buflen2,
						       *thisansp,
						       *thisansp
						       + *thisanssizp))) {
			/*
			 * response contains wrong query? ignore it.
			 * XXX - potential security hazard could
			 *	 be detected here.
			 */
			goto wait;
		}
		if (anhp-&gt;rcode == SERVFAIL ||
		    anhp-&gt;rcode == NOTIMP ||
		    anhp-&gt;rcode == REFUSED) {  //  rx4. 处理服务器不愿意处理请求的情况
		next_ns:
			if (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)) {
			  *resplen2 = 0;
			  return resplen;
			}
			if (buf2 != NULL)
			  {
			    /* No data from the first reply.  */
			    resplen = 0;
			    /* We are waiting for a possible second reply.  */
			    if (hp-&gt;id == anhp-&gt;id)
			      recvresp1 = 1;
			    else
			      recvresp2 = 1;

			    goto wait;  // 事件类型仍然是POLLIN，会导致超时
			  }

			/* don't retry if called from dig */
			if (!statp-&gt;pfcode)
			  return close_and_return_error (statp, resplen2);
			__res_iclose(statp, false);
		}
		if (anhp-&gt;rcode == NOERROR &amp;&amp; anhp-&gt;ancount == 0 // rx.4 处理nodata的情况，名字请求，请求的记录类型不存在
		    &amp;&amp; anhp-&gt;aa == 0 &amp;&amp; anhp-&gt;ra == 0 &amp;&amp; anhp-&gt;arcount == 0) {
			goto next_ns;
		}
		if (!(statp-&gt;options &amp; RES_IGNTC) &amp;&amp; anhp-&gt;tc) { // rx.4 处理应答截断的情况
			/*
			 * To get the rest of answer,
			 * use TCP with same server.
			 */
			*v_circuit = 1; // 提示使用TCP重发请求
			__res_iclose(statp, false);
			// XXX if we have received one reply we could
			// XXX use it and not repeat it over TCP...
			if (resplen2 != NULL)
			  *resplen2 = 0;
			return (1);
		}
		/* Mark which reply we received.  */
		if (recvresp1 == 0 &amp;&amp; hp-&gt;id == anhp-&gt;id)
			recvresp1 = 1;
		else
			recvresp2 = 1;
		/* Repeat waiting if we have a second answer to arrive.  */
		if ((recvresp1 &amp; recvresp2) == 0) { // 如果只有一个查询请求，recvresp2一开始就标记为1，因此不会走到这个分支
			if (single_request) { // 如果是串行模式，这里开始处理第2个请求
				pfd[0].events = POLLOUT;
				if (single_request_reopen) {  // 如果需要关闭并重新打开套接字
					__res_iclose (statp, false);
					retval = reopen (statp, terrno, ns);
					if (retval &lt;= 0)
					  {
					    if (resplen2 != NULL)
					      *resplen2 = 0;
					    return retval;
					  }
					pfd[0].fd = EXT(statp).nssocks[ns];
				}
			}
			goto wait;  // 事件类型已经改为POLLOUT，因此不会发生超时
		}
		/* All is well.  We have received both responses (if
		   two responses were requested).  */
		return (resplen); // rx.5 DNS查询完毕
	} else if (pfd[0].revents &amp; (POLLERR | POLLHUP | POLLNVAL)) // poll出现错误
	  /* Something went wrong.  We can stop trying.  */
	  return close_and_return_error (statp, resplen2);
	else {
		/* poll should not have returned &gt; 0 in this case.  */
		abort ();
	}
}</pre>
<p>注释中tx.标注了DNS查询请求发送的基本过程，rx.则标注了DNS查询应答接收的基本过程。调试查询keydb-0.keydb时该函数的行为，发现以下事实：</p>
<ol>
<li>查询时串行发送的，而不是并行。因此正常流程应该是发送A查询，接收A应答，发送AAAA查询，接收AAAA应答</li>
<li>仅执行了1225行，没有执行1223行。也就是说仅仅发送了A查询，没有发送AAA查询</li>
<li>走到了1241行的分支，也就是说，<span style="background-color: #c0c0c0;">A请求的应答报文是接收到的</span>：<br />
<pre class="crayon-plain-tag">// (gdb) i r eax
// eax            0x2d     45   A应答长度45
		*thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp,
					 *thisanssizp, 0,
					(struct sockaddr *)&amp;from, &amp;fromlen);</pre></p>
<p>由于接收到的应答是servfail，因此走到这个分支：</p>
<pre class="crayon-plain-tag">if (anhp-&gt;rcode == SERVFAIL ||
		    anhp-&gt;rcode == NOTIMP ||
		    anhp-&gt;rcode == REFUSED) {
		next_ns:
			if (recvresp1 || (buf2 != NULL &amp;&amp; recvresp2)) {
			  *resplen2 = 0;
			  return resplen;
			}
			if (buf2 != NULL)
			  {
			    /* No data from the first reply.  */
			    resplen = 0;
			    /* We are waiting for a possible second reply.  */
			    if (hp-&gt;id == anhp-&gt;id)
			      recvresp1 = 1;  // 接收到第一个应答
			    else
			      recvresp2 = 1;
                // 由于同时需要进行A和AAAA查询，这里仅仅接收到A应答（串行发送）
			    goto wait; // 因此需要跳转到这里，等待套接字可写，以发送AAAA请求
			  }</pre>
</li>
<li>
<p>CoreDNS应答A查询SERVFAIL，重新跳转到wait标签：
<pre class="crayon-plain-tag">if (need_recompute) { // 等待A应答的时候，设置了超时 need_recompute，因此再次wait执行这个分支
	recompute_resend:
		evNowTime(&amp;now);
		if (evCmpTime(finish, now) &lt;= 0) {
		poll_err_out: // 如果超时了，直接关闭套接字并返回错误
			return close_and_return_error (statp, resplen2);
		}
		evSubTime(&amp;timeout, &amp;finish, &amp;now);
		need_recompute = 0;
	}
	/* Convert struct timespec in milliseconds.  */
	ptimeout = timeout.tv_sec * 1000 + timeout.tv_nsec / 1000000;

	n = 0;
	if (nwritten == 0)
	  n = __poll (pfd, 1, 0);  // 发送A请求的时候在这里pull，等待套接字可写。timeout 0表示立即返回
	if (__glibc_unlikely (n == 0))       {
		n = __poll (pfd, 1, ptimeout);  // 接收A应答的时候在这里poll，等待套接字可读
		need_recompute = 1; // 发送AAAA请求时，在这里等待套接字可写
	}</pre>
</li>
<li>
<p> 这时，由于nwritten已经被设置为1，因此走带有timeout的poll分支。然后在1110行出现5秒超时，并因为poll返回值是0而导致send_dg函数退出。在一次A请求处理过程中，有两次在1110行poll：
<ol>
<li>第一次是尝试A请求的应答，poll前的pollfd是{fd = 87, events = 1, revents = 4}，之后是{fd = 87, events = 1, revents = 1}</li>
<li>第二次就是因为这个跳转，poll前的pollfd是{fd = 87, events = 1, revents = 1}，超时之后是{fd = 87, events = 1, revents = 0}</li>
</ol>
</li>
</ol>
<p>poll函数原型：<pre class="crayon-plain-tag">int poll(struct pollfd *fds, nfds_t nfds, int timeout);</pre>，它等待文件描述符集合中的某个可用（可执行I/O）。文件描述符集合由参数fds指定，它是pollfd结构的数组：</p>
<pre class="crayon-plain-tag">struct pollfd {
    // 打开文件的描述符
    int   fd;         
    // 输入参数，应用程序感兴趣的事件类型。如果置零则revents中仅能返回POLLHUP,POLLERR,POLLNVAL事件
    short events;
    // 输出参数，内核填充实际发生的事件
    short revents;    
};</pre>
<p>如果文件描述符集中没有任何一个发生了events中指定的事件，则该函数会阻塞，直到超时或者被信号处理器中断。</p>
<p><span style="background-color: #c0c0c0;">事件类型1表示POLLIN，即有数据可读；事件类型4表示POLLOUT</span>，即文件描述符可写。正常情况下该函数返回就绪的（revents非零）文件描述符数量，超时返回0，出现错误则返回-1</p>
<p>第2次在1110行的poll行为难以理解：</p>
<ol>
<li>A的应答已经接收到，而由于进行的是串行发送A/AAAA，此时尚未发送AAAA请求，因此可以<span style="background-color: #c0c0c0;">预期后续不会有可读事件</span></li>
<li>poll时events设置为POLLIN（肯定会导致超时），难道不是应该设置为POLLOUT，尝试发送AAAA请求或重试A请求么？</li>
</ol>
<p>为了进行对照，我们由调试了没有发生缓慢问题的keydb-2.keydb。它在第2次执行1110行的poll时没有超时，pollfd的状态是{fd = 88, events = 1, revents = 1}。连续两次poll到可读事件，这提示进行了并行A/AAAA查询。检查变量single_request_reopen、single_request果然都是false，从CoreDNS日志上也可以看到A/AAAA</p>
<p>可能的情况是，keydb-1.keydb最初是并行发送A/AAAA查询的，后来由于某种原因，改为串行发送，从而导致出现5秒超时相关的缓慢现象。根源应该还是在glibc中，因为KeyDB调用getaddrinfo的方式是固定的。</p>
<p>回顾一下send_dg的代码，可以发现<pre class="crayon-plain-tag">statp-&gt;options</pre>决定了是否进行串行发送，statp是<pre class="crayon-plain-tag">resolv_context</pre>的一个字段，后者则是一个线程本地变量。如果某次并行发送请求后，可以接收到第一个应答，而在继续等待第二个应答时出现超时（1113行），则send_dg函数会修改statp-&gt;options，改为串行发送，<span style="background-color: #c0c0c0;">这个修改具有全局性影响</span>，以后KeyDB的复制定时任务（总是由同一线程执行）调用getaddrinfo，都会使用串行方式发送请求。</p>
<p>改变为串行方式后，由于CoreDNS应答keydb-0.keydb.以SERVFAIL，导致跳转到wait标签（1363行），进而执行了一次必然超时的poll调用。CoreDNS应答其它（加了search domain后缀的）域名以NXDOMAIN，则不会导致超时的poll调用，<span style="background-color: #c0c0c0;">因为会在1396行修改事件类型为POLLOUT</span>。</p>
<div class="blog_h1"><span class="graybg">解决方案</span></div>
<p>触发本文中的glibc缺陷，需要满足以下条件：</p>
<ol>
<li>出现某个KeyDB节点宕机的情况，并且没有修复。这会导致复制定时任务反复执行DNS查询，从而可能触发缺陷</li>
<li>某个DNS查询的应答UDP包丢失，导致当前线程串行发送DNS请求。由于UDP本身的不可靠性，随着程序不断运行，最终会发生</li>
<li>DNS服务器返回SERVFAIL、NOTIMP或者REFUSED应答</li>
</ol>
<p>第1、2个条件都是随机性的，我们没法干预，只有从第3个条件入手。作为最快速的解决方案，只需要配置KeyDB，使用全限定域名来指定replicaof即可。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/debugging-slow-keydb">记录一次KeyDB缓慢的定位过程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/debugging-slow-keydb/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>通过ExternalDNS集成外部DNS服务</title>
		<link>https://blog.gmem.cc/integrate-with-external-dns-provider</link>
		<comments>https://blog.gmem.cc/integrate-with-external-dns-provider#comments</comments>
		<pubDate>Sat, 29 Feb 2020 07:16:55 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32651</guid>
		<description><![CDATA[<p>简介 ExternalDNS项目的目的是，将Kubernetes的Service/Ingress暴露的服务（的DNS记录）同步给外部的DNS Provider。 ExternalDNS的设计思想类似于KubeDNS，都是从多种K8S API资源中推断需要生成的DNS记录。不同之处是，ExternalDNS本身不提供DNS服务，它必须集成一个外部的DNS服务器，将DNS记录写进去。 大量场景下，使用ExternalDNS你可以基于K8S资源（主要是Ingress和LoadBalancer类型的Service）来动态的控制DNS记录，而不需要知晓DNS服务器的技术细节。这是因为ExternalDNS项目已经集成了多种知名DNS服务提供商。 快速起步 本章我们尝试以CodeDNS（启用Etcd插件）作为DNS Provider，安装和配置ExternalDNS，并将K8S中的Ingress、Service的DNS记录写到此CoreDNS中。 安装Etcd 在K8S中安装 [crayon-69e29aa70f97c621866941/] 运行为Docker容器 [crayon-69e29aa70f980222090027/] 安装CoreDNS CoreDNS的Etcd插件，实现了SkyDNS风格的服务发现服务。该插件仅仅支持一部分DNS记录类型，因此不适合作为通用的DNS Zone数据插件。此外此插件不去处理subdomain、delegation。存储在Etcd中的数据，以SkyDNS消息的格式编码。Etcd插件通过forward插件的扩展用法，来将请求转发给网络上的服务器。 在K8S中安装 要启用Etcd插件，你需要修改stable/coredns的Values： [crayon-69e29aa70f983984049499/] 然后进行安装：  [crayon-69e29aa70f985548110882/] <a class="read-more" href="https://blog.gmem.cc/integrate-with-external-dns-provider">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/integrate-with-external-dns-provider">通过ExternalDNS集成外部DNS服务</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p><a href="https://github.com/kubernetes-sigs/external-dns">ExternalDNS</a>项目的目的是，将Kubernetes的Service/Ingress暴露的服务（的DNS记录）同步给外部的DNS Provider。</p>
<p>ExternalDNS的设计思想类似于KubeDNS，都是从多种K8S API资源中推断需要生成的DNS记录。不同之处是，ExternalDNS本身不提供DNS服务，它必须集成一个外部的DNS服务器，将DNS记录写进去。</p>
<p>大量场景下，使用ExternalDNS你可以基于K8S资源（主要是Ingress和LoadBalancer类型的Service）来动态的控制DNS记录，而不需要知晓DNS服务器的技术细节。这是因为ExternalDNS项目已经集成了多种知名DNS服务提供商。</p>
<div class="blog_h1"><span class="graybg">快速起步</span></div>
<p>本章我们尝试以CodeDNS（启用Etcd插件）作为DNS Provider，安装和配置ExternalDNS，并将K8S中的Ingress、Service的DNS记录写到此CoreDNS中。</p>
<div class="blog_h2"><span class="graybg">安装Etcd</span></div>
<div class="blog_h3"><span class="graybg">在K8S中安装</span></div>
<pre class="crayon-plain-tag">helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install bitnami/etcd --name kubefed-etcd --namespace kube-federation-system \
  --set global.imageRegistry=docker.gmem.cc \
  --set global.imagePullSecrets[0]=gmemregsecret  \
  --set auth.rbac.enabled=false  --set clusterDomain=k8s.gmem.cc</pre>
<div class="blog_h3"><span class="graybg">运行为Docker容器</span></div>
<pre class="crayon-plain-tag">docker run --name etcd -h etcd --network local --ip 172.21.0.14 --dns 172.21.0.1 \
  -e ALLOW_NONE_AUTHENTICATION=yes \
  docker.gmem.cc/bitnami/etcd:3.4.9</pre>
<div class="blog_h2"><span class="graybg">安装CoreDNS</span></div>
<p>CoreDNS的Etcd插件，实现了SkyDNS风格的服务发现服务。该插件仅仅支持一部分DNS记录类型，因此不适合作为通用的DNS Zone数据插件。此外此插件不去处理subdomain、delegation。存储在Etcd中的数据，以SkyDNS消息的格式编码。Etcd插件通过forward插件的扩展用法，来将请求转发给网络上的服务器。</p>
<div class="blog_h3"><span class="graybg">在K8S中安装</span></div>
<p>要启用Etcd插件，你需要修改stable/coredns的Values：</p>
<pre class="crayon-plain-tag">servers:
 - zones:
   - name: proxy
     parameters: . /etc/resolv.conf
   - name: etcd
     # 此插件是权威的 DNS Zones
     parameters: k8s.gmem.cc
     configBlock: |-
       # 在Etcd中的存储路径
       path /skydns
       # Etcd访问地址
       endpoint http://172.21.0.14:2379</pre>
<p>然后进行安装： </p>
<pre class="crayon-plain-tag">helm install coredns-1.10.1.tgz --name kubefed-coredns --namespace kube-federation-system \
  --set image.repository=docker.gmem.cc/coredns/coredns \
  --set isClusterService=false </pre>
<div class="blog_h3"><span class="graybg">运行为Docker容器</span></div>
<pre class="crayon-plain-tag">etcd k8s.gmem.cc {
  path /skydns
  endpoint http://172.21.0.14:2379
}
forward . 114.114.114.114</pre>
<div class="blog_h2"><span class="graybg">安装ExternalDNS</span></div>
<p>在这里我们安装一个ExternalDNS，并且将CoreDNS作为它的Provider：</p>
<pre class="crayon-plain-tag">helm install bitnami/external-dns --name external-dns --namespace=kube-system \
  --set global.imageRegistry=docker.gmem.cc \
  --set global.imagePullSecrets[0]=gmemregsecret  \
  --set provider=coredns \
  --set coredns.etcdEndpoints=http://172.21.0.14:2379 \
  --set sources="{service,ingress,istio-gateway,crd}" \
  --set publishInternalServices=true \
  --set crd.create=true --set policy=sync --set logLevel=debug</pre>
<div class="blog_h2"><span class="graybg">安装Ingress控制器</span></div>
<p>为了支持将Ingress Controller的外部IP地址设置给Ingress对象的Status中的IP地址，我们需要较新版本的<a href="https://github.com/helm/charts/tree/master/stable/nginx-ingress">nginx-ingress</a>：</p>
<pre class="crayon-plain-tag">helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx/ingress-nginx --name ingress-nginx --namespace kube-system
  --set controller.image.repository=docker.gmem.cc/kubernetes-ingress-controller/nginx-ingress-controller \
  # 将Ingress Controller的Service的IP地址报告为Ingress的Status的IP地址
  --set controller.publishService.enabled=true \
  # 使用的默认SSL证书
  --set controller.extraArgs."default-ssl-certificate"="kube-system/gmemk8scert" \
  # 配合MetalLB使用，让Ingress Controller的Service获得外部IP
  --set controller.service.type=LoadBalancer \
  --set controller.admissionWebhooks.patch.image.repository=docker.gmem.cc/jettech/kube-webhook-certgen \
  --set imagePullSecrets[0].name=gmemregsecret</pre>
<div class="blog_h2"><span class="graybg">测试</span></div>
<p>在K8S集群中创建一个Ingress，并等待其ADDRESS（status.loadBalancer.ingress.ip[0]）被填充：</p>
<pre class="crayon-plain-tag">kubectl -n devops get ingress  grafana 
# NAME      HOSTS                 ADDRESS      PORTS     AGE
# grafana   grafana.k8s.gmem.cc   10.0.11.10   80, 443   534d</pre>
<p>这时，ExternalDNS的日志中会出现DNS记录同步的相关信息： </p>
<p style="padding-left: 30px;">time="2020-06-01T02:46:34Z" level=debug msg="Endpoints generated from ingress: devops/grafana: [grafana.k8s.gmem.cc 0 IN A 10.0.11.10 [] grafana.k8s.gmem.cc 0 IN A 10.0.11.10 []]"</p>
<p>CoreDNS的后端Etcd中则会出现条目：</p>
<pre class="crayon-plain-tag">etcdctl --endpoints http://172.21.0.14:2379 get /skydns/cc/gmem/k8s/grafana --prefix
# /skydns/cc/gmem/k8s/grafana/0e5a5d35
# {"host":"10.0.11.10","text":"\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/devops/grafana\"","targetstrip":1}</pre>
<p>使用nslookup，针对CoreDNS进行DNS查询测试，可以发现解析能够成功。 </p>
<div class="blog_h1"><span class="graybg">命令行</span></div>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 33%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--master=""</td>
<td>K8S API Server地址</td>
</tr>
<tr>
<td>--kubeconfig=""</td>
<td>使用的Kubeconfig</td>
</tr>
<tr>
<td>--request-timeout=30s</td>
<td>K8S API Server访问超时</td>
</tr>
<tr>
<td>--source=source ..</td>
<td>从哪些K8S资源中查找端点：service, ingress, node, fake, connector, istio-gateway, cloudfoundry,  contour-ingressroute, crd, empty, skipper-routegroup</td>
</tr>
<tr>
<td>--namespace=""</td>
<td>从哪些命名空间查找K8S资源，默认所有命名空间</td>
</tr>
<tr>
<td>--annotation-filter=""</td>
<td>基于注解过滤被external-dns管理的source列表，默认所有source</td>
</tr>
<tr>
<td>--fqdn-template=""</td>
<td>
<p>对于不提供hostname的源，使用该参数提供的模板生成DNS名。可以指定逗号分隔的列表 </p>
<p>和fake source联用时则指定hostname后缀</p>
</td>
</tr>
<tr>
<td>--combine-fqdn-annotation</td>
<td>combine fqdn-template和注解，而非覆盖</td>
</tr>
<tr>
<td>--ignore-hostname-annotation</td>
<td>提供了fqdn-template的情况下，生成DNS名称时忽略hostname注解</td>
</tr>
<tr>
<td>--publish-internal-services</td>
<td>发布ClusterIP类型Service的地址</td>
</tr>
<tr>
<td>--publish-host-ip</td>
<td>发布无头服务的host-ip</td>
</tr>
<tr>
<td>--always-publish-not-ready-addresses</td>
<td>总是发布无头服务的尚未就绪的地址</td>
</tr>
<tr>
<td>--crd-source-apiversion</td>
<td>CRD源的API版本，默认externaldns.k8s.io/v1alpha1</td>
</tr>
<tr>
<td>--crd-source-kind="DNSEndpoint"</td>
<td>CRD源的API类型</td>
</tr>
<tr>
<td>--service-type-filter</td>
<td>关注的Service类型，默认all，可选ClusterIP, NodePort, LoadBalancer, ExternalName</td>
</tr>
<tr>
<td>--provider=provider</td>
<td>使用的DNS Provider，可选aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns</td>
</tr>
<tr>
<td>--domain-filter= ...</td>
<td>限制处理的目标DNS Zone，使用domain后缀形式，该参数可以指定多次</td>
</tr>
<tr>
<td>--exclude-domains= ...</td>
<td>排除DNS子域</td>
</tr>
<tr>
<td>--zone-id-filter= ...</td>
<td>使用Zone ID来过滤目标Zone</td>
</tr>
<tr>
<td>--policy=sync</td>
<td>和DNS Provider进行数据同步的方式，默认sync，可选upsert-only, create-only</td>
</tr>
<tr>
<td>--registry=txt</td>
<td>跟踪DNS记录所有权的registry实现方式，默认txt，可选noop, aws-sd</td>
</tr>
<tr>
<td>--txt-owner-id="default"</td>
<td>使用txt registry时，用于识别当前ExternalDNS的ID</td>
</tr>
<tr>
<td>--txt-prefix=""</td>
<td>使用txt registry时，给每个所有权记录前缀的字符串</td>
</tr>
<tr>
<td>--txt-cache-interval=0s</td>
<td>txt缓存同步时间</td>
</tr>
<tr>
<td>--interval=1m0s</td>
<td>两次连续的，到DNS Provider的同步的间隔</td>
</tr>
<tr>
<td>--once</td>
<td>在第一次同步后，退出同步循环</td>
</tr>
<tr>
<td>--dry-run</td>
<td>打印DNS记录变更，不调用DNS Provider</td>
</tr>
<tr>
<td>--events</td>
<td>当source变更时间发生时，也（在定期同步的基础上）触发同步</td>
</tr>
<tr>
<td>--log-format=text</td>
<td>日志格式，可选text, json</td>
</tr>
<tr>
<td>--metrics-address=":7979"</td>
<td>指标和健康检查暴露地址</td>
</tr>
<tr>
<td>--log-level=info</td>
<td>日志级别 panic, debug, info, warning, error, fatal</td>
</tr>
</tbody>
</table>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/integrate-with-external-dns-provider">通过ExternalDNS集成外部DNS服务</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/integrate-with-external-dns-provider/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Kubernetes上和DNS相关的问题</title>
		<link>https://blog.gmem.cc/dns-problems-on-k8s</link>
		<comments>https://blog.gmem.cc/dns-problems-on-k8s#comments</comments>
		<pubDate>Fri, 28 Sep 2018 02:42:10 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32179</guid>
		<description><![CDATA[<p>Conntrack竞争导致的DNS超时 这是一篇译文，原文地址：Racy conntrack and DNS lookup timeouts 最近出现了很多关于K8S中DNS查找超时的BUG报告，某些情况下Pod发起的DNS查找耗时高达5s甚至更久。在这篇文章中我将解释DNS查找延迟的根本原因，讨论缓和此延迟的途径，以及如何修改内核解决此问题。 背景 在K8S中，Pod访问DNS的最常用途径是通过Service，要解释DNS延迟，首先需要知道Service如何工作，以及底层的DNAT机制。 服务如何工作 在默认的Iptables模式下，kube-proxy为每个Service，在宿主机网络命名空间的NAT表中，创建一些iptables规则。 假设kube-dns服务有两个实例，则相应的规则可能是： [crayon-69e29aa70fd08845218159/] 在我们的例子中，每个Pod都在/etc/resolv.conf中包含了DNS服务器条目[crayon-69e29aa70fd0d612093702-i/]，因此，Pod发起的DNS查找会发送给10.96.0.10这个ClusterIP。 从上面的规则中可以看到，经过简单的负载均衡后，请求被DNAT到DNS Pod的IP地址：10.32.0.6 或者 10.32.0.7 内核中的DNAT 通过上面的分析可以看到，iptables模式下的服务依赖于内核的DNAT。 DNAT的主要职责是：同时修改出站封包的目的地址、回复封包的源地址，并确保对所有后续封包执行同样的修改。要保证后一点，需要非常依赖内核中的conntrack模块，此模块负责跟踪系统的网络连接。 <a class="read-more" href="https://blog.gmem.cc/dns-problems-on-k8s">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/dns-problems-on-k8s">Kubernetes上和DNS相关的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">Conntrack竞争导致的DNS超时</span></div>
<p>这是一篇译文，原文地址：<a href="https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts">Racy conntrack and DNS lookup timeouts</a></p>
<p>最近出现了很多<a href="https://github.com/kubernetes/kubernetes/issues/56903">关于K8S中DNS查找超时的BUG报告</a>，某些情况下Pod发起的DNS查找耗时高达5s甚至更久。在这篇文章中我将解释DNS查找延迟的根本原因，讨论缓和此延迟的途径，以及如何修改内核解决此问题。</p>
<div class="blog_h2"><span class="graybg">背景</span></div>
<p>在K8S中，Pod访问DNS的最常用途径是通过Service，要解释DNS延迟，首先需要知道Service如何工作，以及底层的DNAT机制。</p>
<div class="blog_h3"><span class="graybg">服务如何工作</span></div>
<p>在默认的Iptables模式下，kube-proxy为每个Service，<span style="background-color: rgb(192, 192, 192);">在宿主机网络命名空间的NAT表中，创建一些iptables规则</span>。</p>
<p>假设kube-dns服务有两个实例，则相应的规则可能是：</p>
<pre class="crayon-plain-tag">-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

# 如果目的地址是DNS服务的ClusterIP，则跳转
-A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" 
  -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU

# 负载均衡
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -m statistic 
  --mode random --probability 0.50000000000 -j KUBE-SEP-LLLB6FGXBLX6PZF7
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-LRVEW52VMYCOUSMZ

# DNAT到实际Pod
-A KUBE-SEP-LLLB6FGXBLX6PZF7 -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp 
  -j DNAT --to-destination 10.32.0.6:53

-A KUBE-SEP-LRVEW52VMYCOUSMZ -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp 
  -j DNAT --to-destination 10.32.0.7:53</pre>
<p>在我们的例子中，每个Pod都在/etc/resolv.conf中包含了DNS服务器条目<pre class="crayon-plain-tag">nameserver 10.96.0.10</pre>，因此，Pod发起的DNS查找会发送给10.96.0.10这个ClusterIP。</p>
<p>从上面的规则中可以看到，经过简单的负载均衡后，请求被DNAT到DNS Pod的IP地址：10.32.0.6 或者 10.32.0.7</p>
<div class="blog_h3"><span class="graybg">内核中的DNAT</span></div>
<p>通过上面的分析可以看到，iptables模式下的服务依赖于内核的DNAT。</p>
<p>DNAT的主要职责是：同时修改出站封包的目的地址、回复封包的源地址，并<span style="background-color: rgb(192, 192, 192);">确保对所有后续封包执行同样的修改。要保证后一点，需要非常依赖内核中的conntrack模块</span>，此模块负责跟踪系统的网络连接。</p>
<p>在最简单的情况下，<span style="background-color: #c0c0c0;">每个连接在conntrack中呈现为两个元组</span>：</p>
<ol>
<li>一个针对原始请求：IP_CT_DIR_ORIGINAL</li>
<li>一个针对应答：IP_CT_DIR_REPLY</li>
</ol>
<p>对于UDP来说，每个元组由SIP+SPT+DIP+DPT这4个元素组成。应答元组IP_CT_DIR_REPLY的src字段中，存放请求目的真实地址。</p>
<p>例如，如果具有IP地址10.40.0.17的Pod，向kube-dns服务的ClusterIP发送请求，并且被DNAT到10.32.0.6的话，则元组如下：</p>
<ol>
<li>IP_CT_DIR_ORIGINAL：src=10.40.0.17 sport=53378       dst=10.96.0.10 dport=53</li>
<li>IP_CT_DIR_REPLY：src=10.32.0.6 sport=53        dst=10.40.0.17dport=53378</li>
</ol>
<p>有了这两个元组后，内核就能够修改任何相关的封包的目的地址、源地址，而不需要再次遍历DNAT规则。同时，内核也知道如何修改应答，将其转发给最初的请求者。</p>
<p>当一个<span style="background-color: rgb(192, 192, 192);">conntrack条目创建后，它最初处于未确认（unconfirmed）</span>状态。随后，如果内核发现，<span style="background-color: #c0c0c0;">不存在已确认的、具有相同的ORIGINAL元组或者REPLY元组的conntrack条目</span>，则<span style="background-color: rgb(192, 192, 192);">确认这个新条目</span>。</p>
<p>下面是一个简化的conntrack创建、执行DNAT的流程：</p>
<pre class="crayon-plain-tag">+---------------------------+      如果不存在，则为封包创建一个conntrack
|                           |      IP_CT_DIR_REPLY是IP_CT_DIR_ORIGINAL元组的反转
|    1. nf_conntrack_in     |      
|                           |      REPLY元组的源地址尚未改变
+------------+--------------+
             |
             v
+---------------------------+
|                           |
|     2. ipt_do_table       |      找到一个匹配的DNAT规则
|                           |
+------------+--------------+
             |
             v
+---------------------------+
|                           |      修改REPLY元组的源地址部分，同时保证此元组不被现有
|    3. get_unique_tuple    |      conntrack占用
|                           |      
+------------+--------------+
             |
             v
+---------------------------+
|                           |     根据REPLY元组来修改封包的目的地址
|     4. nf_nat_packet      |      
|                           |
+------------+--------------+
             |
             v
+----------------------------+
|                            |    如果没有已确认的、具有相同ORIGINAL/REPLY元组的conntrack条目
|  5. __nf_conntrack_confirm |    则确认之
|                            |     
+----------------------------+    否则增加insert_failed计数，并丢弃封包</pre>
<div class="blog_h2"><span class="graybg">问题</span></div>
<p>当两个UDP包<span style="background-color: #c0c0c0;">通过相同套接字（绑定到相同的源地址/端口）</span>、 在相同时间，通过不同线程发送，就会出现问题。</p>
<p>UDP是无连接的协议，connect系统调用之后，不会发送任何封包，因此也就不会创建conntrack条目。</p>
<p>仅当第一个UDP包发送时，条目才创建。因此可能出现以下<span style="background-color: #c0c0c0;">3种竞态条件</span>：</p>
<ol>
<li>在nf_conntrack_in阶段，两个封包都没有条目，因此它们都创建conntrack，使用相同元组</li>
<li>在1的基础上：封包1的conntrack条目在封包2调用get_unique_tuple 之前确认。封包2得到一个不同的REPLY元组，通常改变了源端口</li>
<li>在1的基础上：两个封包在ipt_do_table选择了不同的目的地（DNS的Pod地址）</li>
</ol>
<p>后果是一样的，其中一个封包在 __nf_conntrack_confirm被丢弃。</p>
<p>这就是发生在DNS查询场景中的问题。glibc、musl libc都会并行的执行A、AAAA查询。其中一个封包可能因为竞态条件而被内核丢弃，客户端会在超时（通常5s）之后重新发送请求。</p>
<p>这并不是K8S特有的问题，<span style="background-color: #c0c0c0;">任何Linux应用程序，只要使用多线程发送UDP，都可能遭遇此问题</span>。</p>
<p>甚至，即使没有DNAT规则，第二种情况也会发生。只要加载了nf_nat内核模块，就会调用get_unique_tuple。</p>
<p>执行命令<pre class="crayon-plain-tag">conntrack -S</pre>，如果计数器insert_failed增加，提示遭遇了此问题。</p>
<div class="blog_h2"><span class="graybg">缓和</span></div>
<p>主要手段是避免UDP并发：</p>
<ol>
<li>禁用并行DNS查找</li>
<li>禁用IPv6，从而禁止AAAA查找</li>
<li>使用TCP协议</li>
<li>将Pod使用的DNS地址设置为Endpoint的真实地址</li>
</ol>
<p>某些手段由于musl libc的限制无法使用，此libc在Alpine Linux中被广泛应用。</p>
<p>IPVS模式不能解决此问题，因为conntrack仍然处于启用状态。使用rr作为负载均衡策略时，在低DNS负载的情况下也容易复现。</p>
<div class="blog_h3"><span class="graybg">禁用并行查找</span></div>
<p>在/etc/resolv.conf中增加single-request-reopen选项：</p>
<pre class="crayon-plain-tag">options rotate timeout:1 attempts:3 single-request-reopen</pre>
<p>DNS解析器使用相同的套接字（源地址+源端口一样）执行A和AAAA查询。某些硬件/DNS服务器会错误的仅发回一个应答，这导致客户端等待第二个应答直到超时。启用此选项后，并向查找被禁用，并且会在发送第二个请求时重新打开端口。</p>
<p>CentOS 5等系统，都是使用独立源端口发起AAAA和A查询的，CentOS 6则使用同一端口，导致并行查找问题。</p>
<div class="blog_h3"><span class="graybg">使用TCP</span></div>
<p>基于一些本地DNS前置缓存的方案，通过TCP访问CoreDNS。例如NodeLocal DNSCache。</p>
<div class="blog_h3"><span class="graybg">避免conntrack</span></div>
<p>如果内核版本足够高，可以使用Cilium kube-proxy这样的方案，不使用iptables。</p>
<p>另一个方向是，Pod直接访问DNS endpoint。这样的endpoint可以是本地前置缓存。</p>
<div class="blog_h1"><span class="graybg">现状</span></div>
<p>直到2020年4月，这3个竞态条件仍然没有完美解决方案：<a href="https://github.com/kubernetes/kubernetes/issues/56903#issuecomment-613589347">https://github.com/kubernetes/kubernetes/issues/56903#issuecomment-613589347</a>。</p>
<p>有人建议使用Daemonset运行DNS服务，且跳过conntrack。他的方案是<span style="background-color: #c0c0c0;">在HostNetwork中运行Dnsmasq的Daemonset，作为CoreDNS的前端</span>。据测试哪怕把CoreDNS打爆OOM也不会出现timeout问题。</p>
<div class="blog_h2"><span class="graybg">Cilium kube-proxy</span></div>
<p>Cilium的<a href="https://docs.cilium.io/en/v1.7/gettingstarted/kubeproxy-free/">Kube Proxy替代品</a>，由于不使用netfilter/iptables，因此不会面临conntrack问题，也可以尝试。注意内核版本要求：v4.19.57, v5.1.16, v5.2.0或者更新版本</p>
<div class="blog_h2"><span class="graybg">NodeLocal DNSCache</span></div>
<p>这个特性在1.18的Kubernetes处于Stable状态：<a href="https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/">https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/</a>。可以认为是Daemonset方案的官方版本。</p>
<div class="blog_h3"><span class="graybg">简介</span></div>
<p>通过在每个节点上运行DNS缓存来改善DNS性能问题，避免了DNAT和conntrack（Pod往本地的DNS查询，在iptables中通过NOTRACK跳过conntrack）。本地DNS缓存通过查询kube dns来应对缓存丢失。</p>
<div class="blog_h3"><span class="graybg">动机</span></div>
<ol>
<li>在当前架构下，高DNS请求负载的Pod可能需要将查询发往不同的DNS节点。引入缓存降低了DNS的负载</li>
<li>跳过iptables DNAT和conntrack，可以减少竞态条件，以及避免UDP DNS条目<span style="background-color: #c0c0c0;">充斥conntrack表</span></li>
<li>从本地缓存代理到kube-dns服务的连接，可以升级为TCP。<span style="background-color: rgb(192, 192, 192);">TCP conntrack条目会在连接关闭时移除</span>。而DNS条目需要等待超时，默认nf_conntrack_udp_timeout=30</li>
<li>将DNS查询从UDP升级为TCP，可以减少尾延迟（tail latency），它这会导致最多30s的超时（3次重试 x 10s超时）。同时本地缓存仍然沿用UDP，应用程序不需要改变</li>
</ol>
<div class="blog_h1"><span class="graybg">其它</span></div>
<div class="blog_h2"><span class="graybg">conntrack表爆满</span></div>
<p>如果并发度极高，依赖于conntrack还会面临表被撑爆的问题：</p>
<pre class="crayon-plain-tag"># 表的容量
sysctl net.netfilter.nf_conntrack_max

# 已经占用的量
sysctl net.netfilter.nf_conntrack_count</pre>
<div class="blog_h2"><span class="graybg">search domain问题</span></div>
<div class="blog_h3"><span class="graybg">性能问题</span></div>
<p>如果DnsPolicy 是 ClusterFirst，则/etc/resolv.conf内容如下：</p>
<pre class="crayon-plain-tag">nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
# 搜索后缀列表，对于针对少于ndots（默认1）个.号的名字的DNS查询，会自动尝试添加这些后缀进行查询
options ndots:5</pre>
<p>这样，当default命名空间中，有个工作负载查询外部域名gmem.cc，需要发起多次DNS查询才能完成：</p>
<ol>
<li>gmem.cc.default.svc.cluster.local.</li>
<li>gmem.cc.svc.cluster.local.</li>
<li>gmem.cc.cluster.local.</li>
<li>gmem.cc.</li>
</ol>
<p>这也增加了DNS服务器的压力。解析外部域名时，如果代码可以控制，使用<span style="background-color: rgb(192, 192, 192);">全限定名称（点号结尾）可以避免反复查询的成本</span></p>
<div class="blog_h3"><span class="graybg">不起作用</span></div>
<p>在使用CoreDNS的forward插件的情况下，如果上游DNS服务器行为异常，会导致搜索后缀无效。</p>
<p>在Linux下，DNS查询失败后，客户端可能会附加搜索列表后缀，继续查询，这个行为是由C运行时库提供的。这里说可能，是<span style="background-color: #c0c0c0;">因为DNS服务器返回某些响应的情况下，客户端就不会遍历后缀列表，从而导致DNS解析失败</span>。具体哪些<a href="/tcp-ip-study-note#dns-rtnmsg">响应会导致不遍历</a>，没有详细的研究，但是REFUESED肯定会导致，SERVFAIL则不会。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/dns-problems-on-k8s">Kubernetes上和DNS相关的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/dns-problems-on-k8s/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>类UNIX系统下使用Dnsmasq</title>
		<link>https://blog.gmem.cc/dnsmasq-under-unix</link>
		<comments>https://blog.gmem.cc/dnsmasq-under-unix#comments</comments>
		<pubDate>Sat, 15 Aug 2015 05:40:03 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Mac OS X]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[Ubuntu]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15431</guid>
		<description><![CDATA[<p>简介 Dnsmasq是一个轻量级的DHCP服务器、DNS缓存服务器。它可以提供本地的DNS服务，把不能解析的请求转交给上级DNS处理。 Dnsmasq可以读取/etc/hosts文件中的条目，来作域名解析。 OS X 安装 可以通过Homebrew安装： [crayon-69e29aa710053317929286/] 创建配置文件： [crayon-69e29aa710057321520978/] 修改网络配置中的DNS配置，将127.0.0.1放到DNS列表的最前面。 服务化 [crayon-69e29aa710059720469026/] Ubuntu 安装 通常Ubuntu已经自带了此软件包，不需要手工安装。如果你的机器上缺少dnsmasq，执行下面的命令安装： [crayon-69e29aa71005b277039972/] 安装后，dnsmasq的守护程序会自动启动。 配置 默认的启动脚本，包含以下参数： [crayon-69e29aa71005d692455037/] 所以我们只需要把配置文件放在/etc/dnsmasq.d目录下就可以了。注意此目录中所有文件都会被读取。添加下面的配置文件： <a class="read-more" href="https://blog.gmem.cc/dnsmasq-under-unix">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/dnsmasq-under-unix">类UNIX系统下使用Dnsmasq</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Dnsmasq是一个轻量级的DHCP服务器、DNS缓存服务器。它可以提供本地的DNS服务，把不能解析的请求转交给上级DNS处理。</p>
<p>Dnsmasq可以读取/etc/hosts文件中的条目，来作域名解析。</p>
<div class="blog_h1"><span class="graybg">OS X</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>可以通过Homebrew安装：</p>
<pre class="crayon-plain-tag">brew install dnsmasq</pre>
<p>创建配置文件：</p>
<pre class="crayon-plain-tag">mkdir -pv $(brew --prefix)/etc/
touch $(brew --prefix)/etc/dnsmasq.conf</pre>
<p>修改网络配置中的DNS配置，将127.0.0.1放到DNS列表的最前面。</p>
<div class="blog_h2"><span class="graybg">服务化</span></div>
<pre class="crayon-plain-tag">sudo cp -v $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons
sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist

# 现在可以看到Dnsmasq已经启动了
sudo launchctl list | grep dns

# 要禁用Dnsmasq服务，可以
sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist

# 注意，Dnsmasq不会监控hosts文件的变动，因此修改hosts后需要重新启动
sudo brew services restart dnsmasq</pre>
<div class="blog_h1"><span class="graybg">Ubuntu</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>通常Ubuntu已经自带了此软件包，不需要手工安装。如果你的机器上缺少dnsmasq，执行下面的命令安装：</p>
<pre class="crayon-plain-tag">sudo apt install dnsmasq</pre>
<p>安装后，dnsmasq的守护程序会自动启动。</p>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>默认的启动脚本，包含以下参数：</p>
<pre class="crayon-plain-tag">/usr/sbin/dnsmasq 
    # 指定PID文件路径
    -x /var/run/dnsmasq/dnsmasq.pid 
    # 以什么身份运行守护程序
    -u dnsmasq 
    # 指定上游DNS服务器，此文件自动生成，在桌面系统中会读取NetworkManager中的DNS设置并写入此文件
    -r /var/run/dnsmasq/resolv.conf 
    # 指定配置文件目录
    -7 /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new</pre>
<p>所以我们只需要把配置文件放在/etc/dnsmasq.d目录下就可以了。注意此目录中所有文件都会被读取。添加下面的配置文件：</p>
<pre class="crayon-plain-tag">expand-hosts
domain=gmem.cc</pre>
<div class="blog_h3"><span class="graybg">配置文件详解</span></div>
<pre class="crayon-plain-tag"># 监听端口
port=53

# 上游DNS服务器的路径
resolv-file=/etc/resolv.conf

# 安装定义顺序逐一尝试上游DNS服务器
strict-order

# 不从任何外部文件读取上游DNS
no-resolv

# 添加上游服务器
# 对于本地网络的DNS查询，转发给192.168.0.1
server=/localnet/192.168.0.1   
# 针对*.svc.k8s.gmem.cc的DNS查询，转发给10.96.0.10
server=/svc.k8s.gmem.cc/10.96.0.10    
# 针对192.168.3/24的DNS反查，转发给10.1.2.3
server=/3.168.192.in-addr.arpa/10.1.2.3
# 和上游DNS 10.1.2.3联系时通过eth1路由
server=10.1.2.3@eth1

# 仅本地域名列表，这些域名的查询仅仅基于/etc/hosts文件或者DHCP完成
local=/localnet/

# 静态指定域名和IP的关系，支持泛域名解析
address=/zircon.gmem.cc/127.0.0.1
# 泛域名解析
address=/.k8s.gmem.cc/10.0.11.10

# 仅仅在指定的网络接口上监听DNS/DHCP请求，要指定多个接口，编写多行
interface=eth0

# 在除了指定网络接口外的任何接口上监听
except-interface=virbr0

# 在指定地址上监听，注意包含127.0.0.1
listen-address=127.0.0.1

# 在指定网络接口上仅仅提供DNS服务
no-dhcp-interface=eth0

# 通配绑定所有接口
bind-interfaces

# 如果不希望读取/etc/hosts中的解析条目
no-hosts

# 读取除了/etc/hosts之外的包含解析条目的文件
addn-hosts=/etc/banner_add_hosts

# 根据domain选项，为hosts文件条目自动添加域名后缀
# 使用带后缀、不带后缀的方式访问此域名，都支持
expand-hosts

# 设置dnsmasq的域，设置后，具有以下行为：
# 1、允许DHCP主机拥有全限定的域名
# 2、设置DHCP的domain选项，进而潜在的设置所有基于DHCP配置的主机的domain
# 3、如果设置了expand-hosts，自动为hosts文件中的条目添加后缀
domain=gmem.cc

# 针对特定子网设置域名后缀
domain=vm.gmem.cc,10.0.0.1/8

# 下面这行启用内置的DHCP服务器，需要指定地址范围，租借时间
# 如果有多个网络，则重复下面的条目
dhcp-range=192.168.0.50,192.168.0.150,12h

# 静态分配IP地址到指定的MAC
dhcp-host=11:22:33:44:55:66,192.168.0.60 

# 记录日志
log-facility=/var/log/dnsmasq.log

# 最大连接数
dns-forward-max=512</pre>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">无法启动</span></div>
<p>报错信息：dnsmasq: setting capabilities failed: Operation not permitted</p>
<p>报错原因：在Docker中运行dnsmasq会出现此问题，将配置user/group改为root即可。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/dnsmasq-under-unix">类UNIX系统下使用Dnsmasq</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/dnsmasq-under-unix/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>TCP/IP协议栈学习笔记</title>
		<link>https://blog.gmem.cc/tcp-ip-study-note</link>
		<comments>https://blog.gmem.cc/tcp-ip-study-note#comments</comments>
		<pubDate>Fri, 30 Mar 2012 09:21:59 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Network]]></category>
		<category><![CDATA[DNS]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=4983</guid>
		<description><![CDATA[<p>名词术语 术语 解释 bogon 这个单词从bogus衍生而来，字面意思是伪造的在TCP/IP协议中，bogon代表那些保留或者未分配的IP段，这些地址不应该出现在因特网上 由于ISP路由过滤、或者恶意软件的原因，Bogon地址的IP数据报可能到达目标机器上，目标机器往往进行Bogon地址过滤 Bogon地址和保留私有地址不是一个概念，但是某些软件可能把保留私有地址作为Bogon看待： [crayon-69e29aa71061d672365742/] Reserved Addresses 即保留私有地址，这些地址用于特殊用途，例如： 多播地址，这类地址为224.0.0.0/4 用于本机的环回地址，这类地址为127.0.0.0/8 Link-local address：用于两台主机在没有（被DHCP）分配IP地址的情况下通信，这类地址是169.254.0.0/16 用于私有网络（局域网）的本地通信，这类地址包括10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 其它特殊用途 简述 11111111 = 377 = <a class="read-more" href="https://blog.gmem.cc/tcp-ip-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/tcp-ip-study-note">TCP/IP协议栈学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">名词术语</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">解释</td>
</tr>
</thead>
<tbody>
<tr>
<td>bogon</td>
<td>
<p>这个单词从bogus衍生而来，字面意思是伪造的<br />在TCP/IP协议中，bogon代表那些<span style="background-color: #c0c0c0;">保留或者未分配的IP段</span>，这些地址不应该出现在<span style="background-color: #c0c0c0;">因特网上</span></p>
<p>由于ISP路由过滤、或者恶意软件的原因，Bogon地址的IP数据报可能到达目标机器上，目标机器往往进行Bogon地址过滤</p>
<p>Bogon地址和保留私有地址不是一个概念，但是某些软件<span style="background-color: #c0c0c0;">可能把保留私有地址作为Bogon看待</span>：</p>
<pre class="crayon-plain-tag">ip firewall address-list
# 输出
# add list="BOGONS" address=0.0.0.0/8
# add list="BOGONS" address=10.0.0.0/8
# add list="BOGONS" address=100.64.0.0/10
# add list="BOGONS" address=127.0.0.0/8
# add list="BOGONS" address=169.254.0.0/16
# add list="BOGONS" address=172.16.0.0/12
# add list="BOGONS" address=192.0.0.0/24
# add list="BOGONS" address=192.0.2.0/24
# add list="BOGONS" address=192.168.0.0/16
# add list="BOGONS" address=198.18.0.0/15
# add list="BOGONS" address=198.51.100.0/24
# add list="BOGONS" address=203.0.113.0/24
# add list="BOGONS" address=224.0.0.0/3</pre>
</td>
</tr>
<tr>
<td>Reserved Addresses</td>
<td>
<p>即保留私有地址，这些地址<span style="background-color: #c0c0c0;">用于特殊用途</span>，例如：
<ol>
<li>多播地址，这类地址为224.0.0.0/4</li>
<li>用于本机的环回地址，这类地址为127.0.0.0/8</li>
<li>Link-local address：用于<span style="background-color: #c0c0c0;">两台主机在没有（被DHCP）分配IP地址的情况下通信</span>，这类地址是169.254.0.0/16</li>
<li>用于私有网络（局域网）的本地通信，这类地址包括10.0.0.0/8、172.16.0.0/12、192.168.0.0/16</li>
<li>其它特殊用途</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">简述</span></div>
<p>11111111 = 377 = 255 = FF</p>
<div class="blog_h2"><span class="graybg">网络协议分层</span></div>
<p>TCP/IP是四个层次上的多个协议的组合：</p>
<ol>
<li>链路层：设备驱动程序与网卡一起处理与电缆（或其它传输媒介）的物理接口细节。某些链路层协议与物理层密切相关，有些则不然</li>
<li>网络层：处理分组在网络中的活动，例如分组的选路。包括 IP协议（网际协议）， ICMP协议（Internet控制报文协议），以及IGMP协议（Internet组管理协议）
<ol>
<li>IP协议：尽可能快的把分组从源结点送到目的结点，但是并不提供任何可靠性保证</li>
<li>ICMP：IP协议的附属协议。 IP协议用它来与其他主机或路由器交换错误报文和其他重要信息</li>
<li>IGMP：用来把一个 UDP数据报多播到多个主机</li>
</ol>
</li>
<li>传输层：为两台主机上的应用程序提供端到端的通信。包括TCP（传输控制协议）和 UDP（用户数据报协议）
<ol>
<li>TCP协议：为两台主机提供高可靠性（通过超时重传、发送和接收端到端的确认分组机制来保证）的数据通信。它把应用程序交给它的数据分成合适的小块交给下面的网络层、确认接收到的分组、设置发送最后确认分组的超时</li>
<li>UDP协议：为应用层提供一种非常简单的服务：把称作数据报的分组，从一台主机发送到另一台主机，但并不保证该数据报能到达另一端</li>
</ol>
</li>
<li>应用层：负责处理特定的应用程序细节。包括多种协议，例如：
<ol>
<li>Telnet远程登陆协议</li>
<li>FTP文件传输协议</li>
<li>HTTP超文本传输协议</li>
<li>SMTP简单邮件传送协议</li>
<li>SNMP简单网络管理协议</li>
</ol>
</li>
</ol>
<p>其中底下3层主要由操作系统内核负责。应用层由应用程序负责。链路层处理了通信媒介的细节，应用层处理了特定用户应用程序（FTP、 Telnet等），网络层、传输层存在的价值何在？要理解这一点，必须考虑多个网络互联（网络的网络，互联网）的情况。可以使用以下方式把网络进行互联：</p>
<ol>
<li>路由器（IP Router），在网络层上对网络进行互连。路由器可以为不同类型的物理网络提供连接，包括以太网、令牌环网、FDDI等。广义上说，任何具有多个网络接口 ( multihomed )的主机都可以作为路由器使用</li>
<li><a href="/network-faq#bridging">网桥：</a>在链路层上对网络进行互连。使得多个局域网组合在一起，这样对上层来说就好像是一个局域网</li>
</ol>
<p>TCP/IP网络各层之间对等实体交换的单位信息称为<span style="background-color: #c0c0c0;">协议数据单元（Protocol data unit，PDU）</span>。PDU中的载荷部分称为服务数据单元（Service data unit，SDU）。除了物理层以外，上层协议的PDU都是传递给下层作为其SDU，由下层代为完成交换的。</p>
<div class="blog_h2"><span class="graybg">五类IP地址</span></div>
<p>互联网络信息中心（InterNIC）负责管理IP地址，它只负责分配网络号</p>
<p>IPv4地址的结构如下图：</p>
<p><img class="aligncenter  wp-image-5012" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ip-addr-type.jpg" alt="ip-addr-type" width="519" height="289" /></p>
<p>IPv4地址分类范围如下图： <img class="aligncenter size-full wp-image-5015" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ip-addr-type2.jpg" alt="ip-addr-type2" width="377" height="203" /></p>
<p>从IP目的地类型来看，IP地址可以分为三类：</p>
<ol>
<li>单播地址（目的为单个主机）</li>
<li>广播地址（目的端为给定网络上的所有主机）</li>
<li>多播地址（目的端为同一组内的所有主机）</li>
</ol>
<p>此外，A/B/C类地址中均有一部分地址供局域网内部使用，称为私有地址（Private Address）：</p>
<ol>
<li>A类私有地址：10.0.0.0 至 10.255.255.255</li>
<li>B类私有地址：172.16.0.0 至 172.31.255.255</li>
<li>C类私有地址：192.168.0.0 至 192.168.255.255</li>
</ol>
<p>私有地址不会出现在Internet上。</p>
<div class="blog_h2"><span class="graybg">域名系统</span></div>
<p>应用程序可以调用标准库函数来查看给定名字的主机的IP地址。大多数使用主机名作为参数的应用程序也可以把IP地址作为参数。</p>
<div class="blog_h2"><span class="graybg">数据包封装</span></div>
<p>通过TCP协议传送数据时，数据被送入协议栈中，每一层协议对收到的数据都要增加一些头/尾部信息。TCP传给IP的数据单元称作TCP报文段（TCP segment）；IP传给链路层的数据单元称作IP数据报（IP datagram）。通过以太网传输的比特流称作帧（Frame），以太网数据帧的物理特性使其长度必须在 46～1500字节之间。数据包封装过程如下图：</p>
<p><img class="aligncenter  wp-image-5019" src="https://blog.gmem.cc/wp-content/uploads/2012/03/tcp.png" alt="tcp" width="628" height="446" /></p>
<p>通过UDP协议传送数据时类似，只是UDP传给IP的数据单元叫UDP数据报（UDP datagram）。UDP头部为8字节。</p>
<p>IP数据报的头部包含8bit的数值，称为协议域，用于表示上层是何种协议，其中：1表示为ICMP协议， 2表示为IGMP协议， 6表示为TCP协议， 17表示为UDP协议。</p>
<p>由于很多应用程序可以使用TCP、UDP来传送数据，故TCP和UDP都用一个16bit的端口号（0-65535）来表示不同的应用程序，TCP、UDP将源、目标端口号均存放在报文头部。</p>
<p>以太网头部也包括16bit的桢类型域，用于区分 IP、 ARP还是ARP数据。</p>
<div class="blog_h2"><span class="graybg">数据包分用（Demultiplexing）</span></div>
<p>当目的主机收到一个以太网数据帧时，数据就开始从协议栈中由底向上升，同时去掉各层协议加上的报文首部。每层协议盒都要去检查报文首部中的协议标识，以确定接收数据的上层协议。具体参考下图，请注意：很难严格的把某些协议划分到特定网络层次中。<img class="aligncenter  wp-image-5025" src="https://blog.gmem.cc/wp-content/uploads/2012/03/Demultiplexing.png" alt="Demultiplexing" width="589" height="363" /></p>
<div class="blog_h2"><span class="graybg">客户端-服务器模型</span></div>
<p>大部分网络应用程序在编写时都假设一端是客户，另一端是服务器，其目的是为了让服务器为客户提供一些特定的服务。<br /> 可以将这种服务分为两种类型：重复型或并发型。</p>
<div class="blog_h2"><span class="graybg">端口号</span></div>
<p>服务器一般都是通过知名端口号来识别的。例如，对于每个TCP/IP实现来说，FTP服务器的TCP端口号都是21，每个Telnet服务器的TCP端口号都是23。这些知名端口号由IANA管理。<br /> 客户端通常对它所使用的端口号并不关心，只需保证该端口号在本机上是唯一的就可以了。客户端口号又称作临时端口号。</p>
<div class="blog_h2"><span class="graybg">API</span></div>
<p>使用TCP/IP协议的应用程序通常采用两种应用编程接口：</p>
<ol>
<li>Socket：有时称作“Berkeley socket”，因其从伯克利版发展而来</li>
<li>TLI：运输层接口，有时称作XTI。起初由AT&amp;T开发</li>
</ol>
<div class="blog_h1"><span class="graybg">链路层</span></div>
<p>在TCP/IP协议族中，链路层的主要职责为：</p>
<ol>
<li>为IP协议发送IP数据报</li>
<li>为ARP协议发送ARP请求与应答</li>
<li>为RARP协议发送RARP请求与应答</li>
</ol>
<p>TCP/IP支持多种链路层协议，这取决于网络硬件，例如：</p>
<ol>
<li>以太网</li>
<li>令牌环网</li>
<li>FDDI</li>
<li>RS-232串行线路</li>
</ol>
<p>链路层的<span style="background-color: #c0c0c0;">PDU被称为帧（Frame）</span>，其长度取决于具体的接口。</p>
<div class="blog_h2"><span class="graybg">以太网和IEEE 802协议</span></div>
<p>以太网指的是DEC、Intel、Xerox等公司联合发布的一种标准，是目前TCP/IP局域网主要使用的技术。使用CSMA/CD（带冲突检测的载波侦听多路接入）作为媒体接入方式。以太网IP数据报的封装在RFC 894中发布。</p>
<p>IEEE 802委员会发布了与以太网稍微不同的标准集，包括802.3、802.4、802.5。它们的帧格式与以太网不一致。IEEE 802网络的IP数据报封装在RFC 1042中发布。</p>
<p>下图是以太网、IEEE 802封装格式的比较：</p>
<p><img class="wp-image-5032 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ieee802.png" alt="ieee802" width="608" height="538" /></p>
<p>在以太网中，主机之间通过交换以太网帧（Frames）来交互，每个主机以MAC地址作为唯一标识。现代NIC支持编程修改MAC地址。</p>
<p>以太网中的每个主机，都可以向任何其他主机直接发送帧。此外，主机还可以<span style="background-color: #c0c0c0;">向特殊的地址ff:ff:ff:ff:ff:ff发送帧，所有主机都会收到，这种帧叫做广播</span>。ARP、DHCP是两个重要的使用广播的协议。由于以太网支持广播，因此<span style="background-color: #c0c0c0;">单个以太网也被称为广播域（broadcast domain）</span></p>
<p>当NIC接收到帧后，它会检查目标MAC地址是否匹配本机地址（或者是广播地址）。如果部匹配，默认情况下会丢弃。如果<span style="background-color: #c0c0c0;">NIC配置为混杂模式（Promiscuous mode），则它不会丢弃不匹配的帧，而是全部交给操作系统处理</span>。</p>
<div class="blog_h2"><span class="graybg">串行线路IP（Serial Line IP，SLIP）协议</span></div>
<p>SLIP是一种在串行线路上对IP数据报进行封装的简单形式，在RFC 1055中发布。SLIP适用于RS-232、调制解调器接入Internet。SLIP封装的规则如下：</p>
<ol>
<li>IP数据报以END字符（oxc0）结束，为了防止噪声干扰，大部分实现在数据报开始处也传送一个END字符</li>
<li>如果IP数据报中存在END字符，以连续两字节0xdb 0xdc转义。0xdb作为SLIP的ESC字符</li>
<li>如果IP数据报中存在SLIP的ESC字符，则以连续两字节0xdb 0xdd转义</li>
</ol>
<p>该封装格式有以下缺陷：</p>
<ol>
<li>每一端必须知道对方的IP地址，没有办法把IP地址传送给对方</li>
<li>数据帧中没有类型字段，如果某条串行线路用于SLIP，则不能同时使用其它协议</li>
<li>数据帧上没有CRC，如果报文因为线路噪声出错，则只能有上层协议发现</li>
</ol>
<p>SLIP还具有压缩的变种CSLIP。</p>
<div class="blog_h2"><span class="graybg">PPP（点对点协议）</span></div>
<p>PPP被用在许多类型的物理网络中，包括串口线、电话线、中继链接、移动电话、特殊无线电链路以及光纤链路。</p>
<p>PPP还用在互联网接入连接上（宽带）。互联网服务提供商（ISP）使用PPP为用户提供到Internet的拨号接入，这是因为IP报文无法在没有数据链路协议的情况下通过调制解调器线路自行传输。PPP的两个派生物PPPoE（PPP over Erhernet，基于以太网的PPP）和PPPoA被ISP广泛用来与用户创建数字用户线路（DSL）Internet服务连接。</p>
<p>PPP被广泛用作连接同步和异步电路的数据链路层协议，取代了陈旧的串行线路IP协议（SLIP）。</p>
<p>PPP被设计用来与许多网络层协议协同工作，包括网际协议（IP）、TRILL、Novell的互联网分组交换协议（IPX）、NBF以及AppleTalk。</p>
<p>PPP数据帧的格式如下图：</p>
<p><img class="wp-image-5040 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/PPP.png" alt="PPP" width="590" height="271" /></p>
<p>标志字符0x7e需要进行转义，同步链路、异步链路使用不同的方式。</p>
<p>比起SLIP，PPP具有以下优点：</p>
<ol>
<li>PPP支持在单根串行线路上运行多种协议，不单单是IP协议</li>
<li>每一帧都有循环冗余检验</li>
<li>通信双方可以进行IP地址的动态协商（使用IP网络控制协议）</li>
<li>与CSLIP类似，对TCP和IP报文首部进行压缩</li>
<li>链路控制协议可以对多个数据链路选项进行设置</li>
</ol>
<div class="blog_h2"><span class="graybg">环回（Loopback）接口</span></div>
<p>环回接口可以认为是链路层的一种实现，通常作为TCP/IP协议栈实现的内置组件。 大部分系统支持环回接口，以允许同一台主机上的客户端/服务器可以通过TCP/IP通信。A类地址的127网络号专门供环回接口使用。</p>
<p>大部分系统把地址127.0.0.1分配给环回接口，并命名为localhost，传给环回接口的IP数据报不会在任何网络上出现。</p>
<p>在Windows中，通过ipconfig看不到此环回接口；在Linux中，通过ifconfig通常可以看到一个名为lo的网络接口，此即环回接口。</p>
<p>在Windows中可以配置一个虚拟的环回网卡（Microsoft  loopback adapter）,主要用于在没有网络环境的情况下进行测试，环回网卡与TCP/IP协议中的环回接口不是一个概念。可以分配任何除了127.0.0.1以外的任何私有地址给环回网卡。</p>
<div class="blog_h2"><span class="graybg">最大传输单元（MTU）</span></div>
<p>以太网、802.3、FDDI、PPP对数据帧的长度均有限制，分别为1500、1492、4352、296。链路层的这一特性称为MTU。<br /> 如果IP数据报的数据长度大于MTU，<span style="background-color: #c0c0c0;">那么IP层必须对数据报进行分片（fragmention）</span>，使每一片小于MTU。</p>
<div class="blog_h3"><span class="graybg">路径MTU</span></div>
<p>如果两台主机通信需要跨越多个网络，则每个网络可能有不同的MTU，那么路由中最小的MTU称为路径MTU。<br /> 由于路由选路不是对称的，因此A到B的MTU可能与B到A的MTU不同。</p>
<div class="blog_h1"><span class="graybg">IP协议（网际协议）</span></div>
<p>IP是TCP/IP协议族中最为核心的协议，TCP、UDP、ICMP、IGMP等协议均要通过IP数据报的格式进行传输。IP协议是不可靠（Unreliable）的，即不保证数据报能到达终点，可靠性必须由上层协议保证（出错时仅仅会发送ICMP报文通知源主机）。</p>
<p>IP是无连接（Connectionless）的，即IP协议不维护任何关于后续数据报的状态信息，每个数据报的处理均是独立的。IP数据报可以不按发送顺序来接收。</p>
<p>IP层的每个<span style="background-color: #c0c0c0;">PDU被称为IP数据报（IP Datagram）</span>。</p>
<div class="blog_h2"><span class="graybg">IP数据报头部</span></div>
<p>一般的，IP数据报头部为20字节（除非包含选项字段），如下图：<img class="wp-image-5059 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/IP-head.png" alt="IP-head" width="630" height="405" /></p>
<p>详解如下：</p>
<ol>
<li>4位版本号：版本号为4或者6，代表IPv4或者IPv6</li>
<li>4位首部长度：即首部占有32bit（4字节长度），由于该字段为4bit，故首部最长为15*4=60字节。普通IP数据报头（无选项），该字段为5</li>
<li>8位TOS：由以下内容组成：
<ol>
<li>3bit的优先权子字段，现已被忽略</li>
<li>4bit的TOS子字段，4位分别表示：最小延时、最大吞吐量、最高可靠性、最小成本，这4位只能有一个为置1</li>
<li>1bit的未使用位，必须置0</li>
</ol>
</li>
<li>16位总长度：整个IP数据报的长度。利用总长-首部长，可以得到数据部分的偏移量。由于该字段为16位，因此IP数据报最大65535字节。</li>
<li>16位标识：主机每发一份数据报，该值增加1。其后的3位标志、13位片偏移和分片、重组有关</li>
<li>8位TTL：生存时间，标识数据报最多可以经过的路由数。其初始值由源主机设值（通常32或者64），一旦经过一个路由器，其值减一，如果为0，数据报就被丢弃，并发送ICMP报文给源主机</li>
<li>8位协议：指明上层协议的类型。1表示为ICMP协议， 2表示为IGMP协议， 6表示为TCP协议， 17表示为UDP协议</li>
<li>16位首部校验和：用于验证IP数据报的有效性。如果出错不会发送ICMP差错报文，由上层发现丢失并重传</li>
<li>32位源、目的IP地址</li>
<li>选项：是数据报中的一个可变长的可选信息，必须是32bit的整数倍（不足插0），以保证IP首部始终是4字节的整数倍</li>
</ol>
<p>IP数据报以大端（Big-endian）方式传输，即，对于上图的每行0-31（最低位0，最高位31），首先传送0-7，然后8-15，然后16-23、最后24-31位 。由于TCP/IP首部所有二进制整数均以大端方式传输，故称大端为网络字节序。大小端区别如下图：<img class="aligncenter size-full wp-image-13747" src="https://blog.gmem.cc/wp-content/uploads/2012/03/Endianess.png" alt="Endianess" width="560" height="250" /></p>
<div class="blog_h2"><span class="graybg">IP分片（Fragmentation）</span></div>
<p>物理网络层一般要限制每次发送数据帧的最大长度，IP层发送数据报时，需要查询网络接口并获得MTU，如果IP数据报相对于MTU过大，则需要进行分片。分片可以发生在原始发送端主机上，也可以发生在中间路由器上（IPv6禁止中途分片，IPv4也应当尽量避免中途分片）。</p>
<p>把一份IP数据报分片以后，只有到达目的地才进行重新组装（Reassembly）。重新组装由目的端的IP层来完成，其目的是使分片和重新组装过程对运输层透明。</p>
<p>IP首部中包含的数据为分片和重新组装提供了足够的信息：</p>
<ol>
<li>标识字段：每一个待发送的IP数据报均有唯一值，该值复制到每个分片中</li>
<li>用于标示“更多的片”的标志位，除最后一片，均置1</li>
<li>片偏移：该片偏移原始数据报开始处的位置</li>
<li>当数据报被分片后，每个片的总长度值要改为该片的长度值</li>
</ol>
<p>当IP数据报被分片后，每一片都成为一个分组，具有自己的IP首部，并在选择路由时与其他分组独立。这样，当数据报的这些片到达目的端时有可能会失序，但是通过IP首部中的信息可以重新正确组装。</p>
<p>注意术语<span style="background-color: #c0c0c0;">IP数据报（IP Datagram）</span>、<span style="background-color: #c0c0c0;">分组（Packet，简称包）</span>的区别：</p>
<ol>
<li>IP数据报是指IP层端到端的传输单元（在分片之前和重新组装之后）</li>
<li>分组是指在IP层和链路层之间传送的数据单元。一个分组可以是一个完整的IP数据报，也可以是IP数据报的一个分片</li>
</ol>
<p>IP层没有重传机制，这意味着，即使分片丢失一个，整个IP数据包就废了。L4协议必须明白这一点，并给出必要的处理机制。</p>
<div class="blog_h2"><span class="graybg">IP路由选择</span></div>
<p>对于主机来说，路由通常很简单：</p>
<ol>
<li>如果与目的主机直连（如PPP链路）或者在一个网络中（以太网、令牌环网），则直接把IP数据报发给目标主机</li>
<li>否则，把数据报发往一默认路由，由路由器转发该数据报</li>
</ol>
<p>一般的说，IP层既可以配置成路由器的功能，也可以配置成主机的功能，几乎所有的Unix都可以配置成一个路由器。</p>
<p>IP可以从上层（TCP、UDP、ICMP、IGMP，即本地生成的）或者从一个网络接口（即待转发的）接收数据报。</p>
<p>IP层在内存中维持一个路由表，要发送每一份数据报时都要搜索该表一次。路由表中的每一项都包含下面这些信息：</p>
<ol>
<li>目标IP地址：可以是主机的完整地址，或者网络地址，由标志字段确定。网络地址的主机号为0，表示目标是该网络（如以太网）中所有主机</li>
<li>下一站（跳）路由器（next-hop router，即直接相连的路由器）的IP地址，或者直接连接的接口IP地址</li>
<li>标志：一个标志用于标识是网络地址还是主机地址；另外一个标识下一跳路由器是真实路由器，还是直接连接的接口</li>
<li>为数据报传输指定的网络接口</li>
</ol>
<p>IP路由是逐跳（hop-by-hop）进行的，IP并不知道到达目的地的完整路径，所有路由器只是为IP数据报提供下一个路由器的IP地址。</p>
<p>IP路由选择的步骤如下：</p>
<ol>
<li>搜索路由表，寻找与目标IP地址完全匹配的条目（网络、主机号都匹配），如果找到，把报文发给下一站路由器或者直连接口</li>
<li>搜索路由表，寻找与目标网络号匹配的条目，如果找到，把报文发给下一站路由器或者直连接口。这种匹配方式必须考虑子网掩码</li>
<li>搜索路由表，寻找标记为默认（Default）的条目，如果找到，把报文发给下一站路由</li>
</ol>
<p>如果以上步骤均不成功，则数据报无法发送，如果数据报来自本机，则应用程序可能获得一个“主机不可达”或“网络不可达”错误。</p>
<p>IP数据报具体处理过程如下：</p>
<ol>
<li>如果数据报来自网络接口：
<ol>
<li>检查目的IP地址是否为本机任一IP地址或者广播地址。如果是，则根据首部协议字段，发送到指定协议模块处理，否则：
<ol>
<li>如果当前IP层被设置为路由器功能，则对数据报进行转发，否则</li>
<li>丢弃数据报</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>下面是路由的一个例子（bdsi主机期望发送IP数据报到Internet的192.48.96.9）：</p>
<p><img class="wp-image-5065 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/route-example.png" alt="route-example" width="673" height="549" /></p>
<p>该例子的路由过程如下：</p>
<ol>
<li>bdsi搜索路由表，没有匹配目标地址主机号、网络号的条目，故使用默认路由，将其发送给主机sun。注意bsdi把链路层目的地址设为sun的MAC地址，该地址使用ARP协议获得</li>
<li>sun接收到报文，发现其目标IP不是任何接口的IP地址，而且sun配置了路由功能，故进行转发。经搜索路由表，使用了默认条目，将其发送到下一站路由器netb，该路由地址为140.252.1.183，通过SLIP与sun连接。注意SLIP报文没有以太网报文那样的首部</li>
<li>netb接收到报文后，执行了与sun类似的动作，使用默认路由将其转发给网关140.252.1.4</li>
<li>网关也使用自己的默认路由，该默认路由指定下一站路由为140.252.104.2</li>
</ol>
<p>注意该例子里面的几个关键点：</p>
<ol>
<li>所有主机和路由器都使用了默认路由。实际上，大多数主机和一些路由器可以用默认路由来处理任何目的，除非它在本地局域网上</li>
<li>数据报中的目的IP地址始终不发生任何变化。所有的路由选择决策都是基于这个目的IP地址</li>
<li>每个链路层可能具有不同的数据帧首部，而且链路层的目的地址（如果有的话）始终指的是下一站的链路层地址</li>
</ol>
<div class="blog_h2"><span class="graybg">子网寻址</span></div>
<p>所有现代主机都支持子网编址，不是把IP地址简单的分为网络号+主机号，而是进一步把主机号分解为子网号+主机号。这样做的原因是A、B类地址为主机号预留了过多的空间。</p>
<p>获取到IP网络号后，由系统管理员确定划分 多少bit给子网号、多少bit给主机号。例如对于B类网络地址140.252，如果剩下的16bit，8位分配给子网，8位分配给主机，则可以有254个子网、每个子网254台主机。</p>
<p>子网对外部路由器来说隐藏了内部网络组织（一个校园或公司内部）的细节，例如，对于网络140.252，外部路由器只需要一个路由表目，不管其内部划分多少个子网。子网对于内部路由器则不透明。</p>
<div class="blog_h2"><span class="graybg">子网掩码</span></div>
<p>子网掩码是一个32bit的值，其中值为1的比特留给网络号和子网号，为0的比特留给主机号：</p>
<p><img class="wp-image-5076 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/mask.png" alt="mask" width="584" height="152" /></p>
<p>给定IP地址、子网掩码后，主机可以确定IP数据报的目的地址的位置是哪里：</p>
<ol>
<li>本子网上的主机</li>
<li>本网络中其他子网中的主机</li>
<li>其他网络上的主机</li>
</ol>
<div class="blog_h1"><span class="graybg">地址解析协议（ARP）</span></div>
<p>数据链路层，例如以太网、令牌环网，都有自己的寻址机制，上层协议必须遵从，这种寻址机制与IP无关。</p>
<p>对于以太网，当一台主机发送以太网数据帧给同一局域网中另外一台主机时，是使用48位以太网地址来确定目标网络接口的，设备驱动从来不会关注IP数据报中的目标IP地址。</p>
<p>ARP是链路层和网络层的地址转换桥梁，它提供了IP地址与数据链路层任何类型的地址之间的映射，因而根据IP数据报可以获取以太网数据帧需要的48位地址。ARP通常自动化、动态的完成IP与硬件地址的映射，通常用户、系统管理员关心。</p>
<div class="blog_h2"><span class="graybg">举例：Linux下建立FTP连接的过程</span></div>
<p><a href="/wp-content/uploads/2012/03/ftp.png"><img class=" wp-image-5080 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ftp.png" alt="ftp" width="533" height="630" /></a></p>
<p>&nbsp;</p>
<p>该例子详解如下：</p>
<ol>
<li>FTP客户端调用gethostbyname函数，把主机名转换为IP地址。该函数载DNS系统中称为解析器，它要么使用DNS服务器，要么使用/etc/hosts静态列表</li>
<li>FTP客户端使用获得的IP地址，创建TCP连接</li>
<li>TCP发送连接请求分段到FTP服务器，即使用上述IP地址发送一个IP数据报</li>
<li>如果FTP服务器在本地网络（以太网、令牌环、PPP对端），则IP数据报可以直接送达。否则，通过IP选路函数确定下一站路由，并由其转发
<ol>
<li>假设FTP服务在本以太网内，那么发送端主机必须把32bit的IP地址转换为48Bbit以太网地址，这个转换就是ARP的功能</li>
<li>ARP发送一份ARP请求的以太网数据帧，给以太网上每台主机，这是一种广播方式。该请求数据帧中包含了主机的IP地址，其意义是：如果你是该IP的主机，那么请回答你的硬件地址</li>
<li>FTP服务器收到ARP广播报文后，发送包含硬件地址、IP地址的应答</li>
<li>FTP客户端收到ARP应答，就可以进行IP数据报的传送了</li>
<li>发送IP数据报到FTP服务器</li>
</ol>
</li>
</ol>
<p> 在ARP后面有个基本概念，即网络接口具有唯一的硬件地址，在硬件层次上进行数据帧交换，必须有正确的硬件地址。TCP/IP协议栈虽然有自己的IP地址，但是知道IP地址并不能确定要发到那一台主机。</p>
<p>点对点链路不需要使用ARP，在建立链路时，必须告知内核，链路每一端的IP地址，像以太网那样的硬件地址并不涉及。</p>
<div class="blog_h2"><span class="graybg">ARP高速缓存</span></div>
<p>每个主机上都有一个ARP高速缓存。这个高速缓存存放了最近IP地址到硬件地址之间的映射记录。高速缓存中每一项的生存时间一般为20分钟。 使用arp命令可以查询高速缓存。</p>
<div class="blog_h2"><span class="graybg">ARP分组格式</span></div>
<p>ARP可以用于非以太网络，也可以用于解析非IP地址。ARP请求和应答分组的格式如下图所示：</p>
<p><a href="/wp-content/uploads/2012/03/arp.png"><img class="wp-image-5085 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/arp.png" alt="arp" width="579" height="106" /></a>其中：</p>
<ol>
<li>以太网目的地址：如果为全1，则表示是广播地址。电缆上的所有以太网接口都要接收广播的数据帧</li>
<li>2字节以太网帧类型：表示后续的数据帧类型，对于ARP请求应答，该字段为0x0806</li>
<li>硬件类型：表示硬件地址的类型。1表示以太网地址</li>
<li>协议类型：要映射的协议地址类型。0x0800表示IP地址</li>
<li>硬件地址长度和协议地址长度分别指出硬件地址和协议地址的长度，以字节为单位</li>
<li>op操作字段：指出四种操作类型， ARP请求1、ARP应答2、RARP请求3、RARP应答4</li>
<li>对于ARP请求来说，目的以太网地址为空</li>
</ol>
<div class="blog_h2"><span class="graybg">使用tcpdump命令检测网络上的ARP包</span></div>
<pre class="crayon-plain-tag">sudo tcpdump -en -i eth0</pre>
<p>输出内容类似下面：</p>
<pre class="crayon-plain-tag">15:07:52.484316 48:5a:b6:9e:a6:79 &gt; ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Request who-has 192.168.1.254 tell 192.168.1.48, length 46
15:07:52.689430 d4:c9:ef:fe:78:c6 &gt; 33:33:00:01:00:02, ethertype IPv6 (0x86dd), length 161: fe80::d6c9:efff:fefe:78c6.546 &gt; ff02::1:2.547: dhcp6 solicit
15:07:54.332364 e0:06:e6:c7:c9:24 &gt; ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: 192.168.1.61.68 &gt; 255.255.255.255.67: BOOTP/DHCP, Request from e0:06:e6:c7:c9:24, length 300</pre>
<p>输出内容中，首先是时间戳，然后是源-目标以太网地址，然后是以太网帧类型，对于ARP，后续会包含其长度，以及who-has等信息。</p>
<div class="blog_h2"><span class="graybg">ARP高速缓存超时设置</span></div>
<p>从伯克利系统演变而来的系统一般对完整的表项设置超时值为20分钟，而对不完整的表项设置超时值为3分钟。</p>
<p>管理员可以用arp命令把地址放入高速缓存中而不设置超时。</p>
<div class="blog_h2"><span class="graybg">ARP代理</span></div>
<p>如果ARP请求发往另一个网络上的主机，那么连接这两个网络的路由器就可以回答该请求，这个过程称作ARP代理(Proxy ARP)。这样可以欺骗发起ARP请求的发送端，使它误以为路由器就是目的主机，路由器的功能相当于目的主机的代理。</p>
<p>ARP代理可以用在没有设置默认网关的网络中。</p>
<div class="blog_h1"><span class="graybg">因特网控制报文协议（ICMP）</span></div>
<p>ICMP经常被认为是IP层的一个组成部分，用于传递差错报文以及其他信息。ICMP报文通常被IP层或更高层协议（ TCP或UDP）使用。ICMP报文封装在IP报文内部：</p>
<p><img class="wp-image-5114 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/icmp.png" alt="icmp" width="396" height="117" /></p>
<p>ICMP报文的格式如下图所示：</p>
<p><img class="wp-image-5115 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/icmp2.png" alt="icmp2" width="545" height="179" /></p>
<div class="blog_h2"><span class="graybg">ICMP报文类型</span></div>
<p>ICMP报文的类型由8位类型字段、8位代码字段共同确定，如下表（最后两列说明该ICMP是查询还是差错报文）：</p>
<p><a href="/wp-content/uploads/2012/03/icmp-types.png"><img class="wp-image-5116 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/icmp-types.png" alt="icmp-types" width="712" height="741" /></a></p>
<p>&nbsp;</p>
<p>区分查询报文、差错报文的原因是：差错报文需要特殊处理。例如，对于差错报文，永远不会再生成一个差错报文。</p>
<p>当发送一个ICMP差错报文时，产生ICMP报文的IP数据报的前8字节始终包含其中，这样就可以确定产生ICMP的协议、用户进程（根据端口号确定）。</p>
<p>以下场景不会产生ICMP差错报文：</p>
<ol>
<li>ICMP差错报文（但是ICMP查询报文可能产生）</li>
<li>目的地址是广播、多播地址的IP数据报</li>
<li>作为链路层广播的数据报</li>
<li>不是IP分片的第一片</li>
<li>源地址不是单个主机的数据报。即，源地址是零地址、环回地址、广播地址、多播地址的数据报不会产生ICMP差错报文</li>
</ol>
<div class="blog_h2"><span class="graybg">Ping程序</span></div>
<p>Ping程序的目的是为了测试另一台主机是否可达，该程序发送一份ICMP回显请求报文给主机，并等待返回ICMP回显应答。通常来说，如果不能Ping通目标主机，那么就不能使用Telnet、FTP等高层协议。</p>
<p>大部分TCP/IP实现在内核中支持Ping服务器（不是用户进程）。</p>
<p>当返回Ping的ICMP应答时，会打印TTL、往返消耗时间。</p>
<p>在广域网中进行Ping，可能出现重复分组、失序分组（同一个序号出现多次，或者顺序乱掉）</p>
<div class="blog_h3"><span class="graybg">Ping请求/应答报文格式</span></div>
<p><img class="wp-image-5120 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ping-msg.png" alt="ping-msg" width="555" height="202" /></p>
<ol>
<li>标识符：Unix实现中，通常将其设置为Ping客户端进程的ID</li>
<li>序号：从0开始，每发一次回显请求就增加1</li>
</ol>
<div class="blog_h3"><span class="graybg">记录路由（RR）选项</span></div>
<p>使用ping的-R选项可以提供记录路由功能。该选项导致ping程序发送的IP数据报（即ICMP报文）设置IP的RR选项，这样每个处理该IP数据报的路由器都把自己的IP地址放入数据报的选项字段中。</p>
<p>通过该方式记录路由，有一个缺陷：IP首部的空间有限，最多能存放9个IP地址。</p>
<p>IP数据报头部的RR选项格式如下：</p>
<p style="text-align: center;"><img class="alignnone  wp-image-5139" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ip-with-PP.png" alt="ip-with-PP" width="511" height="115" /></p>
<ol>
<li>code：选项类型，对于RR，其为7</li>
<li>len：选项的长度</li>
<li>ptr：基于1的指针，指向下一个IP的位置</li>
</ol>
<div class="blog_h2"><span class="graybg">Traceroute程序</span></div>
<p>Traceroute只需要在目的端运行一个UDP模块不需要任何特殊的服务器应用程序。其原理是：当路由器收到一份IP数据报，如果其TTL字段是0或1，则路由器不转发该数据报，而是丢弃该数据报，并给信源机发一份ICMP“超时”信息，Traceroute程序检查该ICMP报文的IP头的源地址，即为路由器地址。具体步骤如下：</p>
<ol>
<li>客户端发送TTL=1的IP数据报给目的主机，得到路由器1的地址</li>
<li>客户端发送TTL=2的IP数据报给目的主机，得到路由器2的地址</li>
<li>……重复以上过程，直到数据报能够到达目标主机</li>
</ol>
<p>判断是否到达目标主机，依赖于最小TTL的判断，做法时，发送一个端口不可能（&gt;65535）的UDP数据报给目的主机，让目的主机产生端口不可达错误的ICMP报文，从中判断TTL</p>
<div class="blog_h3"><span class="graybg">输出分析</span></div>
<pre class="crayon-plain-tag">root@localhost:/# traceroute www.google.com
//目标主机名、IP地址，最大的TTL为30。40字节的数据包括20字节的IP头、8字节的UDP头
traceroute to www.google.com (173.194.117.48), 30 hops max, 60 byte packets
//对于每个TTL值，发送3个UDP数据报，每接收到一个ICMP报文，就打印往返时间
 1  106.187.33.3 (106.187.33.3)  0.860 ms  0.840 ms  1.132 ms
 2  124.215.199.173 (124.215.199.173)  1.261 ms  1.249 ms  1.235 ms
//三次走了不同的路由
 3  otejbb206.int-gw.kddi.ne.jp (124.215.194.177)  1.813 ms otejbb206.int-gw.kddi.ne.jp (124.215.194.162)  32.599 ms otejbb205.int-gw.kddi.ne.jp (124.215.194.178)  1.827 ms
//如果在5秒种内仍未收到3份数据报的任意一份的响应，则打印一个星号，并发送下一份数据报
 4  * * *</pre>
<div class="blog_h1"><span class="graybg">IP选路</span></div>
<p>IP层最重要的功能就是选路，IP层的简单处理流程如下图所示：</p>
<p><img class=" wp-image-5147 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ip-process-flow.png" alt="ip-process-flow" width="634" height="447" /></p>
<div class="blog_h2"><span class="graybg">选路原理</span></div>
<p>route命令的说明请参考：<a href="/linux-command-faq#network-command-route">Linux命令知识集锦</a></p>
<p>下面是一个时机的例子，注意以下几点：</p>
<ol>
<li>G用于区分是直接路由还是间接路由。
<ol>
<li>直接路由一般是指通过以太网直接访问（通过LAN路由器），不设置G标记，直接路由的底层报文分组中不单包含了IP地址，还包括目的地的链路层地址</li>
<li>间接路由的 IP地址指明的是最终的目的地，但是链路层地址指明的是网关（即下一站路由器）</li>
</ol>
</li>
<li>关于子网掩码：子网掩码实际上就是路由条目使用的网络接口的子网掩码，由于内核知道每个条目对应的网络接口，因此可以推断出子网掩码</li>
</ol>
<pre class="crayon-plain-tag">root@localhost:~ route -ne
Kernel IP routing table
Destination     Gateway         Genmask          Flags   MSS Window  irtt Iface
#默认路由。每个主机都有一个或多个默认路由，如果在路由表中没有匹配条目则将数据报发往106.185.46.1
0.0.0.0         106.185.46.1    0.0.0.0          UG        0 0          0 eth0
#局域网路由
106.185.46.0    0.0.0.0         255.255.255.0    U         0 0          0 eth0
#环回路由
127.0.0.0       0.0.0.0         255.0.0.0        U         0 0          0 lo
#特殊路由：如果目的是主机（H）66.51.111.12，则路由器把IP数据报发给网关106.185.46.77
66.51.111.12    106.185.46.77  255.255.255.255  UGH       0 0          0 eth1

#下面是位于106.185.46.7的PPTP服务器连接了两个客户端后，自动生成的路由条目
#PPTP服务器
root@localhost:~# ifconfig

#以太网卡
eth0      Link encap:Ethernet  HWaddr f2:3c:91:50:b5:86
          inet addr:106.185.46.7  Bcast:106.185.46.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:29052 errors:0 dropped:0 overruns:0 frame:0
          TX packets:36072 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:25331473 (25.3 MB)  TX bytes:26714476 (26.7 MB)
#环回网卡
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:281 errors:0 dropped:0 overruns:0 frame:0
          TX packets:281 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:28249 (28.2 KB)  TX bytes:28249 (28.2 KB)
#对于每一个PPP连接，显示一个网络接口，P-t-P为对端的地址
ppp0      Link encap:Point-to-Point Protocol
          inet addr:192.168.10.1  P-t-P:192.168.10.100  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1396  Metric:1
          RX packets:13175 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16974 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:3
          RX bytes:3497419 (3.4 MB)  TX bytes:20714351 (20.7 MB)

ppp1      Link encap:Point-to-Point Protocol
          inet addr:192.168.10.1  P-t-P:192.168.10.101  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1496  Metric:1
          RX packets:25 errors:0 dropped:0 overruns:0 frame:0
          TX packets:19 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:3
          RX bytes:1485 (1.4 KB)  TX bytes:1262 (1.2 KB)

root@localhost:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         106.185.46.1    0.0.0.0         UG    0      0        0 eth0
106.185.46.0    0.0.0.0         255.255.255.0   U     0      0        0 eth0
#针对每一个PPTP对端主机的路由设置，均直连，通过对应的PPP接口
192.168.10.100  0.0.0.0         255.255.255.255 UH    0      0        0 ppp0
192.168.10.101  0.0.0.0         255.255.255.255 UH    0      0        0 ppp1

#PPTP客户端
eth0      Link encap:Ethernet  HWaddr 00:0c:29:cc:9f:33  
          inet addr:192.168.1.111  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fecc:9f33/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:117 errors:0 dropped:0 overruns:0 frame:0
          TX packets:156 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:12406 (12.4 KB)  TX bytes:19847 (19.8 KB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:247 errors:0 dropped:0 overruns:0 frame:0
          TX packets:247 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:27029 (27.0 KB)  TX bytes:27029 (27.0 KB)

ppp0      Link encap:Point-to-Point Protocol  
          inet addr:192.168.10.101  P-t-P:192.168.10.1  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1400  Metric:1
          RX packets:19 errors:0 dropped:0 overruns:0 frame:0
          TX packets:25 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:3 
          RX bytes:1262 (1.2 KB)  TX bytes:1485 (1.4 KB)

alex@amethystine:~$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
#默认路由被修改，这导致了VPN的默认行为 ———— 所有连接通过VPN走
0.0.0.0         0.0.0.0         0.0.0.0         U     0      0        0 ppp0
#通往PPTP服务器的主机路由，走网关192.168.1.1（一个路由器）
106.185.46.7    192.168.1.1     255.255.255.255 UGH   0      0        0 eth0
#局域网路由
192.168.1.0     0.0.0.0         255.255.255.0   U     1      0        0 eth0
#通往PPTP服务器的PPP链路，走ppp0直连
192.168.10.1    0.0.0.0         255.255.255.255 UH    0      0        0 ppp0</pre>
<p>主机路由表的复杂性取决于主机所在网络的拓扑结构：</p>
<ol>
<li>如果主机没有联网，TCP/IP协议仍然可以用于该主机。这种情况下的路由表只包含环回接口一项</li>
<li>如果主机连在一个局域网上，只能访问局域网上的主机。这时路由表包含两项：一项是环回接口，另一项是局域网（如以太网）</li>
<li>如果主机能够通过单个路由器访问其他网络（如Internet）。这时路由表增加一个默认表项指向该路由器</li>
<li>如果要新增其他的特定主机或网络路由。这需要额外的路由表配置</li>
</ol>
<div class="blog_h3"><span class="graybg">路由表的初始化</span></div>
<p>每当使用ifconfig初始化一个网络接口时，内核自动为接口创建一个直接路由：</p>
<ol>
<li>对于PPP、Loopback，该直接路由的的目的是主机（H）</li>
<li>对于广播接口（例如以太网），该直接路由到达整个局域网</li>
</ol>
<p>到达主机/网络的路由不是直连的，就必须加入路由表，其中一种方式是在系统引导时，手工添加route add命令。某些系统运行在特定的配置文件添加路由，例如/etc/defaultrouter</p>
<div class="blog_h3"><span class="graybg">没有到达目的地的路由</span></div>
<p>如果最终没有找到匹配项，则：</p>
<ol>
<li>如果IP数据报是当前主机产生的：给发出报文的应用程序报告“主机不可达差错”或者是“网络不可达差错”错误</li>
<li>如果IP数据报是转发的：给原始报文发送端发送ICMP主机不可达差错报文</li>
</ol>
<div class="blog_h2"><span class="graybg">让主机转发（Forward）数据报</span></div>
<p>一般主机不转发IP数据报，除非对它们进行特殊配置而作为路由器使用。 大多数伯克利派生出来的系统都有一个内核变量ipforwarding，其值控制是否转发IP数据报。不同系统的设置方式不同，有些系统默认允许转发IP数据报。</p>
<div class="blog_h2"><span class="graybg">ICMP主机不可达差错</span></div>
<p>当路由器收到一份IP数据报但又不能转发时，就要发送一份ICMP“主机不可达”差错报文</p>
<div class="blog_h2"><span class="graybg">ICMP重定向差错</span></div>
<p>当IP数据报应该被发送到其它路由器时，收到数据报的路由器就要发送ICMP重定向差错报文给IP数据报的发送端。 具体场景如下：</p>
<ol>
<li>主机发送一份IP数据报给R1，通过默认路由</li>
<li>R1收到数据报并且检查它的路由表，发现R2是发送该数据报的下一站。R1检测到R2在同一局域网上</li>
<li>R1 发送一份ICMP重定向报文给主机，告诉它以后把数据报发送给R 2而不是R1</li>
</ol>
<p>示意图如下：</p>
<p><img class=" wp-image-5179 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ICMP-redirect.png" alt="ICMP-redirect" width="503" height="292" /></p>
<p>重定向一般用来让具有很少选路信息的主机逐渐建立更完善的路由表，允许TCP/IP主机在进行选路时不需要具备智能特性，而把所有的智能特性放在路由器端。</p>
<div class="blog_h2"><span class="graybg">ICMP路由器发现报文</span></div>
<p>除了在配置文件中添加静态路由，还可以使用ICMP路由器发现报文初始化路由表。</p>
<p>一般认为，主机在引导以后要广播或多播传送一份路由器请求报文。一台或更多台路由 器响应一份路由器通告报文。另外，路由器定期地广播或多播传送它们的路由器通告报文， 允许每个正在监听的主机相应地更新它们的路由表。</p>
<div class="blog_h1"><span class="graybg">用户数据报协议（UDP）</span></div>
<p>UDP是一个简单的面向数据报的运输层协议：进程的每个输出操作都正好产生一个UDP数据报，并组装成一份待发送的IP数据报。</p>
<p>UDP封装格式如下：</p>
<p><img class="wp-image-5184 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/UDP.png" alt="UDP" width="380" height="152" /></p>
<p>UDP不提供可靠性：它把应用程序传给IP层的数据发送出去，但是并不保证它们能到达目的地。</p>
<p>应用程序必须关心IP数据报的长度。如果它超过网络的MTU，那么就要对IP数据报进行分片。如果需要，源端到目的端之间的每个网络都要进行分片。</p>
<div class="blog_h2"><span class="graybg">UDP头部</span></div>
<p><img class="wp-image-5185 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/UDPHead.png" alt="UDPHead" width="523" height="189" /></p>
<ol>
<li>源端口号、目的端口号：注意UDP端口号与TCP端口号是相互独立的，尽管某种知名服务如果同时提供TCP、UDP服务，会使用同一个端口号</li>
<li>UDP长度字段：指的是UDP首部和UDP数据的字节长度，最少8</li>
<li>UDP检验和：覆盖UDP首部和UDP数据。与TCP校验和不同，UDP校验和是可选的。如果接收端计算校验和失败，UDP数据报被悄悄丢掉</li>
</ol>
<p>使用UDP很容易导致IP分片。如果分别发送1471、1472、1473、1474长度的4个UDP数据报，使用tcpdump可以看到：</p>
<pre class="crayon-plain-tag">#前两份UDP数据报（第1行和第2行）能装入以太网数据帧，没有被分片
0.0                  host.1112  &gt;  svr4.discard:  udp 1471  #UDP数据报的长度
21.008303 (21.0083)  host.1114  &gt;  svr4.discard:  udp 1472  #UDP数据报的长度
#写1473字节的IP数据报长度为1501（1473+20+8），超过MTU=1500，需要分片
#IP数据报被分片后，会打印额外的信息
                                                       #frag后面的数字26304：IP头的标识字段
                                                       #:与@之间的数字1480：除去IP头的数据报长度，其中用户数据1472字节
                                                       #分片时，除了最后一片，除IP头的数据部分必须是8的倍数
                                                       #@后的数字是从数据报开始处计算的片偏移值
50.449704 (29.4414)  host.1116  &gt;  svr4.discard:  udp 1473 (frag 26304:1480@0+)
50.450040 (0.0003)   host &gt; svr4:  (frag 26304:1@1480) #第一份数据报的第二个分片，只有1字节的用户数据，偏移1480
75.328650 (24.8786)  host.1118  &gt;  svr4.discard:  udp 1474 (frag 26313:148060+)
75.328982 (0.0003)   host &gt; svr4:  (frag 26313:2@1480)</pre>
<p>其中1473字节数据报分片情况如下：</p>
<p><img class="wp-image-5197 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ip-frag.png" alt="ip-frag" width="576" height="258" /></p>
<div class="blog_h2"><span class="graybg">ICMP不可达差错（需要分片）</span></div>
<p>当路由器收到一份需要分片的数据报，而在<span style="background-color: #c0c0c0;">IP首部又设置了不分片（DF）的标志比特</span>，也会产生ICMP不可达差错。<br /> 该差错可以被利用来测算网络路径中最小的MTU（路径MTU）。</p>
<div class="blog_h2"><span class="graybg">最大UDP数据报长度</span></div>
<p><span style="background-color: #c0c0c0;">理论上，IP数据报的最大长度是65535字节</span>，去除20字节的IP首部和8个字节的UDP首部， UDP数据报中用户数据的最长长度为65507字节。但是实际上大多数实现允许的值比理论值小，现在的<span style="background-color: #c0c0c0;">大部分系统都默认提供了可读写大于8192字节的UDP数据报</span>。</p>
<div class="blog_h2"><span class="graybg">ICMP源站抑制差错</span></div>
<p>可以使用UDP产生ICMP“源站抑制(source quench)”差错。当一个系统（路由器或主机）接收数据报的速度比其处理速度快时，可能产生这个差错。注意是“可能”————即使一个系统已经没有缓存并丢弃数据报，也不要求它一定要发送源站抑制报文。</p>
<p>该报文格式如下：</p>
<p><img class="wp-image-5202 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ICMP-sourcequench.png" alt="ICMP-sourcequench" width="604" height="217" /></p>
<div class="blog_h1"><span class="graybg">广播与多播</span></div>
<p>IP地址分为三类：单播（unicast）地址、广播（broadcast）地址和多播（multicast）地址。广播和多播仅应用于UDP。</p>
<p>所谓广播，是指主机要向网上的所有其他主机发送数据报；所谓组播，是指向属于多播组的多个主机发送数据报。</p>
<p>要理解广多播，需要首先理解主机对由信道传送过来帧的过滤过程：</p>
<p><img class="wp-image-5205 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/frame-filter.png" alt="frame-filter" width="242" height="337" /></p>
<ol>
<li>网卡查看由信道传送过来的帧，确定是否接收该帧，若接收后就将它传往设备驱动程序——通常网卡只接收目的地址为网卡物理地址或广播地址的帧。另外多数接口均被设置为混合模式，这种模式能接收每个帧的一个复制，tcpdump就是以此为原理工作。目前，大部分网卡经过配置，都可以接收目的地址为多播地址或某些子网多播地址的帧。对于以太网，当地址中最高字节的最低位设置为1时表示该地址是一个多播地址</li>
<li>如果网卡收到一个帧，这个帧将被传送给设备驱动程序；如果帧检验和错，网卡将丢弃该帧</li>
<li>设备驱动程序将进行另外的帧过滤：
<ol>
<li>帧类型中必须指定要使用的协议（IP、ARP等等）</li>
<li>其次，进行多播过滤，检测该主机是否属于多播地址对应多播组</li>
</ol>
</li>
<li>设备驱动程序随后将数据帧传送给下一层，对于IP数据报，就是传递给IP层。IP根据IP地址中的源地址和目的地址进行更多的过滤检测。如果正常，就将数据报传送给传输层（TCP/UDP）</li>
<li>每次UDP收到由IP传送来的数据报，进行过滤：
<ol>
<li>根据目的端口号，有时还有源端口号进行数据报过滤。如果当前没有进程使用该目的端口号，就丢弃该数据报并产生一个ICMP不可达报文</li>
<li>如果UDP数据报存在检验和错，将被丢弃</li>
</ol>
</li>
</ol>
<p>多播之于广播，好处是避免增加对广播数据不感兴趣主机的处理负荷，从上面的过滤过程看到，直到UDP层才能把广播数据丢弃。</p>
<p>使用多播，主机可<span style="background-color: #c0c0c0;">加入一个或多个多播组</span>。这样，网卡将知道该主机属于哪个多播组，然后仅接收主机所在多播组的那些多播帧。</p>
<div class="blog_h2"><span class="graybg">广播</span></div>
<p>广播地址分为四种，本节一一描述。</p>
<div class="blog_h3"><span class="graybg">受限的广播</span></div>
<p>受限的广播地址是255.255.255.255。该地址用于主机配置过程中IP数据报的目的地址，此时主机可能不知道所在网络的子网掩码，甚至IP都不知道。</p>
<p>在任何情况下，路由器都不转发目的地址为受限的广播地址的数据报，这样的数据报仅出现在本地网络中。</p>
<div class="blog_h3"><span class="graybg">指向网络的广播</span></div>
<p>指向网络的广播地址是主机号为全1的地址。例如：A类网络广播地址为netid.255. 255.255，其中netid为A类网络的网络号。</p>
<p>路由器可以转发指向网络的广播。</p>
<div class="blog_h3"><span class="graybg">指向子网的广播</span></div>
<p>指向子网的广播地址为主机号为全1且有特定子网号的地址。</p>
<p>作为子网直接广播地址的IP地址需要了解子网的掩码。例如，如果路由器收到发往128.1.2.255的数据报，当B类网络28.1的子网掩码为255.255.255.0时，该地址就是指向子网的广播地址；但如果该子网的掩码为255.255.254.0，该地址就不是指向子网的广播地址。</p>
<div class="blog_h3"><span class="graybg">指向所有子网的广播</span></div>
<p>指向所有子网的广播也需要了解目的网络的子网掩码，以便与指向网络的广播地址区分开。指向所有子网的广播地址的子网号及主机号为全1。例如，如果目的子网掩码为255.255.255.0，那么IP地址128.1.255.255是一个指向所有子网的广播地址。然而，如果网络没有划分子网，这就是一个指向网络的广播。</p>
<div class="blog_h2"><span class="graybg">多播</span></div>
<p>IP多播向多个目的地址传送数据，其主要应用场景包括交互式视频会议。</p>
<div class="blog_h3"><span class="graybg">多播组地址</span></div>
<p>D类IP地址用于组播，请格式如下：</p>
<p><img class="wp-image-5211 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/IP-D.png" alt="IP-D" width="529" height="61" /></p>
<p>与A、B、C三类地址不同，多播地址的28bit均用作多播组号，与主机号、子网号等概念无关。</p>
<p>用十进制表示，多播地址的范围是224.0.0.0 - 239.255.255.255。</p>
<p>能够接收发往一个特定多播组地址数据的主机集合称为主机组(host group)。其特征为：</p>
<ol>
<li>一个主机组可跨越多个网络</li>
<li>主机组中成员可随时加入或离开主机组</li>
<li>主机组中对主机的数量没有限制</li>
<li>不属于某一主机组的主机可以向该组发送信息</li>
</ol>
<p>某些多播地址被IANA确定为知名地址：</p>
<ol>
<li>224.0.0.1：子网内的所有系统组</li>
<li>224.0.0.2：子网内的所有路由器组</li>
<li>224.0.1.1：用作网络时间协议（NTP）</li>
<li>224.0.0.9：用作RIP-2</li>
</ol>
<div class="blog_h3"><span class="graybg">多播组地址到以太网地址的转换</span></div>
<p>IANA拥有一个以太网地址块（高位24bit为00:00:5e）：00:00:5e:00:00:00到00:00:5e:ff:ff:ff，其中1/2被分配为多播地址，并且限定多播地址首字节必须为01，所有，对应于IP多播的以太网地址范围是：01:00:5e:00:00:00到01:00:5e:7f:ff:ff。</p>
<p>这种地址分配将使以太网多播地址中的23bit与IP多播组号对应起来，通过将多播组号中的低位23bit映射到以太网地址中的低位23bit实现：</p>
<p><img class="wp-image-5212 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/ip-mac-multicast-mapping.png" alt="ip-mac-multicast-mapping" width="642" height="229" /></p>
<p>由于多播组号中的最高5 bit在映射过程中被忽略，因此每个以太网多播地址对应的多播组是不唯一的。设备驱动程序或IP层需要对数据报进行过滤。</p>
<p>单个物理网络的多播流程：</p>
<ol>
<li>多播进程将目的IP地址指明为多播地址</li>
<li>设备驱动程序将它转换为相应的以太网地址，并发送出去</li>
<li>接收进程加入多播组——通知IP层，需要接收某个多播组的数据报</li>
<li>当一个主机收到多播数据报时向属于那个多播组的每个进程均传送一个副本</li>
</ol>
<p>当把多播扩展到单个物理网络以外需要通过路由器转发多播数据时，情况比较复杂。需要使用IGMP（因特网组管理协议）来确定网络中属于确定多播组的任何一个主机。</p>
<div class="blog_h1"><span class="graybg">Internet组管理协议（IGMP）</span></div>
<p>IGMP用于支持跨网络的多播，IGMP被认为是IP层的一部分，使用IP数据报传输。与其他报文不同IGMP有固定的报文长度，没有可选数据，IGMP报文通过IP首部中协议字段值为2来指明。IGMP报文的格式如下：</p>
<p><img class="wp-image-5214 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/igmp-format.png" alt="igmp-format" width="598" height="124" /></p>
<ol>
<li>IGMP类型：1表示多播路由器发出的查询报文；2表示主机发出的报告报文</li>
<li>组地址：查询报文中置0；报告报文中设置为需加入的组地址</li>
</ol>
<div class="blog_h2"><span class="graybg">IGMP协议</span></div>
<div class="blog_h3"><span class="graybg">加入一个多播组</span></div>
<p>某个用户进程可以在主机的某个<span style="background-color: #c0c0c0;">网络接口</span>上加入<span style="background-color: #c0c0c0;">多播组（这里暗示可以通过一个网络接口+一个多播地址唯一确定多播组）</span>，某个网络接口上的多播组的成员是动态变化的，进程可能加入或者退出。一个进程可以在多个网络接口上加入某个多播组。</p>
<div class="blog_h3"><span class="graybg">IGMP报告和查询</span></div>
<p>多播路由器使用IGMP报文来记录与该路由器相连网络中组成员的变化情况，规则如下：</p>
<ol>
<li>当第一个进程加入一个组时，主机就推送一个IGMP报告。如果多个进程同时加入同一多播组，则只发送一个报告</li>
<li>进程离开一个组时，主机不发送IGMP报告，即使是最后一个进程</li>
<li>如果所有进程都离开组，那么主机不再应答后续的IGMP查询报文</li>
<li>多播路由器定时发送IGMP查询来了解是否还有任何主机包含有属于多播组的进程</li>
<li>主机通过发送IGMP报告来响应一个IGMP查询</li>
</ol>
<div class="blog_h1"><span class="graybg"><a id="dns"></a>域名系统（DNS）</span></div>
<p>DNS是一种用于TCP/IP应用程序的分布式数据库，提供：</p>
<ol>
<li>主机名与IP地址之间的转换</li>
<li>电子邮件选路信息</li>
</ol>
<p>对应应用程序来说，对DNS的访问通过地址解析器（resolver）完成，解析器不是系统内核的一部分，在Unix系统中，解析器对应的库函数为gethostbyname()和gethostbyaddr()，前者根据主机名返回IP地址，后者反之。</p>
<div class="blog_h2"><span class="graybg">DNS基础</span></div>
<p>DNS的名字空间和Unix的文件系统相似，呈现出一种树形结构：</p>
<p><img class="wp-image-5220 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/dns.png" alt="dns" width="637" height="476" /></p>
<p>该名字空间具有以下特点：</p>
<ol>
<li>根是一个未命名的节点</li>
<li>每个节点至多有63个字符</li>
<li>不区分大小写</li>
<li>任何一个节点的域名，就是把该节点与最高节点使用点号（.）连接起来</li>
</ol>
<p>以点号结尾的域名称为绝对域名或完全限定的域名（FQDN，Full-qualified Domain Name），例如blog.gmem.cc.。任何不以点号结束的域名是不完全的。</p>
<p>顶级域被分为三部分：</p>
<ol>
<li>arpa：用作地址到名字转换的特殊域</li>
<li>7个3字符长的普通域</li>
<li>若干2字符长的普通域，均为国家代码（国家域、地理域）</li>
</ol>
<p>一棵独立管理的DNS子树称为域（Zone），常见的例如gmem.cc这样的二级域。一旦接受授权机构的委派，来管理一个Zone，就必须为该Zone提供一台或者多台DNS 服务器。</p>
<p>一个DNS服务器至少管理一个Zone，反过来，一个Zone的管理者必须为其指定一个主DNS、一个辅助DNS。</p>
<p>如果像DNS发送一个查询请求，而该DNS的数据库中没有相应信息时，该DNS就同其它DNS服务器联系。一个DNS不一定知道域在哪个DNS，但是它必须知道如何与根服务器进行联系（知晓其IP）。</p>
<p>DNS一旦获取一条信息（IP-域名映射），则放入高速缓存，避免重复向其它DNS索取数据。</p>
<div class="blog_h2"><span class="graybg">DNS报文格式</span></div>
<p>DNS请求/应答报文由12字节的首部，4个可变长字段组成：</p>
<p><img class="wp-image-5224 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/dns-msg.png" alt="dns-msg" width="577" height="404" /></p>
<ol>
<li>标识字段由客户端设置，服务器返回之，用于匹配请求与应答</li>
<li>16bit的标志字段：<img class="wp-image-5225 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/dns-msg-header-flag.png" alt="dns-msg-header-flag" width="544" height="73" />
<ol>
<li>QR：0表示查询，1表示响应</li>
<li>opcode：通常值为0（标准查询），其他值为1（反向查询）和2（服务器状态请求）</li>
<li>AA：表示“授权回答”——该名字服务器是授权于该域的</li>
<li>TC：表示“可截断的”，使用U D P时，它表示当应答的总长度超过512字节时，只返回前512个字节</li>
<li>RD：表示“期望递归”。这个标志告诉名字服务器必须处理这个查询，也称为一个递归查询。如果该位为0，且被请求的名字服务器没有一个授权回答，它就返回一个能解答该查询的其他名字服务器列表，这称为迭代查询</li>
<li>RA：表示“可用递归”。如果名字服务器支持递归查询，则在响应中将该比特设置为1</li>
<li>随后3bit必须为0</li>
<li>rcode：返回码字段。通常的值为0（没有差错）和3（名字差错）。名字差错只有从一个授权名字服务器上返回，它表示在查询中指定的域名不存在</li>
</ol>
</li>
<li>问题数、资源记录数、授权资源记录数、额外资源记录数，这4个字段用于记录后续4个变长字段包含数据的个数
<ol>
<li>对于查询报文，问题数通常为1，其它为0</li>
<li>对于应答报文，回答数至少1，后续两项可以为0</li>
</ol>
</li>
<li>查询问题，格式如下：<img class=" wp-image-5226 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2012/03/dns-msg-question.png" alt="dns-msg-question" width="520" height="140" />
<ol>
<li>查询名：需要查找的名字，它是一个或多个标识符的序列，每个名字以0结束。例如gemini.tuc.noao.edu的表示如下：<img class="aligncenter  wp-image-5227" src="https://blog.gmem.cc/wp-content/uploads/2012/03/dns-msg-question-name.png" alt="dns-msg-question-name" width="493" height="98" /></li>
<li>查询类型：每个问题有一个查询类型，而每个响应（也称资源记录）也有一个类型。最常用的查询类型是A类型，表示期望获得查询名的IP地址。一个PTR查询则请求获得一个IP地址对应的域名。查询类型、类型如下表：<img class="aligncenter  wp-image-5228" src="https://blog.gmem.cc/wp-content/uploads/2012/03/dns-query-type.png" alt="dns-query-type" width="486" height="226" />
<ol>
<li>A：即地址（Address）记录，用来指定主机名（域名）对应的IP地址记录</li>
<li>AAAA：把主机名（域名）解析到IPv6地址</li>
<li>MX：即邮件交换（Mail Exchange）记录</li>
<li>CNAME：别名解析，可以把多个域名转到一个域名记录上</li>
<li>URL：网址转发</li>
<li>NS：指定域名由哪个DNS服务器来解析</li>
<li>PTR：用于DNS反查，即根据IP地址来查域名</li>
</ol>
</li>
<li>查询类：通常是1，指互联网地址</li>
</ol>
</li>
<li>资源记录（仅响应报文）：最后的三个字段，回答字段、授权字段和附加信息字段，均采用一种称为资源记录RR（Resource Record）的相同格式：<img class="aligncenter  wp-image-5230" src="https://blog.gmem.cc/wp-content/uploads/2012/03/rr.png" alt="rr" width="468" height="236" />
<ol>
<li>域名：RR中对应的名字，与查询名字段格式一致</li>
<li>类型：RR的类型代码，与查询类型值一致</li>
<li>类：通常是1，指互联网地址</li>
<li>生存时间：是客户程序保留该资源记录的秒数。RR通常的生存时间值为2天</li>
<li>资源数据长度：资源数据的数量。该数据的格式依赖于类型字段的值。对于类型1（A记录）资源数据是4字节的IP地址</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg"><a id="dns-rtnmsg"></a>DNS应答状态</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">返回消息</td>
<td style="width: 100px; text-align: center;">响应码<br />RCODE</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NOERROR</td>
<td>0</td>
<td>
<p>成功</p>
<p>也可能对应nodata响应，域名记录是存在的，但是对应请求的记录类型</p>
</td>
</tr>
<tr>
<td>FORMERR</td>
<td>1</td>
<td>DNS查询格式错误</td>
</tr>
<tr>
<td>SERVFAIL</td>
<td>2</td>
<td>服务器处理失败，没能完成请求处理</td>
</tr>
<tr>
<td>NXDOMAIN</td>
<td>3</td>
<td>域名不存在</td>
</tr>
<tr>
<td>NOTIMP</td>
<td>4</td>
<td>功能没有实现</td>
</tr>
<tr>
<td>REFUSED</td>
<td>5</td>
<td>服务器拒绝处理请求</td>
</tr>
<tr>
<td>YXDOMAIN</td>
<td>6</td>
<td>不应该存在的名字</td>
</tr>
<tr>
<td>XRRSET</td>
<td>7</td>
<td>RRset that should not exist, does exist</td>
</tr>
<tr>
<td>NOTAUTH</td>
<td>8</td>
<td>服务器不是Zone的权威</td>
</tr>
<tr>
<td>NOTZONE</td>
<td>9</td>
<td>名字不在Zone中</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">态主机配置协议（DHCP）</span></div>
<p>动态主机配置协议（Dynamic Host Configuration Protocol，DHCP），属于应用层协议，<span style="background-color: #c0c0c0;">前身是BOOTP协议</span>。它使用UDP协议工作，常用的2个端口：</p>
<ol>
<li>67（DHCP server）</li>
<li>68（DHCP client）</li>
</ol>
<p>DHCP<span style="background-color: #c0c0c0;">通常被用于局域网环境</span>，主要作用是<span style="background-color: #c0c0c0;">集中的管理、分配IP地址</span>，使Client<span style="background-color: #c0c0c0;">动态的获得IP地址、Gateway地址、DNS服务器地址等信息</span>，并能够提升地址的使用率。简单来说，DHCP就是一个不需要账号密码登录的、自动给内网机器分配IP地址等信息的协议。</p>
<div class="blog_h2"><span class="graybg">报文结构</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/02/dhcp-msg.png"><img class="wp-image-36417 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/02/dhcp-msg.png" alt="dhcp-msg" width="656" height="381" /></a>主要字段说明：</p>
<ol>
<li>Xid：随机生成的一段字符串，两个数据包拥有相同的xid说明他们属于同一次会话</li>
<li>Ciaddr：客户端会在发送请求时将自己的ip地址放在此处</li>
<li>Yiaddr：服务器会将想要分配给客户端的ip地址放在此处</li>
<li>Siaddr：一般来说是服务器的IP地址</li>
<li>Chaddr：客户端的mac地址</li>
<li>Giaddr：如果需要跨子网进行DHCP地址发放，则在此处填入经过的路由器的ip地址</li>
<li>Sname：服务器主域名</li>
<li>Options：可以自由添加的部分，用于存放客户端向服务器请求信息和服务器的应答信息</li>
</ol>
<div class="blog_h2"><span class="graybg">报文类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">报文类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">DHCP DISCOVER</td>
<td>
<p>客户端开始DHCP过程发送的包，是DHCP协议的开始</p>
<p>由于客户端不知道DHCP服务器在哪，因此这个报文以<span style="background-color: #c0c0c0;">广播的方式发送</span></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td class="blog_h3">DHCP OFFER</td>
<td>服务器接收到DHCP DISCOVER之后做出的响应，它包括了给予客户端的IP（yiaddr）、客户端的MAC地址、租约过期时间、服务器的识别符以及其他信息</td>
</tr>
<tr>
<td class="blog_h3">DHCP REQUEST</td>
<td>客户端对于服务器发出的DHCP OFFER所做出的响应。在续约租期的时候同样会使用</td>
</tr>
<tr>
<td class="blog_h3">DHCP ACK</td>
<td>服务器在接收到客户端发来的DHCP REQUEST之后发出的成功确认的报文。在建立连接的时候，<span style="background-color: #c0c0c0;">客户端在接收到这个报文之后才会确认分配给它的IP和其他信息可以被允许使用</span></td>
</tr>
<tr>
<td class="blog_h3">DHCP NAK</td>
<td>DHCP ACK的相反的报文，表示服务器拒绝了客户端的请求</td>
</tr>
<tr>
<td class="blog_h3">DHCP RELEASE</td>
<td>一般出现在客户端关机、下线等状况。这个报文将会使DHCP服务器释放发出此报文的客户端的IP地址</td>
</tr>
<tr>
<td class="blog_h3">DHCP INFORM</td>
<td>客户端发出的向服务器请求一些信息的报文</td>
</tr>
<tr>
<td class="blog_h3">DHCP DECLINE</td>
<td>当客户端发现服务器分配的IP地址无法使用（如IP地址冲突时），将发出此报文，通知服务器</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">交互流程</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/02/dhcp.png"><img class=" wp-image-36415 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/02/dhcp.png" alt="dhcp" width="463" height="457" /></a></p>
<p>&nbsp;</p>
<p>当一台没有IP地址的主机接入到了网络中时，如果设置的时DHCP自动获取地址，就会向网络中发送DHCP请求获得IP地址，<span style="background-color: #c0c0c0;">源IP为0.0.0.0，目的IP为255.255.255.255，源MAC地址正常，目的Mac地址为全F的广播包</span>。</p>
<p>当客户端为 windows主机时，网卡配置为 DHCP获得地址时，就开始向网络中请求地址，先发送一个广播包，等待 1 秒之后，如果没有服务器应答，就发送第二个广播包，如果 9 秒后没有收到应答，则发送第三个广播包，等 13 秒，还没有应答，最后再发送一个包，等待16 秒后，最终在四个广播包没有应答的情况下，<span style="background-color: #c0c0c0;">默认是放弃请求，为网卡自动配上一个私有 IP 地址，地址段为169.254.0.0/16</span>，网络状态为“受限制或无连接”。</p>
<p>169.254.0.0/16为Link-local地址，不通过路由器转发，因此网关为0.0.0.0。该地址的生成逻辑：</p>
<ol>
<li>需要将自已的IP和掩码网关都设为0，并随机生成一个IP，地址范围169.254.1.0到169.254.254.255，RFC3927中<span style="background-color: #c0c0c0;">建议使用MAC来生成IP地址</span>，这样可以使每个设备生成的IP都不一样，将设备同时探测同一个IP的可能性降到了最低</li>
<li>需要进行ARP探测，如果地址已经被占用，则重新选择</li>
</ol>
<p>Link-local地址可以确保在DHCP不可用的情况下，设备不至于连接不上。</p>
<div class="blog_h1"><span class="graybg">传输控制协议</span></div>
<div class="blog_h2"><span class="graybg">TCP简介</span></div>
<p>与UDP不同，TCP提供一种面向连接的、可靠的字节流服务。面向连接意味着两个使用TCP的应用（通常是一个客户和一个服务器）在彼此交换数据之前必须先建立一个TCP连接。在一个TCP连接中，仅有两方进行彼此通信，多播、广播不适用于TCP。</p>
<p>TCP通过下列方式来提供可靠性：</p>
<ol>
<li>应用数据被分割成<span style="background-color: #c0c0c0;">TCP认为最适合发送的数据块（而UDP中，用户产生的数据报的长度不变）</span>。由TCP传递给IP的协议信息单位（PDU）称为报文段或段（segment）。<span style="background-color: #c0c0c0;">MSS影响最大报文段的大小，相应的影响IP数据报的大小，它可用于确保避免分片</span></li>
<li>当TCP发出一个段后，它启动一个定时器，等待目的端确认收到这个报文段。如果目的端不能及时收到一个确认，将重发这个报文段</li>
<li>当收到发自TCP连接另一端的数据，它将发送一个确认。由于确认报文很小，因此通常在数据分组中“捎带”发送，此捎带发送的方式由TCP实现的延迟确认算法决定，延迟确认算法会在一个特定的窗口时间（例如100-200ms）内，将确认报文放在缓冲区，以寻找能捎带之的分组，如果没有找到，则会单独发送确认报文</li>
<li>TCP将保持它首部和数据的检验和。这是一个端到端的检验和，目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错， TCP将丢弃这个报文段和不确认收到此报文段（等待发送端超时并重发）</li>
<li>由于IP数据报的到达可能会失序，因此其上的TCP报文段的到达也可能会失序。如果必要， TCP将对收到的数据进行重新排序，将收到的数据以正确的顺序交给应用层</li>
<li>由于IP数据报会发生重复， TCP的接收端必须丢弃重复的数据</li>
<li>TCP可以提供流量控制：连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出</li>
</ol>
<p>TCP提供“面向字节流的服务”：两个应用程序通过TCP连接交换8 bit字节构成的字节流，TCP不在字节流中插入记录标识符。如果发送端分三次分别传递30、40、10字节，接收端将不知道发送端是这样三次发送的，它可能以每次20字节的方式分4次接收。TCP对字节流的内容不进行任何解释。</p>
<div class="blog_h2"><span class="graybg">TCP首部</span></div>
<p>TCP数据被封装在一个IP数据报中：</p>
<p><img class="aligncenter  wp-image-5242" src="https://blog.gmem.cc/wp-content/uploads/2012/03/tcp-structure.png" alt="tcp-structure" width="548" height="182" /></p>
<p>其中TCP首部的格式如下：</p>
<p><img class="aligncenter  wp-image-5243" src="https://blog.gmem.cc/wp-content/uploads/2012/03/tcp-head.png" alt="tcp-head" width="526" height="347" /></p>
<ol>
<li>源端口号、目的端口号：用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。有时<span style="background-color: #c0c0c0;">一个IP地址与一个端口号被称为插口</span>（Socket），插口对（Socket Pair）可以唯一确定每个TCP连接的双方</li>
<li>序号：用来标识从TCP发端向TCP收端发送的数据字节流。TCP使用序号来为传输的每个字节进行计数，到达2^32-1后从0重新计数。当新建一个连接时，SYN标志变为1。序号字段为主机为本次连接选择的ISN（初始序号），主机要发送的第一个字节序号标为ISN + 1（SYN占用了一个序号），发送一个段，其第一个数据字节是本次会话的第100字节时，则序号字段为ISN + 100</li>
<li>确认序号：应当是上次已成功收到数据字节序号加1（如果上一个段1-1024字节已经收到，下一个段包含字节序号2049-3072，接收端不能确认这一新报文段，而只能发回确认序号为1025的ACK；此外接收方<span style="background-color: #c0c0c0;">不能对一个接收到的报文段进行否认</span>，例如如果对收到的1025-2048报文段校验和失败，接收方能做的只是发回确认序号为1025的ACK）。只有<span style="background-color: #c0c0c0;">ACK标志为1时，确认序号字段才有意义</span>。注意，发送ACK无需额外代价，它与确认序号一样，是TCP头的组成部分，一旦TCP连接成功建立，ACK总是置1。TCP为应用层提供全双工服务，数据能在两个方向上<span style="background-color: #c0c0c0;">独立地进行传输，因此，连接的每一端必须独立维护每个方向上的序号</span></li>
<li>首部长度：首部包含的32bit的个数，只有4位，因此首部最多60字节。如果报文段没有选项字段，则头部应该是20字节</li>
<li>TCP首部包含6个比特位标记：
<ol>
<li>URG　紧急指针（ urgent pointer）有效</li>
<li>ACK　确认序号有效</li>
<li>PSH　接收方应该尽快将这个报文段交给应用层</li>
<li>RST　重置连接</li>
<li>SYN　同步序号，用来发起一个连接</li>
<li>FIN　发端已经完成发送任务</li>
</ol>
</li>
<li>窗口大小：连接的两端分别声明一个窗口大小，用于进行TCP的流量控制。最大65535字节</li>
<li>校验和：覆盖整个TCP报文段：TCP首部和TCP数据。由发端计算和存储，并由收端进行验证</li>
<li>紧急指针：URG为1时有效。紧急指针是一个正的偏移量，和序号字段中的值相加表示紧急数据最后一个字节的序号。用于发送端向另一端发送紧急数据</li>
<li>选项，包括以下常用字段：
<ol>
<li>最长报文段大小（MSS）：每个连接方通常都在通信的第一个报文段（为建立连接而设置SYN标志的那个段）中指明这个选项</li>
</ol>
</li>
<li>数据：数据部分是可选的。到在一个连接建立和一个连接终止时，双方交换的报文段仅有TCP首部</li>
</ol>
<div class="blog_h2"><span class="graybg">TCP性能影响因素</span></div>
<div class="blog_h3"><span class="graybg">延迟确认</span></div>
<p>如本章前面所描述，“捎带”发送确认分组，可能带来性能问题。当出现<span style="background-color: #c0c0c0;">双峰（bimodal）</span>特征的通信时，需要数据分组用来捎带确认分组时，可能<span style="background-color: #c0c0c0;">没有数据分组可用</span>，从而导致窗口时间超时后单独发送确认分组。这种场景下，可以调整或者禁止延迟确认算法。</p>
<div class="blog_h3"><span class="graybg">TCP慢启动</span></div>
<p>所谓慢启动，是指随着TCP连接使用时间的增长，传输速度会提高。慢启动可以防止因特网突然过载和拥堵，但是可能导致<span style="background-color: #c0c0c0;">新连接的性能相对已经交换过一定数量的、“已调谐”的连接较差</span>。</p>
<p>慢启动特征限制了TCP端点在任意时刻可以传输的分组数量，简单的说，发送端的每一个分组被接收到，它就获得额外传送两个分组的权限，当这两个分组被确认后，就获得四个分组，这种逐渐可以发送更多分组的方式称为“<span style="background-color: #c0c0c0;">打开拥塞窗口</span>”。</p>
<div class="blog_h3"><span class="graybg">Nagle算法与TCP_NODELAY</span></div>
<p>每个TCP段至少包含40字节的头部，如果大量TCP报文只包含极少的数据，那么网络的性能就会严重下降。</p>
<p>Nagle算法试图在发送一个分组前，<span style="background-color: #c0c0c0;">将大量的TCP数据绑定在一起</span>，以提高网络效率。该算法鼓励使用全尺寸（MTU）的TCP段，只有所有其它分组都被确认后，Nagle才允许发送非全尺寸的分组。</p>
<p>Nagle与延迟确认算法存在交互问题：前者要求分组确认到达再发送分全尺寸数据；后者则可能导致确认延迟发送100-200ms。</p>
<p>通过设置TCP_NODELAY，可以禁用Nagle算法，但是应当保证向TCP写入大块数据，避免产生大量小的分组。</p>
<div class="blog_h3"><span class="graybg">TIME_WAIT累积与端口耗尽</span></div>
<p>当TCP端点关闭连接后，内存中会维护一个小的控制块，时间为报文段最大生存期的两倍（2MSL，通常2分钟），用于确保<span style="background-color: #c0c0c0;">该时间段内不会创建相同地址和端口号的连接</span>，该机制用于防止新连接接收到旧连接的报文段。</p>
<p>由于现代高速路由器的应用，某些操作系统将2MSL设置为较小的值。</p>
<div class="blog_h1"><span class="graybg">TCP连接的建立与终止</span></div>
<p>TCP是一个面向连接的协议。无论哪一方向另一方发送数据之前，都必须先在双方之间 建立一条连接。</p>
<div class="blog_h2"><span class="graybg">连接的建立与终止</span></div>
<p>这里使用一个telnet命令的例子来说明TCP连接的建立和终止过程：</p>
<pre class="crayon-plain-tag">#命令和输出序列：使用telnet登陆服务，并立刻退出。
telnet remotehost service 
Trying 140.252.13.35 ... 
Connected to remotehost. 
telnet&gt; quit
Connection closed

tcpdump -S  #总是显示完整序号而不是偏移量
#tcpdump输出，包含7个报文段，这7个TCP报文段仅包含TCP首部。没有任何数据
#对于每一个TCP报文段，tcpdump输出格式为： 源 &gt; 目的: 标志
#分组序号字段：1415531521，报文段中数据字节数为0。只有在：报文至少包含一个字节，或者SYN、FIN、RST之一置1时，分组序号显示在tcpdump结果中
#win 4096表示发送端通告的窗口大小；表示发送端的最大报文段长度选项（发送端将不接收超过这个长度的报文段）
1  0.0                  localhost.1037    &gt; remotehost.service:  S  1415531521:1415531521(0) win 4096 
#ack后为确认序号，只有在首部ACK字段被置1时显示，注意ack可以单独发送，也可以附在其它报文段上一起发送
2  0.002402 (0.0024)    remotehost.service &gt; localhost.1037:     S  1823083521:1823083521(0) ack 1415531522 win 4096 
3  0.007224 (0.0048)    localhost.1037    &gt; remotehost.service:  .  ack 1823083522 win 4096
#上面是打开连接的3次握手
#下面是关闭连接的4次握手
4  4.155441 (4.1482)    localhost.1037    &gt; remotehost.service:  F  1415531522:1415531522(0) ack 1823083522 win 4096
5  4.156747 (0.0013)    remotehost.service &gt; localhost.1037:     .  ack 1415531523 win 4096
6  4.158144 (0.0014)    remotehost.service &gt; localhost.1037:     F  1823083522:1823083522(0) ack 1415531523 win 4096
7  4.180662 (0.0225)    localhost.1037    &gt; remotehost.service:  .  ack 1823083523 win 4096</pre>
<p>在tcpdump输出中，使用一位字母来代表TCP首部的标志比特（TCP首部中的其他两个标志比特：ACK 、URG将作特殊显示）：</p>
<p><img class="aligncenter  wp-image-5255" src="https://blog.gmem.cc/wp-content/uploads/2012/03/flag-in-tcpdump.png" alt="flag-in-tcpdump" width="421" height="128" /></p>
<div class="blog_h3"><span class="graybg">建立连接（三次握手，three-way handshake）</span></div>
<p>上面例子建立连接的过程分为三步：</p>
<ol>
<li>请求端（Client）发送一个SYN段指明客户打算连接的服务器的端口以及初始序号（ISN）。对应序号为1的报文段</li>
<li>服务器发回包含服务器的初始序号的SYN报文段作为应答。同时，将确认序号设置为客户的ISN + 1以对客户的SYN报文段进行确认。注意SYN占用一个序号。对应序号为2的报文段</li>
<li>请求端将确认序号设置为服务器的ISN+1以对服务器的SYN报文段进行确认。对应序号为3的报文段</li>
</ol>
<p>通过以上三个步骤——SYN、SYN+ACK、ACK，TCP连接正式建立，这称为三次握手。在这个例子里面，发送第一个S YN的一端将执行主动打开（Active Open），接收这个SYN并发回下一个SYN的另一端执行被动打开（ Passive Open），双方都主动打开也是支持的。</p>
<div class="blog_h3"><span class="graybg">终止连接（四次握手）</span></div>
<p>关闭连接需要四次握手——这是由于TCP连接具有半关闭（Half-close）的特性：TCP连接是全双工的（即数据在两个方向上能同时传递），因此必须在每个方向单独地进行关闭。当一方完成它的数据发送任务后就能发送一个FIN（通常由应用层执行）来终止这个方向连接，对端收到该FIN后，应当立即通知应用层。</p>
<p>收到一个FIN，只意味着对端到本端的数据流动结束，本端依旧可以向对端发送数据，但是现实应用中使用这种半关闭特性的场景是较少见的。</p>
<p>在上面的例子中，首先进行关闭的一方（即发送第一个FIN）将执行主动关闭，而另一方（收到这个FIN）执行被动关闭。双方都主动关闭也是支持的。</p>
<p>连接是由客户端发起的，通常，连接的关闭也应该由客户端发起。</p>
<div class="blog_h2"><span class="graybg">连接建立的超时</span></div>
<p>如果无法连接到服务器，客户端会进行重试，tcpdump输出类似下面：</p>
<pre class="crayon-plain-tag">#尝试了3次连接，第二次间隔第一次为5.8秒，第三次间隔第二次为24秒
#大多数伯克利系统将建立一个新连接的最长时间限制为75秒
#tos为IP数据报的服务类型字段
1    0.0                 localhost.1024 &gt;    remotehost.service：   S    291008001:291008001(0) win 4096  [tos 0x10]
2    5.814797 (5.8148)   localhost.1024 &gt;    remotehost.service:    S    291008001:291008001(0) win 4096  [tos 0x10]
3    29.815436(24.0006)  localhost.1024 &gt;    remotehost.service:    S    291008001:291008001(0) win 4096  [tos 0x10]</pre>
<div class="blog_h2"><span class="graybg">最大报文段长度（MSS）</span></div>
<p>MSS表示传往对端的最大数据块的长度，当一个连接建立时，连接的双方都要通告各自的MSS。如果一方不接受对方的MSS，则MSS置为默认值536。</p>
<p>一般来说，在没有分段发送的情况下，MSS越大越好。TCP模块发起/接受连接时，能自动把MSS设置为MTU-IP首部长度-TCP首部长度，对于以太网，结果是1460字节；对于IEEE 802.3，结果是1452字节。</p>
<div class="blog_h2"><span class="graybg">TCP的半关闭</span></div>
<p>半关闭需要API提供支持，对于Unix，调用shutdown而不是close，并且传递第二个参数为1，则进行半关闭。所谓<span style="background-color: #c0c0c0;">半关闭，是指可以关闭TCP的输入、输出通道中的一个</span>；相对的完全关闭则是输入输出通道一起关闭。关闭输出通道总是很安全的，对端会在从缓冲区中读取所有数据后收到一个通知，表示流已经结束；关闭输入通道则比较危险，除非知道对端不会发送其它数据了。</p>
<p>半关闭的流程示例如下：</p>
<p><img class="aligncenter  wp-image-5272" src="https://blog.gmem.cc/wp-content/uploads/2012/03/half-close.png" alt="half-close" width="440" height="398" /></p>
<ol>
<li>客户端发送FIN（调用shutdown），服务器确认</li>
<li>服务器进行发送数据给客户端，客户端读取并确认</li>
<li>服务器发送FIN（调用close），客户端确认，连接彻底关闭</li>
</ol>
<div class="blog_h2"><span class="graybg">TCP的状态变迁图</span></div>
<p><img class="aligncenter  wp-image-5273" src="https://blog.gmem.cc/wp-content/uploads/2012/03/tcp-flow.png" alt="tcp-flow" width="514" height="654" /></p>
<p>查看该变迁图时需要注意：</p>
<ol>
<li>粗的实线箭头表示正常的客户端状态变迁，用粗的虚线箭头表示正常的服务器状态变迁</li>
<li>两个导致进入ESTABLISHED状态的变迁对应打开一个连接；两个导致从ESTABLISHED状态离开的变迁对应关闭一个连接</li>
<li>ESTABLISHED状态是连接双方能够进行双向数据传递的状态</li>
<li>左下角虚线框（主动关闭），包含4个状态</li>
<li>右下角虚线框（被动关闭），包含2个状态</li>
<li>图中11个状态的名称，与netstat命令显示的状态名一致</li>
<li>CLOSED不是一个真实的状态，作为状态图假想的起终点</li>
<li>从LISTEN到SYN_SENT的变迁是正确的，但是某些TCP实现不支持</li>
<li> 只有SYN_RCVD（图中SYN收到）是从LISTEN进入，而不是SYN_SENT进入，SYN_RCVD到LISTEN的状态变迁才是有效的。这意味着：执行被动关闭进入LISTEN，收到一个SYN、发送ACK+SYN进入SYN_RCVD，然后收到一个RST而不是ACK,便又回到LISTEN状态并等待另一个连接请求到来</li>
</ol>
<p>下图显示了正常情况下，TCP连接的建立与终止过程中，客户与服务器所经历的不同状态：</p>
<p><img class="aligncenter  wp-image-5277" src="https://blog.gmem.cc/wp-content/uploads/2012/03/normal-tcp-status-trans.png" alt="normal-tcp-status-trans" width="460" height="463" /></p>
<div class="blog_h3"><span class="graybg"><a id="2msl"></a>2MSL等待状态</span></div>
<p>TIME_WAIT又称2MSL等待状态。</p>
<p>每个TCP实现必须选择一个报文段最大生存时间（Maximum Segment Lifetime，MSL）—— 是任何报文段被丢弃前在网络内的最长时间。此外，由于IP数据报具有TTL，因此在网络上存在是有限制的。后者是基于跳数，而不是定时器。MSL的值通常是 30秒、1分钟或者2分钟。</p>
<p>对一个具体实现所给定的MSL值，处理的原则是：<span style="background-color: #c0c0c0;">当TCP执行一个主动关闭，并发回最后一个ACK，该连接必须在TIME_WAIT状态停留的时间为2倍的MSL</span>，这样可让TCP再次发送最后的ACK以防这个ACK丢失（另一端超时并重发最后的FIN）。</p>
<p>2MSL等待的结果是，2MSL期间定义连接的Socket（即：唯一标识Socket的四元组）不能再被使用。但是某些实现允许重用处于2MSL状态的端口（指定：SO_REUSEADDR）。</p>
<p>服务器通常是被动关闭，不会进入TIME_WAIT状态。</p>
<div class="blog_h3"><span class="graybg">平静时间</span></div>
<p>对于来自某个连接的较早替身（相同端口对的上一个连接）的迟到报文段， 2MSL等待可防止将它解释成使用相同端口对的新连接的一部分。但是如果主机出现故障，则其会在MSL秒内重启，因而可能错误的接收替身报文段。</p>
<p>为了防止上述情况，RFC 793规定，TCP在启动后MSL秒内不接受任何连接，这段时间就是所谓的平静时间。</p>
<p>很少有实现遵守该规则。</p>
<div class="blog_h3"><span class="graybg">FIN_WAIT_2状态</span></div>
<p>在FIN_WAIT_2状态，本端已经发送FIN，对端已经ACK。除非进行半关闭，否则对端的应用层已经意识到需要进行关闭，并向本端发送FIN来关闭另一方向的连接，只有对端完成这个关闭，本端才从FIN_WAIT_2进入TIME_WAIT状态。</p>
<p>这也意味着，本端可能一直处于FIN_WAIT_2，对端则一直处于CLOSE_WAIT，为了防止处于FIN_WAIT_2的无限等待，TCP实现中使用定时器进行处理。</p>
<div class="blog_h2"><span class="graybg">复位报文段</span></div>
<p>TCP首部的RST比特用于“复位”，无论何时，<span style="background-color: #c0c0c0;">只要向TCP连接发送的报文段出现错误</span>，均会发出一个复位报文段。</p>
<div class="blog_h3"><span class="graybg">到不存在的端口的连接请求</span></div>
<p>产生复位的一个常见原因是目标端口没有进程在监听。对于UDP，没有监听会导致一个ICMP端口不可达消息；对于TCP，则会产生复位（重置）。</p>
<p>下面是一个示例与响应的tcpdump输出：</p>
<pre class="crayon-plain-tag">#远程登陆一个没有使用的端口
telnet remotehost 20000
#tcpdump输出
1 0.0                 localhost.1087 &gt; remotehost.20000: S 297416193:297416193(0) win 4096  [tos 0x10]
#复位，由于ACK在到达远程服务器的报文中没有置1，所以复位报文段中的序号被置为0
2 0.003771 (0.0038) remotehost.20000 &gt; localhost.1087:   R 0:0(0) ack 297416194 win 0</pre>
<div class="blog_h3"><span class="graybg">异常终止一个连接</span></div>
<p>以FIN方式来终止连接是常规的方式，有时称为有序释放（Orderly Release）。</p>
<p>可以直接发送一个复位报文段，而不是FIN来中途释放一个连接有时称这为异常释放（abortive release）。<span style="background-color: #c0c0c0;">需要注意的是RST报文段不会导致对端产生任何响应，对端收到RST的后将立即终止该连接</span>，并通知应用层连接复位（一般提示：Connection reset by peer）</p>
<p>异常释放对于应用来说，有以下特点：</p>
<ol>
<li>丢弃任何待发数据并立即发送复位报文段</li>
<li>RST的接收方会区分另一端执行的是异常关闭还是正常关闭</li>
</ol>
<p>Socket API通过SO_LINGER选项提供这种异常释放的能力。</p>
<div class="blog_h3"><span class="graybg">检测半打开连接</span></div>
<p>如果本端已经正常/异常终止连接，而对端却不知道，这种连接叫做半打开的（Half-Open），任何一端主机出现故障（例如突然断电），均会导致半打开出现。只要不打算在半打开连接上传输数据，仍处于连接状态的一方就<span style="background-color: #c0c0c0;">不会检测另一方已经出现异常</span>。</p>
<p>用TCP的keepalive选项能使TCP的一端发现另一端已经消失。</p>
<div class="blog_h3"><span class="graybg">服务器立即ACK+RST</span></div>
<p>服务器可能在Ack客户端的SYN请求时立即RST，可能是服务器没有资源服务请求</p>
<div class="blog_h3"><span class="graybg">报文错误</span></div>
<p>如果接收到无法处理的报文，也会立即发送RST，例如接收到错误的SLE、SRE字段后。</p>
<div class="blog_h2"><span class="graybg">TCP选项</span></div>
<p>TCP首部可以包含选项部分。如下图所示：</p>
<p><img class="aligncenter  wp-image-5284" src="https://blog.gmem.cc/wp-content/uploads/2012/03/tcp-options.png" alt="tcp-options" width="608" height="432" /></p>
<ol>
<li>每个选项的开始是1字节kind字段，说明选项的类型</li>
<li>kind字段为0和1的选项仅占1个字节。其他的选项在kind字节后还有len字节。len用于表示选项的总长度，包括kind、len占用的2字节</li>
</ol>
<div class="blog_h1"><span class="graybg">TCP的超时、重传、坚持、保活</span></div>
<p>通过确认机制，TCP提供了可靠的传输层，但是数据和确认都有可能会丢失。TCP通过在发送时设置一个定时器来解决这种问题。如果当定时器超时时还没有收到确认，它就重传该数据。超时和重传的策略对于TCP实现很关键。</p>
<p>对每个连接，TCP管理4个不同的定时器：</p>
<ol>
<li>重传定时器：确定多久对方没ACK，需要进行重传</li>
<li>坚持(Persist )定时器：使窗口大小信息保持不断流动，即使另一端关闭了其接收窗口</li>
<li>保活( Keepalive )定时器：检测到一个空闲连接的另一端何时崩溃或重启</li>
<li>2MSL定时器：测量一个连接处于TIME_WAIT状态的时间</li>
</ol>
<div class="blog_h2"><span class="graybg">超时与重传</span></div>
<p>下面是通过拔掉网线导致的超时重传的例子：</p>
<pre class="crayon-plain-tag">localhost % telnet remotehost service 
Trying 140.252.13.34... 
Connected to remotehost. 
Escape character is '^]'.
hello, world                          #正常发送本行
and hi                                #在发送本行前，拔掉网线
Connection closed by foreign host.    #9分钟后本端TCP放弃
#正常的TCP连接建立过程
1    0.0                       localhost.1029    &gt;    remotehost.service:   S    1747921409:1747921409(0)  win 4096             
2    0.004811      (0.0048)    svx4.discard &gt; localhost.1029:               S    3416685569:3416685569(0)  ack 1747921410 win 4096 
3    0.006441      (0.0016)    localhost.1029    &gt;    remotehost.service:   .    ack 1 win 4096         
#hello,world的传输与确认       
4    6.102290      (6.0958)    localhost.1029    &gt;    remotehost.service:   P    1:15(14}    ack    1    win    4096
5    6.259410      (0.1571)    remotehost.service &gt; localhost.1029:         .                ack    15   win    4096
#and hi的传输，以及12次重传过程                
6    24.480158     (18.2207)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
#注意重传时差，去整后分别是1、3、6、12、24、48和多个64秒，这种指数级的延迟模式称为：指数退避（exponential backoff）
7    25.493733     (1.0136)    localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
8    28.493795     (3.0001)    localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
9    34.493971     (6.0002)    localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
10   46.484427     (11.9905)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
11   70.485105     (24.0007)   localhost.1029    &gt;    remotehost.service：  P    15:23(8)    ack    1    win    4096
12   118.486408    (48.0013)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
13   182.488164    (64.0018)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
14   246.489921    (64.0018)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
15   310.491678    (64.0018)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
16   374.493431    (64.0018)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
17   438.495196    (64.0018)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
18   502.486941    (63.9917)   localhost.1029    &gt;    remotehost.service:   P    15:23(8)    ack    1    win    4096
#发送端TCP最终放弃并发送一个复位信号的过程
19   566.488478    (64.0015)   localhost*1029    &gt;    remotehost.service:   R    23:23(0)    ack    1    win    4096</pre>
<p>TCP超时与重传中最重要的部分就是对一个给定连接的<span style="background-color: #c0c0c0;">往返时间（RTT）的测量</span>。由于路由器和网络流量均会变化，因此我们认为这个时间可能经常会发生变化，<span style="background-color: #c0c0c0;"> TCP应该跟踪这些变化并相应地改变其超时时间</span>。</p>
<p>当TCP超时并重传时，它不一定要重传同样的报文段。相反， TCP允许进行重新分组而发送一个较大的报文段，这将有助于提高性能（当然，这个较大的报文段<span style="background-color: #c0c0c0;">不能够超过接收方声明的MSS</span>）。在协议中这是允许的，因为TCP是使用字节序号而不是报文段序号来进行识别它所要发送的数据和进行确认。</p>
<div class="blog_h2"><span class="graybg">坚持定时器</span></div>
<p>TCP通过让接收方指明希望从发送方接收的数据字节数（即窗口大小）来进行流量控制。如果窗口大小为0将有效地阻止发送方传送数据，直到窗口变为非0为止。</p>
<p>如果接收方修改窗口为非0的报文没有被发送方收到，那么发送方将一直等待。为了避免这种情况，发送方使用一个坚持定时器来周期性地向接收方查询，以便发现窗口是否已增大，这种查询报文称为窗口探查( Window probe）。</p>
<div class="blog_h2"><span class="graybg">保活定时器</span></div>
<p>使用TCP可能遇到的一个神奇的情况是，没有任何数据流通过一个空闲的TCP连接，空闲时间可以是数小时、数天、数个星期或者数月，只要通信双方主机没有重启，则连接依然保持建立。</p>
<p>出现这种情况的原因是，应用层没有任何机制来探测非活动状态。尽管很多观点认为保活探测应该由应用层完成，但是这种探测可以由传输层本身进行，很多时候服务器希望知道客户主机是否崩溃并关机或者崩溃又重新启动。很多TCP实现提供的保活定时器可以提供这种能力（保活并不是TCP规范中的一部分）。</p>
<p>保活定时器的工作原理：</p>
<ol>
<li>具有一个记时器，默认情况下2小时没有收到对方的封包，则开始发送探测报文</li>
<li>探测报文每75s发送一次，如果连续10次对方都没有回应，则认为对方出现故障，关闭TCP连接</li>
<li>每当收到对方的封包，记时器都会复位</li>
</ol>
<p>Linux内核中和保活有关的参数：</p>
<pre class="crayon-plain-tag"># 记时器触发延迟
/proc/sys/net/ipv4/tcp_keepalive_time
# 保活封包发送间隔
/proc/sys/net/ipv4/tcp_keepalive_intvl
# 保活封包发送次数
/proc/sys/net/ipv4/tcp_keepalive_probes </pre>
<div class="blog_h2"><span class="graybg">2MSL定时器</span></div>
<p>在TCP连接终止期间使用。参考<a href="#2msl">2MSL等待状态</a>。</p>
<div class="blog_h1"><span class="graybg">传输层安全协定（TLS）</span></div>
<p>TLS是一个包装其它协议（例如HTTP）的协议，它在确保通信双方身份的前提下，保证数据被安全、完整的传输。在身份认证上，TLS依赖于CA发布的数字证书。</p>
<div class="blog_h1"><span class="graybg">简单网络管理协议（SNMP）</span></div>
<p>参考：<a href="/snmp-study-note">SNMP协议学习笔记</a></p>
<div class="blog_h1"><span class="graybg">超文本传送（转移）协议（HTTP）</span></div>
<p>参考：<a href="/http-study-note">HTTP协议学习笔记</a></p>
<div class="blog_h1"><span class="graybg">实时通信协议族</span></div>
<p>参考：<a href="/realtime-communications-protocols">实时通信协议族</a></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/tcp-ip-study-note">TCP/IP协议栈学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/tcp-ip-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
