<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>好记忆不如烂笔头</title>
  
  <subtitle>问题记录，学习笔记</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://github.com/fafucoder/"/>
  <updated>2024-03-18T13:57:45.931Z</updated>
  <id>https://github.com/fafucoder/</id>
  
  <author>
    <name>Dawn</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>kafka 基本原理</title>
    <link href="https://github.com/fafucoder/2024/03/18/kafka-basic/"/>
    <id>https://github.com/fafucoder/2024/03/18/kafka-basic/</id>
    <published>2024-03-18T04:46:06.000Z</published>
    <updated>2024-03-18T13:57:45.931Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>Kafka是一个分布式数据流平台，可以运行在单台服务器上，也可以在多台服务器上部署形成集群。它提供了发布和订阅功能，使用者可以发送数据到Kafka中，也可以从Kafka中读取数据（以便进行后续的处理）。Kafka具有<strong>高吞吐、低延迟、高容错、可水平扩展、支持流数据处理</strong>等特点。</p><h3 id="Kafka架构概念"><a href="#Kafka架构概念" class="headerlink" title="Kafka架构概念"></a>Kafka架构概念</h3><p>Kafka作为一个高度可扩展可容错的消息系统，一个典型的kafka集群中包含若干producer，若干broker，若干consumer，以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置，选举leader，以及在consumer group发生变化时进行rebalance。producer使用push模式将消息发布到broker，consumer使用pull模式从broker订阅并消费消息：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181639076.png" alt="kafka架构" title="">            </p><p>下面是kafka的一些专业术语：</p><ul><li>Broker：消息中间件处理结点，一个Kafka节点就是一个broker，多个broker可以组成一个Kafka集群。</li><li>Topic：一类消息，Kafka集群能够同时负责多个topic的分发。</li><li>Partition：topic物理上的分组，一个topic可以分为多个partition，每个partition是一个有序的队列。</li><li>Replication：每一个分区都有多个副本，副本的作用是做备胎。当主分区（Leader）故障的时候会选择一个备胎（Follower）上位，成为Leader。<strong>在kafka中默认副本的最大数量是10个，且副本的数量不能大于Broker的数量</strong>，follower和leader绝对是在不同的机器，同一机器对同一个分区也只可能存放一个副本（包括自己）。</li><li>Segment：partition物理上由多个segment组成。</li><li>offset：每个partition都由一系列有序的、不可变的消息组成，这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset，用于partition唯一标识一条消息。</li><li>Producer：负责发布消息到Kafka broker。</li><li>Consumer：消息消费者，向Kafka broker读取消息的客户端。</li><li>Consumer Group：每个Consumer属于一个特定的Consumer Group。</li><li>Leader：每个分区多个副本的“主”副本，生产者发送数据的对象，以及消费者消费数据的对象，都是 Leader。</li><li>Follower：每个分区多个副本的“从”副本，实时从 Leader 中同步数据，保持和 Leader 数据的同步。Leader 发生故障时，某个 Follower 还会成为新的 Leader。</li><li>ZooKeeper：Kafka 集群能够正常工作，需要依赖于 ZooKeeper，ZooKeeper 帮助 Kafka 存储和管理集群信息。</li></ul><p>Kafka实现了零拷贝原理来快速移动数据，避免了内核之间的切换。Kafka可以将数据记录分批发送，从生产者到文件系统（Kafka主题日志）到消费者，可以端到端的查看这些批次的数据。批处理能够进行更有效的数据压缩并减少I/O延迟，Kafka采取顺序写入磁盘的方式，避免了随机磁盘寻址的浪费，总结一下其实就是四个要点：</p><ul><li>顺序读写：因为硬盘是机械结构，每次读写都会寻址，写入，其中寻址是一个“机械动作”，它是最耗时的。所以硬盘最“讨厌”随机I/O，最喜欢顺序I/O。为了提高读写硬盘的速度，Kafka就是使用顺序I/O。</li><li>零拷贝：在Linux Kernal 2.2之后出现了一种叫做“零拷贝(zero-copy)”系统调用机制，就是跳过“用户缓冲区”的拷贝，建立一个磁盘空间和内存空间的直接映射，数据不再复制到“用户态缓冲区”系统上下文切换减少2次，可以提升一倍性能。</li><li>消息压缩：消息都是经过压缩传递、存储的，降低网络与磁盘的负担。</li><li>分批发送：批量处理是一种非常有效的提升系统吞吐量的方法，在Kafka内部，消息都是以“批”为单位处理的。</li></ul><h3 id="kafka-topic的基本组成"><a href="#kafka-topic的基本组成" class="headerlink" title="kafka topic的基本组成"></a>kafka topic的基本组成</h3><p>在kafka中，消息都是以 topic 进行分类的，生产者和消费者都是面向topic的，在 kafka 中，一个 topic 可以分为多个 partition，一个 partition可以分为多个segment, 每个 segment 对应两个文件：.index 和 .log 文件；topic 是逻辑上的概念，而 patition 是物理上的概念，每个 patition 对应一个 log 文件，而 log 文件中存储的就是 producer 生产的数据，patition 生产的数据会被不断的添加到 log 文件的末端，且每条数据都有自己的 offset。消费组中的每个消费者，都是实时记录自己消费到哪个 offset，以便出错恢复，从上次的位置继续消费。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181653360.png" alt="kafka topic组成" title="">            </p><h3 id="消息存储机制"><a href="#消息存储机制" class="headerlink" title="消息存储机制"></a>消息存储机制</h3><p>由于生产者生产的消息会不断追加到 log 文件末尾，为防止 log 文件过大导致数据定位效率低下，Kafka 采取了分片和索引机制，将每个 partition 分为多个 segment。每个 segment 对应两个文件——.index文件和 .log文件。这些文件位于一个文件夹下，该文件夹的命名规则为：topic名称+分区序号。</p><p>如下，我们创建一个只有一个分区一个副本的 topic：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bin&#x2F;kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic dawn</span><br></pre></td></tr></table></figure><p>然后可以在 kafka-logs 目录（server.properties 默认配置）下看到会有个名为 dawn-0 的文件夹。如果，starfish 这个 topic 有三个分区，则其对应的文件夹为 dawn-0，dawn-1，dawn-2。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181706158.png" alt="消息存储机制" title="">            </p><p>这些文件的含义如下：</p><table><thead><tr><th align="left">类别</th><th align="left">作用</th></tr></thead><tbody><tr><td align="left">.index</td><td align="left">偏移量索引文件，存储数据对应的偏移量</td></tr><tr><td align="left">.timestamp</td><td align="left">时间戳索引文件</td></tr><tr><td align="left">.log</td><td align="left">日志文件，存储生产者生产的数据</td></tr><tr><td align="left">.snaphot</td><td align="left">快照文件</td></tr><tr><td align="left">leader-epoch-checkpoint</td><td align="left">保存了每一任leader开始写入消息时的offset，会定时更新。follower被选为leader时会根据这个确定哪些消息可用</td></tr></tbody></table><p>index 和 log 文件以当前 segment 的第一条消息的 offset 命名。偏移量 offset 是一个 64 位的长整形数，固定是20 位数字，长度未达到，用 0 进行填补，索引文件和日志文件都由此作为文件名命名规则。</p><p>从上图可以看出，我们的偏移量是从 0 开始的，.index 和 .log 文件名称都为 00000000000000000000。</p><p>在server.properties 文件中会配置日志文件的最大值，当生产者生产数据量较多，一个 segment 存储不下触发分片时，在日志 topic 目录下会看到类似如下所示的文件：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">00000000000000000000.index </span><br><span class="line">00000000000000000000.log </span><br><span class="line">00000000000000170410.index </span><br><span class="line">00000000000000170410.log </span><br><span class="line">00000000000000239430.index </span><br><span class="line">00000000000000239430.log</span><br></pre></td></tr></table></figure><h3 id="Topic-副本机制"><a href="#Topic-副本机制" class="headerlink" title="Topic 副本机制"></a>Topic 副本机制</h3><p>在Kafka中，Topic的同一个 partition 可能会有多个 replication( 对应 server.properties 配置中的 default.replication.factor=N)。</p><p>没有 replication 的情况下，一旦 broker 宕机，其上所有 patition 的数据都不可被消费，同时 producer 也不能再将数据存于其上的 patition。比如一个有 3 台 Broker 的 Kafka 集群上的副本分布情况。主题 1 分区 0 的 3 个副本分散在 3 台 Broker 上，其他主题分区的副本也都散落在不同的 Broker 上，从而实现数据冗余。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181724240.png" alt="topic副本机制" title="">            </p><p>​    引入 replication 之后，同一个 partition 可能会有多个 replication，而这时需要在这些 replication 之间选出一 个 leader， producer 和 consumer 只与这个 leader 交互，其它 replication 作为 follower 从 leader 中复制数据。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181724105.png" alt="Leader选举机制" title="">            </p><h3 id="kafka-Producer写入流程"><a href="#kafka-Producer写入流程" class="headerlink" title="kafka Producer写入流程"></a>kafka Producer写入流程</h3><p>producer 写入消息流程如下：</p><ol><li>producer连接ZK集群，从 zookeeper 中拿到对应 topic 的 partition 信息和partition的Leader的相关信息</li><li>producer 连接leader 对应的broker, 并将消息发送给该 leader</li><li>leader 将消息写入本地 log</li><li>followers 从 leader pull 消息，写入本地 log 后向 leader 发送 ACK</li><li>leader 收到所有 ISR 中的 replication 的 ACK 后，增加 HW(high watermark，最后 commit 的 offset)并向 producer 发送 ACK</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181723683.png" alt="producer写入流程" title="">            </p><p>producer 采用推(push) 模式将消息发布到 broker，每条消息都被追加(append) 到分区(patition) 中，属于顺序写磁盘(顺序写磁盘效率比随机写内存要高，保障 kafka 吞吐率)。</p><h4 id="分区策略"><a href="#分区策略" class="headerlink" title="分区策略"></a>分区策略</h4><p>​        Kafka 的消息组织方式实际上是三级结构：主题 - 分区 - 消息。因此topic下的每条消息只会保存在某一个分区中，而不会在多个分区中被保存多份。分区提供了系统的负载均衡能力，能够实现系统的高伸缩性，不同的分区能够被放置到不同节点的机器上，而数据的读写操作也都是针对分区这个粒度而进行的，这样每个节点的机器都能独立地执行各自分区的读写请求处理。这样，当性能不足的时候可以通过添加新的节点机器来增加整体系统的吞吐量。</p><p>​        producer将消息发送到哪个分区又分区策略决定，kafka为我们提供了默认的分区策略，同时支持用户自定义分区策略，常见的分区策略有：</p><p>​    <strong>1. 轮询策略：</strong>Round-robin 策略，即顺序分配到每个分区。该策略总是能保证消息最大限度地被平均分配到所有分区上，故默认情况下它是最合理的分区策略，也是最常用的分区策略之一。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182042927.png" alt="round-robin分区策略" title="">            </p><p><strong>2. 随机策略-Randomness 策略：</strong>随机就是随意地将消息放置到任意一个分区上。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182044021.png" alt="random策略" title="">            </p><p><strong>3. 按消息键保序策略- Key ordering 策略：</strong>为每条消息定义消息键Key，相同 Key 的所有消息都进入到相同的分区里面。</p><p>数据分区投递的基本原则：</p><ol><li>如果指定的partition，那么直接进入该partition。</li><li>如果没有指定partition，但是指定了key，将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值</li><li>如果既没有指定partition，也没有指定key，使用轮询策略选择partition。</li></ol><h4 id="批量发送"><a href="#批量发送" class="headerlink" title="批量发送"></a>批量发送</h4><p>当生产者发送多个消息到同一个topic时，为了减少网络带来的开销，kafka会对消息进行批量发送；主要涉及3个参数如下，满足其一即会批量发送：</p><ul><li>batch-size ：超过收集的消息数量的最大量，默认16KB</li><li>buffer-memory ：超过收集的消息占用的最大内存 , 默认32M</li><li>linger.ms ：超过收集的时间的最大等待时长，单位：毫秒，默认为0ms, 即有消息就发送</li></ul><h4 id="数据可靠性保证"><a href="#数据可靠性保证" class="headerlink" title="数据可靠性保证"></a>数据可靠性保证</h4><p>为保证 producer 发送的数据，能可靠的发送到指定的 topic，topic 的每个 partition 收到 producer 数据后，都需要向 producer 发送 ack ( acknowledgement 确认收到)，如果 producer 收到 ack，就会进行下一轮的发送，否则重新发送数据。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182050435.png" alt="数据可靠性保证" title="">            </p><h5 id="数据同步策略"><a href="#数据同步策略" class="headerlink" title="数据同步策略"></a>数据同步策略</h5><p>Leader将消息写入到本地后，需要确保有 Follower 与 Leader 同步完成后才能发送 ACK，这样才能保证 Leader 挂掉之后，能在 Follower 中选举出新的 Leader 而不丢数据，Follower数据同步发送ACK主要有两种策略：</p><table><thead><tr><th align="left">方案</th><th align="left">优点</th><th align="left">缺点</th></tr></thead><tbody><tr><td align="left">半数以上完成同步，就发送ack</td><td align="left">延迟低</td><td align="left">选举新的 leader 时，容忍n台节点的故障，需要2n+1个副本</td></tr><tr><td align="left">全部完成同步，才发送ack</td><td align="left">选举新的 leader 时，容忍n台节点的故障，需要 n+1 个副本</td><td align="left">延迟高</td></tr></tbody></table><p>Kafka 选择了第二种方案，原因如下：</p><ul><li>同样为了容忍 n 台节点的故障，第一种方案需要的副本数相对较多，而 Kafka 的每个分区都有大量的数据，第一种方案会造成大量的数据冗余;</li><li>虽然第二种方案的网络延迟会比较高，但网络延迟对 Kafka 的影响较小。</li></ul><h5 id="LSR"><a href="#LSR" class="headerlink" title="LSR"></a>LSR</h5><p>采用第二种方案之后，会出现一个问题：leader 收到数据，所有 follower 都开始同步数据，但此时有一个 follower 故障导致不能与 leader 保持同步，此时leader 得一直等下去，直到它完成同步，才能发送 ack，这会造成消息无法确定。</p><p>为了解决此问题，leader 维护了一个动态的 in-sync replica set(ISR)，意为和 leader 保持同步的 follower 集合。当 ISR 中的follower 完成数据的同步之后，leader 就会给 follower 发送 ack。如果 follower 长时间未向 leader 同步数据，则该 follower 将会被踢出 ISR，该时间阈值由 replica.lag.time.max.ms 参数设定。leader 发生故障之后，就会从 ISR 中选举新的 leader。</p><h5 id="ack应答机制"><a href="#ack应答机制" class="headerlink" title="ack应答机制"></a>ack应答机制</h5><p>对于某些不太重要的数据，对数据的可靠性要求不是很高，能够容忍数据的少量丢失，所以没必要等 ISR 中的follower全部接收成功。所以Kafka为用户提供了三种可靠性级别，用户根据对可靠性和延迟的要求进行权衡</p><p><strong>0：</strong>producer 不等待 broker 的 ack，这一操作提供了一个最低的延迟，broker 一接收到还没有写入磁盘就已经返回，当 broker 故障时有可能丢失数据;</p><p><strong>1：</strong>producer 等待 broker 的 ack，partition 的 leader 落盘成功后返回 ack，如果在 follower 同步成功之前 leader 故障，那么将会丢失数据;</p><p><strong>-1(all)：</strong>producer 等待 broker 的 ack，partition 的 leader 和 follower 全部落盘成功后才返回 ack。但是 如果在 follower 同步完成后，broker 发送 ack 之前，leader 发生故障，那么就会造成数据重复。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182108033.png" alt="ack应答机制" title="">            </p><h5 id="故障处理"><a href="#故障处理" class="headerlink" title="故障处理"></a>故障处理</h5><p>​    由于我们并不能保证 Kafka 集群中每时每刻 follower 的长度都和 leader 一致(即数据同步是有时延的)，那么当leader 挂掉选举某个 follower 为新的 leader 的时候(原先挂掉的 leader 恢复了成为了 follower)，可能会出现leader 的数据比 follower 还少的情况。为了解决这种数据量不一致带来的混乱情况，Kafka 提出了以下概念：</p><ul><li>LEO(Log End Offset)：指的是每个副本最后一个offset;</li><li>HW(High Wather)：指的是消费者能见到的最大的 offset，ISR 队列中最小的 LEO。</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182111530.png" alt="LEO & HW" title="">            </p><p><strong>消费者和 leader 通信时，只能消费 HW 之前的数据，HW 之后的数据对消费者不可见</strong>，因此：</p><ul><li>当follower发生故障时：follower 发生故障后会被临时踢出 ISR，待该 follower 恢复后，follower 会读取本地磁盘记录的上次的 HW，并将 log 文件高于 HW 的部分截取掉，从 HW 开始向 leader 进行同步。等该 follower 的 LEO 大于等于该 Partition 的 HW，即 follower 追上 leader 之后，就可以重新加入 ISR 了。</li><li>当leader发生故障时：leader 发生故障之后，会从 ISR 中选出一个新的 leader，之后，为保证多个副本之间的数据一致性，其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉，然后从新的 leader 同步数据。</li></ul><p>所以数据一致性并不能保证数据不丢失或者不重复，这是由 ack 控制的。HW 规则只能保证副本之间的数据一致性!</p><h4 id="Exactly-Once"><a href="#Exactly-Once" class="headerlink" title="Exactly Once"></a>Exactly Once</h4><p>将服务器的 ACK 级别设置为 -1，可以保证 Producer 到 Server 之间不会丢失数据，即 At Least Once 语义。相对的，将服务器 ACK 级别设置为 0，可以保证生产者每条消息只会被发送一次，即 At Most Once语义。</p><p>At Least Once 可以保证数据不丢失，但是不能保证数据不重复。相对的，At Most Once 可以保证数据不重复，但是不能保证数据不丢失。但是，对于一些非常重要的信息，比如说交易数据，下游数据消费者要求数据既不重复也不丢失，即 Exactly Once 语义。在 0.11 版本以前的 Kafka，对此是无能为力的，只能保证数据不丢失，再在下游消费者对数据做全局去重。对于多个下游应用的情况，每个都需要单独做全局去重，这就对性能造成了很大的影响。</p><p>0.11 版本的 Kafka，引入了一项重大特性：幂等性。所谓的幂等性就是指 Producer 不论向 Server 发送多少次重复数据。Server 端都会只持久化一条，幂等性结合 At Least Once 语义，就构成了 Kafka 的 Exactily Once 语义，即：At Least Once + 幂等性 = Exactly Once</p><p>要启用幂等性，只需要将 Producer 的参数中 enable.idompotence 设置为 true 即可。Kafka 的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID，发往同一 Partition 的消息会附带 Sequence Number。而 Broker 端会对Sequence Number做校验，如果发现PID对应的Sequence Number重复，就会丢弃此消息</p><p>但是 PID 重启就会变化，同时不同的 Partition 也具有不同主键，所以幂等性无法保证跨分区会话的 Exactly Once。</p><h3 id="Kafka-Consumer消费过程"><a href="#Kafka-Consumer消费过程" class="headerlink" title="Kafka Consumer消费过程"></a>Kafka Consumer消费过程</h3><p>Consumer 采用 Pull（拉取）模式从 Broker 中读取数据。Pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。Pull 模式不足之处是，如果 Kafka没有数据，消费者可能会陷入循环中，一直返回空数据。</p><p>因为消费者从 Broker 主动拉取数据，需要维护一个长轮询，针对这一点， Kafka 的消费者在消费数据时会传入一个时长参数 timeout。如果当前没有数据可供消费，Consumer 会等待一段时间之后再返回，这段时长即为 timeout。</p><h4 id="消费者组"><a href="#消费者组" class="headerlink" title="消费者组"></a>消费者组</h4><p>消费者是以 consumer group 消费者组的方式工作，由一个或者多个消费者组成一个组， 共同消费一个 topic。每个分区在同一时间只能由 group 中的一个消费者读取，但是多个 group 可以同时消费这个 partition。</p><p>通过消费者组的模式，消费者可以通过水平扩展的方式同时读取大量的消息。另外，如果一个消费者失败了，那么其他的 group 成员会自动负载均衡读取之前失败的消费者读取的分区。</p><p>消费者组最为重要的一个功能是实现广播与单播的功能。一个消费者组可以确保其所订阅的 Topic 的每个分区只能被从属于该消费者组中的唯一一个消费者所消费；如果不同的消费者组订阅了同一个 Topic，那么这些消费者组之间是彼此独立的，不会受到相互的干扰。</p><p>如果我们希望一条消息可以被多个消费者所消费，那么可以将这些消费者放到不同的消费者组中，这实际上就是广播的效果；如果希望一条消息只能被一个消费者所消费，那么可以将这些消费者放到同一个消费者组中，这实际上就是单播的效果。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182133768.png" alt="consumer group" title="">            </p><h4 id="分区策略-1"><a href="#分区策略-1" class="headerlink" title="分区策略"></a>分区策略</h4><p>一个 consumer group 中有多个 consumer，一个 topic 有多个 partition，所以必然会涉及到 partition 的分配问题，即确定哪个 partition 由哪个 consumer 来消费。</p><p>Kafka 有两种分配策略，一是 RoundRobin，一是 Range。</p><p><strong>1. RoundRobin：</strong>RoundRobin 即轮询的意思，比如现在有一个三个消费者 ConsumerA、ConsumerB 和 ConsumerC 组成的消费者组，同时消费 TopicA 主题消息，TopicA 分为 7 个分区，如果采用 RoundRobin 分配策略，过程如下所示：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182137474.png" alt="RoundRobin" title="">            </p><p><strong>2. Range：</strong> Range 顾名思义就是按范围划分的意思，Kafka 默认采用 Range 分配策略，</p><p>比如现在有一个三个消费者 ConsumerA、ConsumerB 和 ConsumerC 组成的消费者组，同时消费 TopicA 主题消息，TopicA分为7个分区，如果采用 Range 分配策略，过程如下所示：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182138001.png" alt="range" title="">            </p><h4 id="offset的维护"><a href="#offset的维护" class="headerlink" title="offset的维护"></a>offset的维护</h4><p>由于 consumer 在消费过程中可能会出现断电宕机等故障，consumer 恢复后，需要从故障前的位置继续消费，所以 consumer 需要实时记录自己消费到了哪个 offset，以便故障恢复后继续消费。</p><p>Kafka 0.9 版本之前，consumer 默认将 offset 保存在 Zookeeper 中，从 0.9 版本开始，consumer 默认将 offset保存在 Kafka 一个内置的 topic 中，该 topic 为 _consumer_offsets。</p><h3 id="kafka事务"><a href="#kafka事务" class="headerlink" title="kafka事务"></a>kafka事务</h3><p>Kafka 事务基于幂等性实现，<strong>通过事务机制，Kafka 可以实现对多个 Topic 、多个 Partition 的原子性的写入</strong>，即处于同一个事务内的所有消息，最终结果是要么全部写成功，要么全部写失败。</p><blockquote><p>Kafka 事务分为生产者事务和消费者事务，但它们并不是强绑定的关系，消费者主要依赖自身对事务进行控制，因此这里我们主要讨论的是生产者事务。</p></blockquote><p>为了了实现跨分区跨会话的事务，需要引入一个全局唯一的 TransactionID，并将 Producer 获得的 PID 和Transaction ID 绑定。这样当 Producer 重启后就可以通过正在进行的 TransactionID 获得原来的 PID。</p><p>为了管理 Transaction，Kafka 引入了一个新的组件 Transaction Coordinator。Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic，这样即使整个服务重启，由于事务状态得到保存，进行中的事务状态可以得到恢复，从而继续进行。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182145718.png" alt="kafka事务" title="">            </p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://www.51cto.com/article/622050.html" target="_blank" rel="noopener">一文讲清 Kafka 工作流程和存储机制</a> — 推荐阅读</li><li><a href="https://zhuanlan.zhihu.com/p/610324541" target="_blank" rel="noopener">一文理解Kafka的设计原理</a>  — 推荐阅读</li><li><a href="https://ftn8205.medium.com/kafka-%E4%BB%8B%E7%B4%B9-golang%E7%A8%8B%E5%BC%8F%E5%AF%A6%E4%BD%9C-2b108481369e" target="_blank" rel="noopener">Kafka 介紹 + Golang程序实现</a>  — 简单看看</li><li><a href="https://yangyangmm.cn/tags/Kafka/page/2/#board" target="_blank" rel="noopener">kafka序列文章</a> — 简单看看</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;Kafka是一个分布式数据流平台，可以运行在单台服务器上，也可以在多台服务器上部署形成集群。它提供了发布和订阅功能，使用者可以发送数据到Ka
      
    
    </summary>
    
    
      <category term="kafka" scheme="https://github.com/fafucoder/categories/kafka/"/>
    
    
      <category term="kafka" scheme="https://github.com/fafucoder/tags/kafka/"/>
    
  </entry>
  
  <entry>
    <title>Flink 基础概念</title>
    <link href="https://github.com/fafucoder/2024/03/04/flink-basic/"/>
    <id>https://github.com/fafucoder/2024/03/04/flink-basic/</id>
    <published>2024-03-04T06:43:29.000Z</published>
    <updated>2024-03-18T14:26:26.328Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>Apache Flink 诞生于柏林工业大学的一个研究性项目，原名 StratoSphere 。2014 年，由 StratoSphere 项目孵化出 Flink，并于同年捐赠 Apache，之后成为 Apache 的顶级项目。</p><p>Flink 是一个分布式的流处理框架，它能够对有界和无界的数据流进行高效的处理。Flink 的核心是流处理，当然它也能支持批处理，Flink 将批处理看成是流处理的一种特殊情况，即数据流是有明确界限的。</p><blockquote><p>有界数据集：数据是静止不动的，例如mysql中的数据。对数据的处理不需要考虑追加写入的数据。在某个时间内的结果进行计算，这种计算称之为批计算，批处理。Batch Processing</p><p>无界数据集：数据会持续变更，例如实时日志数据。对于此类持续变更、追加的数据的计算方式称之为流计算。Streaming Processing</p></blockquote><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182212471.png" alt="flink基本概述" title="">            </p><h3 id="Flink架构"><a href="#Flink架构" class="headerlink" title="Flink架构"></a>Flink架构</h3><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182221857.png" alt="flink架构" title="">            </p><h3 id="Flink组件"><a href="#Flink组件" class="headerlink" title="Flink组件"></a>Flink组件</h3><p>Flink 核心架构的第二层是 Runtime 层， 该层采用标准的 Master - Slave 结构， 其中，Master 部分又包含了三个核心组件：Dispatcher、ResourceManager 和 JobManager，而 Slave 则主要是 TaskManager 进程。它们的功能分别如下：</p><ul><li><strong>JobManagers</strong> (也称为 <em>masters</em>) ：JobManagers 接收由 Dispatcher 传递过来的执行程序，该执行程序包含了作业图 (JobGraph)，逻辑数据流图 (logical dataflow graph) 及其所有的 classes 文件以及第三方类库 (libraries) 等等 。紧接着 JobManagers 会将 JobGraph 转换为执行图 (ExecutionGraph)，然后向 ResourceManager 申请资源来执行该任务，一旦申请到资源，就将执行图分发给对应的 TaskManagers 。因此每个作业 (Job) 至少有一个 JobManager；高可用部署下可以有多个 JobManagers，其中一个作为 <em>leader*，其余的则处于 *standby</em> 状态。</li><li><strong>TaskManagers</strong> (也称为 <em>workers</em>) : TaskManagers 负责实际的子任务 (subtasks) 的执行，每个 TaskManagers 都拥有一定数量的 slots。Slot 是一组固定大小的资源的合集 (如计算能力，存储空间)。TaskManagers 启动后，会将其所拥有的 slots 注册到 ResourceManager 上，由 ResourceManager 进行统一管理。</li><li><strong>Dispatcher</strong>：负责接收客户端提交的执行程序，并传递给 JobManager 。除此之外，它还提供了一个 WEB UI 界面，用于监控作业的执行情况。</li><li><strong>ResourceManager</strong> ：负责管理 slots 并协调集群资源。ResourceManager 接收来自 JobManager 的资源请求，并将存在空闲 slots 的 TaskManagers 分配给 JobManager 执行任务。Flink 基于不同的部署平台，如 YARN , Mesos，K8s 等提供了不同的资源管理器，当 TaskManagers 没有足够的 slots 来执行任务时，它会向第三方平台发起会话来请求额外的资源。</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182222680.png" alt="flink 组件" title="">            </p><h4 id="task-amp-sub-task"><a href="#task-amp-sub-task" class="headerlink" title="task &amp; sub task"></a>task &amp; sub task</h4><p>上面我们提到：TaskManagers 实际执行的是 SubTask，而不是 Task，这里解释一下两者的区别：</p><p>在执行分布式计算时，Flink 将可以链接的操作 (operators) 链接到一起，这就是 Task。之所以这样做， 是为了减少线程间切换和缓冲而导致的开销，在降低延迟的同时可以提高整体的吞吐量。 但不是所有的 operator 都可以被链接，如下 keyBy 等操作会导致网络 shuffle 和重分区，因此其就不能被链接，只能被单独作为一个 Task。 简单来说，一个 Task 就是一个可以链接的最小的操作链 (Operator Chains) 。如下图，source 和 map 算子被链接到一块，因此整个作业就只有三个 Task：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182223975.png" alt="task & sub task" title="">            </p><p>解释完 Task ，我们在解释一下什么是 SubTask，其准确的翻译是： <em>A subtask is one parallel slice of a task</em>，即一个 Task 可以按照其并行度拆分为多个 SubTask。如上图，source &amp; map 具有两个并行度，KeyBy 具有两个并行度，Sink 具有一个并行度，因此整个虽然只有 3 个 Task，但是却有 5 个 SubTask。Jobmanager 负责定义和拆分这些 SubTask，并将其交给 Taskmanagers 来执行，每个 SubTask 都是一个单独的线程。</p><h4 id="资源管理"><a href="#资源管理" class="headerlink" title="资源管理"></a>资源管理</h4><p>理解了 SubTasks ，我们再来看看其与 Slots 的对应情况。一种可能的分配情况如下：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182224500.png" alt="资源管理" title="">            </p><p>这时每个 SubTask 线程运行在一个独立的 TaskSlot， 它们共享所属的 TaskManager 进程的TCP 连接（通过多路复用技术）和心跳信息 (heartbeat messages)，从而可以降低整体的性能开销。此时看似是最好的情况，但是每个操作需要的资源都是不尽相同的，这里假设该作业 keyBy 操作所需资源的数量比 Sink 多很多 ，那么此时 Sink 所在 Slot 的资源就没有得到有效的利用。</p><p>基于这个原因，Flink 允许多个 subtasks 共享 slots，即使它们是不同 tasks 的 subtasks，但只要它们来自同一个 Job 就可以。假设上面 souce &amp; map 和 keyBy 的并行度调整为 6，而 Slot 的数量不变，此时情况如下：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182225804.png" alt="共享solt" title="">            </p><p>可以看到一个 Task Slot 中运行了多个 SubTask 子任务，此时每个子任务仍然在一个独立的线程中执行，只不过共享一组 Sot 资源而已。那么 Flink 到底如何确定一个 Job 至少需要多少个 Slot 呢？Flink 对于这个问题的处理很简单，默认情况一个 Job 所需要的 Slot 的数量就等于其 Operation 操作的最高并行度。如下， A，B，D 操作的并行度为 4，而 C，E 操作的并行度为 2，那么此时整个 Job 就需要至少四个 Slots 来完成。通过这个机制，Flink 就可以不必去关心一个 Job 到底会被拆分为多少个 Tasks 和 SubTasks。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403182225628.png" alt="solt" title="">            </p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://www.yijiyong.com/da/rtaflink/10-summary.html" target="_blank" rel="noopener">https://www.yijiyong.com/da/rtaflink/10-summary.html</a></li><li><a href="https://blog.csdn.net/a805814077/article/details/108095451" target="_blank" rel="noopener">https://blog.csdn.net/a805814077/article/details/108095451</a></li><li><a href="https://blog.csdn.net/weixin_43114209/article/details/131675299" target="_blank" rel="noopener">https://blog.csdn.net/weixin_43114209/article/details/131675299</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;Apache Flink 诞生于柏林工业大学的一个研究性项目，原名 StratoSphere 。2014 年，由 StratoSphere 
      
    
    </summary>
    
    
      <category term="flink" scheme="https://github.com/fafucoder/categories/flink/"/>
    
    
      <category term="flink" scheme="https://github.com/fafucoder/tags/flink/"/>
    
  </entry>
  
  <entry>
    <title>elasticsearch lucene存储引擎相关知识</title>
    <link href="https://github.com/fafucoder/2024/02/20/elasticsearch-lucene/"/>
    <id>https://github.com/fafucoder/2024/02/20/elasticsearch-lucene/</id>
    <published>2024-02-20T11:29:49.000Z</published>
    <updated>2024-03-18T08:20:11.233Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>Elasticsearch是一个基于Apache Lucene的开源搜索引擎，其依靠Lucene完成索引创建和搜索功能，可以将ElstaicSearch理解为是一个Lucene的分布式封装的搜索引擎。</p><h3 id="前置知识"><a href="#前置知识" class="headerlink" title="前置知识"></a>前置知识</h3><h4 id="倒排索引"><a href="#倒排索引" class="headerlink" title="倒排索引"></a>倒排索引</h4><p>搜索的核心需求是全文检索，全文检索简单来说就是要在大量文档中找到包含某个单词出现的位置，在传统关系型数据库中，数据检索只能通过 like 来实现，例如需要在手机数据中查询名称包含苹果的手机，需要通过如下 sql 实现：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select * from phone_table where phone_brand like &#39;%苹果手机%&#39;;</span><br></pre></td></tr></table></figure><p>这种实现方式实际会存在很多问题，例如需要全表扫描，性能差，无法得内容与搜索条件的相关性等，为了高效的实现全文检索，我们可以通过倒排索引来解决</p><p>倒排索引是区别于正排索引的概念</p><ul><li>正排索引：是以文档对象的唯一 ID 作为索引，以文档内容作为记录的结构。</li><li>倒排索引：Inverted index，指的是将文档内容中的单词作为索引，将包含该词的文档 ID 作为记录的结构。</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202402211646164.png" alt="倒排索引" title="">            </p><h4 id="FST算法"><a href="#FST算法" class="headerlink" title="FST算法"></a>FST算法</h4><p>FST全程Finite State Transducer 全称有穷状态转换器；FST是一种类似于字典的数据结构，但是它是k:v结构的，使用FST有两个优先</p><ul><li>空间占用小：通过对字典中单词前缀和后缀的重复利用，压缩了存储空间</li><li>查询效率快：查询速度不会超过 <code>O(len(str))</code> </li></ul><p>如果我们要对 <code>cat、 deep、 do、 dog、dogs</code>这五个单词进行插入构建FST（工具演示：<a href="http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it!" target="_blank" rel="noopener">http://examples.mikemccandless.com/fst.py?terms=&amp;cmd=Build+it%21</a>），执行过程如下：</p><ol><li><p>插入cat, 每个字母一条边，其中t边指向终点</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403041545936.png" alt="插入cat"></p></li><li><p>插入deep, 与前一个单词 cat 进行最大前缀匹配，发现没有匹配则直接插入，P边指向终点</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403041547077.png" alt="插入deep"></p></li><li><p>插入do, 与前一个单词deep进行最大前缀匹配，发现是 d，则在d边后增加新边o，o边指向终点</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403041548288.png" alt="插入do"></p></li><li><p>插入dog, 与前一个单词 do 进行最大前缀匹配，发现是 do，则在o边后增加新边g，g边指向终点</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403041549483.png" alt="插入dog"></p></li><li><p>插入dogs, 与前一个单词 dog 进行最大前缀匹配，发现是dog，则在g后增加新边s，s边指向终点。</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403041552844.png" alt="插入dogs"></p></li></ol><h3 id="Lucene写入原理"><a href="#Lucene写入原理" class="headerlink" title="Lucene写入原理"></a>Lucene写入原理</h3><h4 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h4><p>​        在Lucene中，写入数据的基本单元称之为Document，一个Document是由Field构成，Field是Term的集合，Segment是最小的独立索引单元，由多个Documents构成；在每个Segment范畴内，每个Document都被分配到了一个唯一的数字ID, 称之为DocID, 同时根据每个Field的名字定一个唯一的FieldNaming，对于索引字段进到Postings之前也会被分配一个唯一的TermID。Field除了FieldNaming之外也有一个FieldNumber，与DocID和TermID一样都是一个自增的数值。</p><p>​        倒排索引需要将 terms 映射到包含该单词 （term） 的文档列表，这样的映射列表我们称之为：<strong>倒排列表（postings list）</strong>。具体某一条映射数据称之为：<strong>倒排索引项（Posting）</strong>。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181603729.png" alt="倒排索引" title="">            </p><h4 id="Lucene中倒排索引的结构"><a href="#Lucene中倒排索引的结构" class="headerlink" title="Lucene中倒排索引的结构"></a>Lucene中倒排索引的结构</h4><p>根据倒排索引的概念，最简单的我们可以用一个Map来描述这个结构， Map 的 Key 存储分词后的单词（也叫Term），Map的Value存储一些列的文档ID的集合，但是全文搜索引擎在海量数据的情况下需要存储大量的文本，如果是用Map存储Directory的话有这样几个明显的缺点:</p><ol><li>Dictionary 是非常大的（比如我们搜索中的一个字段在Directory里可能有上千万个Term）因此存储所有的Term需要大量的内存</li><li>Postings 也可能会占据大量的存储空间（一个Term多的有几百万个doc）</li><li>Posting List 不仅仅需要包含文档的id，为了加快搜索的速度还需要包含更多信息，比如下面这些<ul><li>文档 id（DocId, Document Id），包含单词的所有文档唯一id，用于去正排索引中查询原始数据</li><li>词频（TF，Term Frequency），记录 Term 在每篇文档中出现的次数，用于后续相关性算分</li><li>位置（Position），记录 Term 在每篇文档中的分词位置（多个），用于做词语搜索（Phrase Query）</li><li>偏移（Offset），记录 Term 在每篇文档的开始和结束位置，用于高亮显示等</li></ul></li></ol><p>因此上面说的基于 Map 的实现方式几乎是不可行的。在海量数据背景下，倒排索引的实现直接关系到存储成本以及搜索性能，为此，Lucene 引入了多种巧妙的数据结构和算法，同时也把倒排索引的结构组织成如下图所示:</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181408110.png" alt="倒排索引" title="">            </p><ol><li>Lucene把用于存储Term的索引文件叫Terms Index，它的后缀是<code>.tip</code></li><li>把Postings信息分别存储在<code>.doc</code>、<code>.pay</code>、<code>.pox</code>，分别记录Postings的DocId信息和Term的词频、Payload信息、pox是记录位置信息</li><li>Terms Dictionary的文件后缀称为<code>.tim</code>，它是Term与Postings的关系纽带，存储了Term和其对应的Postings文件指针。</li></ol><p>通过Terms Index(.tip) 能够快速地在Terms Dictionary(.tim) 中找到你的想要的Term，以及它对应的Postings文件指针与Term在Segment作用域上的统计信息。</p><blockquote><p><em>postings: 实际上Postings包含的东西并不仅仅是DocIDs（我们通常把这一个有序文档编号系列叫DocIDs），它还包括文档编号、以及词频、Term在文档中的位置信息、还有Payload数据。</em></p></blockquote><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181557104.png" alt="倒排索引" title="">            </p><h4 id="什么是Term-Index"><a href="#什么是Term-Index" class="headerlink" title="什么是Term Index"></a>什么是Term Index</h4><p>Terms Index存储在<code>.tip</code>文件中，实际张是有一个或者多个FST组成的，Segment上每个字段都有自己的一个FST（FST Index）记录在<code>.tip</code>上，所以图中FST Index的个数即是Segment拥有字段的个数。</p><h4 id=""><a href="#" class="headerlink" title=""></a><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181442646.png" alt="Term Directory" title="">            </p></h4><h4 id="什么是Term-Directory"><a href="#什么是Term-Directory" class="headerlink" title="什么是Term Directory"></a>什么是Term Directory</h4><p>Term Directory存储了Index field 经过去重、时态统一、大小写统一、近义词处理等操作后的词项数据，最后存储在.tim文件中（也就是Term Directory 存储所有的Term数据），同时它也是 Term 与 Postings 的关系纽带，存储了每个 Term 和其对应的 Postings 文件位置指针。通过Term Directory可以知道Term的统计信息，包括Term在Segment中出现的频率等。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181445503.png" alt="Term Directory" title="">            </p><p>Terms Dictionary 内部采用 NodeBlock 这种结构对 Term 进行压缩存储，处理过程会将相同前缀的 Term 压缩为一个 NodeBlock，然后将每个 Term 的后缀以及对应 Term 的 Posting 关联信息处理为一个 Entry 保存到 Block，如下图所示:</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181552883.png" alt="term directory" title="">            </p><h4 id="什么是PostingList"><a href="#什么是PostingList" class="headerlink" title="什么是PostingList"></a>什么是PostingList</h4><p>PostingList 包含文档 id、词频、位置等多个信息，这些数据之间本身是相对独立的，因此 Lucene 将 Postings List 拆成三个文件存储：</p><ul><li>.doc文件：记录 Postings 的 docId 信息和 Term 的词频</li><li>.pay文件：记录 Payload 信息和偏移量信息</li><li>.pos文件：记录位置信息</li></ul><p>这个做法有没有点列式存储的味道？其好处也很明显：一来可以提高读取效率，因为基本所有的查询都会用 .doc 文件获取文档 id，但一般的查询仅需要用到 .doc 文件就足够了，只有对于近似查询等位置相关的查询才需要用位置相关数据， 二来这样存储数据很方便对数据进行压缩</p><p>三个文件整体实现差不太多，这里仅介绍.doc 文件，.doc 文件存储的是每个 Term 对应的文档 Id 和词频。每个 Term 都包含一对 TermFreqs 和 SkipData 结构，其中 TermFreqs 存放 docId 和词频信息，SkipData 为跳表结构，用于实现 TermFreqs 内部的快速跳转</p><p>Posting List 采用多个文件进行存储，最终我们可以得到每个 Term 的如下信息：</p><ul><li>SkipOffset：用来描述当前 term 信息在 .doc 文件中跳表信息的起始位置。</li><li>DocStartFP：是当前 term 信息在 .doc 文件中的文档 ID 与词频信息的起始位置。</li><li>PosStartFP：是当前 term 信息在 .pos 文件中的起始位置。</li><li>PayStartFP：是当前 term 信息在 .pay 文件中的起始位置。</li></ul><p>为了压缩Posting List的存储空间，需要对Posting List进行压缩，Lucene采用的压缩算法是: FOR  + RBM</p><h3 id="Lucene的搜索过程"><a href="#Lucene的搜索过程" class="headerlink" title="Lucene的搜索过程"></a>Lucene的搜索过程</h3><p>lucene的搜索过程如下：</p><ul><li>通过 Term Index 数据（.tip文件）中的 StartFP 获取指定字段的 FST</li><li>通过 FST 找到指定 Term 在 Term Dictionary（.tim 文件）可能存在的 Block</li><li>将对应 Block 加载内存，遍历 Block 中的 Entry，通过后缀（Suffix）判断是否存在指定 Term</li><li>存在则通过 Entry 的 TermStat 数据中各个文件的 FP 获取 Posting 数据</li><li>如果需要获取 Term 对应的所有 DocId 则直接遍历 TermFreqs，如果获取指定 DocId 数据则通过 SkipData快速跳转</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403181611084.png" alt="Lucene搜索过程" title="">            </p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://neteric.top/posts/elasticsearch_principle_four/" target="_blank" rel="noopener">捋一捋ElasticSearch（三）| Lucene存储引擎</a>  — 推荐阅读</li><li><a href="https://www.nasuiyile.cn/463.html" target="_blank" rel="noopener">ES的FST原理</a> — 推荐阅读</li><li><a href="https://www.cnblogs.com/lavender-pansy/p/16620974.html" target="_blank" rel="noopener">ElasticSearch倒排索引使用算法FST</a> — 可以看看~</li><li><a href="https://www.cnblogs.com/bonelee/p/6226185.html" target="_blank" rel="noopener">lucene字典实现原理——FST </a>  – 也可以看看~</li><li><a href="https://www.cnblogs.com/wuzhenzhao/p/13842858.html" target="_blank" rel="noopener">ElasticSearch索引核心原理</a></li><li><a href="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/Lucene.pdf" target="_blank" rel="noopener">Lucene 3.0 原理与代码分析完整版</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;Elasticsearch是一个基于Apache Lucene的开源搜索引擎，其依靠Lucene完成索引创建和搜索功能，可以将Elstaic
      
    
    </summary>
    
    
      <category term="elasticsearch" scheme="https://github.com/fafucoder/categories/elasticsearch/"/>
    
    
      <category term="elasticsearch" scheme="https://github.com/fafucoder/tags/elasticsearch/"/>
    
  </entry>
  
  <entry>
    <title>golang代码测试方法论</title>
    <link href="https://github.com/fafucoder/2023/02/25/golang-test/"/>
    <id>https://github.com/fafucoder/2023/02/25/golang-test/</id>
    <published>2023-02-25T10:45:20.000Z</published>
    <updated>2023-02-28T03:37:39.153Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>在项目开发中，正对项目的测试分为多个分类，比如单元测试、集成测试、端到端测试等，按照Mike Cohn提出的“测试金字塔”概念，测试分为4个层次：</p><p class="img-lightbox">                <img src="https://i.loli.net/2019/05/21/5ce34ea15584577479.png" alt="test" title="">            </p><p>最下面的是单元测试，单元测试针对的是代码进行测试，针对的是局部代码功能；再而之上是集成测试，它针对的是服务的接口进行测试；接着网上是端到端的测试，也就是我们所说的链路测试，它针对的是服务的功能进行测试，负责从一个链路的入口输入测试用例，验证输出的系统的结果；再上一层是我们最常用的UI测试，就是测试人员在UI界面上根据功能进行点击测试。</p><p>测试金字塔建议我们：</p><p>（1）<strong>尽可能地多做单元测试 和 集成测试</strong>，因为他们的执行速度相较于上层的几个测试类型来说快很多且相对稳定，可以一天多次执行。</p><p>（2）<strong>尽可能地少做 组件测试、端到端测试 和 探索性测试</strong>，因为他们的执行速度相较单元测试 和 集成测试 会慢很多，且不够稳定，无法做到一天多次执行，每次执行都要等很久才能获得反馈结果。</p><blockquote><p>画外音：金字塔里，越往下速度越快且越稳定，那么就可以频繁执行，反正执行一次也花不了多久时间，开发人员还可以知道我的代码有没有影响到其他模块。越往上则速度越慢且越不稳定，跑一次要N久，开发人员往往会觉得还是先继续开发吧，到时候出了bug再说，我可不想加班等测试结果。</p></blockquote><h3 id="单元测试"><a href="#单元测试" class="headerlink" title="单元测试"></a>单元测试</h3><h4 id="什么是单元你测试"><a href="#什么是单元你测试" class="headerlink" title="什么是单元你测试"></a>什么是单元你测试</h4><p>单元是应用的最小可测试部件。在过程化编程中，一个单元就是单个程序、函数、过程等；对于面向对象编程，最小单元就是方法，包括基类、超类、抽象类等中的方法。单元测试就是软件开发中对最小单位进行正确性检验的测试工作。</p><p>不同地方对单元测试有的定义可能会有所不同，但有一些基本共识：</p><ul><li>单元测试是比较<strong>底层</strong>的，关注代码的<strong>局部</strong>而不是整体。</li><li>单元测试是开发人员在写代码时候写的。</li><li>单元测试需要比其他测试运行得<strong>快</strong>。</li></ul><h4 id="单元测试的意义"><a href="#单元测试的意义" class="headerlink" title="单元测试的意义"></a>单元测试的意义</h4><ul><li><strong>提高代码质量</strong>: 代码测试都是为了帮助开发人员发现问题从而解决问题，提高代码质量。</li><li><strong>尽早发现问题</strong>: 问题越早发现，解决的难度和成本就越低。</li><li><strong>保证重构正确性</strong>: 随着功能的增加，重构（修改老代码）几乎是无法避免的。很多时候我们不敢重构的原因，就是担心其它模块因为依赖它而不工作。有了单元测试，只要在改完代码后运行一下单测就知道改动对整个系统的影响了，从而可以让我们放心的重构代码。</li><li><strong>简化调试过程</strong>: 单元测试让我们可以轻松地知道是哪一部分代码出了问题。</li><li><strong>简化集成过程</strong>: 由于各个单元已经被测试，在集成过程中进行的后续测试会更加容易。</li><li><strong>优化代码设计</strong>: 编写测试用例会迫使开发人员仔细思考代码的设计和必须完成的工作，有利于开发人员加深对代码功能的理解，从而形成更合理的设计和结构。</li><li><strong>单元测试是最好的文档</strong>: 单元测试覆盖了接口的所有使用方法，是最好的示例代码。而真正的文档包括注释很有可能和代码不同步，并且看不懂。</li></ul><h4 id="单元测试基本原则"><a href="#单元测试基本原则" class="headerlink" title="单元测试基本原则"></a>单元测试基本原则</h4><ol><li>单元测试文件名以xxx_test.go命名，测试文件跟被测试文件处于同个包中;</li><li>测试用例以Test开头, 测试函数为TestXxx,  整体风格保持一致;</li><li>测试用例应该完备，应考虑到各个边界情况，推荐使用table-driven的方式实现;</li><li>单元测试的结果是确定且一致的，跟环境无关的;</li><li>测试用例是独立的，不同的测试用例之间不能有互相依赖;</li><li>单元测试程序不应该有用户输入，测试结果应该能直接被电脑获取，不应该由人来判断;</li><li>函数中通过调用testing.T的Error, Errorf, FailNow, Fatal, FatalIf等方法记录测试的信息;</li><li>可以通过benchmark做并发测试，验证代码性能;</li></ol><h4 id="常见的测试框架"><a href="#常见的测试框架" class="headerlink" title="常见的测试框架"></a>常见的测试框架</h4><p>golang自带的testing包已经可以完美支持单元测试，我们在写一个文件，函数的时候，可以直接在需要单元测试的文件旁边增加一个_test.go的文件。而后直接使用 <code>go test</code> 直接跑测试用例就可以了。但是因为单元测试是与环境无关的，因此在编写测试用例时总是需要把一些第三发依赖给mock掉，因此需要借助其他的测试组件来完成，下面是一些常见的mock框架：</p><ol><li>基于接口的Mock框架-goMock  </li><li>全局变量、函数、过程打桩框架-goStub</li><li>动态打桩框架(非并发安全)-goMonkey</li><li>数据库和缓存等中间件mock框架(sqlMock, redisMock)</li><li>http请求mock框架-httpmock</li></ol><h5 id="goMonkey"><a href="#goMonkey" class="headerlink" title="goMonkey"></a>goMonkey</h5><p><a href="https://github.com/agiledragon/gomonkey">go monkey</a> 可以动态的执行打桩，目标是让用户在单元测试中低成本的完成打桩，从而将精力聚焦于业务功能的开发。gomonkey支持多种打桩方式，可以无入侵的实现打桩，但是需要注意的是：</p><ol><li><p>运行 monkey需要关闭 Go 语言的内联优化才能生效，也就是编译时需要添加 <code>-gcflags=all=-l</code> 参数才行</p><blockquote><p>注意：</p><ol><li>如果是 M1 系统，编译需要指定 goarch 为 amd64<br>@MAC-M1#  GOARCH=amd64 go build -gcflags=all=-l  -o xxx  main.go<br>@MAC-INTEL# go build -gcflags=all=-l  -o xxx  main.go<br>解决的问题：monkey mac m1 execute syscall.Mprotect error：panic: permission denied [recovered]<br>ref: <a href="https://github.com/agiledragon/gomonkey/issues/57">https://github.com/agiledragon/gomonkey/issues/57</a><br>ref: <a href="https://blog.csdn.net/qw790707988/article/details/119710144" target="_blank" rel="noopener">https://blog.csdn.net/qw790707988/article/details/119710144</a></li><li>monkey不应该用于生产系统, 否则可能引发未知事故</li><li>monkey 需要在运行的时候修改内存代码段，因而无法在一些对安全性要求比较高的系统上工作</li></ol></blockquote></li></ol><h3 id="集成测试与E2E测试"><a href="#集成测试与E2E测试" class="headerlink" title="集成测试与E2E测试"></a>集成测试与E2E测试</h3><p>go开发者一般使用Ginkgo + Gomega 实现集成测试与E2E测试。Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发（BDD， Behavior-Driven Development）风格的测试框架，通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。</p><h4 id="Ginkgo简介"><a href="#Ginkgo简介" class="headerlink" title="Ginkgo简介"></a>Ginkgo简介</h4><p>inkgo是Go语言的一个行为驱动开发（BDD， Behavior-Driven Development）风格的测试框架，通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。</p><h5 id="安装使用"><a href="#安装使用" class="headerlink" title="安装使用"></a>安装使用</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">go get -u github.com&#x2F;onsi&#x2F;ginkgo&#x2F;ginkgo</span><br><span class="line"></span><br><span class="line">ginkgo bootstrap</span><br></pre></td></tr></table></figure><h5 id="测试模块"><a href="#测试模块" class="headerlink" title="测试模块"></a>测试模块</h5><ul><li>It: 是测试例的基本单位，即It包含的代码就算一个测试用例</li><li>Context和Describe: 是将一个或多个测试例归类</li><li>BeforeEach: 是每个测试例执行前执行该段代码</li><li>AfterEach: 是每个测试例执行后执行该段代码</li><li>JustBeforeEach: 是在BeforeEach执行之后，测试例执行之前执行</li><li>BeforeSuite: 是在该测试集执行前执行，即该文件夹内的测试例执行之前</li><li>AfterSuite: 是在该测试集执行后执行，即该文件夹内的测试例执行完后</li><li>By: 是打印信息，内容只能是字符串，只会在测试例失败后打印，一般用于调试和定位问题</li><li>Fail: 是标志该测试例运行结果为失败，并打印里面的信息</li><li>Specify: 和It功能完全一样，It属于其简写</li></ul><h5 id="测试方法列表"><a href="#测试方法列表" class="headerlink" title="测试方法列表"></a>测试方法列表</h5><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 断言</span></span><br><span class="line">Expect(ACTUAL).Should(Equal(EXPECTED))</span><br><span class="line">Expect(ACTUAL).To(Equal(EXPECTED))</span><br><span class="line">Expect(ACTUAL).ShouldNot(Equal(EXPECTED))</span><br><span class="line">Expect(ACTUAL).NotTo(Equal(EXPECTED))</span><br><span class="line">Expect(ACTUAL).ToNot(Equal(EXPECTED))</span><br><span class="line"></span><br><span class="line">// 断言没有发生错误</span><br><span class="line">Expect(err).ShouldNot(HaveOccurred())</span><br><span class="line">Expect(DoSomethingSimple()).Should(Succeed())</span><br><span class="line"></span><br><span class="line">// 断言注解</span><br><span class="line">Expect(ACTUAL).To(Equal(EXPECTED), <span class="string">"My annotation %d"</span>, foo)</span><br><span class="line">Expect(ACTUAL).To(Equal(EXPECTED), func() string &#123; <span class="built_in">return</span> <span class="string">"My annotation"</span> &#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 相等</span></span><br><span class="line">// 如果ACTUAL和EXPECTED都为nil，断言会失败</span><br><span class="line">Expect(ACTUAL).Should(Equal(EXPECTED))</span><br><span class="line"> </span><br><span class="line">Expect(ACTUAL).Should(BeEquivalentTo(EXPECTED))</span><br><span class="line"> </span><br><span class="line">// 使用 == 进行比较</span><br><span class="line">BeIdenticalTo(expected interface&#123;&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 空值/零值</span></span><br><span class="line">// 断言ACTUAL为Nil</span><br><span class="line">Expect(ACTUAL).Should(BeNil())</span><br><span class="line"> </span><br><span class="line">// 断言ACTUAL为它的类型的零值，或者是Nil</span><br><span class="line">Expect(ACTUAL).Should(BeZero())</span><br><span class="line"></span><br><span class="line"><span class="comment"># 布尔值</span></span><br><span class="line">Expect(ACTUAL).Should(BeTrue())</span><br><span class="line">Expect(ACTUAL).Should(BeFalse())</span><br><span class="line"></span><br><span class="line"><span class="comment"># 错误</span></span><br><span class="line">Expect(ACTUAL).Should(HaveOccurred())</span><br><span class="line"> </span><br><span class="line">// 没有错误</span><br><span class="line">err := SomethingThatMightFail()</span><br><span class="line">Expect(err).ShouldNot(HaveOccurred())</span><br><span class="line"></span><br><span class="line">// 如果ACTUAL为Nil则断言成功</span><br><span class="line">Expect(ACTUAL).Should(Succeed())</span><br><span class="line"></span><br><span class="line"><span class="comment"># 字符串</span></span><br><span class="line">// 子串判断</span><br><span class="line">Expect(ACTUAL).Should(ContainSubstring(STRING, ARGS...))</span><br><span class="line"> </span><br><span class="line">// 前缀判断</span><br><span class="line">Expect(ACTUAL).Should(HavePrefix(STRING, ARGS...))</span><br><span class="line"> </span><br><span class="line">// 后缀判断</span><br><span class="line">Expect(ACTUAL).Should(HaveSuffix(STRING, ARGS...))</span><br><span class="line"> </span><br><span class="line">// 正则式匹配</span><br><span class="line">Expect(ACTUAL).Should(MatchRegexp(STRING, ARGS...))</span><br><span class="line"></span><br><span class="line"><span class="comment"># JSON/XML/YML</span></span><br><span class="line">Expect(ACTUAL).Should(MatchJSON(EXPECTED))</span><br><span class="line">Expect(ACTUAL).Should(MatchXML(EXPECTED))</span><br><span class="line">Expect(ACTUAL).Should(MatchYAML(EXPECTED))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 集合（string, array, map, chan, slice）</span></span><br><span class="line">// 断言为空</span><br><span class="line">Expect(ACTUAL).Should(BeEmpty())</span><br><span class="line"> </span><br><span class="line">// 断言长度</span><br><span class="line">Expect(ACTUAL).Should(HaveLen(INT))</span><br><span class="line"> </span><br><span class="line">// 断言容量</span><br><span class="line">Expect(ACTUAL).Should(HaveCap(INT))</span><br><span class="line"> </span><br><span class="line">// 断言包含元素</span><br><span class="line">Expect(ACTUAL).Should(ContainElement(ELEMENT))</span><br><span class="line"> </span><br><span class="line">// 断言等于 其中之一</span><br><span class="line">Expect(ACTUAL).Should(BeElementOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))</span><br><span class="line"> </span><br><span class="line">// 断言元素相同，不考虑顺序</span><br><span class="line">Expect(ACTUAL).Should(ConsistOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))</span><br><span class="line">Expect(ACTUAL).Should(ConsistOf([]SOME_TYPE&#123;ELEMENT1, ELEMENT2, ELEMENT3, ...&#125;))</span><br><span class="line"> </span><br><span class="line">// 断言存在指定的键，仅用于map</span><br><span class="line">Expect(ACTUAL).Should(HaveKey(KEY))</span><br><span class="line"></span><br><span class="line">// 断言存在指定的键值对，仅用于map</span><br><span class="line">Expect(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 数字/时间</span></span><br><span class="line">// 断言数字意义（类型不感知）上的相等</span><br><span class="line">Expect(ACTUAL).Should(BeNumerically(<span class="string">"=="</span>, EXPECTED))</span><br><span class="line"> </span><br><span class="line">// 断言相似，无差不超过THRESHOLD（默认1e-8）</span><br><span class="line">Expect(ACTUAL).Should(BeNumerically(<span class="string">"~"</span>, EXPECTED, &lt;THRESHOLD&gt;))</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line">Expect(ACTUAL).Should(BeNumerically(<span class="string">"&gt;"</span>, EXPECTED))</span><br><span class="line">Expect(ACTUAL).Should(BeNumerically(<span class="string">"&gt;="</span>, EXPECTED))</span><br><span class="line">Expect(ACTUAL).Should(BeNumerically(<span class="string">"&lt;"</span>, EXPECTED))</span><br><span class="line">Expect(ACTUAL).Should(BeNumerically(<span class="string">"&lt;="</span>, EXPECTED))</span><br><span class="line"> </span><br><span class="line">Expect(number).Should(BeBetween(0, 10))</span><br></pre></td></tr></table></figure><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://www.cnblogs.com/yjf512/p/10905352.html" target="_blank" rel="noopener">golang项目的测试实践</a></li><li><a href="https://blog.csdn.net/sd7o95o/article/details/107804441" target="_blank" rel="noopener">What Is 测试金字塔？</a></li><li><a href="https://blog.csdn.net/u013276277/article/details/104993370" target="_blank" rel="noopener">gomonkey调研文档和学习</a></li><li><a href="https://blog.gmem.cc/ginkgo-study-note" target="_blank" rel="noopener">ginkgo学习笔记</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;在项目开发中，正对项目的测试分为多个分类，比如单元测试、集成测试、端到端测试等，按照Mike Cohn提出的“测试金字塔”概念，测试分为4个
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>etcd使用手记</title>
    <link href="https://github.com/fafucoder/2023/02/25/golang-etcd/"/>
    <id>https://github.com/fafucoder/2023/02/25/golang-etcd/</id>
    <published>2023-02-25T10:38:58.000Z</published>
    <updated>2024-03-19T16:20:13.212Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p><a href="https://etcd.io/" target="_blank" rel="noopener">etcd</a>是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统，可以用于配置共享和服务的注册和发现。</p><p>类似项目有zookeeper和consul。</p><p>etcd具有以下特点：</p><ul><li>完全复制：集群中的每个节点都可以使用完整的存档</li><li>高可用性：Etcd可用于避免硬件的单点故障或网络问题</li><li>一致性：每次读取都会返回跨多主机的最新写入</li><li>简单：包括一个定义良好、面向用户的API（gRPC）</li><li>安全：实现了带有可选的客户端证书身份验证的自动化TLS</li><li>快速：每秒10000次写入的基准速度</li><li>可靠：使用Raft算法实现了强一致、高可用的服务存储目录</li></ul><h3 id="etcd安装使用方法"><a href="#etcd安装使用方法" class="headerlink" title="etcd安装使用方法"></a>etcd安装使用方法</h3><p>etcd支持单机模式，以及使用集群模式部署，支持部署在各种操作系统中。</p><h4 id="mac安装etcd单机模式"><a href="#mac安装etcd单机模式" class="headerlink" title="mac安装etcd单机模式"></a>mac安装etcd单机模式</h4><p>mac安装etcd整体上来说非常简单，只需要一个命令即可（linux其实也是一个命令即可）</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">brew install etcd</span><br><span class="line">brew searvices start etcd</span><br><span class="line">brew services list</span><br><span class="line"></span><br><span class="line"><span class="comment"># etcdctl endpoint health</span></span><br><span class="line">➜  ~ etcdctl endpoint health</span><br><span class="line">127.0.0.1:2379 is healthy: successfully committed proposal: took = 3.219075ms</span><br></pre></td></tr></table></figure><h3 id="etcdctl命令使用"><a href="#etcdctl命令使用" class="headerlink" title="etcdctl命令使用"></a>etcdctl命令使用</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line">NAME:</span><br><span class="line">etcdctl - A simple <span class="built_in">command</span> line client <span class="keyword">for</span> etcd3.</span><br><span class="line"></span><br><span class="line">COMMANDS:</span><br><span class="line">alarm disarmDisarms all alarms</span><br><span class="line">alarm listLists all alarms</span><br><span class="line">auth <span class="built_in">disable</span>Disables authentication</span><br><span class="line">auth <span class="built_in">enable</span>Enables authentication</span><br><span class="line">auth statusReturns authentication status</span><br><span class="line">check datascaleCheck the memory usage of holding data <span class="keyword">for</span> different workloads on a given server endpoint.</span><br><span class="line">check perfCheck the performance of the etcd cluster</span><br><span class="line">compactionCompacts the event <span class="built_in">history</span> <span class="keyword">in</span> etcd</span><br><span class="line">defragDefragments the storage of the etcd members with given endpoints</span><br><span class="line">delRemoves the specified key or range of keys [key, range_end)</span><br><span class="line">electObserves and participates <span class="keyword">in</span> leader election</span><br><span class="line">endpoint hashkvPrints the KV <span class="built_in">history</span> <span class="built_in">hash</span> <span class="keyword">for</span> each endpoint <span class="keyword">in</span> --endpoints</span><br><span class="line">endpoint healthChecks the healthiness of endpoints specified <span class="keyword">in</span> `--endpoints` flag</span><br><span class="line">endpoint statusPrints out the status of endpoints specified <span class="keyword">in</span> `--endpoints` flag</span><br><span class="line">getGets the key or a range of keys</span><br><span class="line"><span class="built_in">help</span>Help about any <span class="built_in">command</span></span><br><span class="line">lease grantCreates leases</span><br><span class="line">lease keep-aliveKeeps leases alive (renew)</span><br><span class="line">lease listList all active leases</span><br><span class="line">lease revokeRevokes leases</span><br><span class="line">lease timetoliveGet lease information</span><br><span class="line">lockAcquires a named lock</span><br><span class="line">make-mirrorMakes a mirror at the destination etcd cluster</span><br><span class="line">member addAdds a member into the cluster</span><br><span class="line">member listLists all members <span class="keyword">in</span> the cluster</span><br><span class="line">member promotePromotes a non-voting member <span class="keyword">in</span> the cluster</span><br><span class="line">member removeRemoves a member from the cluster</span><br><span class="line">member updateUpdates a member <span class="keyword">in</span> the cluster</span><br><span class="line">move-leaderTransfers leadership to another etcd cluster member.</span><br><span class="line">putPuts the given key into the store</span><br><span class="line">role addAdds a new role</span><br><span class="line">role deleteDeletes a role</span><br><span class="line">role getGets detailed information of a role</span><br><span class="line">role grant-permissionGrants a key to a role</span><br><span class="line">role listLists all roles</span><br><span class="line">role revoke-permissionRevokes a key from a role</span><br><span class="line">snapshot restoreRestores an etcd member snapshot to an etcd directory</span><br><span class="line">snapshot saveStores an etcd node backend snapshot to a given file</span><br><span class="line">snapshot status[deprecated] Gets backend snapshot status of a given file</span><br><span class="line">txnTxn processes all the requests <span class="keyword">in</span> one transaction</span><br><span class="line">user addAdds a new user</span><br><span class="line">user deleteDeletes a user</span><br><span class="line">user getGets detailed information of a user</span><br><span class="line">user grant-roleGrants a role to a user</span><br><span class="line">user listLists all users</span><br><span class="line">user passwdChanges password of user</span><br><span class="line">user revoke-roleRevokes a role from a user</span><br><span class="line">versionPrints the version of etcdctl</span><br><span class="line">watchWatches events stream on keys or prefixes</span><br><span class="line"></span><br><span class="line">OPTIONS:</span><br><span class="line">      --cacert=<span class="string">""</span>verify certificates of TLS-enabled secure servers using this CA bundle</span><br><span class="line">      --cert=<span class="string">""</span>identify secure client using this TLS certificate file</span><br><span class="line">      --<span class="built_in">command</span>-timeout=5stimeout <span class="keyword">for</span> short running <span class="built_in">command</span> (excluding dial timeout)</span><br><span class="line">      --debug[=<span class="literal">false</span>]<span class="built_in">enable</span> client-side debug logging</span><br><span class="line">      --dial-timeout=2sdial timeout <span class="keyword">for</span> client connections</span><br><span class="line">  -d, --discovery-srv=<span class="string">""</span>domain name to query <span class="keyword">for</span> SRV records describing cluster endpoints</span><br><span class="line">      --discovery-srv-name=<span class="string">""</span>service name to query when using DNS discovery</span><br><span class="line">      --endpoints=[127.0.0.1:2379]gRPC endpoints</span><br><span class="line">  -h, --<span class="built_in">help</span>[=<span class="literal">false</span>]<span class="built_in">help</span> <span class="keyword">for</span> etcdctl</span><br><span class="line">      --hex[=<span class="literal">false</span>]<span class="built_in">print</span> byte strings as hex encoded strings</span><br><span class="line">      --insecure-discovery[=<span class="literal">true</span>]accept insecure SRV records describing cluster endpoints</span><br><span class="line">      --insecure-skip-tls-verify[=<span class="literal">false</span>]skip server certificate verification (CAUTION: this option should be enabled only <span class="keyword">for</span> testing purposes)</span><br><span class="line">      --insecure-transport[=<span class="literal">true</span>]<span class="built_in">disable</span> transport security <span class="keyword">for</span> client connections</span><br><span class="line">      --keepalive-time=2skeepalive time <span class="keyword">for</span> client connections</span><br><span class="line">      --keepalive-timeout=6skeepalive timeout <span class="keyword">for</span> client connections</span><br><span class="line">      --key=<span class="string">""</span>identify secure client using this TLS key file</span><br><span class="line">      --password=<span class="string">""</span>password <span class="keyword">for</span> authentication (<span class="keyword">if</span> this option is used, --user option shouldn<span class="string">'t include password)</span></span><br><span class="line"><span class="string">      --user=""username[:password] for authentication (prompt if password is not supplied)</span></span><br><span class="line"><span class="string">  -w, --write-out="simple"set the output format (fields, json, protobuf, simple, table)</span></span><br></pre></td></tr></table></figure><p>etcdctl 命令分为几大模块，分别是跟etcd集群现网的命令例如member, endpoint, 第二个是租约有关的lease（类似ttl）, 第三个是数据相关的，包括watch、put、get等，第四个是跟角色认证相关的信息，常见的命令如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"># -w 参数用于输出执行的格式，只是fields, json, protobuf, sample, table 默认sample</span><br><span class="line"># --endpoints 参数用于etcdctl跟ectd集群建联，默认是127.0.0.1:2379</span><br><span class="line"># --cacert, --cert等参数用于指定ca证书信息</span><br><span class="line">etcdctl -w table endpoint health</span><br><span class="line">etcdctl -w table member list</span><br><span class="line">etcdctl -w json endpoint status</span><br><span class="line"></span><br><span class="line"># 增删改查</span><br><span class="line">etcdctl put hello word</span><br><span class="line">etcdctl get hello</span><br><span class="line">etcdctl watch hello</span><br><span class="line">etcdctl del hello</span><br><span class="line"></span><br><span class="line"># 可以通过--prefix参数监听指定前缀的参数</span><br><span class="line">etcdctl watch --prefix&#x3D;true hello # 可以监听到hello1、hello2等参数数据</span><br><span class="line">etcdctl get --prefix&#x3D;true hello  # 可以获取到hello1、hello2等参数数据</span><br></pre></td></tr></table></figure><h3 id="golang操作使用etcd"><a href="#golang操作使用etcd" class="headerlink" title="golang操作使用etcd"></a>golang操作使用etcd</h3><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"context"</span></span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"log"</span></span><br><span class="line"><span class="string">"time"</span></span><br><span class="line"></span><br><span class="line">clientv3 <span class="string">"go.etcd.io/etcd/client/v3"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">cli, err := clientv3.New(clientv3.Config&#123;</span><br><span class="line">Endpoints:   []<span class="keyword">string</span>&#123;<span class="string">"127.0.0.1:2379"</span>&#125;,</span><br><span class="line">DialTimeout: <span class="number">5</span> * time.Second,</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> cli.Close()</span><br><span class="line"></span><br><span class="line">etcdBase(cli)</span><br><span class="line">etcdWatch(cli)</span><br><span class="line">etcdLease(cli)</span><br><span class="line">etcdKeepAlive(cli)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// etcdBase 基础的创建跟删除</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">etcdBase</span><span class="params">(client *clientv3.Client)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> key = <span class="string">"/hb/etcd/base"</span></span><br><span class="line"><span class="keyword">if</span> _, err := client.Put(context.Background(), key, <span class="string">"123456"</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"error is: %#v \n"</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resp, err := client.Get(context.Background(), key)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"error is: %#v \n"</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, kvs := <span class="keyword">range</span> resp.Kvs &#123;</span><br><span class="line">fmt.Printf(<span class="string">"Get key=%v, value=%v \n"</span>, <span class="keyword">string</span>(kvs.Key), <span class="keyword">string</span>(kvs.Value))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, err := client.Delete(context.Background(), key); err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"error is: %#v \n"</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// etcd watch机器</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">etcdWatch</span><span class="params">(client *clientv3.Client)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> watchKey = <span class="string">"/hb/etcd/watch"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10</span>; i++ &#123;</span><br><span class="line">_, _ = client.Put(context.Background(), fmt.Sprintf(<span class="string">"%s/key_%d"</span>, watchKey, i), <span class="string">"world"</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10</span>; i++ &#123;</span><br><span class="line">_, _ = client.Delete(context.Background(), fmt.Sprintf(<span class="string">"%s/key_%d"</span>, watchKey, i))</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">watchCh := client.Watch(context.Background(), watchKey, clientv3.WithPrefix())</span><br><span class="line"><span class="keyword">for</span> resp := <span class="keyword">range</span> watchCh &#123;</span><br><span class="line"><span class="keyword">for</span> _, ev := <span class="keyword">range</span> resp.Events &#123;</span><br><span class="line">fmt.Printf(<span class="string">"Watch Type: %s; Key: %s; Value: %s\n"</span>, ev.Type, ev.Kv.Key, ev.Kv.Value)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// etcdLease etcd租约</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">etcdLease</span><span class="params">(client *clientv3.Client)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> key = <span class="string">"/hb/etcd/lease"</span></span><br><span class="line"><span class="comment">// 创建一个5秒的租约</span></span><br><span class="line">grantResp, err := client.Grant(context.Background(), <span class="number">5</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"hello"</span>)</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 5秒钟之后, key就会被移除</span></span><br><span class="line">_, err = client.Put(context.Background(), key, <span class="string">"lease"</span>, clientv3.WithLease(grantResp.ID))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resp, err := client.Get(context.Background(), key)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, kvs := <span class="keyword">range</span> resp.Kvs &#123;</span><br><span class="line">fmt.Printf(<span class="string">"Lease key=%v, value=%v \n"</span>, <span class="keyword">string</span>(kvs.Key), <span class="keyword">string</span>(kvs.Value))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">time.Sleep(<span class="number">6</span> * time.Second)</span><br><span class="line"></span><br><span class="line">resp, err = client.Get(context.Background(), key)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, kvs := <span class="keyword">range</span> resp.Kvs &#123;</span><br><span class="line">fmt.Printf(<span class="string">"Lease Expired key=%v, value=%v \n"</span>, <span class="keyword">string</span>(kvs.Key), <span class="keyword">string</span>(kvs.Value))</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// etcdKeepAlive etcd keepAlive</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">etcdKeepAlive</span><span class="params">(client *clientv3.Client)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> key = <span class="string">"/hb/etcd/keepalive"</span></span><br><span class="line">resp, err := client.Grant(context.TODO(), <span class="number">5</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">_, err = client.Put(context.TODO(), key, <span class="string">"keepalive"</span>, clientv3.WithLease(resp.ID))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// the key 'foo' will be kept forever</span></span><br><span class="line">ch, err := client.KeepAlive(context.TODO(), resp.ID)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">ka := &lt;-ch</span><br><span class="line">fmt.Println(<span class="string">"KeepAlive TTL: "</span>, ka.TTL)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="etcd工作原理"><a href="#etcd工作原理" class="headerlink" title="etcd工作原理"></a>etcd工作原理</h3><p>etcd 是一个基于 Raft 共识算法实现的分布式键值存储服务，在项目结构上采用了模块化设计，其中最主要的三个部分是实现分布式共识的 Raft 模块、实现数据持久化的 WAL 模块和实现状态机存储的 MVCC 模块。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403200019132.png" alt="etcd组件" title="">            </p><h4 id="Leader-选举"><a href="#Leader-选举" class="headerlink" title="Leader 选举"></a>Leader 选举</h4><p>Raft 是一种用来管理日志复制过程的算法，Raft 通过『领导选举机制』选举出一个 Leader，由它全权管理日志复制来实现一致性。一个 Raft 集群包含若干个服务器节点，每一个节点都有一个唯一标识 ID。Raft 算法论文规定了三种节点身份：Leader、Follower 和 Candidate，etcd 的实现中又添加了 PreCandidate 和 Learner 这两种身份。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403200020059.png" alt="etcd原理" title="">            </p><h5 id="leader选择过程"><a href="#leader选择过程" class="headerlink" title="leader选择过程"></a>leader选择过程</h5><ol><li>集群启动时所有节点初始状态均为 Follower，同时启动选举定时器（时间随机，降低冲突概率），随后会有一个节点选举成功成为 Leader，在绝大多数时间里集群内的节点会处于这两种身份之一。</li><li>当某个节点当选成为Leader后，需要定期的向Follower发送心跳包，Follower收到心跳包后会重置选举定时器。</li><li>当一个 Follower 节点的选举计时器超时后（节点在指定的时间之内没有收到leader或者candidate的有效消息时会发起选举），会先进入<code>preVote</code>状态（切换为 PreCandidate 身份），在它准备发起一次选举之前，需要尝试连接集群中的其他节点，并询问它们是否愿意参与选举</li><li>如果集群中的其它节点能够正常收到 Leader 的心跳消息，那么会拒绝参与选举。</li><li>如果有超过法定人数的节点响应并表示参与新一轮选举，该节点会从 PreCandidate 身份切换到 Candidate，发起新一轮的选举。</li></ol><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://www.liwenzhou.com/posts/Go/etcd/" target="_blank" rel="noopener">go操作etcd</a></li><li><a href="https://wingsxdu.com/posts/database/etcd/#top" target="_blank" rel="noopener">分布式键值存储 etcd 原理与实现 · Analyze</a></li><li><a href="https://draveness.me/etcd-introduction/" target="_blank" rel="noopener">高可用分布式存储 etcd 的实现原理</a></li><li><a href="https://www.codedump.info/post/20210628-etcd-wal/" target="_blank" rel="noopener">Etcd Raft库的日志存储</a></li><li><a href="https://pkg.go.dev/go.etcd.io/etcd/client/v3" target="_blank" rel="noopener">https://pkg.go.dev/go.etcd.io/etcd/client/v3</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://etcd.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;etcd&lt;/a&gt;是使用G
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>新浪图床图片替换为腾讯COS</title>
    <link href="https://github.com/fafucoder/2023/02/04/golang-sina-image/"/>
    <id>https://github.com/fafucoder/2023/02/04/golang-sina-image/</id>
    <published>2023-02-04T13:39:04.000Z</published>
    <updated>2023-02-04T14:07:35.345Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>近期，有同学反馈，所有调用的微博图床图片都无法加载并提示“403 Forbidden”了。百度上说新浪开启了防盗链，查遍了网上一堆复制/粘贴出来的文章，不是开启反向代理就是更改请求头，但是这些方案都不行。最终方案只能自己自建图床（其实自建图床的成本并不高，只是自己懒得搞，注册了个七牛云的对象存储，但是需要绑定域名，所以上次只能作罢，这次就使用腾讯COS作为图床吧~），为了批量把文档中的新浪图片替换成腾讯cos，也是花了丢时间写了个脚本用于批量替换。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"context"</span></span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"io"</span></span><br><span class="line"><span class="string">"io/fs"</span></span><br><span class="line"><span class="string">"net/http"</span></span><br><span class="line"><span class="string">"net/url"</span></span><br><span class="line"><span class="string">"os"</span></span><br><span class="line"><span class="string">"path"</span></span><br><span class="line"><span class="string">"path/filepath"</span></span><br><span class="line"><span class="string">"regexp"</span></span><br><span class="line"><span class="string">"strings"</span></span><br><span class="line"></span><br><span class="line"><span class="string">"github.com/tencentyun/cos-go-sdk-v5"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">sinaImageCacheDir = <span class="string">"/tmp"</span></span><br><span class="line">mdFileDir         = <span class="string">"xxx"</span></span><br><span class="line">CosSecretId       = <span class="string">"xxx"</span></span><br><span class="line">CosSecretKey      = <span class="string">"xxx"</span></span><br><span class="line">CosBucketUrl      = <span class="string">"xxx"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> mdFile <span class="keyword">struct</span> &#123;</span><br><span class="line">fileName       <span class="keyword">string</span></span><br><span class="line">filePath       <span class="keyword">string</span></span><br><span class="line">fileContent    <span class="keyword">string</span></span><br><span class="line">sinaImages     <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line">downloadImages <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line">cosImages      <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> err := createSinaImageCacheDir(); err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">mdFiles, err := listFileContent(mdFileDir)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, file := <span class="keyword">range</span> mdFiles &#123;</span><br><span class="line">downloadSinaImage(file)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, file := <span class="keyword">range</span> mdFiles &#123;</span><br><span class="line">uploadTencentCos(file)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, file := <span class="keyword">range</span> mdFiles &#123;</span><br><span class="line">replaceSinaImageUrl(file)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">createSinaImageCacheDir</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">_, err := os.Stat(sinaImageCacheDir)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">if</span> os.IsNotExist(err) &#123;</span><br><span class="line"><span class="keyword">return</span> os.Mkdir(sinaImageCacheDir, fs.ModePerm)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">listFileContent</span><span class="params">(dir <span class="keyword">string</span>)</span> <span class="params">([]*mdFile, error)</span></span> &#123;</span><br><span class="line">fileName, err := filepath.Glob(fmt.Sprintf(<span class="string">"%s/*.md"</span>, dir))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">mdFiles := <span class="built_in">make</span>([]*mdFile, <span class="number">0</span>, <span class="built_in">len</span>(fileName))</span><br><span class="line"><span class="keyword">for</span> _, name := <span class="keyword">range</span> fileName &#123;</span><br><span class="line"><span class="comment">// 获取文件列表</span></span><br><span class="line">_, file := path.Split(name)</span><br><span class="line">fileContent, err := os.ReadFile(name)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取新浪图片</span></span><br><span class="line">simaImages := <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;&#125;</span><br><span class="line">sinaImages := regexp.MustCompile(<span class="string">`https://tva1.sinaimg.cn/large/(\w+).jpg|.jpeg|.png`</span>).FindAllString(<span class="keyword">string</span>(fileContent), <span class="number">-1</span>)</span><br><span class="line">fmt.Printf(<span class="string">"sinaimages"</span>)</span><br><span class="line"><span class="keyword">for</span> _, imageUrl := <span class="keyword">range</span> sinaImages &#123;</span><br><span class="line">_, imageName := path.Split(imageUrl)</span><br><span class="line">simaImages[imageUrl] = imageName</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">mdFiles = <span class="built_in">append</span>(mdFiles, &amp;mdFile&#123;</span><br><span class="line">fileName:    file,</span><br><span class="line">filePath:    name,</span><br><span class="line">fileContent: <span class="keyword">string</span>(fileContent),</span><br><span class="line">sinaImages:  simaImages,</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> mdFiles, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">downloadSinaImage</span><span class="params">(file *mdFile)</span></span> &#123;</span><br><span class="line">downloadImagePaths := <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> sinaImageUrl, sinaFileName := <span class="keyword">range</span> file.sinaImages &#123;</span><br><span class="line">resp, err := http.Get(sinaImageUrl)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> || resp.StatusCode != http.StatusOK &#123;</span><br><span class="line">fmt.Printf(<span class="string">"failed get sina image, error=%v, statusCode=%v"</span>, err, resp.StatusCode)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fileName := fmt.Sprintf(<span class="string">"%s/%s"</span>, sinaImageCacheDir, sinaFileName)</span><br><span class="line">out, err := os.Create(fileName)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"failed create file, err=%v, fileName=%v"</span>, err, fileName)</span><br><span class="line">_ = resp.Body.Close()</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 然后将响应流和文件流对接起来</span></span><br><span class="line">_, err = io.Copy(out, resp.Body)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"failed io.Copy, err=%v"</span>, err)</span><br><span class="line">_ = resp.Body.Close()</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">downloadImagePaths[sinaImageUrl] = fileName</span><br><span class="line">_ = resp.Body.Close()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">file.downloadImages = downloadImagePaths</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 上传到腾讯cos</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">uploadTencentCos</span><span class="params">(file *mdFile)</span></span> &#123;</span><br><span class="line">u, _ := url.Parse(CosBucketUrl)</span><br><span class="line">b := &amp;cos.BaseURL&#123;BucketURL: u&#125;</span><br><span class="line">c := cos.NewClient(b, &amp;http.Client&#123;</span><br><span class="line">Transport: &amp;cos.AuthorizationTransport&#123;</span><br><span class="line">SecretID:  CosSecretId,</span><br><span class="line">SecretKey: CosSecretKey,</span><br><span class="line">&#125;,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">cosImageUrls := <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> sinaImageUrl, localFileName := <span class="keyword">range</span> file.downloadImages &#123;</span><br><span class="line">fd, err := os.Open(localFileName)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fileName, ok := file.sinaImages[sinaImageUrl]</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">_, err = c.Object.Put(context.Background(), fileName, fd, <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"put image to cos failed, err=%v"</span>, err)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line">cosImageUrls[sinaImageUrl] = fmt.Sprintf(<span class="string">"%s/%s"</span>, CosBucketUrl, fileName)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">file.cosImages = cosImageUrls</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">replaceSinaImageUrl</span><span class="params">(file *mdFile)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> sinaImageUrl, cosImageUrl := <span class="keyword">range</span> file.cosImages &#123;</span><br><span class="line">file.fileContent = strings.ReplaceAll(file.fileContent, sinaImageUrl, cosImageUrl)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := os.WriteFile(file.filePath, []<span class="keyword">byte</span>(file.fileContent), <span class="number">0644</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">"failed write file, err=%v"</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;近期，有同学反馈，所有调用的微博图床图片都无法加载并提示“403 Forbidden”了。百度上说新浪开启了防盗链，查遍了网上一堆复制/粘贴
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>RFC QUIC草案文档阅读整理</title>
    <link href="https://github.com/fafucoder/2022/08/13/linux-quic/"/>
    <id>https://github.com/fafucoder/2022/08/13/linux-quic/</id>
    <published>2022-08-13T14:02:18.000Z</published>
    <updated>2024-03-18T16:50:27.337Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>quic草案的阅读总是晦涩难懂的，如果没有多读几遍，压根就不懂这说的是啥意思，阅读草案之前建议读者先了解相关知识，带着相关知识去阅读更易理解。</p><h3 id="QUIC之基础概念"><a href="#QUIC之基础概念" class="headerlink" title="QUIC之基础概念"></a>QUIC之基础概念</h3><h4 id="connection"><a href="#connection" class="headerlink" title="connection"></a>connection</h4><p>连接用途在客户端和服务器之间建立连接。和tcp不同的是(tcp是通过连接四元组[client ip、client port、server ip、server port]来确定一个连接)，quic是通过连接id(connectionId)来标识一个连接。</p><h4 id="stream"><a href="#stream" class="headerlink" title="stream"></a>stream</h4><p>stream是一个抽象的概念，它表达了一个有序传输的字节流，而这些字节其实就是由Stream Frame(一系列的帧)排在一起构成。在一个quic connection上，可以同时传输多条流。</p><p>流可以是单向的或双向的，单向流只能往一个方向传输数据，双向流允许双端向对端发送数据。</p><p>在连接中，通过StreamID来标志一个流，通过流ID的最小有效位标志流的发起者，流ID的次小有效位标志流的类型。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190017727.png" alt="stream" title="">            </p><p>流帧封装应用层发送的数据。终端使用流帧的流ID及偏移字段整理数据并将流数据以一个有序字节流传递给应用层，终端可以从一条流的同一个偏移位置多次接收数据，如果数据已经被接收过，则直接丢弃此数据。</p><h4 id="流状态"><a href="#流状态" class="headerlink" title="流状态"></a>流状态</h4><p>流有两种状态，分为发送流和接收流</p><p>在流的发送部分，应用层协议可以：</p><ul><li>写数据，只有当流量控制给数据写出留足空间，才能成功写入；</li><li>结束流（清理并关闭），发送一个设置FIN位为1的流帧；</li><li>重置流（中止并关闭），当流未处在终止状态时发送一个RESET_STREAM帧。</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190019877.png" alt="流发送状态" title="">            </p><p>在流的接收部分，应用层协议可以:</p><ul><li>读数据</li><li>中止读取流数据并请求关闭流，该操作可能需要发送STOP_SENDING帧</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190020930.png" alt="流接收状态" title="">            </p><h4 id="Frame帧"><a href="#Frame帧" class="headerlink" title="Frame帧"></a>Frame帧</h4><p>在对QUIC数据包进行解密且去除掉header后，packet的荷载里都是frame（至少包括1个）。</p><p>如果packet的荷载里，不包括ACK, PADDING, CONNECTION_CLOSE这种三种类型的帧，那么这个packet则被定义为ack确认帧，意味着对端必须对这种packet生成相应的ack通知发送方，以确保数据没有丢失。</p><p>packet的荷载里frames的类型在多达30种类型，每种类型都有自己的应用场景，如ACK Frame用于可靠传输（Recovery)，Crypto用于安全传输（TLS握手），Stream Frame用于业务数据传递，MAX_DATA/DATA_BLOCKED用于流控，PING Frame可以用于mtu探测，具体参考(<a href="https://autumnquiche.github.io/RFC9000_Chinese_Translation/#19_Frame_Types_and_Formats" target="_blank" rel="noopener">https://autumnquiche.github.io/RFC9000_Chinese_Translation/#19_Frame_Types_and_Formats</a>)</p><h4 id="QUIC数据包"><a href="#QUIC数据包" class="headerlink" title="QUIC数据包"></a>QUIC数据包</h4><p>一个UDP报文包含一个或者多个数据包，QUIC定义了两类数据包头，长包头和短包头。区分长包头还是短包头主要是根据第一个字节的最高位来区分。</p><p>除了1-RTT属于短包头外，其余的数据包都属于长包头(initial包，0-RTT包，handshake包,重试数据包都属于长包头)，长包头被用于在1-RTT密钥建立前发送的数据包。一旦有了1-RTT密钥，发送方就会改用短包头发送数据包</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190023285.png" alt="数据包" title="">            </p><h3 id="QUIC之地址校验"><a href="#QUIC之地址校验" class="headerlink" title="QUIC之地址校验"></a>QUIC之地址校验</h3><p>地址校验主要是用于确保端点不会被用于流量放大攻击（traffic amplification attack）。攻击者如果伪造数据包的源地址为受害者的地址，发送大量的数据包给服务端，如果服务端没有进行地址验证，直接响应大量数据包给源地址（受害者），就会被攻击者利用、进行流量放大攻击。</p><p>QUIC 针对放大攻击的主要防御措施是验证端点是否能够在其声明的传输地址接收数据包。地址验证在连接建立（connection establishment）期间和连接迁移（connection migration）期间进行。</p><h4 id="连接建立时的地址校验"><a href="#连接建立时的地址校验" class="headerlink" title="连接建立时的地址校验"></a>连接建立时的地址校验</h4><p>连接建立时，为了验证客户端的地址是否是攻击者伪造的，服务端会生成一个令牌（token）并通过重试包（Retry packet）响应给客户端。客户端需要在后续的初始包（Initial packet）带上这个令牌，以便服务端进行地址验证。</p><p>服务端可以在当前连接中通过 NEW_TOKEN 帧预先发布令牌，以便客户端在后续的新连接使用，这是 QUIC 实现 0-RTT 很重要的一个功能。</p><p>重试数据包（Retry packet）中提供的令牌只能立即使用，不能用于后续连接的地址验证。而 NEW_TOKEN 帧生成的令牌可以在一个时间范围内使用，这个令牌应该有一个过期时间，可以是显式的过期时间，也可以是可用于动态计算过期时间的时间戳（timestamp）。服务端可以存储过期时间，也可以在令牌中以加密的形式包含它。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190024609.png" alt="建连" title="">            </p><p><strong>需要注意的是：</strong></p><ol><li><p>在验证客户端的地址之前，服务端发送的字节数不能超过它接收到的字节数的三倍，用于避免攻击者在地址验证之前进行放大攻击。</p></li><li><p>客户端必须确保初始数据包（Initial packets）的大小至少1200 字节，如果少于 1200 字节则可以添加 PADDING 帧填充。</p></li><li><p>如果客户端没有收到来自服务端发送的初始数据包或者握手包，而且客户端没有发送额外的初始包或者握手包，那么这会引发死锁。因此为了防止死锁，客户端必须在探测超时(PTO)时（重新）发送数据包，如果客户端没有握手秘钥，那么他必须（重新）发送初始化数据包，如果有握手秘钥，那么它应该发送一个握手数据包</p></li></ol><h4 id="路径验证"><a href="#路径验证" class="headerlink" title="路径验证"></a>路径验证</h4><p>路径验证用于（端点）连接迁移时校验更新后路径是否可达，在路径验证中，端点会校验本地地址和对端地址间的可达性(这里说的地址是指IP+端口组成的二元组)</p><p>路径验证测试是在一条路径上发送给对端的数据包有没有被对端收到，使用地址验证用于确保端点从迁移方收到的数据包不携带伪造的源地址。</p><h5 id="启动路径验证"><a href="#启动路径验证" class="headerlink" title="启动路径验证"></a>启动路径验证</h5><p>端点通过发送一个PATH_CHALLENGE帧用于启动路径验证，PATH_ChALLENGE帧中的必须包含一个不可预测的 payload，以便它可以将对端响应的PATH_CHALLENGE帧关联起来。</p><p>端点可以发送多个 PATH_CHALLENGE 帧以防止数据包丢失。但是不应该在同一个数据包（packet）中发送多个 PATH_CHALLENGE 帧，而是要分别在不同的数据包（packet）中发送。</p><p>端点必须将包含PATH_CHALLENGE帧的数据包扩展到至少1200字节(如果不足1200字节需要使用PADDING帧填充)</p><p>端点不应该以高于初始化数据包(Initial包)的频率发送PATH_CHALLENGE帧，以确保连接迁移不会比建立新连接带来更多的载荷</p><h5 id="响应路径验证"><a href="#响应路径验证" class="headerlink" title="响应路径验证"></a>响应路径验证</h5><p>端点在接收到 PATH_CHALLENGE 帧时，必须通过 PATH_RESPONSE 帧响应，PATH_RESPONSE帧的payload跟PATH_CHALLENGE帧一致。除非受到拥塞控制（congestion control）的限制，否则端点不得延迟传输包含 PATH_RESPONSE 帧的数据包。</p><p>PATH_RESPONSE 帧必须在接收到 PATH_CHALLENGE 的那条路径上发送。</p><p>端点(也)必须将包含 PATH_RESPONSE 帧的数据报扩展到 至少1200 字节。</p><p>端点不能发送多个 PATH_RESPONSE 帧来响应一个 PATH_CHALLENGE 帧。</p><h3 id="QUIC之版本协商"><a href="#QUIC之版本协商" class="headerlink" title="QUIC之版本协商"></a>QUIC之版本协商</h3><p>QUIC标准化的过程中，发布了多个版本的草案，市面上的QUIC协议实现可能基于不同的版本(daft-29,daft-30),这意味着客户端跟服务端支持的quic协议版本不一样，因此在建立连接时需要先进行版本协商，使用双发都支持的一个版本。</p><h4 id="发送版本协商包"><a href="#发送版本协商包" class="headerlink" title="发送版本协商包"></a>发送版本协商包</h4><p>客户端发送的第一个包(Initial包)决定服务端是否发送版本协商包，客户端和服务端创建连接时，客户端在首次发起请求时需要带上它支持的协议版本号。</p><ul><li>如果服务端可以支持客户端的版本， 服务端将为连接的整个生命周期使用这个协议版本。</li><li>如果服务端不支持该版本，服务端将发送版本协商包附上它所支持的版本集合，这将增加 1-RTT（Round-Trip Time） 的延迟开销。</li></ul><p>需要注意的是：</p><ul><li>为了减少放大攻击（amplification attacks），QUIC 协议要求客户端发送的初始数据包大小（Initial Datagram Size）最少为 1200 字节。如果初始数据包小于 1200 字节，需要使用 PADDING frame 填充，不然该数据包会被服务端丢弃。</li><li>只有服务端可以发送版本协商包（Version Negotiation packet），客户端不能发送。</li><li>服务端识别到 0-RTT 数据包（之前有成功连接过），可以选择不发送版本协商包，以减少额外的 1-RTT 版本协商延迟。</li><li>服务端响应发送的初始（Initial）数据包或版本协商数据包可能丢失，客户端可以继续发送新的数据包、直到它成功接收到服务端响应，或者放弃连接尝试。</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190025235.png" alt="数据包格式" title="">            </p><h4 id="处理版本协商包"><a href="#处理版本协商包" class="headerlink" title="处理版本协商包"></a>处理版本协商包</h4><p>客户端收到版本协商包后，从服务端所支持的版本集合里面挑选它所支持的版本。</p><ul><li>如果所有的版本都不支持，则客户端需要丢弃连接。</li><li>如果有匹配到支持的版本，客户端尝试使用该版本创建新连接。新连接必须使用一个新的随机目标连接ID（Destination Connection ID）。</li></ul><p>如果客户端已接收并成功处理了任何其他包（包括早期的版本协商包），则客户端必须丢弃它后来新收到的版本协商包。</p><p><strong>关于QUIC的版本：</strong></p><p>QUIC 版本使用 32 位无符号数字标识，版本号 0x00000000 被保留用来表示版本协商。版本号 0x00000001 作为 RFC 发布的协议版本</p><p>0x?a?a?a?a 格式的版本号被保留（reserved）用于强制执行版本协商</p><h4 id="版本协商包格式"><a href="#版本协商包格式" class="headerlink" title="版本协商包格式"></a>版本协商包格式</h4><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190025535.png" alt="quic版本" title="">            </p><p>注意事项：</p><ul><li>版本协商包不需要 ACK</li><li>版本协商包没有 Packet Number 和 Length 字段。因此，它将使用整个 UDP 数据报（datagram）。</li><li>服务端不能在单个 UDP 数据报（datagram）里面发送多个版本协商包。</li></ul><h3 id="QUIC之连接建立"><a href="#QUIC之连接建立" class="headerlink" title="QUIC之连接建立"></a>QUIC之连接建立</h3><h4 id="Packet-Number及其上下文"><a href="#Packet-Number及其上下文" class="headerlink" title="Packet Number及其上下文"></a>Packet Number及其上下文</h4><p>Packet Number 为整型变量，其值在 0 到 2^62-1 之间，它也用于生成数据包加密所需的 nonce。通讯双方维护各自的 Packet Number 体系， 并且分为三个独立的上下文空间:</p><ul><li>Initial 空间：所有的 Initial 数据包的 Packet Number 均在这个上下文空间里；</li><li>Handshake 空间：所有的握手数据包；</li><li>应用数据空间：所有的 0-RTT 和 1-RTT 包。</li></ul><p>所谓的 Packet Number 空间，指得是一种上下文关系，在这个上下文关系里，数据包被处理, 被确认。数据包在不同的Packet Number Namespace有不同的加密等级，初始数据包只能使用初始数据包专用的密钥，也只能确认初始数据包。类似的， 握手包只能使用握手包专用的密钥，也只能确认握手数据包。</p><p>从 Initial 阶段进入 Handshake 阶段后， Initial 阶段使用的密钥就可以被丢弃了，0-RTT 和 1-RTT 共享同一个 Packet Number 空间，这样做是为了更容易实现这两类数据包的丢包处理算法。</p><p>在同一连接同一个 Packet Number 空间里，你不能复用包号，包号必须是单调递增的，当然，具体实现的时候草案并不强制要求每次都递增1， 你可以递增 20，30。当 Packet Number 达到 2^62 -1 时，发送方必须关闭该连接。</p><p>需要注意的是：在特定的包号空间里，有些帧是被禁止使用的(<a href="https://autumnquiche.github.io/RFC9000_Chinese_Translation/#12.4_Frames_and_Frame_Types" target="_blank" rel="noopener">https://autumnquiche.github.io/RFC9000_Chinese_Translation/#12.4_Frames_and_Frame_Types</a>)</p><ul><li>Padding帧、Ping帧和Crypto帧<em>可以</em>出现在任何数据包号空间中。</li><li>标志着QUIC层错误（类型为<code>0x1c</code>）的连接关闭帧<em>可以</em>出现在任何数据包号空间中。标志着应用错误（类型为<code>0x1d</code>）的连接关闭帧<em>必须</em>只能出现在应用数据空间中。</li><li>ACK帧<em>可以</em>出现在任何数据包号空间中，但是只能确认在同一个数据包号空间中的数据包。0-RTT数据包不能包含ACK帧。</li><li>所有其他类型的帧<em>必须</em>只能出现在应用数据空间中。</li></ul><h4 id="连接之建立过程"><a href="#连接之建立过程" class="headerlink" title="连接之建立过程"></a>连接之建立过程</h4><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190026147.png" alt="连接建立" title="">            </p><h4 id="传输参数定义"><a href="#传输参数定义" class="headerlink" title="传输参数定义"></a>传输参数定义</h4><p>在连接建立期间，双端会对各自的传输参数作出验证声明，传输参数通过在quic_transport_parameters字段中定义，传输参数包括最大超时时间(max_idle_timeout)， 无状态重置令牌(stateless_reset_token), 最大udp有效载荷（max_udp_payload_size），初始最大数据量(initial_max_data)等,具体参数参考(<a href="https://autumnquiche.github.io/RFC9000_Chinese_Translation/#18_Transport_Parameter_Encoding" target="_blank" rel="noopener">https://autumnquiche.github.io/RFC9000_Chinese_Translation/#18_Transport_Parameter_Encoding</a>)</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190026076.png" alt="传输参数定义" title="">            </p><p>同一个传输参数在特定的传输扩展中不能声明多次，一旦握手完成，由对端声明的传输参数就生效了</p><p>开启0-RTT，终端需要保存服务端传输参数的值及在连接上收到的任何会话票据（session ticket），终端也需要保存其他任何应用协议或加密握手所需要的信息。但是需要注意的是不是所有的传输参数都需要保存(因为部分参数不会在0-RTT建连期间起作用)， ack_delay_exponent、max_ack_delay、initial_source_connection_id等这些个参数的值不能被保存。</p><p>如果终端不支持某一些传输参数，那么必须忽略他(例如B定义了一个传输参数x, A不支持此参数，那么需要忽略他)</p><h4 id="协商连接ID"><a href="#协商连接ID" class="headerlink" title="协商连接ID"></a>协商连接ID</h4><p>当客户端发送初始包时，会生成不可预测值填充发送的初始包的目标连接ID字段。目标连接ID的长度必须至少8字节。客户端在一条连接上必须使用同一个目标连接ID，直到收到服务端发来的数据包为止。</p><p>客户端发送的首个初始数据包的目标连接ID字段用于确定初始数据包的包保护密钥。这些密钥在收到重试数据包后变更。(初始秘钥值是通过对值为0x38762cf7f55934b34d179ae6a4c80cadccbb7f0a的盐和值为目标连接ID进行HKDF算法而得出）</p><p>在首次收到从服务端发来的初始数据包或重试数据包后，客户端将服务的提供的源连接ID作为后续发送数据包的目标连接ID，包括任何0-RTT包。这意味着客户端可能需要在连接建立阶段将目标连接ID字段的值变更两次：一次是响应服务端发来的重试数据包，一次是响应服务端发来的初始数据包</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190027319.png" alt="协商连接ID" title="">            </p><h3 id="QUIC之连接迁移"><a href="#QUIC之连接迁移" class="headerlink" title="QUIC之连接迁移"></a>QUIC之连接迁移</h3><p>TCP的连接标识是通过“源ip+源端口+目标ip+目标端口+协议”唯一五元组构成，当其中的任何一个变量改变，都会造成tcp的重新建连。而quic也有唯一标识，他是通过一个64位的Connection ID表示，当用户在WIFI和移动网络发生网络切换时，用户的IP和Port可能会发生改变，但是quic的Connection ID不会发生改变，因此无需重新建立连接，这种用户无感知的网络切换，叫做连接迁移。</p><p> 探测帧（<strong>probing frames</strong>）：PATH_CHALLENGE、PATH_RESPONSE、NEW_CONNECTION_ID、PADDING，发起的包是探测包（<strong>probing packet</strong>）</p><p>非探测帧（<strong>non-probing frames</strong>）：除探测帧之外的是非探测帧，发起的包是非探测包（<strong>non-probing packet</strong>）</p><h4 id="探测新路径"><a href="#探测新路径" class="headerlink" title="探测新路径"></a>探测新路径</h4><p>在发起连接之前，客户端会发起探测帧，校验新地址到对端是否可达，也就是路径验证，校验不通过表示该条路径不通，但不会关闭连接。</p><h4 id="发起连接迁移"><a href="#发起连接迁移" class="headerlink" title="发起连接迁移"></a>发起连接迁移</h4><p>客户端发起非探测帧数据包给对端，从而实现连接迁移。新路径没有老路径的发送速率，所以新路径会重置拥塞控制和RTT相关配置。</p><h4 id="响应连接"><a href="#响应连接" class="headerlink" title="响应连接"></a>响应连接</h4><p>当对端收到非探测帧数据包时，意味着连接迁移成功。</p><h3 id="QUIC之连接关闭"><a href="#QUIC之连接关闭" class="headerlink" title="QUIC之连接关闭"></a>QUIC之连接关闭</h3><p>共有三种方法终止一个已建立的quic连接：</p><ol><li>空闲超时</li><li>立即关闭</li><li>无状态重置</li></ol><h4 id="空闲超时"><a href="#空闲超时" class="headerlink" title="空闲超时"></a>空闲超时</h4><p>每个终端会在传输参数中指定的最大空闲超时时间(max_idle_timeout)，如果某个连接持续空闲时间超过了两个终端宣告的max_idle_timeout中较小值，那么这个连接就会被静默关闭，并且它的状态会被丢弃。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190030296.png" alt="空闲超时" title="">            </p><p>当终端收到并且处理了一个来自对端的数据包时，终端需要重置它的空闲计时器。当正要发送ACK触发包时，如果自上一次接收并处理数据包后还没有发送过任何ACK触发包，那么终端也会重启它的空闲计时器。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190031587.png" alt="空闲超时" title="">            </p><p>如果终端正在等待响应数据但是没有或无法发送应用数据，那么终端可能需要发送ACK触发包以避免空闲超时。终端可以定期发送一个Ping帧，使得对端重置自己的空闲超时定时器。</p><h4 id="立即关闭"><a href="#立即关闭" class="headerlink" title="立即关闭"></a>立即关闭</h4><p>终端可以发送连接关闭帧(CONNECTION_CLOSE)来终止连接，连接关闭帧会使所有的流都被立即关闭，当终端发起立即关闭帧后进入关闭状态，当终端收到连接关闭帧后立即进入排空状态(排空状态许多方面都跟关闭状态一致，但是处于排空状态的终端必须不发送任何数据包。一旦连接处于排空状态，就没有必要再保留数据包保护密钥了)</p><p>终端在收到连接关闭帧后在进入排空状态之前可以发送一个包含连接关闭帧的数据包。</p><p>处于关闭状态的连接在收到连接关闭帧后可以转化为排空状态</p><h4 id="无状态重置"><a href="#无状态重置" class="headerlink" title="无状态重置"></a>无状态重置</h4><p>崩溃或中断可能造成对端持续向一个没有正常地维持连接的终端发送数据，终端可以在接收到一个它无法关联到某个活跃连接的数据包时发送无状态重置作为响应。</p><p>无状态重置不适合用来表明在活跃连接中出现的错误。想要传达致命的连接错误这一消息的终端在有能力的情况下必须使用连接关闭帧。</p><p>为了支持无状态重置，终端会签发一个无状态重置令牌，它是一个难以猜测的16字节长的值。如果对端后续收到了无状态重置，也就是一个以那个无状态重置令牌结尾的UDP数据报，那么对端将立即结束这条连接。</p><h3 id="QUIC之不可靠传输"><a href="#QUIC之不可靠传输" class="headerlink" title="QUIC之不可靠传输"></a>QUIC之不可靠传输</h3><p>QUIC传输协议[RFC9000]为传输可靠的应用数据流提供了一个安全的、多路复用的连接。QUIC使用携带了多种类型帧的数据包传输数据，需要可靠传输的应用数据流使用 STREAM 帧发送。但是有些应用，尤其是需要传输实时数据的应用，更倾向于使用不可靠传输，QUIC为了实现不可靠传输，通过定义新的Datagram帧类型。</p><h4 id="Datagram帧传输参数"><a href="#Datagram帧传输参数" class="headerlink" title="Datagram帧传输参数"></a>Datagram帧传输参数</h4><ol><li><p>QUIC在握手期间可以用传输参数(name=max_datagram_frame_size，value=0x20)来通告对端是否支持datagram帧，默认值为0表示不支持DATAGRAM帧，大于 0 的值表示端点支持 DATAGRAM 帧类型并且告诉对端自己可以接收datagram帧的最大长度。</p></li><li><p>端点在握手期间（如果使用0-RTT，则是上一次握手期间），在未收到具有非零值的 max_datagram_frame_size 传输参数之前，不得发送 DATAGRAM 帧， 端点不得发送大于对端通告的 max_datagram_frame_size 长度的 DATAGRAM 帧，如果未收到是否支持datagram帧的通道而收到datagram帧，那么需要以PROTOCOL_VIOLATION类型错误而终止。</p></li><li><p>max_datagram_frame_size 传输参数可以是单向的，也就是可以单端使用。</p></li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190037357.png" alt="不可靠传输" title="">            </p><h4 id="Datagram帧的发送和接收处理"><a href="#Datagram帧的发送和接收处理" class="headerlink" title="Datagram帧的发送和接收处理"></a>Datagram帧的发送和接收处理</h4><ol><li><p>当应用在QUIC连接上发送数据报时，QUIC将生成一个新的 DATAGRAM 帧并在第一个可用数据包中发送。该帧应该尽快投递并且可以与其他帧合并。当 QUIC 端点接收到一个有效的 DATAGRAM 帧时，应该立即传递给应用。</p></li><li><p>与 STREAM 帧一样，DATAGRAM 帧包含应用数据，并且必须使用 0-RTT 或 1-RTT 密钥进行保护。</p></li><li><p>虽然 DATAGRAM 帧在丢包检测时不会重传，但它们也是 ACK 触发帧，所以接收方应该支持延迟发送 ACK 帧以对接收到仅包含 DATAGRAM 帧的数据包做出响应，因为即使这些包短期内未被确认，发送方也不会采取任何行动。</p></li><li><p>与任何 ACK 触发帧一样，当发送方怀疑仅包含 DATAGRAM 帧的数据包丢失时，它会发送探测包以引发更快的 ACK 确认。如果发送方检测到包含特定 DATAGRAM 帧的数据包可能已经丢失，则QUIC实现可以通知应用它认为数据报已丢失了。</p></li><li><p>如果包含 DATAGRAM 帧的数据包被确认，则QUIC实现可以通知发送方，应用数据报已被成功发送和接收，需要注意的是，对 DATAGRAM 帧的确认仅表明接收方的传输层接收并处理了该帧，并不保证接收方的应用层成功处理了该数据。</p></li><li><p>DATAGRAM帧没有明确的流控信号且DATAGRAM帧不影响其他流的流量控制(也就是DATAGRAM帧不在stream的流量控制范围内)。</p></li><li><p>DATAGRAM 帧的拥塞控制属于连接级别，QUIC实现可以选择让应用指定一个发送过期时间，超过该时间，受拥塞控制的 DATAGRAM 帧应该被丢弃不传输。</p></li></ol><h3 id="MPQUIC实现多路径传输"><a href="#MPQUIC实现多路径传输" class="headerlink" title="MPQUIC实现多路径传输"></a>MPQUIC实现多路径传输</h3><h4 id="多路径协商"><a href="#多路径协商" class="headerlink" title="多路径协商"></a>多路径协商</h4><ol><li>端点在握手期间需要使用传输参数(enable_multipath（实验使用0xbabf))来协商是否支持多路径，0表示禁用多路径。</li><li>如果任何一个端点，该参数不存在或设置为 0，则端点必须回退到QUIC模式</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190039195.png" alt="mp-quic" title="">            </p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190042582.png" alt="multipath" title="">            </p><h4 id="路径的启动和关闭"><a href="#路径的启动和关闭" class="headerlink" title="路径的启动和关闭"></a>路径的启动和关闭</h4><ol><li><p>当协商多路径选项时，想要使用附加路径的客户端必须首先使用PATH_CHALLENGE 和 PATH_RESPONSE 帧启动地址验证过程。在新路径上接收到来自客户端的数据包后，如果服务器决定使用新路径，则服务器必须执行路径验证，除非它之前已经验证了该地址。</p></li><li><p>如果验证成功，客户端可以在新路径上发送非探测的 1-RTT 数据包。服务端收到新路径上的非探测1-RTT数据包表示新路径可以使用，但是不能表示连接迁移到新路径。</p></li><li><p>每个端点可以管理一组路径，如果端点想要关闭指定路径，应该通过PATH_ABANDON帧来终止路径，一旦路径被标记为“已放弃”，端点可以释放与该路径相关的资源，例如已使用的连接ID。</p></li><li><p>当包含 PATH_ABANDON 帧的数据包对端被确认时，发送 PATH_ABANDON 帧的端点应该认为一条路径已被放弃。当释放该路径的资源时，端点应该为路径上使用的连接 ID 发送一个 RETIRE_CONNECTION_ID 帧，如果有的话。</p></li><li><p>PATH_ABANDON 帧的接收者不应立即释放其资源，而应等待接收到已使用的连接 ID的RETIRE_CONNECTION_ID 帧或 3 个 RTO 的 RETIRE_CONNECTION_ID 帧。</p></li><li><p>PATH_ABANDON 帧向接收对等方指示发送方不再打算在该路径上发送任何数据包，PATH_ABANDON 帧的接收者也可以发送一个 PATH_ABANDON 帧来表示它自己愿意不再在这条路径上发送任何数据包。</p></li><li><p>PATH_ABANDON 帧可以在任何路径上发送，而不仅仅是在要关闭的路径上发送。如果在废弃路径上发送并被认为丢失的可重传帧应该在其他路径上重传。</p></li><li><p>QUIC中如果只有一条路径存活并且服务端收到了PATH_ABANDON帧，那么服务端应该发送CONNECTION_CLOSE 帧并进入关闭状态，如果客户端在唯一一条存活路径上收到PATH_ABANDON 帧，它可能会尝试打开新路径（如果可用），并且仅在路径验证失败或从服务器接收到 CONNECTION_CLOSE 帧时才启动连接关闭。</p></li><li><p>在路径验证过程中，端点可以通过不发送PATH_RESPONSE帧来拒绝对等方发起的新路径建立。</p></li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190042194.png" alt="状态机" title="">            </p><h4 id="路径的状态管理"><a href="#路径的状态管理" class="headerlink" title="路径的状态管理"></a>路径的状态管理</h4><p>端点使用 PATH_STATUS 帧来通知对等方应该按照这些帧表达的偏好发送数据包。需要注意的是，端点可能不遵循对等方的通告</p><p>PATH_STATUS 帧描述了路径的两种状态：</p><ul><li>将路径标记为“可用”，即允许在当前路径上发送流量。</li><li>将路径标记为“备用”，即建议如果另一条路径可用，则不应在该路径上发送流量。</li></ul><p>端点使用 PATH_STATUS 帧中的路径标识符字段来标识哪个路径的状态将被更改。PATH_STATUS 帧可以通过不同的路径发送。如果端点收到的路径状态帧会使所有路径都不可用，那么端点可以忽略PATH_STATUS帧。</p><p>PATH_STATUS利用PATH_ID来标识它希望改变哪个路径的状态。另外利用path-status-seq-num来标记PATH_STATUS的有效性：</p><ol><li>这是个递增的序号，只有收到更高序号的PATH_STATUS才判定为有效</li><li>不同路径上的序号不互相干涉，也就是说如果收到path-1上的高序号，不会去影响path2的状态。</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403190043914.png" alt="image-20230213233905630" title="">            </p><h4 id="路径的拥塞控制"><a href="#路径的拥塞控制" class="headerlink" title="路径的拥塞控制"></a>路径的拥塞控制</h4><ol><li>每个路径需要为每个路径都维护一个独立的拥塞状态</li><li>mpquic连接和普通quic连接并不使用统一的拥塞控制机制，因为这可能导致带宽分配不均，带宽会被倾向分配给多路径的connection。</li><li>协议推荐在mpquic下使用LIA拥塞控制机制：RFC6356</li></ol><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://autumnquiche.github.io/RFC9000_Chinese_Translation" target="_blank" rel="noopener">RFC9000中译：QUIC传输协议</a></li><li><a href="https://autumnquiche.github.io/RFC9001_Chinese_Translation/#6_Key_Update" target="_blank" rel="noopener">QUIC-TLS协议中文版（rfc9001）</a></li><li><a href="https://datatracker.ietf.org/doc/draft-ietf-quic-multipath/" target="_blank" rel="noopener">MP-QUIC协议草案</a></li><li><a href="https://datatracker.ietf.org/doc/pdf/draft-bonaventure-iccrg-schedulers-02" target="_blank" rel="noopener">多路径调度器草案</a></li><li><a href="https://github.com/alibaba/xquic">https://github.com/alibaba/xquic</a>)</li><li><a href="https://www.sofastack.tech/blog/deeper-into-http/3-evolution-of-the-protocol-from-the-creation-and-closing-of-quic-links/" target="_blank" rel="noopener">深入 HTTP/3（一）｜从 QUIC 链接的建立与关闭看协议的演进</a></li><li><a href="https://segmentfault.com/a/1190000041890681" target="_blank" rel="noopener">深入 HTTP/3（2）｜不那么 Boring 的 SSL</a></li><li><a href="https://segmentfault.com/a/1190000038852325" target="_blank" rel="noopener">揭秘QUIC的性能与安全</a></li><li><a href="https://segmentfault.com/a/1190000040916771" target="_blank" rel="noopener">积跬步至千里：QUIC 协议在蚂蚁集团落地之综述</a></li><li><a href="https://cloud.tencent.com/developer/article/1908451" target="_blank" rel="noopener">提速 30%！腾讯TQUIC 网络传输协议</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=MzI2NjIzNTcyNA==&mid=2247508340&idx=1&sn=ab28ba4d5033aad62ef4912ca978ea14&chksm=ea93db57dde452412c426b024b77ee9af73292f32f741308f243f52656fdc06d707dc9ec1408&mpshare=1&scene=1&srcid=0808E1BLOg7emAKOEllJ0aoj&sharer_sharetime=1659944636274&sharer_shareid=4610f9caf4ad583725d03d2e1814afc7&st=F4ECAAEE693B943F0ABBDE227020B17ED1D57ADB2D546FB41469E21E7D143BF2AF405C33E5104FEDA6CFF90669705D98C29B165EA337F4CA97F71641F51F1DA60EDAC285D689B9CE3066254A14F13EB5958F7E5E95DE6285E41B035F162306AB90AEB04EB01A2C779337FB3C6931BE8D25F27AB45B8C0A9B1DAB02EC2EC6CAC7A8EFF629835963597034EE4BA66F5AE026D885A81180A81D09A801DF8E39BDC3381012FCC6770A3B3C2AF7144BCECD3F1784624A9F5B4B3A3A046B98016160D9F2939056F0EEE5ECA3D3B1C7C3C9B9140EC0FDFFB6C10DDCB411ADADF1042619B61D0D486D96093B9FD0EEDE00E3F223&vid=1688851418270751&cst=D0B9120D9A603047FC5153C2EE5E94DD44B46E2FBE6238B25F6E69D78EAB52519A6D3B4233B4C5351FC7354C47D4AD2C&deviceid=aac8bf02-19c1-4b8b-9c33-fe2985ca7604&version=4.0.9.90603&platform=mac" target="_blank" rel="noopener">淘宝多路径 quic 服务</a></li><li><a href="https://blog.csdn.net/weixin_43408952/article/details/124727681" target="_blank" rel="noopener">TLS1.2 和 TLS1.3的简要区别</a></li><li><a href="https://www.cnblogs.com/zipxzf/articles/14346467.html" target="_blank" rel="noopener">https原理–ECDHE密钥协商算法</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;quic草案的阅读总是晦涩难懂的，如果没有多读几遍，压根就不懂这说的是啥意思，阅读草案之前建议读者先了解相关知识，带着相关知识去阅读更易理解
      
    
    </summary>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/categories/linux/"/>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>linux lvs原理及实践</title>
    <link href="https://github.com/fafucoder/2021/12/19/linux-lvs/"/>
    <id>https://github.com/fafucoder/2021/12/19/linux-lvs/</id>
    <published>2021-12-19T09:01:20.000Z</published>
    <updated>2023-02-04T13:33:00.754Z</updated>
    
    <content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>负载均衡(Load Balance)的职责是将网络请求，或者其他形式的负载“均摊”到不同的机器上，让每台服务器获取到适合自己处理能力的负载。在为高负载服务器分流的同时，还可以避免资源浪费。负载均衡的原理就是当用户的请求到达前端负载均衡器(Director Server)时，通过设置好的调度算法，智能均衡的将请求分发到后端真正服务器上(Real Server)。根据请求类型的不同可以将负载均衡分为四层负载均衡(L4)和七层负载均衡(L7)， 常见的负载均衡器包括LVS，Nginx, HAProxy等。</p><h3 id="LVS概述"><a href="#LVS概述" class="headerlink" title="LVS概述"></a>LVS概述</h3><p>LVS是Linux Virtual Server的简称， 也就是 Linux 虚拟服务器，工作在 OSI 模型的传输层，即四层负载均衡。LVS主要由两部分组成，包括ipvs和ipvsadm。</p><ul><li>ipvs(ip virtual server)：工作在内核空间，是真正生效实现调度的代码。</li><li>ipvsadm：工作在用户空间，叫 ipvsadm，负责为 ipvs 内核框架编写规则，定义谁是集群服务，而谁是后端真实的服务器(Real Server)</li></ul><h3 id="LVS-基本工作原理"><a href="#LVS-基本工作原理" class="headerlink" title="LVS 基本工作原理"></a>LVS 基本工作原理</h3><p>LVS的底层是利用NETIFILTER的钩子能力：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gxv02bh3vzj30ow0c5dgv.jpg" alt="ipvs工作原理" title="">            </p><ol><li>当用户向负载均衡调度器（Director Server）发起请求，调度器将请求发往至内核空间</li><li>PREROUTING 链首先会接收到用户请求，判断目标 IP 确定是本机 IP，将数据包发往 INPUT 链</li><li>IPVS 是工作在 INPUT 链上的，当用户请求到达 INPUT 时，IPVS 会将用户请求和自己已定义好的集群服务进行比对，如果用户请求的就是定义的集群服务，那么此时 IPVS 会强行修改数据包里的目标 IP 地址及端口，并将新的数据包发往 POSTROUTING 链</li><li>POSTROUTING 链接收数据包后发现目标 IP 地址刚好是自己的后端服务器，那么此时通过选路，将数据包最终发送给后端的服务器</li></ol><h3 id="LVS-调度算法"><a href="#LVS-调度算法" class="headerlink" title="LVS 调度算法"></a>LVS 调度算法</h3><p>前言说到负载均衡器原理是根据负载均衡算法把请求转发到后端真实服务器上。LVS作为四层负载均衡器，针对不同的网络服务需求和服务器配置，LVS调度器实现了多种负载均衡调度算法，主要包括如下：</p><ol><li><p><strong>轮询调度 rr（Round Robin）</strong>：这种算法是最简单的，就是按依次循环的方式将请求调度到不同的服务器上，该算法最大的特点就是简单。轮询算法假设所有的服务器处理请求的能力都是一样的，调度器会将所有的请求平均分配给每个真实服务器，不管后端 RS 配置和处理能力，非常均衡地分发下去。</p></li><li><p><strong>加权轮叫 wrr（Weighted Round Robin）</strong>：这种算法比 rr 的算法多了一个权重的概念，可以给 RS 设置权重，权重越高，那么分发的请求数越多，权重的取值范围 0 – 100。主要是对 rr 算法的一种优化和补充，LVS 会考虑每台服务器的性能，并给每台服务器添加要给权值，如果服务器 A 的权值为 1，服务器 B 的权值为 2，则调度到服务器 B 的请求会是服务器 A 的 2 倍。权值越高的服务器，处理的请求越多。</p></li><li><p><strong>最少链接 lc（Least Connections）</strong>：这个算法会根据后端 RS 的连接数来决定把请求分发给谁，比如 RS1 连接数比 RS2 连接数少，那么请求就优先发给 RS1。</p></li><li><p><strong>加权最少链接 wlc（Weighted Least Connections）</strong>：这个算法比最少链接 lc算法多了一个权重的概念。</p></li><li><p><strong>基于局部性的最少链接 lblc（Locality-Based Least Connections）</strong>：这个算法是针对目标 IP 地址的负载均衡，目前主要用于 Cache 集群系统。该算法根据请求的目标 IP 地址找出该目标 IP 地址最近使用的服务器，若该服务器 是可用的且没有超载，将请求发送到该服务器；若服务器不存在，或者该服务器超载且有服务器处于一半的工作负载，则用 “最少链接” 的原则选出一个可用的服务 器，将请求发送到该服务器。</p></li><li><p><strong>复杂的基于局部性最少的连接算法 lblcr（Locality-Based Least Connections with Replication）*</strong>：这个算法也是针对目标 IP 地址的负载均衡，目前主要用于 Cache 集群系统。它与 LBLC 算法的不同之处是它要维护从一个 目标 IP 地址到一组服务器的映射，而 LBLC 算法维护从一个目标 IP 地址到一台服务器的映射。该算法根据请求的目标 IP 地址找出该目标 IP 地址对应的服务 器组，按 “最小连接” 原则从服务器组中选出一台服务器，若服务器没有超载，将请求发送到该服务器，若服务器超载；则按 “最小连接” 原则从这个集群中选出一 台服务器，将该服务器加入到服务器组中，将请求发送到该服务器。同时，当该服务器组有一段时间没有被修改，将最忙的服务器从服务器组中删除，以降低复制的 程度。</p></li><li><p><strong>目标地址散列 dh（Destination Hashing）</strong>：这个算法根据请求的目标 IP 地址，作为散列键（Hash Key）从静态分配的散列表找出对应的服务器，若该服务器是可用的且未超载，将请求发送到该服务器，否则返回空。</p></li><li><p><strong>源地址散列 sh（Source Hashing）</strong>：这个调度算法根据请求的源 IP 地址，作为散列键（Hash Key）从静态分配的散列表找出对应的服务器，若该服务器是可用的且未超载，将请求发送到该服务器，否则返回空。</p></li></ol><h3 id="LVS-工作模式"><a href="#LVS-工作模式" class="headerlink" title="LVS 工作模式"></a>LVS 工作模式</h3><p>根据负载均衡器对数据包的处理方式分类，LVS支持三种工作模式，分别为NAT模式，DR模式以及TUN模式。原生的LVS工作模式不支持FULLNAT，FULLNAT模式需要自己重新编译LVS。在介绍LVS工作模式之前有必要先解释下相关名词：</p><ul><li>DS：Director Server，指的是前端负载均衡器节点。</li><li>RS：Real Server，后端真实的工作服务器。</li><li>VIP：向外部直接面向用户请求，作为用户请求的目标的 IP 地址。</li><li>DIP：Director Server IP，主要用于和内部主机通讯的 IP 地址。</li><li>RIP：Real Server IP，后端服务器的 IP 地址。</li><li>CIP：Client IP，访问客户端的 IP 地址。</li></ul><h4 id="LVS-NAT模式"><a href="#LVS-NAT模式" class="headerlink" title="LVS NAT模式"></a>LVS NAT模式</h4><h5 id="LVS-NAT的工作原理"><a href="#LVS-NAT的工作原理" class="headerlink" title="LVS NAT的工作原理"></a>LVS NAT的工作原理</h5><ol><li>当用户请求到达 Director Server，此时请求的数据报文会先到内核空间的 PREROUTING 链。 此时报文的源 IP 为 CIP，目标 IP 为 VIP</li><li>PREROUTING 检查发现数据包的目标 IP 是本机，将数据包送至 INPUT 链</li><li>IPVS 比对数据包请求的服务是否为集群服务，若是，修改数据包的目标 IP 地址为后端服务器 IP，然后将数据包发至 POSTROUTING 链。 此时报文的源 IP 为 CIP，目标 IP 为 RIP</li><li>POSTROUTING 链通过选路，将数据包发送给 Real Server</li><li>Real Server 比对发现目标为自己的 IP，开始构建响应报文发回给 Director Server。 此时报文的源 IP 为 RIP，目标 IP 为 CIP</li><li>Director Server 在响应客户端前，此时会将源 IP 地址修改为自己的 VIP 地址，然后响应给客户端。 此时报文的源 IP 为 VIP，目标 IP 为 CIP</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gxv0a244zyj30ou0dtmys.jpg" alt="LVS NAT模式" title="">            </p><h5 id="LVS-NAT模式特点"><a href="#LVS-NAT模式特点" class="headerlink" title="LVS NAT模式特点"></a>LVS NAT模式特点</h5><ul><li><strong>RS 应该使用私有地址，RS 的网关必须指向 DIP。</strong></li><li><strong>DIP 和 RIP 必须在同一个网段内。</strong></li><li>支持端口映射。</li><li>RS 可以使用任意操作系统。</li><li>请求和响应报文都需要经过 Director Server，高负载场景中，Director Server 易成为性能瓶颈。</li></ul><h5 id="LVS-NAT模式实践"><a href="#LVS-NAT模式实践" class="headerlink" title="LVS NAT模式实践"></a>LVS NAT模式实践</h5><p>一、使用Docker模拟(适用于没虚拟机的情况)</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 1. 起两个nginx容器，分别是 </span></span><br><span class="line">[root@localhost ~]# docker run -d -p 8000:8000 --name first -t jwilder/whoami</span><br><span class="line">[root@localhost ~]# docker run -d -p 8001:8000 --name second -t jwilder/whoami</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 2. 获取容器的IP地址</span></span><br><span class="line">[root@localhost ~]# docker inspect -f '&#123;&#123;range .NetworkSettings.Networks&#125;&#125;&#123;&#123;.IPAddress&#125;&#125;&#123;&#123;end&#125;&#125;' first</span><br><span class="line">[root@localhost ~]# docker inspect -f '&#123;&#123;range .NetworkSettings.Networks&#125;&#125;&#123;&#123;.IPAddress&#125;&#125;&#123;&#123;end&#125;&#125;' second</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 执行curl获取结果(ip为步骤二返回的结果)</span></span><br><span class="line">[root@localhost ~]# curl 172.17.0.2:8000</span><br><span class="line">[root@localhost ~]# curl 172.17.0.3:8000</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 创建ipvs规则</span></span><br><span class="line">[root@localhost ~]# ipvsadm -A -t 192.168.56.102:80 -s rr</span><br><span class="line">[root@localhost ~]# ipvsadm -a -t 192.168.56.102:80 -r 172.17.0.2:8000 -m</span><br><span class="line">[root@localhost ~]# ipvsadm -a -t 192.168.56.102:80 -r 172.17.0.3:8000 -m</span><br><span class="line">[root@localhost ~]# ipvsadm -Ln</span><br><span class="line">TCP  192.168.56.102:80 rr</span><br><span class="line"><span class="meta">  -&gt;</span><span class="bash"> 172.17.0.2:8000              Masq    1      0          0         </span></span><br><span class="line"><span class="meta">  -&gt;</span><span class="bash"> 172.17.0.3:8000              Masq    1      0          0   </span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 验证是否生效(返回结果一次是A，一次是B)</span></span><br><span class="line">[root@localhost ~]# curl 192.168.56.102</span><br><span class="line">I'm fdb291a03b87</span><br><span class="line">[root@localhost ~]# curl 192.168.56.102</span><br><span class="line">I'm 9799eb62225c</span><br></pre></td></tr></table></figure><p>二、使用虚拟机模拟</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 前置条件：</span></span><br><span class="line"><span class="meta">#</span><span class="bash">   1. 虚拟机网段一致</span></span><br><span class="line"><span class="meta">#</span><span class="bash">   2. 服务器2，3上部署了http服务器,且内容不一致</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 在虚拟机1上执行， (本机IP为192.168.56.101)</span></span><br><span class="line">ipvsadm -A -t 192.168.57.4:80 -s rr</span><br><span class="line">ipvsadm -a -t 192.168.57.4:80 -r 192.168.56.102:80 -m</span><br><span class="line">ipvsadm -a -t 192.168.57.4:80 -r 192.168.56.104:80 -m</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"><span class="comment"># 开启ip forward</span></span></span><br><span class="line">echo 1 &gt;/proc/sys/net/ipv4/ip_forward</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 在虚拟机2，3上执行，(本机IP为192.168.56.102， 192.168.56.104), 如果默认路由不设置为虚拟机1，发现是访问不通的</span></span><br><span class="line">ip route del default</span><br><span class="line">ip route add default via 192.168.56.101</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 检查是否能够访问, 在虚拟机1上执行curl</span></span><br><span class="line">curl 192.168.57.4</span><br></pre></td></tr></table></figure><h4 id="LVS-DR模式"><a href="#LVS-DR模式" class="headerlink" title="LVS DR模式"></a>LVS DR模式</h4><p>DR（Direct Routing 直接路由模式）模式时 LVS 调度器只接收客户发来的请求并将请求转发给后端服务器，后端服务器处理请求后直接把内容直接响应给客户，而不用再次经过 LVS 调度器。LVS 只需要将网络帧的 MAC 地址修改为某一台后端服务器 RS 的 MAC，该包就会被转发到相应的 RS 处理，注意此时的源 IP 和目标 IP 都没变。RS 收到 LVS 转发来的包时，链路层发现 MAC 是自己的，到上面的网络层，发现 IP 也是自己的，于是这个包被合法地接受，RS 感知不到前面有 LVS 的存在。而当 RS 返回响应时，只要直接向源 IP（即用户的 IP）返回即可，不再经过 LVS。</p><h5 id="LVS-DR模式工作原理"><a href="#LVS-DR模式工作原理" class="headerlink" title="LVS DR模式工作原理"></a>LVS DR模式工作原理</h5><ol><li>当用户请求到达 Director Server，此时请求的数据报文会先到内核空间的 PREROUTING 链。 此时报文的源 IP 为 CIP，目标 IP 为 VIP。</li><li>PREROUTING 检查发现数据包的目标 IP 是本机，将数据包送至 INPUT 链。</li><li>IPVS 比对数据包请求的服务是否为集群服务，若是，将请求报文中的源 MAC 地址修改为 DIP 的 MAC 地址，将目标 MAC 地址修改 RIP 的 MAC 地址，然后将数据包发至 POSTROUTING 链。 此时的源 IP 和目的 IP 均未修改，仅修改了源 MAC 地址为 DIP 的 MAC 地址，目标 MAC 地址为 RIP 的 MAC 地址。</li><li>由于 DS 和 RS 在同一个网络中，所以是通过二层来传输。POSTROUTING 链检查目标 MAC 地址为 RIP 的 MAC 地址，那么此时数据包将会发至 Real Server。</li><li>RS 发现请求报文的 MAC 地址是自己的 MAC 地址，就接收此报文。处理完成之后，将响应报文通过 lo 接口传送给 eth0 网卡然后向外发出。 此时的源 IP 地址为 VIP，目标 IP 为 CIP。</li><li>响应报文最终送达至客户端。</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gxvmcgh1xwj30oy0dwq57.jpg" alt="LVS DR模式" title="">            </p><h5 id="LVS-DR模式特点"><a href="#LVS-DR模式特点" class="headerlink" title="LVS DR模式特点"></a>LVS DR模式特点</h5><ul><li>保证前端路由将目标地址为 VIP 报文统统发给 Director Server，而不是 RS。</li><li>RS 可以使用私有地址；也可以是公网地址，如果使用公网地址，此时可以通过互联网对 RIP 进行直接访问。</li><li>VIP与DIP，RIP可以不在同一网段中.</li><li>所有的请求报文经由 Director Server，但响应报文必须不能进过 Director Server。</li><li>不支持地址转换，也不支持端口映射。</li><li>RS 可以是大多数常见的操作系统。</li><li>RS 的网关绝不允许指向 DIP（因为我们不允许他经过 Director Server）。</li><li>RS 上的 lo 接口配置 VIP 的 IP 地址。</li><li><strong>RS 和 DS 必须在同一个物理网络中(同一机房中)。</strong></li></ul><h5 id="LVS-DR模式的相关问题"><a href="#LVS-DR模式的相关问题" class="headerlink" title="LVS DR模式的相关问题"></a>LVS DR模式的相关问题</h5><p>为了解决<code>保证前端路由将目标地址为 VIP 报文统统发给 Director Server，而不是 RS。</code>的问题一般有如下解决方案：</p><ul><li>在前端路由器做静态地址路由绑定，将对于 VIP 的地址仅路由到 Director Server。但用户未必有路由操作权限，因为有可能是运营商提供的，所以这个方法未必实用。</li><li>arptables：在 arp 的层次上实现在 ARP 解析时做防火墙规则，过滤 RS 响应 ARP 请求。这是由 iptables 提供的。</li><li>修改 RS 上内核参数（<code>arp_ignore</code> 和 <code>arp_announce</code>）将 RS 上的 VIP 配置在 lo 接口的别名上，并限制其不能响应对 VIP 地址解析请求。</li></ul><h5 id="关于arp-ignore和arp-announce"><a href="#关于arp-ignore和arp-announce" class="headerlink" title="关于arp_ignore和arp_announce"></a>关于arp_ignore和arp_announce</h5><p>arp_ignore：定义接收 ARP 请求时的响应级别 </p><ul><li>0：响应任意网卡上接收到的对本机 IP 地址的 ARP 请求（包括环回网卡），不论目的 IP 地址是否在接收网卡上</li><li>1：只响应目的 IP 地址为接收网卡地址的 ARP 请求 </li><li>2：只响应目的 IP 地址为接收网卡地址的 ARP 请求，且 ARP 请求的源 IP 地址必须和接收网卡的地址在同网段</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">arp_ignore - INTEGER</span><br><span class="line">    Define different modes for sending replies in response to</span><br><span class="line">    received ARP requests that resolve local target IP addresses:</span><br><span class="line">        0 - (default): reply for any local target IP address, configured on any interface</span><br><span class="line">        1 - reply only if the target IP address is local address configured on the incoming interface</span><br><span class="line">        2 - reply only if the target IP address is local address configured on the incoming interface</span><br><span class="line">            and both with the sender&#39;s IP address are part from same subnet on this interface</span><br><span class="line">        3 - do not reply for local addresses configured with scope host,</span><br><span class="line">            only resolutions for global and link addresses are replied</span><br><span class="line">        4-7 - reserved</span><br><span class="line">        8 - do not reply for all local addresses</span><br><span class="line"></span><br><span class="line">    The max value from conf&#x2F;&#123;all,interface&#125;&#x2F;arp_ignore is used</span><br><span class="line">    when ARP request is received on the &#123;interface&#125;</span><br></pre></td></tr></table></figure><p>arp_announce：定义将自己地址向外通告时的通告级别 </p><ul><li>0：允许任意网卡上的任意地址向外通告 </li><li>1：尽量仅向目标网络通告与其网络匹配的地址 </li><li>2：仅向与本地接口上地址匹配的网络进行通告</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">arp_announce - INTEGER</span><br><span class="line">    Define different restriction levels for announcing the local source IP address</span><br><span class="line">    from IP packets in ARP requests sent on interface:</span><br><span class="line">        0 - (default) Use any local address, configured on any interface</span><br><span class="line">        1 - Try to avoid local addresses that are not in the target&#39;s subnet for this interface.</span><br><span class="line">            This mode is useful when target hosts reachable via this interface require the source IP</span><br><span class="line">            address in ARP requests to be part of their logical network configured on the receiving</span><br><span class="line">            interface. When we generate the request we will check all our subnets that include the</span><br><span class="line">            target IP and will preserve the source address if it is from such subnet.</span><br><span class="line">            If there is no such subnet we select source address according to the rules for level 2.</span><br><span class="line">        2 - Always use the best local address for this target. In this mode we ignore the source</span><br><span class="line">            address in the IP packet and try to select local address that we prefer for talks</span><br><span class="line">            with the target host. Such local address is selected by looking for primary IP addresses</span><br><span class="line">            on all our subnets on the outgoing interface that include the target IP address.</span><br><span class="line">            If no suitable local address is found we select the first local address we have on the</span><br><span class="line">            outgoing interface or on all other interfaces, with the hope we will receive reply</span><br><span class="line">            for our request and even sometimes no matter the source IP address we announce.</span><br><span class="line"></span><br><span class="line">    The max value from conf&#x2F;&#123;all,interface&#125;&#x2F;arp_announce is used.</span><br><span class="line"></span><br><span class="line">    Increasing the restriction level gives more chance for receiving answer from the resolved target</span><br><span class="line">    while decreasing the level announces more valid sender&#39;s information.</span><br></pre></td></tr></table></figure><h5 id="LVS-DR模式实践"><a href="#LVS-DR模式实践" class="headerlink" title="LVS DR模式实践"></a>LVS DR模式实践</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 前置条件：</span></span><br><span class="line"><span class="meta">#</span><span class="bash">   1. 虚拟机网段一致</span></span><br><span class="line"><span class="meta">#</span><span class="bash">   2. 服务器2，3上部署了http服务器,且内容不一致</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 在虚拟机1上执行， (本机IP为192.168.56.101)</span></span><br><span class="line">ip addr add 192.168.56.200/32 dev enp0s8</span><br><span class="line">ipvsadm -A -t 192.168.56.200:80 -s wrr</span><br><span class="line">ipvsadm -a -t 192.168.56.200:80 -r 192.168.56.102:80 -g -w 1</span><br><span class="line">ipvsadm -a -t 192.168.56.200:80 -r 192.168.56.104:80 -g -w 3</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"><span class="comment"># 开启ip forward</span></span></span><br><span class="line">echo 1 &gt;/proc/sys/net/ipv4/ip_forward</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 在虚拟机2，3上执行，(本机IP为192.168.56.102， 192.168.56.104)</span></span><br><span class="line">ip addr add 192.168.56.200/32 dev lo</span><br><span class="line">ip route add 192.168.56.200 dev lo</span><br><span class="line">echo "1" &gt;/proc/sys/net/ipv4/conf/lo/arp_ignore</span><br><span class="line">echo "2" &gt;/proc/sys/net/ipv4/conf/lo/arp_announce</span><br><span class="line">echo "1" &gt;/proc/sys/net/ipv4/conf/all/arp_ignore</span><br><span class="line">echo "2" &gt;/proc/sys/net/ipv4/conf/all/arp_announce</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 检查是否能够访问, 在外部主机上执行curl</span></span><br><span class="line">curl 192.168.56.200</span><br></pre></td></tr></table></figure><h4 id="LVS-TUN-模式"><a href="#LVS-TUN-模式" class="headerlink" title="LVS TUN 模式"></a>LVS TUN 模式</h4><blockquote><p>TUN工作在三层，TAP工作在二层</p></blockquote><h5 id="LVS-TUN模式工作原理"><a href="#LVS-TUN模式工作原理" class="headerlink" title="LVS TUN模式工作原理"></a>LVS TUN模式工作原理</h5><ol><li>当用户请求到达 Director Server，此时请求的数据报文会先到内核空间的 PREROUTING 链。 此时报文的源 IP 为 CIP，目标 IP 为 VIP。</li><li>PREROUTING 检查发现数据包的目标 IP 是本机，将数据包送至 INPUT 链。</li><li>IPVS 比对数据包请求的服务是否为集群服务，若是，在请求报文的首部再次封装一层 IP 报文，封装源 IP 为 DIP，目标 IP 为 RIP。然后发至 POSTROUTING 链。 此时源 IP 为 DIP，目标 IP 为 RIP。</li><li>POSTROUTING 链根据最新封装的 IP 报文，将数据包发至 RS（因为在外层封装多了一层 IP 首部，所以可以理解为此时通过隧道传输）。 此时源 IP 为 DIP，目标 IP 为 RIP。</li><li>RS 接收到报文后发现是自己的 IP 地址，就将报文接收下来，拆除掉最外层的 IP 后，会发现里面还有一层 IP 首部，而且目标是自己的 lo 接口 VIP，那么此时 RS 开始处理此请求，处理完成之后，通过 lo 接口送给 eth0 网卡，然后向外传递。 此时的源 IP 地址为 VIP，目标 IP 为 CIP。</li><li>响应报文最终送达至客户端。</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gxvobeirpcj30p00e340q.jpg" alt="LVS TUN模式" title="">            </p><h5 id="LVS-TUN模式特点"><a href="#LVS-TUN模式特点" class="headerlink" title="LVS TUN模式特点"></a>LVS TUN模式特点</h5><ul><li>不改变请求数据包，而是在请求数据包上新增一层 IP 首部信息。因此负载均衡器不能对端口进行转发，但可以和真实服务器不在同一局域网内，且真实服务器需要支持能够解析两层 IP 首部信息，即需要支持“IP Tunneling”或“IP Encapsulation”协议</li><li>真实服务器中的 VIP，只能被自己 “看见”，其他设备不可知。因此 VIP 必须绑定在真实服务器的 lo 网卡上，并且不允许将此网卡信息经过 ARP 协议对外通告</li><li>请求的数据包经过负载均衡器后，直接由真实服务器返回给客户端，响应数据包不需要再经过负载均衡器</li><li>RS 的网关不会也不可能指向 DIP。</li></ul><h5 id="LVS-TUN模式实践"><a href="#LVS-TUN模式实践" class="headerlink" title="LVS TUN模式实践"></a>LVS TUN模式实践</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 前置条件：</span></span><br><span class="line"><span class="meta">#</span><span class="bash">   2. 服务器2，3上部署了http服务器,且内容不一致</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 在虚拟机1上执行， (本机IP为192.168.56.101)</span></span><br><span class="line">ip addr add 192.168.56.210/32 dev enp0s8</span><br><span class="line">ipvsadm -A -t 192.168.56.210:80 -s rr</span><br><span class="line">ipvsadm -a -t 192.168.56.210:80 -r 192.168.56.102:80 -i</span><br><span class="line">ipvsadm -a -t 192.168.56.210:80 -r 192.168.56.104:80 -i</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 在虚拟机2，3上执行，(本机IP为192.168.56.102， 192.168.56.104)</span></span><br><span class="line">modprobe ipip // 启用ipip</span><br><span class="line">lsmod | grep ipip</span><br><span class="line">ip addr add 192.168.56.210/32 dev tunl0</span><br><span class="line">ip link set tunl0 up</span><br><span class="line">ip route add 192.168.56.210 dev tunl0</span><br><span class="line"></span><br><span class="line">echo 1 &gt;/proc/sys/net/ipv4/conf/lo/arp_ignore</span><br><span class="line">echo 2 &gt;/proc/sys/net/ipv4/conf/lo/arp_announce</span><br><span class="line">echo 1 &gt;/proc/sys/net/ipv4/conf/all/arp_ignore</span><br><span class="line">echo 2 &gt;/proc/sys/net/ipv4/conf/all/arp_announce</span><br><span class="line">echo 0 &gt; /proc/sys/net/ipv4/conf/tunl0/rp_filter</span><br><span class="line">echo 0 &gt; /proc/sys/net/ipv4/conf/all/rp_filter</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 检查是否能够访问, 在外部主机上执行curl</span></span><br><span class="line">curl 192.168.56.210</span><br></pre></td></tr></table></figure><h4 id="LVS-FULLNAT模式"><a href="#LVS-FULLNAT模式" class="headerlink" title="LVS FULLNAT模式"></a>LVS FULLNAT模式</h4><p>LVS NAT, DR模式中，RS跟DS必须在同一个VLAN中，当集群规模较小时，使用 NAT、DR 模式都是没有问题的，当集群内有几十台以上时，那么这些服务器通常都不在同一个 VLAN/网段 内了。这时，必须再研发出一种能支持跨 VLAN/网段 通信的模式，FULLNAT 模式就是为了解决这个问题而生的。</p><p>LVS FullNAT 模式几乎和 LVS NAT 模式相同，不同之处即是：引入 Local Address（内网 IP 地址）。CIP-&gt;VIP 转换换为 LIP-&gt;RIP，而 LIP 和 RIP 均为 IDC 内网 IP，因此可以跨 VLAN 通讯。LVS原生模式不支持FULLNAT，因此需要自己手动编译内核～</p><h5 id="LVS-FULLNAT内核编译"><a href="#LVS-FULLNAT内核编译" class="headerlink" title="LVS FULLNAT内核编译"></a>LVS FULLNAT内核编译</h5><blockquote><p>注意：内核编译的内核包需要跟自己的操作系统内核版本对应上(大版本对应上~)，暂时未找到centos7对应的内核包在哪儿。</p><p>本机的内核版本为：2.6.32-220</p></blockquote><ol><li>安装依赖</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">yum -y install elfutils-devel.x86_64 audit-libs-devel.x86_64</span><br><span class="line">yum -y install rpmdevtools yum-utils</span><br><span class="line">yum -y install redhat-rpm-config</span><br><span class="line">yum -y install gcc xmlto patchutils asciidoc zlib-devel binutils-devel newt-devel python-devel hmaccalc perl-ExtUtils-Embed.x86_64</span><br><span class="line">yum -y install rng-tools 随机数生成器</span><br><span class="line">yum -y install bison</span><br></pre></td></tr></table></figure><ol start="2"><li>下载内核包，ipvs补丁包</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 内核包</span></span><br><span class="line">wget ftp://ftp.redhat.com/pub/redhat/linux/enterprise/6Server/en/os/SRPMS/kernel-2.6.32-220.23.1.el6.src.rpm</span><br><span class="line"><span class="meta">#</span><span class="bash"> lvs fullnat包</span></span><br><span class="line">wget http://kb.linuxvirtualserver.org/images/a/a5/Lvs-fullnat-synproxy.tar.gz</span><br></pre></td></tr></table></figure><ol start="3"><li>生成内核代码</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">rpm -ivh kernel-2.6.32-220.23.1.el6.src.rpm</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 生成随机数(一定要生成随机数，否则rpmbuild的时候会卡住)</span></span><br><span class="line">rngd -r /dev/urandom   </span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> rpmbuild -bp kernel.spec</span></span><br><span class="line">cd ~/rpmbuild/SPECS</span><br><span class="line"></span><br><span class="line">rpmbuild -bp --target=$(uname -m) kernel.spec</span><br></pre></td></tr></table></figure><pre><code>4. 打 LVS补丁</code></pre><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf Lvs-fullnat-synproxy.tar.gz</span><br><span class="line"></span><br><span class="line">cp ~/lvs-fullnat-synproxy/lvs-2.6.32-220.23.1.el6.patch ~/rpmbuild/BUILD/kernel-2.6.32-220.23.1.el6/linux-2.6.32-220.23.1.el6.x86_64 &amp;&amp; cd $_</span><br><span class="line"></span><br><span class="line">patch -p1 &lt; lvs-2.6.32-220.23.1.el6.patch</span><br></pre></td></tr></table></figure><ol start="5"><li>编译内核</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 最好不要重新修改内核版本(遇到过改了内核版本后，编译没问题，重启后触发Kernel panic – not syncing: Attempted to <span class="built_in">kill</span> init的问题)</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 使用-j 加快内核编译速度</span></span><br><span class="line">make -j16</span><br><span class="line">make modules_install</span><br><span class="line">make install</span><br></pre></td></tr></table></figure><ol start="6"><li>重启操作系统</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">reboot</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 重启后发现系统版本已经改变了， Linux localhost.localdomain 2.6.32 <span class="comment">#2 SMP Thu Dec 30 03:38:53 EST 2021 x86_64 x86_64 x86_64 GNU/Linux</span></span></span><br></pre></td></tr></table></figure><ol start="7"><li>编译ipvsadm, keepalived等工具</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">cd ~/lvs-fullnat-synproxy &amp;&amp; tar -zxvf lvs-tools.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 编译 keepalived</span></span><br><span class="line">cd ~/lvs-fullnat-synproxy/tools/keepalived</span><br><span class="line">./configure --with-kernel-dir="/lib/modules/`uname -r`/build" &amp;&amp; make &amp;&amp; make install</span><br><span class="line"> </span><br><span class="line"><span class="meta">#</span><span class="bash"> 编译 ipvsadm</span></span><br><span class="line">cd ~/lvs-fullnat-synproxy/tools/ipvsadm</span><br><span class="line">make &amp;&amp; make install</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 编译 quaage</span></span><br><span class="line">cd ~/lvs-fullnat-synproxy/tools/quagga</span><br><span class="line">./configure --disable-ripd --disable-ripngd --disable-bgpd --disable-watchquagga --disable-doc  --enable-user=root --enable-vty-group=root --enable-group=root --enable-zebra --localstatedir=/var/run/quagga &amp;&amp; make &amp;&amp; make install</span><br></pre></td></tr></table></figure><ol start="8"><li>验证是否编译成功</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 输入 ipvsadm -h 可以看到有fullnat 模式</span></span><br><span class="line"></span><br><span class="line">  --gatewaying   -g                   gatewaying (direct routing) (default)</span><br><span class="line">  --ipip         -i                   ipip encapsulation (tunneling)</span><br><span class="line">  --fullnat      -b                   fullnat mode</span><br><span class="line">  --masquerading -m                   masquerading (NAT)</span><br></pre></td></tr></table></figure><h5 id="LVS-FULLNAT模式工作原理"><a href="#LVS-FULLNAT模式工作原理" class="headerlink" title="LVS FULLNAT模式工作原理"></a>LVS FULLNAT模式工作原理</h5><ol><li>当用户请求到达 Director Server，此时请求的数据报文会先到内核空间的 PREROUTING 链。 此时报文的源 IP 为 CIP，目标 IP 为 VIP</li><li>PREROUTING 检查发现数据包的目标 IP 是本机，将数据包送至 INPUT 链</li><li>IPVS 比对数据包请求的服务是否为集群服务，若是，修改数据包的源IP地址分发服务器IP，目标 IP 地址为后端服务器 IP，然后将数据包发至 POSTROUTING 链。 此时报文的源 IP 为 DIP，目标 IP 为 RIP</li><li>POSTROUTING 链通过选路，将数据包发送给 Real Server</li><li>Real Server 比对发现目标为自己的 IP，开始构建响应报文发回给 Director Server。 此时报文的源 IP 为 RIP，目标 IP 为 DIP</li><li>Director Server 在响应客户端前，此时会将源 IP 地址修改为自己的 VIP 地址，然后响应给客户端。 此时报文的源 IP 为 VIP，目标 IP 为 CIP</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gxw0gf5xdlj30lr0ck0tc.jpg" alt="LVS FULLNAT模式" title="">            </p><h5 id="LVS-FULLNAT模式实践"><a href="#LVS-FULLNAT模式实践" class="headerlink" title="LVS FULLNAT模式实践"></a>LVS FULLNAT模式实践</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> 虚拟机4的IP地址为192.168.58.100， 虚拟机2，3的IP地址为192.168.56.102， 192.168.56.104, 在虚拟机上执行：</span></span><br><span class="line">ip addr add 192.168.58.200/32 dev eth1</span><br><span class="line"></span><br><span class="line">ipvsadm -A -t 192.168.58.200:80 -s wrr</span><br><span class="line">ipvsadm -a -t 192.168.58.200:80 -r 192.168.56.102:80 -b -w 2</span><br><span class="line">ipvsadm -a -t 192.168.58.200:80 -r 192.168.56.104:80 -b -w 1</span><br><span class="line">ipvsadm -P -t 192.168.58.200:80 -z 192.168.58.100</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> 集群外执行curl, 验证是否成功</span></span><br><span class="line">curl 192.168.56.200</span><br></pre></td></tr></table></figure><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://github.com/xgfone/snippet/blob/master/snippet/docs/architecture/ha-lb/lvs-lb-and-install.md">LVS 负载均衡原理及安装配置详解</a>  // 推荐阅读</li><li><a href="https://wsgzao.github.io/post/lvs/" target="_blank" rel="noopener">LVS 原理介绍和配置实践</a>  // 推荐阅读</li><li><a href="https://segmentfault.com/a/1190000039819984" target="_blank" rel="noopener">深入浅出 LVS 负载均衡系列（一）：NAT、FULLNAT 模型原理</a>  //这系列文章不错</li><li><a href="https://segmentfault.com/a/1190000039932089" target="_blank" rel="noopener">深入浅出 LVS 负载均衡系列（二）：DR、TUN 模型原理</a>  //这系列文章不错</li><li><a href="https://segmentfault.com/a/1190000040190992" target="_blank" rel="noopener">深入浅出 LVS 负载均衡（三）实操 NAT、DR 模型</a>    //这系列文章不错</li><li><a href="https://www.haxi.cc/archives/LVS-FULLNAT实战.html" target="_blank" rel="noopener">LVS FULLNAT 实战</a>   // lvs fullnat 编译，样例</li><li><a href="http://kb.linuxvirtualserver.org/wiki/IPVS_FULLNAT_and_SYNPROXY" target="_blank" rel="noopener">IPVS FULLNAT and SYNPROXY</a>  // 编译 lvs fullnat</li><li><a href="https://blog.csdn.net/chao199512/article/details/81390485" target="_blank" rel="noopener">LVS/fullnat模式（内核编译）</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h3&gt;&lt;p&gt;负载均衡(Load Balance)的职责是将网络请求，或者其他形式的负载“均摊”到不同的机器上，让每台服务器获取到适合自己处理能力的负载。
      
    
    </summary>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/categories/linux/"/>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>Hello World</title>
    <link href="https://github.com/fafucoder/2021/12/17/hello-world/"/>
    <id>https://github.com/fafucoder/2021/12/17/hello-world/</id>
    <published>2021-12-17T07:42:48.328Z</published>
    <updated>2021-12-17T07:42:48.328Z</updated>
    
    <content type="html"><![CDATA[<p>Welcome to <a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/" target="_blank" rel="noopener">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html" target="_blank" rel="noopener">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues">GitHub</a>.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="Create-a-new-post"><a href="#Create-a-new-post" class="headerlink" title="Create a new post"></a>Create a new post</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new <span class="string">"My New Post"</span></span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/writing.html" target="_blank" rel="noopener">Writing</a></p><h3 id="Run-server"><a href="#Run-server" class="headerlink" title="Run server"></a>Run server</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/server.html" target="_blank" rel="noopener">Server</a></p><h3 id="Generate-static-files"><a href="#Generate-static-files" class="headerlink" title="Generate static files"></a>Generate static files</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/generating.html" target="_blank" rel="noopener">Generating</a></p><h3 id="Deploy-to-remote-sites"><a href="#Deploy-to-remote-sites" class="headerlink" title="Deploy to remote sites"></a>Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/one-command-deployment.html" target="_blank" rel="noopener">Deployment</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Welcome to &lt;a href=&quot;https://hexo.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hexo&lt;/a&gt;! This is your very first post. Check &lt;a href=&quot;https://hexo.
      
    
    </summary>
    
    
    
  </entry>
  
  <entry>
    <title>golang中锁的实现原理</title>
    <link href="https://github.com/fafucoder/2021/11/22/golang-lock/"/>
    <id>https://github.com/fafucoder/2021/11/22/golang-lock/</id>
    <published>2021-11-22T13:35:17.000Z</published>
    <updated>2024-03-19T16:18:48.417Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>Go 语言作为一个原生支持用户态进程（Goroutine）的语言，当提到并发编程、多线程编程时，往往都离不开锁这一概念。锁是一种并发编程中的同步原语（Synchronization Primitives），它能保证多个 Goroutine 在访问同一片内存时不会出现竞争条件（Race condition）等问题。go语言在Sync包中提供了用于同步的一些基本原语，包括常见的<a href="https://draveness.me/golang/tree/sync.Mutex" target="_blank" rel="noopener"><code>sync.Mutex</code></a>、<a href="https://draveness.me/golang/tree/sync.RWMutex" target="_blank" rel="noopener"><code>sync.RWMutex</code></a>、<a href="https://draveness.me/golang/tree/sync.WaitGroup" target="_blank" rel="noopener"><code>sync.WaitGroup</code></a>、<a href="https://draveness.me/golang/tree/sync.Once" target="_blank" rel="noopener"><code>sync.Once</code></a> 和 <a href="https://draveness.me/golang/tree/sync.Cond" target="_blank" rel="noopener"><code>sync.Cond</code></a>：</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403200018101.png" alt="golang-basic-sync-primitives" title="">            </p><h3 id="前提知识"><a href="#前提知识" class="headerlink" title="前提知识"></a>前提知识</h3><h5 id="悲观锁与乐观锁"><a href="#悲观锁与乐观锁" class="headerlink" title="悲观锁与乐观锁"></a>悲观锁与乐观锁</h5><p>悲观锁是一种悲观思想，它总认为最坏的情况可能会出现，它认为数据很可能会被其他人所修改，不管读还是写，悲观锁在执行操作之前都先上锁。</p><p>对读对写都需要加锁导致性能低，所以悲观锁用的机会不多。但是在多写的情况下，还是有机会使用悲观锁的，因为乐观锁遇到写不一致的情况下会一直重试，会浪费更多的时间。</p><p>乐观锁的思想与悲观锁的思想相反，它总认为资源和数据不会被别人所修改，所以读取不会上锁，但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁的实现方案主要包含CAS和版本号机制。乐观锁适用于多读的场景，可以提高吞吐量。</p><p>CAS即Compare And Swap（比较与交换），是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步，也就是在没有线程被阻塞的情况下实现变量的同步，所以也叫非阻塞同步。CAS涉及三个关系：指向内存一块区域的指针V、旧值A和将要写入的新值B。CAS实现的乐观锁会带来ABA问题，同时整个乐观锁在遇到数据不一致的情况下会触发等待、重试机制，这对性能的影响较大。</p><p>版本号机制是通过一个版本号version来实现版本控制。</p><h5 id="自旋锁"><a href="#自旋锁" class="headerlink" title="自旋锁"></a>自旋锁</h5><p>之前介绍的CAS就是自旋锁的一种。同一时刻只能有一个线程获取到锁，没有获取到锁的线程通常有两种处理方式：</p><ul><li>一直循环等待判断该资源是否已经释放锁，这种锁叫做自旋锁，它不用将线程阻塞起来(NON-BLOCKING)；</li><li>把自己阻塞起来，等待重新调度请求，这种是互斥锁。</li></ul><p>自旋锁的原理比较简单，如果持有锁的线程能在短时间内释放锁资源，那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态，它们只需要等一等(自旋)，等到持有锁的线程释放锁之后即可获取，这样就避免了用户进程和内核切换的消耗。</p><p>但是如果长时间上锁的话，自旋锁会非常耗费性能，它阻止了其他线程的运行和调度。线程持有锁的时间越长，则持有该锁的线程将被OS调度程序中断的风险越大。如果发生中断情况，那么其他线程将保持旋转状态(反复尝试获取锁)，而持有该锁的线程并不打算释放锁，这样导致的是结果是无限期推迟，直到持有锁的线程可以完成并释放它为止。</p><p>解决上面这种情况一个很好的方式是给自旋锁设定一个自旋时间，等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放，等到获取锁立即进行处理。</p><h3 id="golang锁的实现"><a href="#golang锁的实现" class="headerlink" title="golang锁的实现"></a>golang锁的实现</h3><p>Golang的Mutex其实是在不断改进的，到目前为止Mutex经历的四个阶段的改进，具体可以参考链接二中大佬的博客。</p><p>Go 语言的 <a href="https://draveness.me/golang/tree/sync.Mutex" target="_blank" rel="noopener"><code>sync.Mutex</code></a> 由两个字段 <code>state</code> 和 <code>sema</code> 组成。其中 <code>state</code> 表示当前互斥锁的状态，而 <code>sema</code> 是用于控制锁状态的信号量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Mutex <span class="keyword">struct</span> &#123;</span><br><span class="line">state <span class="keyword">int32</span></span><br><span class="line">sema  <span class="keyword">uint32</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中state的最低三位mutexLocked, mutexWoken, mutexStarving表示锁的三种状态：</p><ul><li><code>mutexLocked</code> — 表示互斥锁的锁定状态；</li><li><code>mutexWoken</code> — 表示从正常模式被从唤醒；</li><li><code>mutexStarving</code> — 当前的互斥锁进入饥饿状态；</li><li><code>waitersCount</code> — 当前互斥锁上等待的 Goroutine 个数；</li></ul><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202403200018662.png" alt="golang-mutex-state" title="">            </p><p>互斥锁的加锁过程比较复杂，它涉及自旋、信号量以及调度等概念：</p><ul><li>如果互斥锁处于初始化状态，会通过置位 <code>mutexLocked</code> 加锁；</li><li>如果互斥锁处于 <code>mutexLocked</code> 状态并且在普通模式下工作，会进入自旋，执行 30 次 <code>PAUSE</code> 指令消耗 CPU 时间等待锁的释放；</li><li>如果当前 Goroutine 等待锁的时间超过了 1ms，互斥锁就会切换到饥饿模式；</li><li>互斥锁在正常情况下会通过 <a href="https://draveness.me/golang/tree/runtime.sync_runtime_SemacquireMutex" target="_blank" rel="noopener"><code>runtime.sync_runtime_SemacquireMutex</code></a> 将尝试获取锁的 Goroutine 切换至休眠状态，等待锁的持有者唤醒；</li><li>如果当前 Goroutine 是互斥锁上的最后一个等待的协程或者等待的时间小于 1ms，那么它会将互斥锁切换回正常模式；</li></ul><p>互斥锁的解锁过程与之相比就比较简单，其代码行数不多、逻辑清晰，也比较容易理解：</p><ul><li>当互斥锁已经被解锁时，调用 <a href="https://draveness.me/golang/tree/sync.Mutex.Unlock" target="_blank" rel="noopener"><code>sync.Mutex.Unlock</code></a> 会直接抛出异常；</li><li>当互斥锁处于饥饿模式时，将锁的所有权交给队列中的下一个等待者，等待者会负责设置 <code>mutexLocked</code> 标志位；</li><li>当互斥锁处于普通模式时，如果没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁，会直接返回；在其他情况下会通过 <a href="https://draveness.me/golang/tree/sync.runtime_Semrelease" target="_blank" rel="noopener"><code>sync.runtime_Semrelease</code></a> 唤醒对应的 Goroutine；</li></ul><h3 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h3><p>​    死锁是指两个或两个以上的进程在执行过程中，由于竞争资源或者由于彼此通信而造成的一种阻塞的现象，若无外力作用，它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁，这些永远在互相等待的进程称为死锁进程。</p><p>​    例如：两个线程A、B各自持有一个无法共享的资源，并且他们都需要获取对方现在持有的资源才能进行下一步，但是他们又必须等对方释放了才能去获取，于是A等待B，B也在等待A。如此这般，死锁就产生了。</p><h3 id="产生死锁的条件及预防死锁"><a href="#产生死锁的条件及预防死锁" class="headerlink" title="产生死锁的条件及预防死锁"></a>产生死锁的条件及预防死锁</h3><p>死锁的产生必须具备如下四个必要条件：</p><ol><li><strong>互斥条件</strong>：资源不能被共享，只能由某一个进程使用</li><li><strong>请求和保持</strong>: 已经得到资源的进程可以再次申请新的资源。</li><li><strong>不可剥夺条件</strong>: 已经分配的资源不能从相应的进程中被强制地剥夺。</li><li><strong>循环等待条件</strong>：系统中若干进程组成环路，该环路中每个进程都在等待相邻进程正占用的资源。</li></ol><p>这四个条件太抽象了，现在就以一个例子说明：</p><p>​    两个线程各自持有一个无法共享(互斥条件)的资源，并且他们都需要获取（请求与保持条件）对方现在持有的资源才能进行下一步，但是他们又必须等对方释放了才能去获取(不可剥夺条件)，于是A等待B，B也在等待A（环路等待条件）。如此这般，死锁就产生了。</p><p>要预防死锁，只需要破坏四个必要条件中的一个或多个，使死锁永远无法满足即可。</p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#cond" target="_blank" rel="noopener">https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#cond</a>  // 面向信仰编程，这文章也挺不错的~</li><li><a href="https://nxw.name/2021/golang-mutexde-shi-xian-yuan-li-1ef30cc7" target="_blank" rel="noopener">https://nxw.name/2021/golang-mutexde-shi-xian-yuan-li-1ef30cc7</a>  // 这篇文章值得好好看</li><li><a href="https://cloud.tencent.com/developer/article/1493418" target="_blank" rel="noopener">https://cloud.tencent.com/developer/article/1493418</a>   // 死锁的产生条件</li><li><a href="https://segmentfault.com/a/1190000039712353" target="_blank" rel="noopener">https://segmentfault.com/a/1190000039712353</a>  // 这文章也不错</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;Go 语言作为一个原生支持用户态进程（Goroutine）的语言，当提到并发编程、多线程编程时，往往都离不开锁这一概念。锁是一种并发编程中的
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>什么是云原生</title>
    <link href="https://github.com/fafucoder/2021/11/22/kubernetes-cloud-native/"/>
    <id>https://github.com/fafucoder/2021/11/22/kubernetes-cloud-native/</id>
    <published>2021-11-22T09:23:10.000Z</published>
    <updated>2023-02-28T15:51:52.946Z</updated>
    
    <content type="html"><![CDATA[<h3 id="什么是云原生"><a href="#什么是云原生" class="headerlink" title="什么是云原生"></a>什么是云原生</h3><p><strong>云原生（Cloud Native）是一套技术体系和方法论</strong>，它由2个词组成，云（Cloud）和原生（Native）。云（Cloud）表示应用程序位于云中，而不是传统的数据中心；原生（Native）表示应用程序从设计之初即考虑到云的环境，原生为云而设计，在云上以最佳状态运行，充分利用和发挥云平台的弹性和分布式优势。</p><h3 id="云原生的技术要素"><a href="#云原生的技术要素" class="headerlink" title="云原生的技术要素"></a>云原生的技术要素</h3><p>根据CNCF的定义，CNCF 将不可变基础设施、微服务、声明式 API、容器和服务网格列为云原生架构的技术块。 </p><h5 id="不可变基础设施"><a href="#不可变基础设施" class="headerlink" title="不可变基础设施"></a><strong>不可变基础设施</strong></h5><p>不可变基础设施意味着用于托管云原生应用程序的服务器在部署后保持不变。如果应用程序需要更多计算资源，则会丢弃旧服务器，并将应用程序移至新的高性能服务器。通过避免手动升级，不可变基础设施使云原生部署成为一个可预测的过程。 </p><h5 id="微服务"><a href="#微服务" class="headerlink" title="微服务"></a><strong>微服务</strong></h5><p>微服务是小型的独立软件组件，它们作为完整的云原生软件共同运行。每个微服务都侧重于一个小而具体的问题。微服务是松散耦合的，这意味着它们是相互通信的独立软件组件。开发人员通过处理单个微服务来更改应用程序。这样，即使一个微服务出现故障，应用程序仍能继续运行。 </p><h5 id="申明式API"><a href="#申明式API" class="headerlink" title="申明式API"></a><strong>申明式API</strong></h5><p><a href="https://aws.amazon.com/what-is/api/" target="_blank" rel="noopener">应用程序编程接口（API）</a>是两个或多个软件程序用来交换信息的方法。云原生系统使用 API 将松散耦合的微服务整合在一起。API 会告诉您微服务想要什么数据以及它能给您带来什么结果，而不是指定实现结果的步骤。 </p><h5 id="服务网格"><a href="#服务网格" class="headerlink" title="服务网格"></a><strong>服务网格</strong></h5><p>服务网格是云基础设施中的一个软件层，用于管理多个微服务之间的通信。开发人员使用服务网格来引入其他功能，而无需在应用程序中编写新代码。 </p><h5 id="容器"><a href="#容器" class="headerlink" title="容器"></a><strong>容器</strong></h5><p>容器是云原生应用程序中最小的计算单元。它们是将微服务代码和其他必需文件打包在云原生系统中的软件组件。通过容器化微服务，云原生应用程序独立于底层操作系统和硬件运行。这意味着软件开发人员可以在本地、云基础设施或混合云上部署云原生应用程序。 开发人员使用容器将微服务与其各自的依赖项（例如主应用程序运行所需的资源文件、库和脚本）打包。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/202302282348479.png" alt="云原生四大要素" title="">            </p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://tech.meituan.com/2020/03/12/cloud-native-security.html" target="_blank" rel="noopener">云原生之容器安全实践</a>   // 美团容器安全实践，说的不错</li><li><a href="https://aws.amazon.com/cn/what-is/cloud-native/" target="_blank" rel="noopener">什么是云原生？</a> // aws 关于云原生的定义</li><li><a href="https://skyao.io/learning-cloudnative/docs/introduction/definition.html" target="_blank" rel="noopener">云原生的定义</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;什么是云原生&quot;&gt;&lt;a href=&quot;#什么是云原生&quot; class=&quot;headerlink&quot; title=&quot;什么是云原生&quot;&gt;&lt;/a&gt;什么是云原生&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;云原生（Cloud Native）是一套技术体系和方法论&lt;/strong&gt;，它由2个词组成，
      
    
    </summary>
    
    
      <category term="kubernetes" scheme="https://github.com/fafucoder/categories/kubernetes/"/>
    
    
      <category term="kubernetes" scheme="https://github.com/fafucoder/tags/kubernetes/"/>
    
  </entry>
  
  <entry>
    <title>golang 垃圾回收算法</title>
    <link href="https://github.com/fafucoder/2021/11/14/golang-gc/"/>
    <id>https://github.com/fafucoder/2021/11/14/golang-gc/</id>
    <published>2021-11-14T02:09:38.000Z</published>
    <updated>2023-02-25T08:18:48.935Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><h3 id="常见的垃圾回收算法"><a href="#常见的垃圾回收算法" class="headerlink" title="常见的垃圾回收算法"></a>常见的垃圾回收算法</h3><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://www.cnblogs.com/33debug/p/12106823.html" target="_blank" rel="noopener">golang垃圾回收gc</a>   // 推荐阅读此大佬的所有go文章，写得针不戳</li><li><a href="https://blog.csdn.net/u010649766/article/details/80582153" target="_blank" rel="noopener">Golang GC 垃圾回收机制详解</a>   // 对常见的gc做了说明</li><li><a href="https://zhuanlan.zhihu.com/p/95056679" target="_blank" rel="noopener">万字长文深入浅出 Golang Runtime</a>   //之前go夜读有看到分享，说的贼好</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;h3 id=&quot;常见的垃圾回收算法&quot;&gt;&lt;a href=&quot;#常见的垃圾回收算法&quot; class=&quot;headerlink&quot; title=&quot;常见的垃圾回收
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>golang中的内存分配</title>
    <link href="https://github.com/fafucoder/2021/11/11/golang-memory/"/>
    <id>https://github.com/fafucoder/2021/11/11/golang-memory/</id>
    <published>2021-11-11T09:21:46.000Z</published>
    <updated>2021-12-17T07:42:48.327Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>Go语言内置运行时(Runtime), 抛弃了传统的内存分配方式，改为自主管理。这样可以自主地实现更好的内存使用模式，比如内存池、预分配等等。这样，不会每次内存分配都需要进行系统调用。</p><p>Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的<code>TCMalloc算法</code>，全称<code>Thread-Caching Malloc</code>。核心思想就是把内存分为多级管理，从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理：每个线程都会自行维护一个独立的内存池，进行内存分配时优先从该内存池中分配，当内存池不足时才会向全局内存池申请，以避免不同线程对全局内存池的频繁竞争。</p><h3 id="TC-Malloc原理"><a href="#TC-Malloc原理" class="headerlink" title="TC Malloc原理"></a>TC Malloc原理</h3><h3 id="go-内存分配"><a href="#go-内存分配" class="headerlink" title="go 内存分配"></a>go 内存分配</h3><h3 id="make与new的区别"><a href="#make与new的区别" class="headerlink" title="make与new的区别"></a>make与new的区别</h3><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><p><a href="https://zhuanlan.zhihu.com/p/95056679" target="_blank" rel="noopener">万字长文深入浅出 Golang Runtime</a>   // go夜读的分享，推荐去看下go夜读</p></li><li><p><a href="https://www.cnblogs.com/jiujuan/p/13869547.html" target="_blank" rel="noopener">TC Malloc 内存分配原理简析</a>   // 简洁tc malloc的原理</p></li><li><p><a href="https://zhuanlan.zhihu.com/p/59125443" target="_blank" rel="noopener">图解Go语言内存分配</a>   // 绕全成大佬写的，通俗易懂</p></li><li><p><a href="https://www.cnblogs.com/33debug/p/12068699.html" target="_blank" rel="noopener">golang内存分配原理及make和new的区别</a>   // 强烈推荐这篇文档，这位大佬的go系列文章都很牛批</p></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;Go语言内置运行时(Runtime), 抛弃了传统的内存分配方式，改为自主管理。这样可以自主地实现更好的内存使用模式，比如内存池、预分配等等
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>goroutine并发数量控制</title>
    <link href="https://github.com/fafucoder/2021/11/09/golang-goroutine-count/"/>
    <id>https://github.com/fafucoder/2021/11/09/golang-goroutine-count/</id>
    <published>2021-11-09T02:02:02.000Z</published>
    <updated>2023-02-04T13:33:00.724Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>在golang语言中创建协程（Goroutine）的成本非常低，因此稍不注意就可能创建出大量的协程，一方面会造成资源的浪费，例如有一万个任务需要处理，如果启用一万个goroutine同时处理，意味了CPU内存资源大量的飙升，所以一般会控制goroutine的数量，例如最多只有一百个goroutine在运行，本章将看下如何控制goroutine的并发数量</p><h3 id="goroutine并发控制"><a href="#goroutine并发控制" class="headerlink" title="goroutine并发控制"></a>goroutine并发控制</h3><h5 id="并发未控制情形"><a href="#并发未控制情形" class="headerlink" title="并发未控制情形"></a>并发未控制情形</h5><p>在说明goroutine并发控制前，先看下并发不控制的代码逻辑</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"sync"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">wg := sync.WaitGroup&#123;&#125;</span><br><span class="line">workerCount := <span class="number">100</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; workerCount; i++ &#123;</span><br><span class="line">wg.Add(<span class="number">1</span>)</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(i <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line">fmt.Printf(<span class="string">"i am goroutine %d \n"</span>, i)</span><br><span class="line">&#125;(i)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">wg.Wait()</span><br><span class="line">fmt.Println(<span class="string">"worker have done"</span>)</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的输出如下, 在多核的场景下，goroutine不一定是顺序输出的:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">......</span><br><span class="line">i am goroutine 88 </span><br><span class="line">i am goroutine 63 </span><br><span class="line">i am goroutine 89 </span><br><span class="line">i am goroutine 98 </span><br><span class="line">i am goroutine 82 </span><br><span class="line">i am goroutine 93 </span><br><span class="line">i am goroutine 87 </span><br><span class="line">i am goroutine 95 </span><br><span class="line">i am goroutine 97 </span><br><span class="line">worker have done</span><br></pre></td></tr></table></figure><p>下图展示了为每个 job 创建一个 goroutine 的情况（换句话说，goroutine 的数量是不受控制的）。此种情况虽然生成了很多的 goroutine，但是每个 CPU 核上同一时间只能执行一个 goroutine；当 job 很多且生成了相应数目的 goroutine 后，会出现很多等待执行的 goroutine，从而造成资源上的浪费。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwa9ewaz1uj31360hijso.jpg" alt="goroutine并发不控制" title="">            </p><h5 id="并发控制概述"><a href="#并发控制概述" class="headerlink" title="并发控制概述"></a>并发控制概述</h5><p>给每个 job 生成一个 goroutine 的方式显得粗暴了很多，那么可以通过什么样的方式控制 goroutine 的数目呢？其实上面的代码通过一个 for-range 循环完成了两件事情：①为每个 job 创建 goroutine；②把任务相关的标识传给相应的 goroutine 执行。为了控制 goroutine 的数目，完全可以把上面的两个过程拆分开：a）先通过一个 for-range 循环创建指定数目的 goroutine，b）然后通过 channel/buffered channel 给每个 goroutine 传递任务相关的信息（这里的channel是否缓冲无所谓，主要用到的是 channel 的线程安全特性）。如下图所示。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwaa7xeiyrj313q0i2wgn.jpg" alt="goroutine并发控制" title="">            </p><h5 id="goroutine并发控制方案一"><a href="#goroutine并发控制方案一" class="headerlink" title="goroutine并发控制方案一"></a>goroutine并发控制方案一</h5><p>针对上面的代码，如果想达到goroutine并发执行的控制，我们可以加个buffer channel来限制最多只有多少个goroutine在执行，代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"sync"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">taskCount := <span class="number">10</span></span><br><span class="line">worker := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>, <span class="number">3</span>)</span><br><span class="line">wg := sync.WaitGroup&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; taskCount; i++ &#123;</span><br><span class="line">wg.Add(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(i <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line">worker &lt;- i</span><br><span class="line">fmt.Println(<span class="string">"i am worker "</span>, i)</span><br><span class="line">&lt;-worker</span><br><span class="line">&#125;(i)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过channel限制最多有三个goroutine在执行，其余的被挂起等待中，但是此方案也有个缺陷，那就是goroutine还是都被创建了，只不过这些goroutine被挂起了而已。</p><h5 id="goroutine并发控制方案二"><a href="#goroutine并发控制方案二" class="headerlink" title="goroutine并发控制方案二"></a>goroutine并发控制方案二</h5><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"sync"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">taskCount, workerCount := <span class="number">10</span>, <span class="number">3</span></span><br><span class="line">worker := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>, workerCount)</span><br><span class="line">wg := sync.WaitGroup&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; workerCount; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> w := <span class="keyword">range</span> worker &#123;</span><br><span class="line">fmt.Println(<span class="string">"i am worker "</span>, w)</span><br><span class="line">wg.Done()</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; taskCount; i++ &#123;</span><br><span class="line">wg.Add(<span class="number">1</span>)</span><br><span class="line">worker &lt;- i</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在方案二中，我们起了三个goroutine一直去消费woker以达到限制goroutine最大并发数的目的。</p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://jingwei.link/2019/09/13/conotrol-goroutines-count.html" target="_blank" rel="noopener">【图示】控制 Goroutine 的并发数量的方式</a>   // jing 维大佬</li><li><a href="https://eddycjy.com/posts/go/talk/2019-01-20-control-goroutine/" target="_blank" rel="noopener">来，控制一下 goroutine 的并发数量</a>        // 煎鱼老师</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;在golang语言中创建协程（Goroutine）的成本非常低，因此稍不注意就可能创建出大量的协程，一方面会造成资源的浪费，例如有一万个任务
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>golang goroutine浅析</title>
    <link href="https://github.com/fafucoder/2021/11/08/golang-goroutine/"/>
    <id>https://github.com/fafucoder/2021/11/08/golang-goroutine/</id>
    <published>2021-11-08T14:32:55.000Z</published>
    <updated>2023-02-04T13:33:00.724Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>什么是goroutine? Goroutine 可以看作对 thread 加的一层抽象，它更轻量级，可以单独执行。</p><h3 id="goroutine-协程-跟-线程的区别"><a href="#goroutine-协程-跟-线程的区别" class="headerlink" title="goroutine(协程) 跟 线程的区别"></a>goroutine(协程) 跟 线程的区别</h3><p>goroutine跟线程的区别可以从<code>内存消耗、创建与销毀、切换</code>三个维度说明</p><ul><li>内存创建： 创建一个 goroutine 的栈内存消耗为 2 KB，实际运行过程中，如果栈空间不够用，会自动进行扩容。创建一个 thread 则需要消耗 1 MB 栈内存，而且还需要一个被称为 “a guard page” 的区域用于和其他 thread 的栈空间进行隔离。</li><li>创建和销毀： 线程创建和销毀都会有巨大的消耗，因为要和操作系统打交道，是内核级的，通常解决的办法就是线程池。而 goroutine 因为是由 Go runtime 负责管理的，创建和销毁的消耗非常小，是用户级。</li><li>切换： 当 threads 切换时，需要保存各种寄存器，以便将来恢复， 而 goroutines 切换只需保存三个寄存器：Program Counter, Stack Pointer and BP。因此goroutines 切换成本比 threads 要小得多。</li></ul><h3 id="G-P-M模型概述"><a href="#G-P-M模型概述" class="headerlink" title="G-P-M模型概述"></a>G-P-M模型概述</h3><p>在 Go 语言中，每一个 goroutine 是一个独立的执行单元，相较于每个 OS 线程固定分配 2M 内存的模式，goroutine 的栈采取了<strong>动态扩容</strong>方式， 初始时仅为<strong>2KB</strong>，随着任务执行按需增长，最大可达 1GB（64 位机器最大是 1G，32 位机器最大是 256M），且完全由 golang 自己的调度器 Go Scheduler 来调度。此外，GC 还会周期性地将不再使用的内存回收，收缩栈空间。 因此，Go 程序可以同时并发成千上万个 goroutine 是得益于它强劲的调度器和高效的内存模型。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwagya06l0j312o0swmyq.jpg" alt="G-P-M模型" title="">            </p><p>将 goroutines 调度到线程上执行，仅仅是 runtime 层面的一个概念，在操作系统之上的层面，在golang中有三个基础的结构体来实现 goroutines 的调度。g，m，p， 俗称GPM模型：</p><ul><li>G: 表示 Goroutine，每个 Goroutine 对应一个 G 结构体，G 存储 Goroutine 的运行堆栈、状态以及任务函数，可重用。G 并非执行体，每个 G 需要绑定到 P 才能被调度执行。</li><li>P: Processor，表示逻辑处理器， 对 G 来说，P 相当于 CPU 核，G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说，P 提供了相关的执行环境(Context)，如内存分配状态(mcache)，任务队列(G)等，P 的数量决定了系统内最大可并行的 G 的数量（前提：物理 CPU 核数 &gt;= P 的数量），P 的数量由用户设置的 GOMAXPROCS 决定，但是不论 GOMAXPROCS 设置为多大，P 的数量最大为 256。</li><li>M: Machine，OS 线程抽象，代表着真正执行计算的资源，在绑定有效的 P 后，进入 schedule 循环；而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G，切换到 G 的执行栈上并执行 G 的函数，调用 goexit 做清理工作并回到 M，如此反复。M 并不保留 G 状态，这是 G 可以跨 M 调度的基础，M 的数量是不定的，由 Go Runtime 调整，为了防止创建过多 OS 线程导致系统调度不过来，目前默认最大限制为 10000 个。</li><li>每个 P 维护一个 G 的本地队列；</li><li>当一个 G 被创建出来，或者变为可执行状态时，就把他放到 P 的本地可执行队列中，如果满了则放入Global；</li><li>当一个 G 在 M 里执行结束后，P 会从队列中把该 G 取出；如果此时 P 的队列为空，即没有其他 G 可以执行， M 就随机选择另外一个 P，从其可执行的 G 队列中取走一半。</li></ul><p>除了GPM外，还有两个比较重要的组件： 全局可运行队列（GRQ）和本地可运行队列（LRQ）。 LRQ 存储本地（也就是具体的 P）的可运行 goroutine，GRQ 存储全局的可运行 goroutine，这些 goroutine 还没有分配到具体的 P。</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwah8kn88jj30y40g0dhm.jpg" alt="LRQ" title="">            </p><h4 id="调度过程"><a href="#调度过程" class="headerlink" title="调度过程"></a>调度过程</h4><ol><li>当通过 go 关键字创建一个新的 goroutine 的时候，它会<strong>优先</strong>被放入 P 的本地队列。</li><li>为了运行 goroutine，M 需要持有（绑定）一个 P，接着 M 会启动一个 OS 线程，循环从 P 的本地队列里取出一个 goroutine 并执行。</li><li>执行调度算法：当 M 执行完了当前 P 的 Local 队列里的所有 G 后，P 也不会就这么在那划水啥都不干，它会先尝试从 Global 队列寻找 G 来执行，如果 Global 队列为空，它会随机挑选另外一个 P，从它的队列里中拿走一半的 G 到自己的队列中执行。</li></ol><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwah30rd4dj312u0mm40a.jpg" alt="G-P-M调度模型" title="">            </p><h4 id="调度时机"><a href="#调度时机" class="headerlink" title="调度时机"></a>调度时机</h4><p>在四种情形下，goroutine 可能会发生调度，但也并不一定会发生，只是说 Go scheduler 有机会进行调度。</p><ol><li>使用关键字 <code>go</code>： go 创建一个新的 goroutine，Go scheduler 会考虑调度</li><li>GC: 由于进行 GC 的 goroutine 也需要在 M 上运行，因此肯定会发生调度。当然，Go scheduler 还会做很多其他的调度，例如调度不涉及堆访问的 goroutine 来运行。GC 不管栈上的内存，只会回收堆上的内存</li><li>系统调用: 当 goroutine 进行系统调用时，会阻塞 M，所以它会被调度走，同时一个新的 goroutine 会被调度上来</li><li>内存同步访问: atomic，mutex，channel 操作等会使 goroutine 阻塞，因此会被调度走。等条件满足后（例如其他 goroutine 解锁了）还会被调度上来继续运行</li></ol><h4 id="同步-异步系统调用"><a href="#同步-异步系统调用" class="headerlink" title="同步/异步系统调用"></a>同步/异步系统调用</h4><p>当 G 需要进行系统调用时，根据调用的类型，它所依附的 M 有两种情况：<code>同步</code>和<code>异步</code>。</p><ul><li><p>对于同步的情况，M 会被阻塞，进而从 P 上调度下来，P 可不养闲人，G 仍然依附于 M。之后，一个新的 M 会被调用到 P 上，接着执行 P 的 LRQ 里嗷嗷待哺的 G 们。一旦系统调用完成，G 还会加入到 P 的 LRQ 里，M 则会被“雪藏”，待到需要时再“放”出来。</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwah99rsymj30ya0i8goh.jpg" alt="异步调用"></p></li></ul><ul><li><p>对于异步的情况，M 不会被阻塞，G 的异步请求会被“代理人” network poller 接手，G 也会被绑定到 network poller，等到系统调用结束，G 才会重新回到 P 上。M 由于没被阻塞，它因此可以继续执行 LRQ 里的其他 G。</p><p><img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwahbnuyfdj30xy0ho0vb.jpg" alt="异步"></p></li></ul><h3 id="goroutine状态流转"><a href="#goroutine状态流转" class="headerlink" title="goroutine状态流转"></a>goroutine状态流转</h3><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gwahdhtlrhj30yq0jmjuc.jpg" alt="状态流转" title="">            </p><h3 id="GPM结构"><a href="#GPM结构" class="headerlink" title="GPM结构"></a>GPM结构</h3><h5 id="G-结构"><a href="#G-结构" class="headerlink" title="G 结构"></a>G 结构</h5><p>g是goroutine的缩写，是goroutine的控制结构，是对goroutine的抽象。看下它内部主要的一些结构：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> g <span class="keyword">struct</span> &#123;</span><br><span class="line">  <span class="comment">//堆栈参数。</span></span><br><span class="line">  <span class="comment">//堆栈描述了实际的堆栈内存：[stack.lo，stack.hi）。</span></span><br><span class="line">  <span class="comment">// stackguard0是在Go堆栈增长序言中比较的堆栈指针。</span></span><br><span class="line">  <span class="comment">//通常是stack.lo + StackGuard，但是可以通过StackPreempt触发抢占。</span></span><br><span class="line">  <span class="comment">// stackguard1是在C堆栈增长序言中比较的堆栈指针。</span></span><br><span class="line">  <span class="comment">//它是g0和gsignal堆栈上的stack.lo + StackGuard。</span></span><br><span class="line">  <span class="comment">//在其他goroutine堆栈上为〜0，以触发对morestackc的调用（并崩溃）。</span></span><br><span class="line">  <span class="comment">//当前g使用的栈空间，stack结构包括 [lo, hi]两个成员</span></span><br><span class="line">  stack       stack   <span class="comment">// offset known to runtime/cgo</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 用于检测是否需要进行栈扩张，go代码使用</span></span><br><span class="line">  stackguard0 <span class="keyword">uintptr</span> <span class="comment">// offset known to liblink</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 用于检测是否需要进行栈扩展，原生代码使用的</span></span><br><span class="line">  stackguard1 <span class="keyword">uintptr</span> <span class="comment">// offset known to liblink</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 当前g所绑定的m</span></span><br><span class="line">  m              *m      <span class="comment">// current m; offset known to arm liblink</span></span><br><span class="line">  <span class="comment">// 当前g的调度数据，当goroutine切换时，保存当前g的上下文，用于恢复</span></span><br><span class="line">  sched          gobuf</span><br><span class="line">  <span class="comment">// goroutine运行的函数</span></span><br><span class="line">  fnstart        *FuncVal</span><br><span class="line">  <span class="comment">// g当前的状态</span></span><br><span class="line">  atomicstatus   <span class="keyword">uint32</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 当前g的id</span></span><br><span class="line">  goid           <span class="keyword">int64</span></span><br><span class="line">  <span class="comment">// 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead</span></span><br><span class="line">  status   <span class="keyword">int16</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 下一个g的地址，通过guintptr结构体的ptr set函数可以设置和获取下一个g，通过这个字段和sched.gfreeStack sched.gfreeNoStack 可以把 free g串成一个链表</span></span><br><span class="line">  schedlink      guintptr</span><br><span class="line">  <span class="comment">// 判断g是否允许被抢占</span></span><br><span class="line">  preempt        <span class="keyword">bool</span>       <span class="comment">// preemption signal, duplicates stackguard0 = stackpreempt</span></span><br><span class="line">  <span class="comment">// g是否要求要回到这个M执行, 有的时候g中断了恢复会要求使用原来的M执行</span></span><br><span class="line">  lockedm        muintptr</span><br><span class="line">  <span class="comment">// 用于传递参数，睡眠时其它goroutine设置param，唤醒时此goroutine可以获取       param  *void</span></span><br><span class="line">  <span class="comment">// 创建这个goroutine的go表达式的pc</span></span><br><span class="line">  <span class="keyword">uintptr</span>    gopc</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中包含了栈信息stackbase和stackguard，有运行的函数信息fnstart。这些就足够成为一个可执行的单元了，只要得到CPU就可以运行。goroutine切换时，上下文信息保存在结构体的sched域中。goroutine切换时，上下文信息保存在结构体的sched域中。goroutine是轻量级的<code>线程</code>或者称为<code>协程</code>，切换时并不必陷入到操作系统内核中，很轻量级。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> Gobuf</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">//这些字段的偏移是libmach已知的（硬编码的）。</span></span><br><span class="line">    sp   uintper;</span><br><span class="line">    pc   *<span class="keyword">byte</span>;</span><br><span class="line">    g    *G;</span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h5 id="P-结构"><a href="#P-结构" class="headerlink" title="P 结构"></a>P 结构</h5><p>P是Processor的缩写。结构体P的加入是为了提高Go程序的并发度，实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> p <span class="keyword">struct</span> &#123;</span><br><span class="line">   lock mutex</span><br><span class="line">   </span><br><span class="line">   id          <span class="keyword">int32</span></span><br><span class="line">   </span><br><span class="line">   <span class="comment">// p的状态</span></span><br><span class="line">   status      <span class="keyword">uint32</span> <span class="comment">// one of pidle/prunning/...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 下一个p的地址，可参考 g.schedlink</span></span><br><span class="line">    link        puintptr　　　　<span class="comment">// 每次调用 schedule 时会加一　　　　</span></span><br><span class="line">    schedtick   <span class="keyword">uint32</span>   　　　　<span class="comment">// 每次系统调用时加一　　　　</span></span><br><span class="line">    syscalltick <span class="keyword">uint32</span>　　　　<span class="comment">// 用于 sysmon 线程记录被监控 p 的系统调用时间和运行时间　　</span></span><br><span class="line">    sysmontick  sysmontick <span class="comment">// last tick observed by sysmon10 </span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// p所关联的m  指向绑定的 m，如果 p 是 idle 的话，那这个指针是 nil</span></span><br><span class="line">    m           muintptr   <span class="comment">// back-link to associated m (nil if idle)</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 内存分配的时候用的，p所属的m的mcache用的也是这个</span></span><br><span class="line">    mcache      *mcache</span><br><span class="line">   </span><br><span class="line">    <span class="comment">// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.</span></span><br><span class="line">    <span class="comment">// 从sched中获取并缓存的id，避免每次分配goid都从sched分配</span></span><br><span class="line">    goidcache    <span class="keyword">uint64</span></span><br><span class="line">    goidcacheend <span class="keyword">uint64</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Queue of runnable goroutines. Accessed without lock.</span></span><br><span class="line">    <span class="comment">// p 本地的runnbale的goroutine形成的队列</span></span><br><span class="line">    runqhead <span class="keyword">uint32</span></span><br><span class="line">    runqtail <span class="keyword">uint32</span></span><br><span class="line">    runq     [<span class="number">256</span>]guintptr</span><br><span class="line"></span><br><span class="line">    <span class="comment">// runnext，如果不是nil，则是已准备好运行的G</span></span><br><span class="line">    <span class="comment">//当前的G，并且应该在下一个而不是其中运行</span></span><br><span class="line">    <span class="comment">// runq，如果运行G的时间还剩时间</span></span><br><span class="line">    <span class="comment">//切片。它将继承当前时间剩余的时间</span></span><br><span class="line">    <span class="comment">//切片。如果一组goroutine锁定在</span></span><br><span class="line">    <span class="comment">//交流等待模式，该计划将其设置为</span></span><br><span class="line">    <span class="comment">//单位并消除（可能很大）调度</span></span><br><span class="line">    <span class="comment">//否则会由于添加就绪商品而引起的延迟</span></span><br><span class="line">    <span class="comment">// goroutines到运行队列的末尾。</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 下一个执行的g，如果是nil，则从队列中获取下一个执行的g</span></span><br><span class="line">    runnext guintptr</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Available G's (status == Gdead)</span></span><br><span class="line">    <span class="comment">// 状态为 Gdead的g的列表，可以进行复用</span></span><br><span class="line">    gfree    *g</span><br><span class="line">    gfreecnt <span class="keyword">int32</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>跟G不同的是，P不存在<code>waiting</code>状态。MCache被移到了P中，但是在结构体M中也还保留着。在P中有一个Grunnable的goroutine队列，这是一个P的局部队列。当P执行Go代码时，它会优先从自己的这个局部队列中取，这时可以不用加锁，提高了并发度。如果发现这个队列空了，则去其它P的队列中拿一半过来，这样实现工作流窃取的调度。这种情况下是需要给调用器加锁的。</p><h5 id="M-结构"><a href="#M-结构" class="headerlink" title="M 结构"></a>M 结构</h5><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> m <span class="keyword">struct</span> &#123;</span><br><span class="line">   <span class="comment">// g0是用于调度和执行系统调用的特殊g</span></span><br><span class="line">   g0      *g             <span class="comment">// goroutine with scheduling stack</span></span><br><span class="line">  </span><br><span class="line">   <span class="comment">// m当前运行的g</span></span><br><span class="line">   curg    *g             <span class="comment">// current running goroutine</span></span><br><span class="line">   <span class="comment">// 当前拥有的p</span></span><br><span class="line">   p        puintptr      <span class="comment">// attached p for executing go code (nil if not executing go code)</span></span><br><span class="line">   <span class="comment">// 线程的 local storage</span></span><br><span class="line">   tls      [<span class="number">6</span>]<span class="keyword">uintptr</span>    <span class="comment">// thread-local storage</span></span><br><span class="line">   <span class="comment">// 唤醒m时，m会拥有这个p</span></span><br><span class="line">   nextp         puintptr</span><br><span class="line">   id            <span class="keyword">int64</span></span><br><span class="line">   <span class="comment">// 如果 !="", 继续运行curg</span></span><br><span class="line">   preemptoff    <span class="keyword">string</span>   <span class="comment">// if != "", keep curg running on this m</span></span><br><span class="line">   <span class="comment">// 自旋状态，用于判断m是否工作已结束，并寻找g进行工作</span></span><br><span class="line">   spinning      <span class="keyword">bool</span>     <span class="comment">// m is out of work and is actively looking for work</span></span><br><span class="line">   <span class="comment">// 用于判断m是否进行休眠状态</span></span><br><span class="line">   blocked       <span class="keyword">bool</span>     <span class="comment">// m is blocked on a note</span></span><br><span class="line">   <span class="comment">// m休眠和唤醒通过这个，note里面有一个成员key，对这个key所指向的地址进行值的修改，进而达到唤醒和休眠的目的</span></span><br><span class="line">   park          note</span><br><span class="line">   <span class="comment">// 所有m组成的一个链表</span></span><br><span class="line">   alllink       *m       <span class="comment">// on allm</span></span><br><span class="line">   <span class="comment">// 下一个m，通过这个字段和sched.midle 可以串成一个m的空闲链表</span></span><br><span class="line">   schedlink     muintptr</span><br><span class="line">   <span class="comment">// mcache，m拥有p的时候，会把自己的mcache给p</span></span><br><span class="line">   mcache        *mcache</span><br><span class="line">   <span class="comment">// lockedm的对应值</span></span><br><span class="line">   lockedg       guintptr</span><br><span class="line">   <span class="comment">// 待释放的m的list，通过sched.freem 串成一个链表</span></span><br><span class="line">   freelink      *m      <span class="comment">// on sched.freem</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>和G类似，M中也有alllink域将所有的M放在allm链表中。lockedg是某些情况下，G锁定在这个M中运行而不会切换到其它M中去。M中还有一个MCache，是当前M的内存的缓存。M也和G一样有一个常驻寄存器变量，代表当前的M。同时存在多个M，表示同时存在多个物理线程。</p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://www.qcrao.com/2019/09/02/dive-into-go-scheduler/" target="_blank" rel="noopener">深度解密go语言之scheduler</a>    //绕全成大佬的解密完全看不懂</li><li><a href="https://studygolang.com/articles/29227?fr=sidebar" target="_blank" rel="noopener">GoLang GPM模型</a>  //写的不错</li><li><a href="https://zhuanlan.zhihu.com/p/95056679" target="_blank" rel="noopener">万字长文深入浅出 Golang Runtime</a>   //之前go夜读有看到分享，说的贼好</li><li><a href="https://www.cnblogs.com/33debug/p/11897627.html" target="_blank" rel="noopener">go中的协程-goroutine的底层实现</a>  // pdd 牛皮</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;p&gt;什么是goroutine? Goroutine 可以看作对 thread 加的一层抽象，它更轻量级，可以单独执行。&lt;/p&gt;
&lt;h3 id=&quot;
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>web应用的未来是webAssembly?</title>
    <link href="https://github.com/fafucoder/2021/10/13/golang-webassembly/"/>
    <id>https://github.com/fafucoder/2021/10/13/golang-webassembly/</id>
    <published>2021-10-13T13:30:10.000Z</published>
    <updated>2021-12-17T07:42:48.328Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是WebAssembly"><a href="#什么是WebAssembly" class="headerlink" title="什么是WebAssembly"></a>什么是WebAssembly</h2><p>在说明什么是Webassembly之前，我们有必要了解一下asm.js。2012年，Mozilla 的工程师 Alon Zakai 在研究 LLVM 编译器时突发奇想：许多 3D 游戏都是用 C / C++ 语言写的，如果能将 C / C++ 语言编译成 JavaScript 代码，它们不就能在浏览器里运行了吗？众所周知，JavaScript 的基本语法与 C 语言高度相似。于是，他开始研究怎么才能实现这个目标，为此专门做了一个编译器项目 Emscripten。这个编译器可以将 C / C++ 代码编译成 JS 代码，但不是普通的 JS，而是一种叫做 asm.js 的 JavaScript 变体，性能差不多是原生代码的50%。</p><p>之后Google开发了Portable Native Client，也是一种能让浏览器运行C/C++代码的技术。 后来可能是因为彼此之间有共同的更高追求，Google, Microsoft, Mozilla, Apple等几家大公司一起合作开发了一个面向Web的通用二进制和文本格式的项目，那就是WebAssembly。asm.js 与 WebAssembly 功能基本一致，就是转出来的代码不一样：asm.js 是文本，WebAssembly 是二进制字节码，因此运行速度更快、体积更小。</p><p>WebAssembly(又称 wasm) 是一种新的字节码格式，主流浏览器都已经支持 WebAssembly。 和 JS 需要解释执行不同的是，WebAssembly 字节码和底层机器码很相似可快速装载运行，因此性能相对于 JS 解释执行大大提升。 也就是说 WebAssembly 并不是一门编程语言，而是一份字节码标准，需要用高级编程语言编译出字节码放到 WebAssembly 虚拟机中才能运行， 浏览器厂商需要做的就是根据 WebAssembly 规范实现虚拟机。</p><p>这里引用MDN上官方对其的解释：WebAssembly是一种新的编码方式，可以在现代的网络浏览器中运行 － 它是一种低级的类汇编语言，具有紧凑的二进制格式，可以接近原生的性能运行，并为诸如C / C ++ / Rust等语言提供一个编译目标，以便它们可以在Web上运行。它也被设计为可以与JavaScript共存，允许两者一起工作(总结一句话：Webassembly就像是JVM虚拟机，可以执行二进制码)。</p><blockquote><p>有人这么评价Webassembly： Webassembly是Rust押宝的唯一使用场景(Rust语言的使用场景太少了，不像golang在云原生跟中间键领域有很多应用)。</p></blockquote><h2 id="为什么需要WebAssembly"><a href="#为什么需要WebAssembly" class="headerlink" title="为什么需要WebAssembly"></a>为什么需要WebAssembly</h2><p>自从 JavaScript 诞生起到现在已经变成最流行的编程语言，这背后正是 Web 的发展所推动的。Web 应用变得更多更复杂，但这也渐渐暴露出了 JavaScript 的问题：</p><ol><li>语法太灵活导致开发大型 Web 项目困难；</li><li>性能不能满足一些场景的需要。</li></ol><p>针对以上两点缺陷，近年来出现了一些 JS 的代替语言，例如：</p><ol><li>微软的<a href="http://www.typescriptlang.org/" target="_blank" rel="noopener"> TypeScript </a>通过为 JS 加入静态类型检查来改进 JS 松散的语法，提升代码健壮性；</li><li>谷歌的<a href="https://www.dartlang.org/" target="_blank" rel="noopener"> Dart </a>则是为浏览器引入新的虚拟机去直接运行 Dart 程序以提升性能；</li><li>火狐的<a href="http://asmjs.org/" target="_blank" rel="noopener"> asm.js </a>则是取 JS 的子集，JS 引擎针对 asm.js 做性能优化。</li></ol><p>以上尝试各有优缺点，其中：</p><ol><li>TypeScript 只是解决了 JS 语法松散的问题，最后还是需要编译成 JS 去运行，对性能没有提升；</li><li>Dart 只能在 Chrome 预览版中运行，无主流浏览器支持，用 Dart 开发的人不多；</li><li>asm.js 语法太简单、有很大限制，开发效率低。</li></ol><p>三大浏览器巨头分别提出了自己的解决方案，互不兼容，这违背了 Web 的宗旨； 是技术的规范统一让 Web 走到了今天，因此形成一套新的规范去解决 JS 所面临的问题迫在眉睫。于是 WebAssembly 诞生了，WebAssembly 是一种新的字节码格式，主流浏览器都已经支持 WebAssembly。 和 JS 需要解释执行不同的是，WebAssembly 字节码和底层机器码很相似可快速装载运行，因此性能相对于 JS 解释执行大大提升。 也就是说 WebAssembly 并不是一门编程语言，而是一份字节码标准，需要用高级编程语言编译出字节码放到 WebAssembly 虚拟机中才能运行， 浏览器厂商需要做的就是根据 WebAssembly 规范实现虚拟机。</p><blockquote><p>总结一句话就是：主流厂商极力主导 + webassembly速度快</p></blockquote><h2 id="Webassembly原理"><a href="#Webassembly原理" class="headerlink" title="Webassembly原理"></a>Webassembly原理</h2><p>要搞懂 WebAssembly 的原理，需要先搞懂计算机的运行原理。 电子计算机都是由电子元件组成，为了方便处理电子元件只存在开闭两种状态，对应着 0 和 1，也就是说计算机只认识 0 和 1，数据和逻辑都需要由 0 和 1 表示，也就是可以直接装载到计算机中运行的机器码。 机器码可读性极差，因此人们通过高级语言 C、C++、Rust、Go 等编写再编译成机器码。</p><p>由于不同的计算机 CPU 架构不同，机器码标准也有所差别，常见的 CPU 架构包括 x86、AMD64、ARM， 因此在由高级编程语言编译成可自行代码时需要指定目标架构。WebAssembly 字节码是一种抹平了不同 CPU 架构的机器码，WebAssembly 字节码不能直接在任何一种 CPU 架构上运行， 但由于非常接近机器码，可以非常快的被翻译为对应架构的机器码，因此 WebAssembly 运行速度和机器码接近，这听上去非常像 Java 字节码。</p><p>在javascript中，JavaScript引擎是执行 JavaScript 代码的程序或解释器。JavaScript引擎可以实现为标准解释器，或者以某种形式将JavaScript编译为字节码的即时编译器。不用的浏览器使用不同的引擎如下所示：</p><ul><li><p><a href="https://link.segmentfault.com/?enc=sUDGcZBE4XE+vSy5zjw84Q==.NqvwmK7YdB1NXJpJlh2DNQLg1aBzu82zEWbf/mLpt9JyjRr3+KoxkLT+FwB8JUSoRUdZr2njfFAdW/ex3PcpnA==" target="_blank" rel="noopener">V8</a> — 开源，由 Google 开发，用 C ++ 编写</p></li><li><p><a href="https://link.segmentfault.com/?enc=Lcfwj3jXwKG3LmIWG3zMdA==.96YdUZxH/6IBmMcriO6tSIZNC1VIRPZ/eQZKc278Lf6Hoz1pxLZVOwjHu0/qcOMVqZ9l7XET4ID0V4QgJc/f7w==" target="_blank" rel="noopener">Rhino</a> — 由 Mozilla 基金会管理，开源，完全用 Java 开发</p></li><li><p><a href="https://link.segmentfault.com/?enc=fEBfBX1Wd1oq35Opx9XqBg==.+OCUcVZ4OMlh0scYGO+X5yDy31T3RhLgz5LU8pR/QG3GrOaSb2UWZAqEPjXXQxNg" target="_blank" rel="noopener">SpiderMonkey</a> — 是第一个支持 Netscape Navigator 的 JavaScript 引擎，目前正供 Firefox 使用</p></li><li><p><a href="https://link.segmentfault.com/?enc=5dW2eusqpsCf0enmAMJYHg==.ecsPVoKRqxuA7xY07xwHk2pUZj9fu69Y5Xk9MFLPPcYUhCtmLH8aLW7GfggLyLZB5Fn3g7TYJR2k0t3941/3mA==" target="_blank" rel="noopener">JavaScriptCore</a> — 开源，以Nitro形式销售，由苹果为Safari开发</p></li><li><p><a href="https://link.segmentfault.com/?enc=wE8dCPOCitI59es0P62axw==.NQQ7Mbn0KzdeG2aWMep/ZoMepjYQU0i2Y70FT0ArpGB6zy+1ygTmPYQDKCYBwp3bzBw4MzXWC2RvNNsv1WCpUg==" target="_blank" rel="noopener">KJS</a> — KDE 的引擎，最初由 Harri Porten 为 KDE 项目中的 Konqueror 网页浏览器开发</p></li><li><p><a href="https://link.segmentfault.com/?enc=4p1lBDVXg9F3LoYDCAYqZw==.69EhewNej7/0qL6i/vfOmKrHRr8J75bQnL6m3/Pq/NJ9B/5ltxTPYbJTUIhqMwAMHFWYhsMI6zbTzo/cs4QMyA==" target="_blank" rel="noopener">Chakra</a> (JScript9) — Internet Explorer</p></li><li><p><a href="https://link.segmentfault.com/?enc=HbF4R5UwNZvgHmR/1wtgDQ==.eZWlURXx88Zj4cbJlV52kYcs6ilppQjxqB9f0e/bKPr6736fvUxWXbL77DDy7W8ONHasBxCyxiS/G07KFi3mIQ==" target="_blank" rel="noopener">Chakra</a> (JavaScript) — Microsoft Edge</p></li><li><p><a href="https://link.segmentfault.com/?enc=JJkYtp97F7aGkuRpJu4j+A==.m2XjtFgQZ+p7pfw2mm6hzwz8hHSy2krktwpxq8U1xVjUx1swsJd9bpTYlTiR4YQF5iEyx+frqx1Ir7CGq4Yi8A==" target="_blank" rel="noopener">Nashorn</a>, 作为 OpenJDK 的一部分，由 Oracle Java 语言和工具组编写</p></li><li><p><a href="https://link.segmentfault.com/?enc=s1goWbasfcZ7FRTX379/UA==.utRib33Njx3A6/klirW/KCvy0haJmdcHrB0fRetgT6Kg+zz1lM2n0QTZNDYWTvPE" target="_blank" rel="noopener">JerryScript</a> — 物联网的轻量级引擎</p></li></ul><p>因此我们看下javascript在V8引擎下的工作机制: javacript运行中，引擎首先解析javascript生成抽象语法树(AST), 然后生成机器码。</p><p class="img-lightbox">                <img src="https://i7drsi3tvf.feishu.cn/space/api/box/stream/download/asynccode/?code=M2NkMjI2MTBlMDY5ZjIzNzQxMzk2NWMxYzI3ZjQwNDFfSUhEV1hsbGlFRFl5bU5xTUs1NEJKV3pjU2ZBZTFRWWNfVG9rZW46Ym94Y25iNERHS1doN25BNkxpdGE2dGdVaDdSXzE2MzQxMzE5MDM6MTYzNDEzNTUwM19WNA" alt="img" title="">            </p><p>接着通过V8 的优化编译器 (TurboFan)的优化，把优化后的机器码推向后端(所谓的后端就是即时编译器JIT)</p><p class="img-lightbox">                <img src="https://i7drsi3tvf.feishu.cn/space/api/box/stream/download/asynccode/?code=ODlmMmQ3ZDBmMDdmYTgyYmE5MGM0YWI2MjdmMzdjNDBfbXNsRXN5TThZMU9mdGJPYU42eVFBd2xrQXY0RjRrTnNfVG9rZW46Ym94Y256MFZjSGFHZHUwc3VoU2M5WG4xYkxpXzE2MzQxMzE5MDM6MTYzNDEzNTUwM19WNA" alt="img" title="">            </p><p>但是webassembly并不需要以上的全部步骤－如下所示是它被插入到执行过程示意图:</p><p class="img-lightbox">                <img src="https://i7drsi3tvf.feishu.cn/space/api/box/stream/download/asynccode/?code=MzlhZTAzZjExYzVhYjFmZTY2MGJhZDdiNTFmNWM0MWFfUTVrRVZuMWxhMVlpMTcwN3lDVmJaYVV4WHZaVm5oNVpfVG9rZW46Ym94Y25aUEFUWlAxUm44SjJjbzZjQ3NUREdnXzE2MzQxMzE5MDM6MTYzNDEzNTUwM19WNA" alt="img" title="">            </p><p>可以看到webassembly并不需要编译阶段，而是直接把编译后的字节码发送给后端，因此速度更快。</p><p>目前能编译成 WebAssembly 字节码的高级语言有：</p><ul><li><p><a href="https://github.com/AssemblyScript/assemblyscript">AssemblyScript</a>:语法和 TypeScript 一致，对前端来说学习成本低，为前端编写 WebAssembly 最佳选择；</p></li><li><p>C\C++:官方推荐的方式，详细使用见<a href="http://webassembly.org.cn/getting-started/developers-guide/" target="_blank" rel="noopener">文档</a>;</p></li><li><p><a href="https://www.rust-lang.org/" target="_blank" rel="noopener">Rust</a>:语法复杂、学习成本高，详细使用见<a href="https://github.com/rust-lang-nursery/rust-wasm">文档</a>;</p></li><li><p><a href="http://kotlinlang.org/" target="_blank" rel="noopener">Kotlin</a>:语法和 Java、JS 相似，语言学习成本低，详细使用见<a href="https://kotlinlang.org/docs/reference/native-overview.html" target="_blank" rel="noopener">文档</a>;</p></li><li><p><a href="https://golang.org/" target="_blank" rel="noopener">Golang</a>:语法简单学习成本低。详细使用见<a href="https://blog.gopheracademy.com/advent-2017/go-wasm/" target="_blank" rel="noopener">文档</a>。</p></li><li><p>其他语言: 详细见<a href="https://github.com/appcypher/awesome-wasm-langs">文档</a>。</p></li></ul><h2 id="Webassembly使用场景："><a href="#Webassembly使用场景：" class="headerlink" title="Webassembly使用场景："></a>Webassembly使用场景：</h2><p>总体而言，WASM不会替代JavaScript，但是却可以辅助解决很多JavaScript无法解决的问题。下面是Webassembly的一些场景：</p><h5 id="浏览器场景："><a href="#浏览器场景：" class="headerlink" title="浏览器场景："></a>浏览器场景：</h5><ul><li><p>更好的让一些语言和工具可以编译到 Web 平台运行。</p></li><li><p>图片/视频编辑。</p></li><li><p>游戏：</p><ul><li>需要快速打开的小游戏</li></ul></li><li><p>AAA 级，资源量很大的游戏。</p></li><li><p>游戏门户（代理/原创游戏平台）</p></li><li><p>P2P 应用（游戏，实时合作编辑）</p></li><li><p>音乐播放器（流媒体，缓存）</p></li><li><p>图像识别</p></li><li><p>视频直播</p></li><li><p>VR 和虚拟现实</p></li><li><p>CAD 软件</p></li><li><p>科学可视化和仿真</p></li><li><p>互动教育软件和新闻文章。</p></li><li><p>模拟/仿真平台(ARC, DOSBox, QEMU, MAME, …)。</p></li><li><p>语言编译器/虚拟机。</p></li></ul><h5 id="非浏览器场景："><a href="#非浏览器场景：" class="headerlink" title="非浏览器场景："></a>非浏览器场景：</h5><ul><li><p>游戏分发服务（便携、安全）。</p></li><li><p>服务端执行不可信任的代码。</p></li><li><p>服务端应用。</p></li><li><p>移动混合原生应用。</p></li><li><p>多节点对称计算</p></li></ul><h2 id="下一步"><a href="#下一步" class="headerlink" title="下一步"></a>下一步</h2><p>基于golang实现webassembly的demo</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;什么是WebAssembly&quot;&gt;&lt;a href=&quot;#什么是WebAssembly&quot; class=&quot;headerlink&quot; title=&quot;什么是WebAssembly&quot;&gt;&lt;/a&gt;什么是WebAssembly&lt;/h2&gt;&lt;p&gt;在说明什么是Webassembly之前，我们
      
    
    </summary>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/categories/golang/"/>
    
    
      <category term="golang" scheme="https://github.com/fafucoder/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>nodejs中require跟import的区别</title>
    <link href="https://github.com/fafucoder/2021/10/12/node-require/"/>
    <id>https://github.com/fafucoder/2021/10/12/node-require/</id>
    <published>2021-10-12T13:25:22.000Z</published>
    <updated>2021-12-17T07:42:48.353Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><blockquote><p> require/exports 出生在野生规范当中，什么叫做野生规范？即这些规范是 JavaScript 社区中的开发者自己草拟的规则，得到了大家的承认或者广泛的应用。比如 CommonJS、AMD、CMD 等等。import/export 则是名门正派。TC39 制定的新的 ECMAScript 版本，即 ES6（ES2015）中包含进来。</p></blockquote><p>关于 <code>import</code> 和 <code>require</code> 的不同，其实可以理解成 CommonJs 和 ES Module 的区别。这两者都是前端模块化的规范。</p><h4 id="CommonJs"><a href="#CommonJs" class="headerlink" title="CommonJs"></a>CommonJs</h4><p>Nodejs 是 CommonJS 规范的主要实践者，在 CommonJs 里每个文件就是一个模块，有自己的作用域。在一个文件里面定义的变量、函数、类，都是私有的，对其他文件不可见。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyClass</span> </span>&#123;</span><br><span class="line">  <span class="keyword">constructor</span>(name) &#123;</span><br><span class="line">    <span class="keyword">this</span>.name = name;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports.MyClass = Myclass</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> MyClass = <span class="built_in">require</span>(<span class="string">'./myclass.js'</span>)</span><br><span class="line"><span class="keyword">const</span> obj = <span class="keyword">new</span> MyClass(<span class="string">'hello'</span>)</span><br></pre></td></tr></table></figure><h4 id="ES-Module"><a href="#ES-Module" class="headerlink" title="ES Module"></a>ES Module</h4><p>ES6 模块的设计思想是尽量的静态化，使得编译时就能确定模块的依赖关系，以及输入和输出的变量。所以ES6 模块不是对象，而是通过 <code>export</code> 命令显式指定输出的代码，再通过 <code>import</code> 命令输入。这种加载称为“编译时加载”或者静态加载，即 ES6 可以在编译时就完成模块加载，效率要比 CommonJS 模块的加载方式高。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">MyClass</span> </span>&#123;</span><br><span class="line"><span class="keyword">constructor</span>(name) &#123;</span><br><span class="line">    <span class="keyword">this</span>.name = name;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> MyClass <span class="keyword">from</span> <span class="string">'./myclass'</span></span><br><span class="line"><span class="keyword">const</span> obj = <span class="keyword">new</span> MyClass(<span class="string">'hello'</span>)</span><br></pre></td></tr></table></figure><h3 id="区别"><a href="#区别" class="headerlink" title="区别"></a>区别</h3><table><thead><tr><th>命令</th><th align="left">规范</th><th>调用</th><th>本质</th><th align="left">特点</th></tr></thead><tbody><tr><td>require</td><td align="left">CommonJS规范</td><td>运行时调用</td><td>赋值过程</td><td align="left">非语言层面的标准。 社区方案，提供了服务器/浏览器的模块加载方案。只能在运行时确定模块的依赖关系及输入/输出的变量，无法进行静态优化。</td></tr><tr><td>import</td><td align="left">es6+的语法标准</td><td>编译时调用</td><td>解构过程</td><td align="left">语言规格层面支持模块功能。支持编译时静态分析，便于JS引入宏和类型检验。动态绑定</td></tr></tbody></table><h3 id="关于调用"><a href="#关于调用" class="headerlink" title="关于调用"></a>关于调用</h3><ol><li>require的引用可以在代码的任何地方。</li><li>import语法规范上是放在文件开头。</li></ol><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://juejin.cn/post/6844903765489745927" target="_blank" rel="noopener">https://juejin.cn/post/6844903765489745927</a> // 值得一看</li><li><a href="https://www.zhihu.com/question/56820346" target="_blank" rel="noopener">https://www.zhihu.com/question/56820346</a>     // 值得一看</li><li><a href="https://segmentfault.com/a/1190000023082896" target="_blank" rel="noopener">https://segmentfault.com/a/1190000023082896</a>  // 值得一看</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;概述&quot;&gt;&lt;a href=&quot;#概述&quot; class=&quot;headerlink&quot; title=&quot;概述&quot;&gt;&lt;/a&gt;概述&lt;/h3&gt;&lt;blockquote&gt;
&lt;p&gt; require/exports 出生在野生规范当中，什么叫做野生规范？即这些规范是 JavaScript 社区中
      
    
    </summary>
    
    
      <category term="nodejs" scheme="https://github.com/fafucoder/categories/nodejs/"/>
    
    
      <category term="nodejs" scheme="https://github.com/fafucoder/tags/nodejs/"/>
    
  </entry>
  
  <entry>
    <title>linux top命令</title>
    <link href="https://github.com/fafucoder/2021/09/18/linux-top/"/>
    <id>https://github.com/fafucoder/2021/09/18/linux-top/</id>
    <published>2021-09-18T07:56:48.000Z</published>
    <updated>2023-02-28T03:28:14.788Z</updated>
    
    <content type="html"><![CDATA[<h3 id="top命令参数信息"><a href="#top命令参数信息" class="headerlink" title="top命令参数信息"></a>top命令参数信息</h3><p>在命令top中可以方便的查看系统的cpu和内存信息(如下图所示), 然后你可能不清楚每一个字段代表的含义，现在就一块揭秘top命令中每个字段的含义</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gut64f02osj61h40octfj02.jpg" alt="top command" title="">            </p><p><code>us</code>：user time，表示 CPU 执行用户进程的时间，包括 nice 时间。通常都是希望用户空间CPU越高越好。</p><p><code>sy</code>：system time，表示 CPU 在内核运行的时间，包括 IRQ 和 softirq。系统 CPU 占用越高，表明系统某部分存在瓶颈。通常这个值越低越好。</p><p><code>ni</code>：nice time，具有优先级的用户进程执行时占用的 CPU 利用率百分比。</p><p><code>id</code>：idle time，表示系统处于空闲期，等待进程运行。</p><p><code>wa</code>：waiting time，表示 CPU 在等待 IO 操作完成所花费的时间。系统不应该花费大量的时间来等待 IO 操作，否则就说明 IO 存在瓶颈。</p><p><code>hi</code>：hard IRQ time，表示系统处理硬中断所花费的时间。</p><p><code>si</code>：soft IRQ time，表示系统处理软中断所花费的时间。</p><p><code>st</code>：steal time，被强制等待（involuntary wait）虚拟 CPU 的时间，此时 Hypervisor 在为另一个虚拟处理器服务</p><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gut6kntlhpj61hc0u0gqf02.jpg" alt="top cpu command" title="">            </p><h4 id="cpu-平均负载Load-Average"><a href="#cpu-平均负载Load-Average" class="headerlink" title="cpu 平均负载Load Average"></a>cpu 平均负载Load Average</h4><p>​    在上面的top命令中跟cpu相关的信息还有一个叫load average, load average这一列中的三个数值分别表示过去一分钟，五分钟，十五分钟这个节点上的load average。那么到底啥是load average呢?</p><p>​    这里的load average表示对CPU资源需求的度量(说的很好，但是等于放屁~)，举个例子你可能就懂了，对于一个单个 CPU 的系统，如果在 1 分钟的时间里，处理器上始终有一个进程在运行，同时操作系统的进程可运行队列中始终都有 9 个进程在等待获取 CPU 资源。那么对于这 1 分钟的时间来说，系统的”load average”就是 1+9=10(这个定义对绝大部分的Unix 系统都适用)。</p><p>总结一句话就是: load average 等于单位时间内正在运行的进程+可运行队列的进程(注意，<strong>这里说的是unix系统</strong>)，因此：</p><ul><li>第一，不论计算机 CPU 是空闲还是满负载，Load Average 都是 Linux 进程调度器中<strong>可运行队列（Running Queue）里的一段时间的平均进程数目。</strong></li><li>第二，计算机上的 CPU 还有空闲的情况下，CPU Usage 可以直接反映到”load average”上，什么是 CPU 还有空闲呢？具体来说就是可运行队列中的进程数目小于 CPU个数，这种情况下，单位时间进程 CPU Usage 相加的平均值应该就是”load average”的值。</li><li>第三，计算机上的 CPU 满负载的情况下，计算机上的 CPU 已经是满负载了，同时还有更多的进程在排队需要 CPU 资源。这时”load average”就不能和 CPU Usage 等同了。比如对于单个 CPU 的系统，CPU Usage 最大只是有 100%，也就 1 个 CPU；而”loadaverage”的值可以远远大于 1，因为”load average”看的是操作系统中可运行队列中进程的个数。</li></ul><p><strong>对于linux系统来说： load average = 可运行队列的进程 + 处于TASK_UNINTERRUPTIBLE状态的进程(D 状态进程)</strong></p><blockquote><p>TASK_UNINTERRUPTIBLE 是 Linux 进程状态的一种，是进程为等待某个系统资源而进入了睡眠的状态，并且这种睡眠的状态是不能被信号打断的。俗称D状态进程</p><p>因此，如果发现系统的load average很高，而CPU还是处于空闲状态，说明有很多进程处于阻塞状态，这时候得检查下代码写的是否有问题啦~(通过 ps 命令可以查阅D状态进程的信息)</p></blockquote><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gut7nhnp8kj61hc0u0ac602.jpg" alt="load average" title="">            </p><h3 id="top命令原理"><a href="#top命令原理" class="headerlink" title="top命令原理"></a>top命令原理</h3><p>分析了top命令中cpu字段的含义，你肯定还有疑问top命令中的数值是怎么计算出来的，</p><h3 id="容器中如何查看cpu信息"><a href="#容器中如何查看cpu信息" class="headerlink" title="容器中如何查看cpu信息"></a>容器中如何查看cpu信息</h3><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://github.com/silenceshell/topic">https://github.com/silenceshell/topic</a>  // 容器中正确展示top信息</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;top命令参数信息&quot;&gt;&lt;a href=&quot;#top命令参数信息&quot; class=&quot;headerlink&quot; title=&quot;top命令参数信息&quot;&gt;&lt;/a&gt;top命令参数信息&lt;/h3&gt;&lt;p&gt;在命令top中可以方便的查看系统的cpu和内存信息(如下图所示), 然后你可能不清楚
      
    
    </summary>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/categories/linux/"/>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>kubernetes pause容器解析</title>
    <link href="https://github.com/fafucoder/2021/09/13/kubernetes-pause/"/>
    <id>https://github.com/fafucoder/2021/09/13/kubernetes-pause/</id>
    <published>2021-09-13T06:25:36.000Z</published>
    <updated>2023-02-28T15:33:58.936Z</updated>
    
    <content type="html"><![CDATA[<h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://zhuanlan.zhihu.com/p/66252461" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/66252461</a>  //知乎专栏</li><li><a href="https://cloud.tencent.com/developer/article/1375877" target="_blank" rel="noopener">https://cloud.tencent.com/developer/article/1375877</a> //腾讯云</li><li><a href="https://github.com/kubernetes/kubernetes/tree/master/build/pause">https://github.com/kubernetes/kubernetes/tree/master/build/pause</a>  // pause 源码解析</li><li><a href="https://o-my-chenjian.com/2017/10/17/The-Pause-Container-Of-Kubernetes/" target="_blank" rel="noopener">https://o-my-chenjian.com/2017/10/17/The-Pause-Container-Of-Kubernetes/</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;参考文档&quot;&gt;&lt;a href=&quot;#参考文档&quot; class=&quot;headerlink&quot; title=&quot;参考文档&quot;&gt;&lt;/a&gt;参考文档&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/66252461&quot; target=&quot;
      
    
    </summary>
    
    
      <category term="kubernetes" scheme="https://github.com/fafucoder/categories/kubernetes/"/>
    
    
      <category term="kubernetes" scheme="https://github.com/fafucoder/tags/kubernetes/"/>
    
  </entry>
  
  <entry>
    <title>linux中的page cache</title>
    <link href="https://github.com/fafucoder/2021/08/24/linux-pagecache/"/>
    <id>https://github.com/fafucoder/2021/08/24/linux-pagecache/</id>
    <published>2021-08-24T12:31:11.000Z</published>
    <updated>2023-02-04T13:33:00.756Z</updated>
    
    <content type="html"><![CDATA[<h3 id="图片"><a href="#图片" class="headerlink" title="图片"></a>图片</h3><p class="img-lightbox">                <img src="https://fafucoder-1252756369.cos.ap-nanjing.myqcloud.com/008i3skNly1gts5qs2lggj30u00wnq4v.jpg" alt="vfs" title="">            </p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://spongecaptain.cool/SimpleClearFileIO/1.%20page%20cache.html" target="_blank" rel="noopener">https://spongecaptain.cool/SimpleClearFileIO/1.%20page%20cache.html</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;图片&quot;&gt;&lt;a href=&quot;#图片&quot; class=&quot;headerlink&quot; title=&quot;图片&quot;&gt;&lt;/a&gt;图片&lt;/h3&gt;&lt;p class=&quot;img-lightbox&quot;&gt;
                &lt;img src=&quot;https://fafucoder-1252
      
    
    </summary>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/categories/linux/"/>
    
    
      <category term="linux" scheme="https://github.com/fafucoder/tags/linux/"/>
    
  </entry>
  
</feed>
