<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zhuyasen-blog</title>
        <link>https://zhuyasen.com</link>
        <description>江上清风游</description>
        <atom:link href="https://zhuyasen.com/rss.html" rel="self" />
        <atom:link href="https://deepzz.superfeedr.com/" rel="hub" />
        <language>zh-CN</language>
        <lastBuildDate>Fri, 15 May 2026 02:20:31 +0800</lastBuildDate>
        
        <item>
            <title>DeepSeek与Sponge黄金组合打造后端高效开发新范式</title>
            <link>https://zhuyasen.com/post/ai-sponge.html</link>
            <comments>https://zhuyasen.com/post/ai-sponge.html#comments</comments>
            <guid>https://zhuyasen.com/post/ai-sponge.html</guid>
            <description>
                <![CDATA[<blockquote>

<h2 id="toc_0">背景介绍</h2>

<h3 id="toc_1">技术演进背景</h3>

<p>随着 DeepSeek 等开源 AI 工具的崛起，智能编程助手正在重塑软件开发流程。对于开发者而言，AI 辅助编码已为生产力工具。虽然目前 AI 尚无法直接根据<strong>需求文档</strong>和<strong>指定技术栈</strong>生成完整生产级项目，但在特定场景下已展现出惊人潜力：基于详细逻辑描述生成代码片段准确率可达 80%以上。目前AI 在项目的工程化能力方面仍显不足，而 Sponge 框架则在工程化能力表现出色，两者恰好形成互补。</p>

<p><a href="https://github.com/go-dev-frame/sponge" rel="nofollow">Sponge</a> 是一个强大的 Go 语言开发框架，目前提供40多个代码生成命令，其独特的逆向工程能力支持通过解析 SQL/Protobuf/JSON 生成模块化代码，模块化代码可灵活组合出 Web、gRPC、HTTP+gRPC、gRPC 网关等多种服务架构。开发者只需在生成的项目代码中填充业务逻辑，即可快速构建生产级后端服务。sponge生成代码框架图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/ai-sponge/sponge-framework.png" alt="sponge生成代码框架图" /></p>

<h3 id="toc_2">黄金组合</h3>

<p>当 Sponge 的工程化能力遇上 DeepSeek 的智能生成，形成了一套完整的高效开发解决方案：</p>

<ul>
<li><strong>Sponge</strong>：负责基础设施代码生成（服务框架、CRUD API 接口、缺少业务逻辑实现的自定义 API 接口等）。</li>
<li><strong>DeepSeek</strong>：专注业务逻辑实现（表 DDL 设计、自定义 API 接口定义、业务逻辑实现代码）。</li>
</ul>

<p>值得一提的是，Sponge 已集成了 DeepSeek 的 API，只需简单执行命令即可自动搜索项目代码中待补全业务逻辑的方法函数，让 DeepSeek 生成业务逻辑实现代码。</p>

<h2 id="toc_3">项目实战示例 —— 从零开始构建家电零售管理平台</h2>

<p>下面以构建一个线下家电实体店的产品管理平台为例，说明如何利用 Sponge 与 DeepSeek 协同开发后端服务。本示例后端技术栈选择 <strong>Web 服务 (Gin + Gorm + Protobuf)</strong>。</p>

<blockquote>
<p>注：这里把 API 接口的请求和返回数据结构定义在 Protobuf 文件中，充分利用 Protobuf 的优势——解析Protobuf来生成框架所需的代码和API接口文档。</p>
</blockquote>

<h3 id="toc_4">1. 生成功能需求文档</h3>

<p>首先，通过 DeepSeek R1 生成详细的功能需求文档。输入以下提示：</p>

<blockquote>
<p>“现在需要实现线下家电实体店铺的产品管理平台的后台服务，请列出详细的功能需求。”</p>
</blockquote>

<p>DeepSeek R1 会生成一个较为全面的需求文档，开发者可以根据实际需要删减不必要的功能，保留真正需要的功能模块，或额外添加补充功能模块。点击查看<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/_15_appliance_store/docs/requirements-document.md" rel="nofollow">家电零售管理平台功能需求文档</a>。</p>

<h3 id="toc_5">2. 生成 MySQL 表结构 DDL</h3>

<p>接下来，根据功能需求文档生成所有 MySQL 表结构的 DDL。输入以下提示：</p>

<blockquote>
<p>“根据功能需求文档，生成后台服务所需的所有 MySQL 表结构的 DDL，要求生成的 SQL 可直接导入 MySQL 创建表，表的每列均需附带中文注释。”</p>
</blockquote>

<p>DeepSeek R1 会根据需求文档生成对应的 Mysql 表结构 DDL，开发者需要校验判断是否完全满足要求，如果不满足可以人工调整。点击查看<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/_15_appliance_store/docs/store.sql" rel="nofollow">家电零售管理平台表结构DDL</a>。</p>

<p>把 Mysql 表结构 DDL 导入 MySQL 后，即可为后续代码生成提供数据结构支持。Sponge 可以根据这些表结构生成各种模块代码，如 CRUD API 代码 和 CRUD Protobuf 定义等。</p>

<h3 id="toc_6">3. API 接口定义</h3>

<h4 id="toc_7">3.1 生成 CRUD API Protobuf</h4>

<p>在 Sponge 的生成代码页面中，依次选择：【Public】→【生成 Protobuf CRUD 代码】，填写参数后点击【下载代码】按钮生成代码，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/ai-sponge/store-protobuf.png" alt="Protobuf CRUD 代码" /></p>

<blockquote>
<p><strong>提示：</strong> 如果生成的 proto 文件较多，建议将它们合并到一个文件中，因为 DeepSeek R1 上传文件数量有限制。</p>
</blockquote>

<h4 id="toc_8">3.2 生成自定义 API Protobuf</h4>

<p>标准的 CRUD API 并不能涵盖所有业务需求，因此需要根据 CRUD API Protobuf 文件和功能需求文档生成自定义 API 的 Protobuf 定义。<br />
在 DeepSeek R1 中上传 CRUD API 的 proto 文件和家电零售管理平台功能需求文档，并输入如下提示：</p>

<pre><code>已确定所有 MySQL 表的标准 CRUD API 的 Protobuf 定义，这些 API 仅涵盖家电零售管理平台后台服务的一部分功能，为了涵盖所有的功能，请依据CRUD API 的 Protobuf 定义和家电零售管理平台的功能需求文档，进行补充自定义API Protobuf，要求如下：
1. 每个 rpc 方法必须包含 option (google.api.http)。
2. rpc 方法及其 message 字段需附带中文注释，rpc 方法需详细描述逻辑实现过程（作为 AI 生成业务逻辑代码的依据）。
3. 补充的 API 需要标识其所属的 Protobuf service。
</code></pre>

<blockquote>
<p><strong>注：</strong> 若生成结果中 Protobuf 描述里的 rpc 方法注释不够详细(例如逻辑实现过程、指定技术栈)，可以适当人工补充完善。</p>
</blockquote>

<p>生成的自定义 API Protobuf 与 CRUD API Protobuf 共同构成了服务完整功能的API接口定义，为后续 Sponge 提供生成代码依据。</p>

<h3 id="toc_9">4. 创建服务代码</h3>

<p>以 Web 服务为例（技术栈：Gin + Gorm + Protobuf）进行后续代码生成和集成。</p>

<h4 id="toc_10">4.1 生成服务基础代码</h4>

<p>在 Sponge 代码生成的页面中，选择：【Protobuf】→【创建 Web 服务】，填写参数后点击【下载代码】按钮生成代码。如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/ai-sponge/store-http-pb.png" alt="基于protobuf创建服务" /></p>

<p>生成的代码包中包含服务的基本框架，解压后进入代码目录。</p>

<h4 id="toc_11">4.2 生成 CRUD API 代码</h4>

<p>同样在 Sponge 代码生成的页面中，选择：【Public】→【生成 Handler CRUD 代码】，填写参数后点击【下载代码】按钮生成代码，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/ai-sponge/store-handler-pb.png" alt="生成handler-pb代码" /></p>

<p>解压文件，将生成的 <code>api</code> 和 <code>internal</code> 目录移动至服务代码目录中。</p>

<p>使用 VS Code 或 Goland 打开项目代码，并将在前面由 DeepSeek R1 生成的自定义 API 的 Protobuf 文件人工合并到 <code>api/store/v1</code> 目录下对应的 proto 文件中。<br />
接着，在项目根目录下执行以下命令生成代码：</p>

<pre><code class="language-bash">make proto
</code></pre>

<blockquote>
<p><strong>注：</strong> 每当修改 proto 文件后，都需重新执行 <code>make proto</code> 命令，可通过指定 proto 文件名生成代码。</p>
</blockquote>

<h3 id="toc_12">5. 业务逻辑代码补全</h3>

<p>Sponge 集成了 DeepSeek API，可自动定位到需要补充业务逻辑的方法函数，让 AI 助手生成业务逻辑实现代码，执行如下命令：</p>

<pre><code class="language-bash">sponge assistant generate --type=deepseek --model=deepseek-reasoner --api-key=xxxx --dir=.
</code></pre>

<p>生成的业务逻辑代码会以 <code>.assistant</code> 为后缀保存在相应目录下，开发者只需将其复制到对应方法函数中即可。这里不采用自动填充的方式，是因为 DeepSeek R1 输出可能包含 markdown 等非纯 Go 代码，故需人工校验复制。</p>

<h3 id="toc_13">6. 测试和验证 API 功能</h3>

<p>至此，通过 Sponge 与 DeepSeek 的协同工作，绝大部分代码均已自动生成。如果AI助手根据详细的提示生成业务逻辑实现代码也无法满足要求，则需要人工编写代码。</p>

<p>接着开发者调试与验证 API 功能，启动服务：</p>

<pre><code class="language-bash">make run
</code></pre>

<p>使用浏览器访问 Swagger 界面进行 API 调试：<a href="http://localhost:8080/apis/swagger/index.html" rel="nofollow">http://localhost:8080/apis/swagger/index.html</a></p>

<p><img src="https://go-sponge.com/assets/images/blog/ai-sponge/store-swagger.png" alt="Swagger 调试界面" /></p>

<p>这是Sponge与DeepSeek协同生成的后端服务示例代码：<a href="https://github.com/go-dev-frame/sponge_examples/tree/main/_15_appliance_store" rel="nofollow">后端服务示例代码</a>。</p>

<p><img src="https://go-sponge.com/assets/images/blog/ai-sponge/web-http-pb-anatomy.png" alt="代码框架鸡蛋模型" /></p>

<h2 id="toc_14">总结</h2>

<p>本文通过一个家电零售管理平台的案例，演示了如何利用 Sponge 与 DeepSeek R1 协同开发后端服务的全过程：</p>

<ol>
<li><strong>需求分析</strong>：利用 DeepSeek R1 生成详细功能需求文档。</li>
<li><strong>数据库设计</strong>：依据需求生成 MySQL 表结构 DDL，并导入数据库。</li>
<li><strong>接口定义</strong>：先生成标准 CRUD API 的 Protobuf，再由 DeepSeek R1 生成自定义 API 的 Protobuf的定义信息。</li>
<li><strong>服务代码生成</strong>：利用 Sponge 分别生成基础服务代码、CRUD API 代码、自定义 API 的 Protobuf代码(只缺业务逻辑实现)。</li>
<li><strong>业务逻辑代码补全</strong>：通过 Sponge 内置 AI 工具自动生成业务逻辑代码，并由开发者进行整合与校验。</li>
<li><strong>调试验证</strong>：启动服务，借助 Swagger 等工具调试并验证 API 接口。</li>
</ol>

<p>这种基于 Sponge 与 DeepSeek R1 协同开发可以快速构建出一个功能完善、逻辑清晰的后端服务项目，实现了“写较少代码完成大部分工作”的目标，让个人开发者也能轻松承担团队级别的任务。同时也正在重新定义软件开发的效率边界：</p>

<ol>
<li><strong>开发角色转变</strong>：工程师更侧重架构设计和质量把控。</li>
<li><strong>敏捷度提升</strong>：原型开发周期从周级缩短至天级。</li>
<li><strong>知识沉淀</strong>：AI 生成的标准化代码更易维护和迭代。</li>
</ol>
<p>本文链接：<a href="https://zhuyasen.com/post/ai-sponge.html">https://zhuyasen.com/post/ai-sponge.html</a>，<a href="https://zhuyasen.com/post/ai-sponge.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>kafka基础和实践</title>
            <link>https://zhuyasen.com/post/kafka.html</link>
            <comments>https://zhuyasen.com/post/kafka.html#comments</comments>
            <guid>https://zhuyasen.com/post/kafka.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">kafka 基础</h3>

<p>kafka 是一个开源的流处理平台，主要用于构建实时数据管道和流应用。它最初由LinkedIn开发，并在2011年成为Apache软件基金会的顶级项目。</p>

<h4 id="toc_1">kafka 架构</h4>

<p>kafka 架构由存储层和计算层组成，如下图所示：</p>

<ul>
<li><strong>存储层</strong>旨在高效存储数据，并且是一个分布式系统，因此如果您的存储需求随着时间的推移而增长，您可以轻松扩展系统以适应增长。</li>
<li><strong>计算层</strong>由生产者、消费者、流和连接器 API 四个核心组件组成，它们允许 kafka 跨分布式系统扩展应用程序。</li>
</ul>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/kafka-framework.jpg" alt="kafka-framework" /></p>

<p>kafka 集群中的功能分为数据平面和控制平面，控制平面负责管理集群中的所有元数据，数据平面负责处理我们写入 kafka 和从 kafka 读取的实际数据。</p>

<p><br></p>

<p>kafka框架图：</p>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/kafka-framework2.jpg" alt="kafka-framework2" /></p>

<p>核心组件：</p>

<ul>
<li><p><strong>Producer（生产者）</strong></p>

<ul>
<li>负责发布消息到kafka的Topic中。</li>
<li>可以选择将消息发送到特定的Partition中，或由kafka的分区策略自动分配。</li>
</ul></li>

<li><p><strong>Consumer（消费者）</strong></p>

<ul>
<li>订阅一个或多个Topic，从中消费消息。</li>
<li>消费者通常会属于一个消费者组（Consumer Group），同一组中的消费者会共同分配并消费Topic的不同Partition中的消息，以实现负载均衡。</li>
</ul></li>

<li><p><strong>Broker</strong></p>

<ul>
<li>kafka集群中的一个服务器称为Broker。</li>
<li>Broker负责接收和存储消息，然后为消费者服务。</li>
</ul></li>

<li><p><strong>Topic</strong></p>

<ul>
<li>一个Topic可以看作是一个消息队列的名字。</li>
<li>kafka中的数据流会被分类发布到不同的Topic中。</li>
</ul></li>

<li><p><strong>Partition（分区）</strong></p>

<ul>
<li>每个Topic可以分为多个Partition。</li>
<li>分区是kafka数据存储的基本单元，所有的消息都会写入一个有序的分区中。</li>
</ul></li>

<li><p><strong>Replica（副本）</strong></p>

<ul>
<li>为了确保数据的高可用性和容错性，每个Partition可以有多个副本。</li>
<li>副本分为Leader副本和Follower副本，Leader副本负责处理读写请求，Follower副本负责备份数据。</li>
</ul></li>

<li><p><strong>Kraft</strong></p>

<ul>
<li>替代传统的 ZooKeeper 作为元数据管理和分布式协调服务，简化运维、提高性能、增强安全性。</li>
<li>使用 Raft 协议来实现分布式一致性和协调，并内置在kafka，简化了系统架构。</li>
</ul></li>
</ul>

<p><br></p>

<p>kafka 中的 topic 始终是多生产者和多订阅者的：一个主题可以有零个、一个或多个向其写入事件的生产者；一个主题可以有零个、一个或多个订阅这些事件的消费者。</p>

<p>主题中的事件可以根据需要随时读取，与传统消息传递系统不同，事件在使用后不会被删除。相反，您可以通过每个主题的配置设置来定义 kafka 应保留事件多长时间，超过该时间后将丢弃旧事件。kafka 的性能在数据大小方面实际上是恒定的，因此长时间存储数据是完全没问题的。</p>

<p>topic 是分区的，这意味着topic分布在位于不同 kafka 代理上的多个“存储桶”中。这种数据的分布式放置对于可扩展性非常重要，因为它允许客户端应用程序同时从多个代理读取数据或向多个代理写入数据。当新事件发布到topic时，它实际上被附加到主题的某个分区中。具有相同事件键（例如客户或车辆 ID）的事件被写入同一个分区，kafka保证给定主题分区的任何消费者将始终按照写入顺序读取该分区的事件。</p>

<p>通过将 Topic 分为多个 Partition，可以实现消息的并行处理和存储，提升系统的吞吐量和可靠性。Broker 提供了 Partition 的物理存储和管理，每个 Partition 又存在多个副本以确保数据的安全性和高可用性。这种设计使得 kafka 能够高效地处理大规模的实时数据流。</p>

<p><br></p>

<h4 id="toc_2">kafka 数据复制</h4>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/partition-copy.png" alt="partition-copy" /></p>

<p>一旦创建了 topic 中所有分区的副本，每个分区的一个副本将被指定为领导者副本，而持有该副本的代理将成为该分区的领导者，其余副本将成为追随者。生产者将写入领导者副本，追随者将获取数据与领导者保持同步。消费者默认从领导者副本获取数据，但可以配置为从追随者获取数据。</p>

<p>领导者则使用获取响应来通知追随者当前的偏移量。由于此过程是异步的，追随者的偏移量通常会落后于领导者所持有的实际偏移量。</p>

<p>所有追随者都已获取到特定偏移量，则该偏移量之前的记录将被视为已提交并可供消费者使用。这是由偏移量指定的。</p>

<p>领导者监控其追随者的进度，如果从追随者上次完全赶上以来经过了可配置的时间量，领导者将从同步副本集中删除该追随者。这允许领导者推进偏移量，以便消费者可以继续使用当前数据。如果追随者重新上线或以其他方式采取行动并赶上领导者，那么它将被重新添加到 ISR。</p>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/leader-balance.png" alt="leader-balance" /></p>

<p>领导者副本的代理比追随者副本的工作量要多一些。因此，最好不要在单个代理上拥有过多的领导者副本。为了防止这种情况，kafka 有一个首选副本的概念。创建主题时，每个分区的第一个副本被指定为首选副本。由于 kafka 已经在努力在可用的代理之间均匀分布分区，因此这通常会导致领导者之间的良好平衡。</p>

<p>由于领导者选举会因各种原因而发生，领导者最终可能会出现在非首选副本上，这可能会导致不平衡。因此，kafka 将定期检查领导者副本是否存在不平衡。它使用可配置的阈值来做出此判断。如果确实发现不平衡，它将执行领导者重新平衡，以使领导者回到其首选副本上。</p>

<p><br></p>

<h4 id="toc_3">kafka 控制平面</h4>

<p> kafka 3.3.1 以后版本使用KRaft替代Zookeeper，KRaft 模式有很多优点。</p>

<ul>
<li><strong>部署和管理更简单</strong>：由于只需安装和管理一个应用程序，kafka 的运营占用空间现在大大减少。这也使得在边缘的小型设备中利用 kafka 变得更加容易。</li>
<li><strong>提高可扩展性</strong>：如图所示，使用 KRaft 的恢复时间比使用 ZooKeeper 快一个数量级。这使我们能够高效地扩展到单个集群中的数百万个分区。使用 ZooKeeper 时，有效限制为数万个。</li>
<li><strong>更高效的元数据传播</strong>：基于日志、事件驱动的元数据传播可提高 kafka 许多核心功能的性能。</li>
</ul>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/kraft.png" alt="kraft" /></p>

<p>在 KRaft 模式下，kafka 集群可以以专用或共享模式运行。在专用模式下，一些节点会将其process.roles配置设置为controller，其余节点会将其设置为broker。对于共享模式，一些节点会将process.roles设置为controller 和 broker，这些节点将承担双重职责。选择哪种方式取决于集群的大小。</p>

<p>在启动kafka集群时以及当前领导者停止（无论是滚动升级还是由于故障）时都需要进行控制器领导者选举，通过投票请求、投票回应、达成共识三个步骤，当旧领导者控制器重新上线时，它跟随新领导者，并将其自己的元数据日志与领导者保持同步。</p>

<p><br></p>

<h4 id="toc_4">消费者组协议</h4>

<p>kafka 将存储与计算分开，存储由代理处理，计算主要由消费者或基于消费者构建的框架（kafka Streams、ksqlDB）处理。消费者组在 kafka 消费者的有效性和可扩展性方面发挥着关键作用。</p>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/consumer-group.png" alt="consumer-group" /></p>

<p><strong>1. 消费者组的定义</strong></p>

<p>消费者组是 kafka 中的一种机制，用于实现消息的并行处理和负载均衡。一个消费者组包含多个消费者实例，这些实例共同消费一个或多个主题（topic）的消息。</p>

<p><strong>2. 消费者组的工作原理</strong></p>

<ul>
<li><strong>负载均衡</strong>：kafka 将一个主题的分区（partition）分配给消费者组中的不同消费者实例，每个分区只会被组内的一个消费者消费。这种机制保证了消息的并行处理和负载均衡。</li>
<li><strong>容错性</strong>：如果组内的某个消费者实例宕机，kafka 会自动将其分配的分区重新分配给其他消费者实例，保证消息的连续消费。</li>
</ul>

<p><strong>3. kafka 使用消费者组的原因</strong></p>

<ul>
<li><strong>高吞吐量</strong>：通过消费者组，kafka 可以实现高吞吐量的消息处理，因为多个消费者实例可以并行处理不同的分区。</li>
<li><strong>扩展性</strong>：消费者组使得 kafka 可以轻松扩展，只需增加更多的消费者实例即可。</li>
<li><strong>容错性和高可用性</strong>：消费者组提供了自动故障转移和重新分配机制，增强了系统的容错性和高可用性。</li>
</ul>

<p><br></p>

<p><strong>消费者组再平衡</strong>是消费者组的一个关键特性，可能触发重新平衡的事件：</p>

<ul>
<li>实例未能在超时之前向协调器发送心跳，因此被从组中删除</li>
<li>实例已添加到组中</li>
<li>已将分区添加到组订阅中的主题</li>
<li>某个组有通配符订阅，并且创建了新的匹配主题</li>
<li>最初的团队启动</li>
</ul>

<p>注：对于静态组成员身份，每个消费者实例都会被分配一个group.instance.id，不发生重新平衡。</p>

<p><img src="https://go-sponge.com/assets/images/blog/kafka/rebalance-consumer-group.png" alt="rebalance-consumer-group" /></p>

<p>为了解决重新平衡时需要暂停处理的问题，引入了 CooperativeStickyAssignor 。此分配器的工作过程分为两个步骤。
- 确定需要撤销哪些分区分配。这些分配将在第一个重新平衡步骤结束时撤销。未撤销的分区可以继续处理。
- 被撤销的分区，它被分配给新的消费者。</p>

<p><br></p>

<h4 id="toc_5">数据持久性和可用性保证</h4>

<p><strong>生产者请求成功或失败的确认设置</strong></p>

<ul>
<li><p>生产者配置acks直接影响持久性保证，它还提供了持久性和延迟之间的几个权衡点之一。设置acks=0（也称为“即发即弃”模式）可提供较低的延迟，因为生产者不会等待代理的响应。但是，此设置无法提供强大的持久性保证，因为分区领导者可能由于暂时的连接问题而永远无法收到数据，或者我们可能正在经历领导者选举。</p></li>

<li><p>使用acks=1时，持久性会稍微好一些，因为知道数据已写入领导者副本，但延迟会稍微高一些，因为正在等待发送请求过程中的所有步骤。</p></li>

<li><p>最高级别的持久性来自 acks=all（或acks=-1），这也是默认设置。使用此设置，在数据写入领导者副本和 ISR（同步副本）列表中的所有追随者副本之前，不会确认发送请求。由于正在等待复制过程完成，因此延迟会更高。</p></li>
</ul>

<p><strong>主题级别配置min.insync.replicas与acks配置配合使用</strong></p>

<p>两者配合使用可以更有效地实施持久性保证，此设置告知代理，除非 ISR 中有 N 个副本，否则不允许将事件写入主题。与acks=all结合使用，可确保在确认事件发送之前，主题上收到的任何事件都将存储在 N 个副本中。</p>

<p>例如复制因子为 3，并且min.insync.replicas 设置为 2，那么我们可以容忍一次故障，并且仍然接收新事件。如果丢失了两个节点，那么生产者发送请求将收到异常，通知生产者副本不足。生产者可以重试，直到有足够的副本，或者将异常冒泡。无论哪种情况，都不会丢失数据。</p>

<p><strong>生产者幂等性</strong></p>

<p>要启用幂等性，我们在生产者上设置enable.idempotence = true，这是 kafka 3.0 的默认值。使用此设置，生产者会用生产者 ID 和序列号标记每个事件。这些值将与事件一起发送并存储在日志中。如果由于故障而再次发送事件，则将包含相同的标识符。如果发送了重复事件，代理将看到生产者 ID 和序列号已经存在，并将拒绝这些事件并向客户端返回DUP响应。</p>

<p>因为生产者幂等性，kafka 具有顺序保证， 会过滤掉重复的事件，事件按发送顺序写入特定分区，消费者按相同顺序读取这些事件。</p>

<p>kafka 多副本机制中的一些重要术语：</p>

<ul>
<li>AR(Assigned Replicas)：一个分区中的所有副本统称为 AR；</li>
<li>ISR(In-Sync Replicas)：Leader 副本和所有保持一定程度同步的 Follower 副本（包括 Leader 本身）组成 ISR；</li>
<li>OSR(Out-of-Sync Raplicas)：与 ISR 相反，没有与 Leader 副本保持一定程度同步的所有Follower 副本组成OSR；</li>
</ul>

<p><br></p>

<h3 id="toc_6">一些 kafka 常见的应用场景</h3>

<table>
<thead>
<tr>
<th>场景</th>
<th>场景描述</th>
<th>具体例子</th>
</tr>
</thead>

<tbody>
<tr>
<td><strong>日志收集</strong></td>
<td>kafka 常用于收集和聚合分布式系统中的日志数据，方便集中处理和分析。</td>
<td><strong>网站日志收集</strong>，一个大型电商网站有多个服务器，每个服务器生成大量的访问日志。可以使用 kafka 将这些日志发送到一个集中的 kafka 集群，然后使用消费者从 kafka 中读取日志进行实时分析或存储到 HDFS 中进行离线分析。</td>
</tr>

<tr>
<td><strong>微服务通信</strong></td>
<td>kafka 可以作为微服务之间的通信中介，确保服务之间的解耦和高可用性。</td>
<td><strong>订单和通知服务</strong>，在一个微服务架构的电商平台中，订单服务可以将订单信息发送到 kafka，通知服务从 kafka 中消费订单信息并发送相应的通知（如短信或邮件）给用户。</td>
</tr>

<tr>
<td><strong>消息队列</strong></td>
<td>kafka 可以作为消息队列系统，处理高吞吐量的消息传递。</td>
<td><strong>订单处理系统</strong>，电商平台的订单处理系统可以使用 kafka 作为消息队列，将用户订单信息发送到 kafka，然后由多个消费者（如库存管理系统、支付系统）从 kafka 中读取订单信息进行处理。</td>
</tr>

<tr>
<td><strong>数据管道</strong></td>
<td>kafka 常用于在不同的数据系统之间传输数据，充当数据管道。</td>
<td><strong>数据同步</strong>，一个公司有多个数据库系统（如 MySQL 和 MongoDB），可以使用 kafka 将数据从一个数据库同步到另一个数据库，确保数据一致性。</td>
</tr>

<tr>
<td><strong>事件溯源</strong></td>
<td>kafka 可以用于事件溯源，记录系统中发生的所有事件。</td>
<td><strong>用户行为追踪</strong>，社交媒体平台使用 kafka 记录用户的所有行为（如点赞、评论、分享），然后可以基于这些事件数据进行用户行为分析和个性化推荐。</td>
</tr>

<tr>
<td><strong>实时流处理</strong></td>
<td>kafka 可以与流处理框架（如 Apache Storm、Apache Flink 和 Apache Spark）结合使用，处理实时数据流。</td>
<td><strong>实时监控</strong>，金融公司使用 kafka 来收集股票交易数据，并通过 Spark Streaming 实时处理这些数据，检测异常交易行为。</td>
</tr>
</tbody>
</table>

<p><br></p>

<h3 id="toc_7">安装 kafka</h3>

<h4 id="toc_8">安装单机版 kafka</h4>

<p>安装kafaka集群有<code>.env</code>和<code>docker-compose.yml</code>两个文件。</p>

<p><code>.env</code>文件内容如下：</p>

<pre><code># 把下面的 192.168.3.37 改为你的ip地址
ACCESS_ADDR=192.168.3.37:9092
</code></pre>

<p><code>docker-compose.yml</code>内容如下：</p>

<pre><code class="language-yaml">version: '3.8'

services:
  broker:
    image: apache/kafka:3.7.0
    container_name: broker
    ports:
      - '9092:9092'
    environment:
      kafka_NODE_ID: 1
      kafka_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
      kafka_ADVERTISED_LISTENERS: 'PLAINTEXT_HOST://${ACCESS_ADDR},PLAINTEXT://broker:19092'
      kafka_PROCESS_ROLES: 'broker,controller'
      kafka_CONTROLLER_QUORUM_VOTERS: '1@broker:29093'
      kafka_LISTENERS: 'CONTROLLER://:29093,PLAINTEXT_HOST://:9092,PLAINTEXT://:19092'
      kafka_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
      kafka_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      CLUSTER_ID: '4L6g3nShT-eMCtK--X86sw'
      kafka_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      kafka_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      kafka_TRANSACTION_STATE_LOG_MIN_ISR: 1
      kafka_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      kafka_LOG_DIRS: '/var/lib/kafka/data'
    volumes:
      - $PWD/data/:/var/lib/kafka/data

  kafka-ui:
    image: provectuslabs/kafka-ui:v0.7.2
    container_name: kafka-ui
    ports:
      - &quot;18080:8080&quot;
    environment:
      kafka_CLUSTERS_0_NAME: 'Local kafka Cluster'
      kafka_CLUSTERS_0_BOOTSTRAPSERVERS: 'broker:19092'
      DYNAMIC_CONFIG_ENABLED: &quot;true&quot;
    depends_on:
      - broker
</code></pre>

<p>第一次使用时，创建一个data文件夹作为数据持久化，并且修改目录data权限，</p>

<pre><code class="language-bash">mkdir data
chmod -R 0777 data
</code></pre>

<p>打开<code>.env</code>文件，修改 kafka broker 外部访问地址，用于外部客户端连接，然后启动kafaka服务：</p>

<pre><code class="language-bash">docker-compose up -d
</code></pre>

<p>启动服务成功后，可以在浏览器打开 <code>http://localhost:18080</code> 查看kafka信息。</p>

<p><br></p>

<h4 id="toc_9">安装 kafka 集群</h4>

<p>安装kafaka集群有<code>.env</code>和<code>docker-compose.yml</code>两个文件。</p>

<p><code>.env</code>文件内容如下：</p>

<pre><code># 把下面的 192.168.3.37 改为你的ip地址
kafka_1_ACCESS_ADDR=192.168.3.37:33001
kafka_2_ACCESS_ADDR=192.168.3.37:33002
kafka_3_ACCESS_ADDR=192.168.3.37:33003
</code></pre>

<p><code>docker-compose.yml</code>内容如下：</p>

<pre><code class="language-yaml">version: &quot;3.8&quot;

services:
  kafka-1:
    image: docker.io/bitnami/kafka:3.7
    container_name: kafka-1
    ports:
      - &quot;33001:9092&quot;
    environment:
      # KRaft settings
      - kafka_CFG_NODE_ID=0
      - kafka_CFG_PROCESS_ROLES=controller,broker
      - kafka_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-1:9093,1@kafka-2:9093,2@kafka-3:9093
      - kafka_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
      # Listeners
      - kafka_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
      #- kafka_CFG_ADVERTISED_LISTENERS=PLAINTEXT://:9092
      - kafka_CFG_ADVERTISED_LISTENERS=PLAINTEXT://${kafka_1_ACCESS_ADDR}
      - kafka_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
      - kafka_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - kafka_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
      # Clustering
      - kafka_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
      - kafka_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
      - kafka_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
    volumes:
      - $PWD/data/kafka-1:/bitnami/kafka
    networks:
      - kafka-net

  kafka-2:
    image: docker.io/bitnami/kafka:3.7
    container_name: kafka-2
    ports:
      - &quot;33002:9092&quot;
    environment:
      # KRaft settings
      - kafka_CFG_NODE_ID=1
      - kafka_CFG_PROCESS_ROLES=controller,broker
      - kafka_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-1:9093,1@kafka-2:9093,2@kafka-3:9093
      - kafka_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
      # Listeners
      - kafka_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
      #- kafka_CFG_ADVERTISED_LISTENERS=PLAINTEXT://:9092
      - kafka_CFG_ADVERTISED_LISTENERS=PLAINTEXT://${kafka_2_ACCESS_ADDR}
      - kafka_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
      - kafka_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - kafka_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
      # Clustering
      - kafka_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
      - kafka_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
      - kafka_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
    volumes:
      - $PWD/data/kafka-2:/bitnami/kafka
    networks:
      - kafka-net

  kafka-3:
    image: docker.io/bitnami/kafka:3.7
    container_name: kafka-3
    ports:
      - &quot;33003:9092&quot;
    environment:
      # KRaft settings
      - kafka_CFG_NODE_ID=2
      - kafka_CFG_PROCESS_ROLES=controller,broker
      - kafka_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-1:9093,1@kafka-2:9093,2@kafka-3:9093
      - kafka_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
      # Listeners
      - kafka_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
      #- kafka_CFG_ADVERTISED_LISTENERS=PLAINTEXT://:9092
      - kafka_CFG_ADVERTISED_LISTENERS=PLAINTEXT://${kafka_3_ACCESS_ADDR}
      - kafka_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
      - kafka_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - kafka_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
      # Clustering
      - kafka_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
      - kafka_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
      - kafka_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
    volumes:
      - $PWD/data/kafka-3:/bitnami/kafka
    networks:
      - kafka-net

  kafka-ui:
    image: provectuslabs/kafka-ui:v0.7.2
    restart: always
    container_name: kafka-ui
    ports:
      - &quot;18080:8080&quot;
    environment:
      - kafka_CLUSTERS_0_NAME=Local-Kraft-Cluster
      - kafka_CLUSTERS_0_BOOTSTRAPSERVERS=kafka-1:9092,kafka-2:9092,kafka-3:9092
      - DYNAMIC_CONFIG_ENABLED=true
      - kafka_CLUSTERS_0_AUDIT_TOPICAUDITENABLED=true
      - kafka_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED=true
    depends_on:
      - kafka-1
      - kafka-2
      - kafka-3
    networks:
      - kafka-net

networks:
  kafka-net:
</code></pre>

<p>第一次使用时，创建一个data文件夹作为数据持久化，并且修改目录data权限：</p>

<pre><code class="language-bash">mkdir data/kafka-1 data/kafka-2 data/kafka-3
chmod -R 0777 data
</code></pre>

<p>打开<code>.env</code>文件，修改 kafka-1、kafka-2、kafka-3 外部访问地址，用于外部客户端连接，然后启动kafaka集群：</p>

<pre><code class="language-bash">docker-compose up -d
</code></pre>

<p>启动服务成功后，可以在浏览器打开 <code>http://localhost:18080</code> 查看kafka信息。</p>

<p><br></p>

<h3 id="toc_10">使用go操作kafka示例</h3>

<h4 id="toc_11">创建 topic 示例</h4>

<pre><code class="language-go">package main

import (
    &quot;flag&quot;
    &quot;fmt&quot;

    &quot;github.com/IBM/sarama&quot;
)

var (
    brokerAddrs = []string{&quot;192.168.3.37:33001&quot;, &quot;192.168.3.37:33002&quot;, &quot;192.168.3.37:33003&quot;}
    topic       string
)

func main() {
    flag.StringVar(&amp;topic, &quot;topic&quot;, &quot;&quot;, &quot;the name of the topic to create&quot;)
    flag.Parsed()
    if topic == &quot;&quot; {
        fmt.Println(&quot;please specify the topic name, usage: go run main.go -topic &lt;topic_name&gt;&quot;)
        return
    }

    // 创建kafka管理员客户端
    admin, err := sarama.NewClusterAdmin(brokerAddrs, sarama.NewConfig())
    if err != nil {
        panic(err)
    }
    defer admin.Close()

    // 创建主题
    topicConfig := &amp;sarama.TopicDetail{
        NumPartitions:     3, // 分区数
        ReplicationFactor: 1, // 副本数
        ConfigEntries:     map[string]*string{},
    }
    if err := CreateTopic(admin, topic, topicConfig); err != nil {
        panic(err)
    }
}

// IsTopicExists checks if a topic exists in the kafka cluster
func IsTopicExists(admin sarama.ClusterAdmin, topic string) bool {
    topics, err := admin.ListTopics()
    if err != nil {
        return false
    }

    _, ok := topics[topic]
    return ok
}

// CreateTopic creates a new topic in the kafka cluster, if topic already exists, it will ignore
func CreateTopic(admin sarama.ClusterAdmin, topic string, topicConfig *sarama.TopicDetail) error {
    if IsTopicExists(admin, topic) {
        return nil
    }

    err := admin.CreateTopic(topic, topicConfig, false)
    if err != nil {
        return err
    }

    fmt.Printf(&quot;topic %s created successfully\n&quot;, topic)
    return nil
}
</code></pre>

<p><br></p>

<h4 id="toc_12">生产者示例</h4>

<p><strong>1. 同步生产者示例</strong></p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;

    &quot;github.com/IBM/sarama&quot;
)

var (
    brokerList = []string{&quot;192.168.3.37:33001&quot;, &quot;192.168.3.37:33002&quot;, &quot;192.168.3.37:33003&quot;}
    topicName = &quot;test_topic&quot;
)

func main() {
    // 创建kafka生产者配置
    config := sarama.NewConfig()
    // 设置kafka版本
    // config.Version = sarama.V3_6_0_0
    // ack类型 WaitForLocal(leader确认), WaitForAll(leader和follow都确认), NoResponse(不需确认)
    config.Producer.RequiredAcks = sarama.WaitForAll
    // 分区策略，默认是NewHashPartitioner、根据业务需要可以选择使用 NewRandomPartitioner、
    //NewRoundRobinPartitioner、NewReferenceHashPartitioner、NewManualPartitioner。
    config.Producer.Partitioner = sarama.NewHashPartitioner
    config.Producer.Retry.Max = 5
    // 成功交付的消息将在success channel返回
    config.Producer.Return.Successes = true
    config.ClientID = &quot;kafka-demo&quot;

    // 创建同步生产者
    producer, err := sarama.NewSyncProducer(brokerList, config)
    if err != nil {
        panic(err)
    }
    defer producer.Close()

    // 发送的消息到指定主题
    hostID := rand.Intn(1000)
    for i := 1; i &lt;= 50; i++ {
        // 构造一个消息
        message := &amp;sarama.ProducerMessage{
            Topic:    topicName,
            Value:    sarama.StringEncoder(fmt.Sprintf(&quot;'%d log content %d'&quot;, hostID, i)),
            Metadata: i,
        }

        // 发送消息
        partition, offset, err := producer.SendMessage(message)
        if err != nil {
            panic(err)
        }

        fmt.Printf(&quot;send msg, topic=%s, partition=%d, offset=%d, i=%v\n&quot;, topicName, partition, offset, i)
    }

    &lt;-time.After(time.Second * 2)
}
</code></pre>

<p><br></p>

<p><strong>2. 异步生产者示例</strong></p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;

    &quot;github.com/IBM/sarama&quot;
)

var (
    brokerList = []string{&quot;192.168.3.37:33001&quot;, &quot;192.168.3.37:33002&quot;, &quot;192.168.3.37:33003&quot;}
    topicName = &quot;test_topic&quot;
)

func main() {
    // 创建kafka生产者配置
    config := sarama.NewConfig()
    // 设置kafka版本
    // config.Version = sarama.V3_6_0_0
    // ack类型 WaitForLocal(leader确认), WaitForAll(leader和follow都确认), NoResponse(不需确认)
    config.Producer.RequiredAcks = sarama.WaitForLocal
    // 分区策略，默认是NewHashPartitioner、根据业务需要可以选择使用 NewRandomPartitioner、
    //NewRoundRobinPartitioner、NewReferenceHashPartitioner、NewManualPartitioner。
    config.Producer.Partitioner = sarama.NewHashPartitioner
    // 成功交付的消息将在success channel返回
    config.Producer.Return.Successes = true
    // 触发批量发送消息数设置
    config.Producer.Flush.Messages = 10
    config.Producer.Flush.Frequency = time.Second

    // 创建异步生产者
    producer, err := sarama.NewAsyncProducer(brokerList, config)
    if err != nil {
        panic(err)
    }
    defer producer.Close()

    // 返回结果状态
    go func() {
        for {
            select {
            case pm := &lt;-producer.Successes():
                fmt.Printf(&quot;send msg, topic=%s, partition=%d, offset=%d, i=%v\n&quot;, pm.Topic, pm.Partition, pm.Offset, pm.Metadata)
            case err := &lt;-producer.Errors():
                fmt.Printf(&quot;send msg failed, err: %v&quot;, err)
            }
        }
    }()

    // 发送的消息到指定主题
    hostID := rand.Intn(1000)
    count := 50
    for i := 1; i &lt;= count; i++ {
        // 构造一个消息
        message := &amp;sarama.ProducerMessage{
            Topic:    topicName,
            Value:    sarama.StringEncoder(fmt.Sprintf(&quot;'%d log content %d'&quot;, hostID, i)),
            Metadata: i,
        }

        // 发送消息
        producer.Input() &lt;- message
    }

    fmt.Println(&quot;send msg done&quot;)
    &lt;-time.After(time.Second * 2)
}
</code></pre>

<p><br></p>

<h4 id="toc_13">消费者示例</h4>

<p><strong>1. 消费者组示例</strong></p>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;time&quot;

    &quot;github.com/IBM/sarama&quot;
)

var (
    brokerList = []string{&quot;192.168.3.37:33001&quot;, &quot;192.168.3.37:33002&quot;, &quot;192.168.3.37:33003&quot;}
    groupID    = &quot;group1&quot;
    topicName  = &quot;test_topic&quot;
)

func main() {
    // 创建kafka消费者配置
    config := sarama.NewConfig()
    //config.Version = sarama.V3_6_0_0
    config.Consumer.Offsets.Initial = sarama.OffsetOldest // 从未消费的消息开始消费，有可能重复消费
    config.Consumer.Offsets.AutoCommit.Enable = true // true：自动提交偏移量，false：手动提交偏移量
    config.Consumer.Offsets.AutoCommit.Interval = time.Second

    // 创建kafka消费者组
    cg, err := sarama.NewConsumerGroup(brokerList, groupID, config)
    if err != nil {
        panic(err)
    }
    defer cg.Close()

    // 消费消息
    ctx := context.Background()
    autoCommit := config.Consumer.Offsets.AutoCommit.Enable
    err = cg.Consume(ctx, []string{topicName}, &amp;consumerHandler{autoCommit: autoCommit})
    if err != nil {
        fmt.Printf(&quot;consume error: %v\n&quot;, err)
    }
}

// Setup 、 Cleanup 和 ConsumeClaim 是 s.handler.ConsumeClaim 的三个接口，需要用户自己实现。
// 可以简单理解为，当需要创建一个会话时，先运行 Setup ，然后在 ConsumeClaim 中处理消息，最后运行 Cleanup 。
type consumerHandler struct {
    autoCommit bool
}

func (h *consumerHandler) Setup(sess sarama.ConsumerGroupSession) error {
    fmt.Println(&quot;setup topic:partitions --&gt;&quot;, sess.Claims()) // 当有新的消费者加入或退出消费者组时，动态平衡后后可以看到本消费者所负责的分区
    return nil
}

func (h *consumerHandler) Cleanup(sess sarama.ConsumerGroupSession) error {
    fmt.Println(&quot;cleanup topic:partitions --&gt;&quot;, sess.Claims())
    return nil
}

func (h *consumerHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
    for msg := range claim.Messages() {
        fmt.Printf(&quot;received msg: topic=%s, partition=%d, offset=%d, key=%s, val=%s\n&quot;, msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value)
        sess.MarkMessage(msg, &quot;&quot;)
        if !h.autoCommit {
            sess.Commit()
        }
    }
    return nil
}
</code></pre>

<p><br></p>

<p><strong>2. 分区消费示例</strong></p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;

    &quot;github.com/IBM/sarama&quot;
)

var (
    brokerList = []string{&quot;192.168.3.37:33001&quot;, &quot;192.168.3.37:33002&quot;, &quot;192.168.3.37:33003&quot;}
    topicName  = &quot;test_topic&quot;
)

func main() {
    // 创建kafka消费者配置
    config := sarama.NewConfig()
    config.Version = sarama.V3_6_0_0
    config.Consumer.Return.Errors = true

    // 创建kafka消费者
    consumer, err := sarama.NewConsumer(brokerList, config)
    if err != nil {
        panic(err)
    }
    defer consumer.Close()

    // 根据topic取到所有的分区
    partitionList, err := consumer.Partitions(topicName)
    if err != nil {
        panic(err)
    }

    // 消费的主题
    for _, partition := range partitionList {
        offset := sarama.OffsetNewest // 可以设置为指定偏移量、最新偏移量sarama.OffsetNewest、历史偏移量sarama.OffsetOldest
        go func(partitionID int32, offset int64) {
            pc, err := consumer.ConsumePartition(topicName, partitionID, offset)
            if err != nil {
                panic(err)
            }
            defer pc.Close()

            for {
                select {
                case msg := &lt;-pc.Messages():
                    fmt.Printf(&quot;received msg: topic=%s, partition=%d, offset=%d, key=%s, val=%s\n&quot;, msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value)
                case err := &lt;-pc.Errors():
                    fmt.Println(&quot;consuming err:&quot;, err)
                }
            }
        }(partition, offset)
    }

    select {}
}
</code></pre>

<p><br></p>

<h4 id="toc_14">获取 topic 堆积数量示例</h4>

<p>topic堆积数量是比较重要的一个指标，直接影响业务的进行，可以在从kafka服务中该指标，也可以实现一个简单客户端从kafka或该指标。</p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;

    &quot;github.com/IBM/sarama&quot;
)

var (
    brokerList = []string{&quot;192.168.3.37:33001&quot;, &quot;192.168.3.37:33002&quot;, &quot;192.168.3.37:33003&quot;}
    topic      = &quot;test_topic&quot;
    groupID    = &quot;test_group&quot;
)

// ClientManager client manager
type ClientManager struct {
    client sarama.Client
    offsetManager sarama.OffsetManager
}

// Backlog info
type Backlog struct {
    Partition         int32 `json:&quot;partition&quot;`  // partition id
    Backlog           int64 `json:&quot;backlog&quot;`    // data backlog
    NextConsumeOffset int64 `json:&quot;nextOffset&quot;` // offset for next consumption
}

// InitClientManager init client manager
func InitClientManager(addrs []string, groupID string) (*ClientManager, error) {
    config := sarama.NewConfig()
    //config.Version = sarama.V3_6_0_0
    client, err := sarama.NewClient(addrs, config)
    if err != nil {
        return nil, err
    }

    offsetManager, err := sarama.NewOffsetManagerFromClient(groupID, client)
    if err != nil {
        return nil, err
    }

    return &amp;ClientManager{
        client:        client,
        offsetManager: offsetManager,
    }, nil
}

// GetBacklog get topic backlog
func (m *ClientManager) GetBacklog(topic string) (int64, []*Backlog, error) {
    var (
        total             int64 = 0
        partitionBacklogs []*Backlog
    )

    partitions, err := m.client.Partitions(topic)
    if err != nil {
        return 0, nil, err
    }

    for _, partition := range partitions {
        // get offset from kafka
        offset, err := m.client.GetOffset(topic, partition, -1)
        if err != nil {
            return 0, nil, err
        }

        // create topic/partition manager
        pom, err := m.offsetManager.ManagePartition(topic, partition)
        if err != nil {
            return 0, nil, err
        }

        var backlog int64
        // call sarama The NextOffset method of PartitionOffsetManager. Return the offset for the next consumption
        // if the consumer group has not consumed the data for this section, the return value will be -1
        n, str := pom.NextOffset()
        if str != &quot;&quot; {
            return 0, nil, fmt.Errorf(&quot;partition %d, %s&quot;, partition, str)
        }
        if n == -1 {
            backlog = offset
        } else {
            backlog = offset - n
        }
        total += backlog

        partitionBacklogs = append(partitionBacklogs, &amp;Backlog{
            Partition:         partition,
            Backlog:           backlog,
            NextConsumeOffset: n,
        })
    }

    return total, partitionBacklogs, nil
}

// Close topic backlog
func (m *ClientManager) Close() error {
    if m != nil &amp;&amp; m.client != nil {
        return m.client.Close()
    }
    return nil
}

func main() {
    m, err := InitClientManager(brokerList, groupID)
    if err != nil {
        panic(err)
    }
    defer m.Close()

    total, backlogs, err := m.GetBacklog(topic)
    if err != nil {
        panic(err)
    }

    fmt.Println(&quot;total backlog:&quot;, total)
    for _, backlog := range backlogs {
        fmt.Printf(&quot;partation=%d, backlog=%d, next_consume_offset=%d\n&quot;, backlog.Partition, backlog.Backlog, backlog.NextConsumeOffset)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_15">总结</h3>

<p>kafka 作为一个分布式流处理平台，应用非常广泛，尤其是在大数据处理领域，它以其高吞吐量、低延迟、可扩展性等特点，成为构建实时数据处理应用的首选平台之一。在使用 kafka 的过程中，需要了解一些kafka相关知识：</p>

<p><strong>1. 深入理解 kafka 的核心概念</strong></p>

<p>kafka 的核心概念包括主题、分区、消费者组、偏移量等。理解这些概念对于理解 kafka 的工作原理和使用 kafka 进行开发至关重要。</p>

<p><strong>2. 掌握 kafka 的基本操作</strong></p>

<p>kafka 提供了丰富的 API 来进行消息的发布和消费。掌握这些 API 是使用 kafka 的基础。</p>

<p><strong>3. 了解 kafka 的常见应用场景</strong></p>

<p>kafka 可以应用于各种场景，例如日志收集、数据分析、实时流处理等。了解 kafka 的常见应用场景可以帮助我们更好地选择 kafka 进行应用开发。</p>

<p><strong>4. 关注 kafka 的性能优化</strong></p>

<p>kafka 的性能优化是一个重要的课题。通过合理的配置和优化，可以提高 kafka 的吞吐量和降低延迟。</p>

<p><strong>5. 学习 kafka 的生态系统</strong></p>

<p>kafka 拥有丰富的生态系统，包括各种工具、框架和库。学习 kafka 的生态系统可以帮助我们更好地使用 kafka。</p>

<ul>
<li>官网： <a href="https://kafka.apache.org/" rel="nofollow">https://kafka.apache.org/</a></li>
<li>文档：<a href="https://kafka.apache.org/documentation/#gettingStarted" rel="nofollow">https://kafka.apache.org/documentation/#gettingStarted</a></li>
<li>go SDK：<a href="https://github.com/IBM/sarama" rel="nofollow">https://github.com/IBM/sarama</a></li>
<li>go SDK示例：<a href="https://github.com/IBM/sarama/tree/main/examples" rel="nofollow">https://github.com/IBM/sarama/tree/main/examples</a></li>
</ul>

<p>总而言之，kafka 是一门强大的工具，可以帮助我们构建实时数据处理应用。通过深入学习和掌握 kafka 的相关知识和技能，我们可以充分发挥 kafka 的优势，为我们的业务带来价值。</p>

<p><br></p>
<p>本文链接：<a href="https://zhuyasen.com/post/kafka.html">https://zhuyasen.com/post/kafka.html</a>，<a href="https://zhuyasen.com/post/kafka.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>使用开发框架sponge快速把单体web服务拆分为微服务</title>
            <link>https://zhuyasen.com/post/community-cluster.html</link>
            <comments>https://zhuyasen.com/post/community-cluster.html#comments</comments>
            <guid>https://zhuyasen.com/post/community-cluster.html</guid>
            <description>
                <![CDATA[<blockquote>

<p>接着上一篇文章 <a href="https://zhuyasen.com/post/community-single.html" rel="nofollow">一天多开发完成一个极简版社区后端服务</a> ，接下来使用工具sponge实战一个微服务集群项目community-cluster，点击查看community-cluster的完整<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster" rel="nofollow">项目代码</a>，</p>

<h2 id="toc_0">单体服务community-single拆分为微服务具体过程</h2>

<p>社区后端服务<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single" rel="nofollow">community-single</a>采用单体web应用架构，为了应对需求增加，造成功能越来越复杂，代码维护和开发变得困难的问题，把community-single拆分成多个微服务，下面是拆分微服务具体步骤：</p>

<p><strong>第一步是进行系统分析和设计</strong>。首先确定哪些功能模块适合独立作为微服务，需要对单体服务community-single进行了仔细的功能分解，将其划分为几个关键的领域，分为用户服务(user)、关系服务(relation)和内容创作服务(creation)三个独立的服务。用户服务负责用户注册登录等功能，关系服务负责好友关系管理等，内容创作服务负责帖子创建、评论、点赞、收藏等功能。这些领域代表了系统的核心功能，并且在不同的领域之间存在较强的逻辑隔离。</p>

<p><strong>第二步是定义服务接口</strong>。每个微服务需要定义清晰的RPC接口供外部调用，接口需要指定输入输出的数据结构。每个服务开发团队需要根据业务设计自己的接口并完成接口文档。</p>

<p><strong>第三步是设计集群架构</strong>。引入了一个rpc网关服务(community_gw)，rpc网关服务作为所有微服务的入口，负责路由请求和负载均衡，它可以根据请求的路由信息将请求转发给相应的微服务。此外，rpc网关服务还提供了身份验证、授权和安全性等共享功能，以确保系统的安全性和一致性，这种架构可以提高整体的扩展性。</p>

<p><strong>第四步是数据迁移</strong>。单体服务community-single使用的单一数据库，现在需要将数据按服务拆分，迁移至每个微服务自己的数据存储中。这里用户服务、关系服务、创作服务使用独立的MySQL，实现各自的数据隔离。</p>

<p><strong>第五步是开发、测试、部署微服务</strong>。在拆分后的微服务集群中，每个微服务都可以独立进行。团队成员可以专注于自己领域的开发工作，并且可以根据需求对各个微服务进行水平扩展，以满足不同的性能需求。此外，微服务架构还提供了更好的可扩展性，提供持续集成和持续交付(CI/CD)，以快速部署和发布新的功能和更新。微服务上线后，需要全面测试各个微服务的功能，确保拆分后的服务可以正常运行、RPC调用正常、满足预期功能。</p>

<p><strong>最后是流量迁移</strong>。当微服务架构正常运行后，将外部流量逐步迁移至新的RPC网关层，停止对community-single的访问，完成从单体架构到微服务架构的过渡。后端服务的扩展和升级将主要在微服务层进行。</p>

<p>通过上述步骤，将单体服务community-single拆分为微服务是一个复杂而耗时的过程，通过系统分析、功能拆分、技术选型、API设计和迁移策略等步骤，实现系统的微服务化，并提升系统的可扩展性、可靠性和性能。微服务架构也带来了分布式事务、运维成本增加等新的挑战，需要综合考虑多个因素。通过持续的评估和优化，可以不断提升系统的灵活性和可维护性，以适应不断变化的业务需求。</p>

<p><br></p>

<h2 id="toc_1">community-cluster 介绍</h2>

<p>community-cluster是由 <strong>gRPC服务</strong> 和 <strong>rpc网关服务</strong> 这两种服务类型组成，gRPC服务是各个功能的实现模块，rpc网关服务主要作用是转发请求给gRPC服务和组装数据。community-cluster服务集群由工具sponge搭建，sponge生成gRPC服务和rpc网关服务代码时都会自动剥离业务逻辑与非业务逻辑两部分代码，剥离业务逻辑与非业务逻辑的好处是让开发者聚焦在核心业务逻辑代码中，极大的减小搭建微服务集群的难度，减少人工编写大量代码。框架图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-cluster-frame.png" alt="community-cluster-frame" /></p>

<p>gRPC服务代码组成结构基于<a href="https://github.com/grpc/grpc-go" rel="nofollow">grpc</a>封装，包括了丰富的服务治理插件、构建、部署脚本，gRPC服务代码组成结构如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/micro-rpc-pb-anatomy.png" alt="micro-rpc-pb-anatomy" />
<p align="center">图1 gRPC服务代码结构图</p></p>

<p>从图1可以看出，开发一个完整的微服务聚焦在<strong>定义数据表</strong>、<strong>定义api接口</strong>、<strong>在模板代码中编写具体业务逻辑代码</strong>这3个节点上，而这3个节点代码在单体web服务<a href="https://github.com/go-dev-frame/sponge_examples/tree/main/7_community-single" rel="nofollow">community-single</a>已经存在，不需要重新编写，直接把这些代码移植过来即可，也就是蛋黄(核心业务逻辑代码)保持不变，只需换蛋壳(web框架换成gRPC框架)和蛋白(http handler相关代码换成rpc service相关代码)，使用工具sponge，很容易完成web服务到gRPC服务的转换。</p>

<p><br></p>

<p>rpc网关服务代码基于<a href="https://github.com/gin-gonic/gin" rel="nofollow">gin</a>封装，包括了丰富的服务治理插件、构建、部署脚本，rpc网关服务代码组成结构如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/micro-rpc-gw-pb-anatomy.png" alt="micro-rpc-gw-pb-anatomy" />
<p align="center">图2 rpc网关服务代码结构图</p></p>

<p>从图2可以看出，开发一个完整rpc网关服务聚焦在<strong>定义api接口</strong>、<strong>在模板代码中编写具体业务逻辑代码</strong>这2个节点上，其中<strong>定义api接口</strong>在单体web服务<a href="https://github.com/go-dev-frame/sponge_examples/tree/main/7_community-single" rel="nofollow">community-single</a>已经存在，不需要重新编写，复制proto文件过来就可以使用。</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-cluster-gen.png" alt="community-cluster-gen" /></p>

<p>这是单体web服务和微服务集群依赖的proto文件对比图，左边是单体web服务依赖的proto文件，所有proto文件都在同一个服务中。右边是微服务依赖的proto文件，根据各个gRPC服务依赖自己的proto文件。</p>

<p>在rpc网关服务中，如果需要从多个微服务中获取的数据组装成一个新的api接口，把这个组装的新api接口描述信息填写到community_gw.proto文件中。</p>

<p><br></p>

<p>下面使用工具sponge从0开始到完成微服务集群过程，开发过程依赖工具sponge，需要先安装sponge，点击查看<a href="https://github.com/go-dev-frame/sponge/blob/main/assets/install-cn.md#%E5%9C%A8linux%E6%88%96macos%E4%B8%8A%E5%AE%89%E8%A3%85sponge" rel="nofollow">安装说明</a>。</p>

<p>创建一个目录community-cluster，把各个独立微服务代码移动到这个目录下。</p>

<p><br></p>

<h2 id="toc_2">gRPC服务</h2>

<h3 id="toc_3">创建user、relation、creation服务</h3>

<p>进入sponge的UI界面，点击左边菜单栏【Protobuf】&ndash;&gt; 【RPC类型】&ndash;&gt;【创建rpc项目】，填写参数，分别生成三个微服务代码。</p>

<h4 id="toc_4">创建user服务</h4>

<p>这是从单体服务community-single复制过来的proto文件<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/user/api/user/v1/user.proto" rel="nofollow">user.proto</a>，用来快速生成用户(user)服务代码，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-rpc-pb-user.png" alt="community-rpc-pb-user" /></p>

<p>解压代码，把目录名称改为user，然后把user目录移动community-cluster目录下。</p>

<p><br></p>

<h4 id="toc_5">创建relation服务</h4>

<p>这是从单体服务community-single复制过来的proto文件<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/relation/api/relation/v1/relation.proto" rel="nofollow">relation.proto</a>，用来快速生成关系(relation)服务，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-rpc-pb-relation.png" alt="community-rpc-pb-relation" /></p>

<p>解压代码，把目录名称改为relation，然后把relation目录移动community-cluster目录下。</p>

<p><br></p>

<h4 id="toc_6">创建creation服务</h4>

<p>这是从单体服务community-single复制过来的proto文件<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/creation/api/creation/v1/post.proto" rel="nofollow">post.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/creation/api/creation/v1/comment.proto" rel="nofollow">comment.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/creation/api/creation/v1/like.proto" rel="nofollow">like.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/creation/api/creation/v1/collect.proto" rel="nofollow">collect.proto</a>，快速生成创作(creation)服务，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-rpc-pb-creation.png" alt="community-rpc-pb-creation" /></p>

<p>解压代码，把目录名称改为creation，然后把creation目录移动community-cluster目录下。</p>

<p><br></p>

<p>经过简单的界面操作就创建了三个gRPC服务(user、relation、creation)，也就是完成了各个gRPC服务各自的图1中蛋壳部分，接下来完成图1中蛋白和蛋黄两部分代码。</p>

<p><br></p>

<h3 id="toc_7">编写user、relation、creation服务的业务逻辑代码</h3>

<p>从上面图1中微服务鸡蛋模型解剖图看出，经过sponge剥离后的业务逻辑代码只包括蛋白和蛋黄两部分，编写业务逻辑代码都是围绕这两部分开展。</p>

<h4 id="toc_8">编写user服务业务逻辑代码</h4>

<p>分三个步骤编写user服务业务逻辑代码。</p>

<p><strong>第一步 生成模板代码</strong>，进入项目user目录，打开终端，执行命令：</p>

<pre><code class="language-bash">make proto
</code></pre>

<p>这个命令生成了api接口模板代码、api接口错误码、rpc客户端测试代码和pb.go相关代码，这些代码对应图1中蛋白部分。</p>

<ul>
<li><strong>api接口模板代码</strong>，在<code>internal/service</code>目录下，文件名称与proto文件名一致，后缀名是<code>_login.go</code>，文件里面的方法函数与proto文件定义的rpc方法名一一对应，默认每个方法函数下有简单的使用示例，只需在每个方法函数里面编写具体的逻辑代码。</li>
<li><strong>api接口错误码</strong>，在<code>internal/ecode</code>目录下，文件名称与proto文件名一致，后缀是<code>_rpc.go</code>，文件里面的默认错误码变量与proto文件定义的rpc方法名一一对应，在这里添加或更改业务相关的错误码，注意错误码不能重复，否则会触发panic。</li>
<li><strong>rpc客户端测试代码</strong>，在<code>internal/service</code>目录下，文件名称与proto文件名一致，后缀是<code>_client_test.go</code>，文件里面的方法函数与proto文件定义的rpc方法名一一对应，填写参数，就可以每个rpc方法。</li>
</ul>

<p><strong>第二步 迁移dao代码</strong>，把单体web服务community-single目录中的<code>internal/model</code>、<code>internal/cache</code>、<code>internal/dao</code>、,<code>internal/ecode</code>这四个目录下user开头的代码文件，复制到user服务目录下，复制后的目录和文件名称不变。复制的这些代码对应图1中蛋白部分。</p>

<p><strong>第三步 迁移具体逻辑代码</strong>，把单体web服务community-single代码文件<code>internal/handler/user_logic.go</code>各个方法函数下的具体逻辑代码，复制到user服务代码文件<code>internal/service/user_logic.go</code>同名的函数下。这些代码是图1中蛋黄的编写业务逻辑代码部分。</p>

<p><br></p>

<h4 id="toc_9">编写relation服务业务逻辑代码</h4>

<p>分三个步骤编写relation服务业务逻辑代码，参考上面user服务的三个步骤。</p>

<p><br></p>

<h4 id="toc_10">编写creation服务业务逻辑代码</h4>

<p>分三个步骤编写creation服务业务逻辑代码，参考上面user服务的三个步骤。</p>

<p><br></p>

<h3 id="toc_11">测试user、relation、creation服务的rpc方法</h3>

<h4 id="toc_12">测试user服务的rpc方法</h4>

<p>编写了业务逻辑代码后，启动服务来测试rpc方法，在第一次启动服务前，先打开配置文件(<code>user/configs/user.yml</code>)设置mysql和redis地址、设置grpc和grpcClient相关参数，然后执行命令编译启动服务：</p>

<pre><code class="language-bash"># 编译、运行服务
make run
</code></pre>

<p>在goland IDE打开user服务代码，进入<code>user/internal/service</code>目录，找到后缀为<code>_client_test.go</code>的代码文件，在各个rpc方法填写参数后进行测试。</p>

<p><br></p>

<h4 id="toc_13">测试relation服务的rpc方法</h4>

<p>测试relation服务的rpc方法，请参考上面user服务的测试rpc方法。</p>

<p><br></p>

<h4 id="toc_14">测试creation服务的rpc方法</h4>

<p>测试creation服务的rpc方法，请参考上面user服务的测试rpc方法。</p>

<p><br></p>

<h2 id="toc_15">rpc网关服务</h2>

<p>完成了user、relation、creation这三个服务后，接着需要完成rpc网关服务community_gw，community_gw作为user、relation、creation服务的统一入口。</p>

<h3 id="toc_16">创建community_gw服务</h3>

<p>进入sponge的UI界面，点击左边菜单栏【Protobuf】&ndash;&gt; 【Web类型】&ndash;&gt;【创建rpc网关项目】，填写一些参数生成rpc网关服务代码。</p>

<p>这是从单体服务community-single复制过来的proto文件<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/api/community_gw/v1/user_gw.proto" rel="nofollow">user_gw.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/api/community_gw/v1/relation_gw.proto" rel="nofollow">relation_gw.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/api/community_gw/v1/post_gw.proto" rel="nofollow">post_gw.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/api/community_gw/v1/comment_gw.proto" rel="nofollow">comment_gw.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/api/community_gw/v1/like_gw.proto" rel="nofollow">like_gw.proto</a>、<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/api/community_gw/v1/collect_gw.proto" rel="nofollow">collect_gw.proto</a>，快速创建rpc网关服务community_gw，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-rpc-gw-pb.png" alt="community-rpc-gw-pb" /></p>

<p>解压代码，把目录名称改为community_gw。</p>

<p><br></p>

<p>因为community_gw服务作为请求入口，使用rpc方式与user、relation、creation通信，因此需要生成连接user、relation、creation服务的代码。进入sponge的UI界面，点击左边菜单栏【Public】&ndash;&gt;【生成rpc服务连接代码】，填写一些参数生成rpc服务连接代码，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-rpc-conn.png" alt="community-rpc-conn" /></p>

<p>解压代码，把目录internal移动到community_gw服务目录下，然后把community_gw移动到community_cluster目录下。</p>

<p>同时把user、relation、creation三个服务的proto文件复制到community_gw的api目录下，如下列表所示。其中community_gw的v1目录下的proto文件是定义http的api接口信息，建议统一约定后缀名<code>_gw.proto</code>。</p>

<pre><code>.
├── community_gw
│   └── v1
│       ├── collect_gw.proto
│       ├── comment_gw.proto
│       ├── like_gw.proto
│       ├── post_gw.proto
│       ├── relation_gw.proto
│       └── user_gw.proto
├── creation
│   └── v1
│       ├── collect.proto
│       ├── comment.proto
│       ├── like.proto
│       └── post.proto
├── relation
│   └── v1
│       └── relation.proto
└── user
    └── v1
        └── user.proto
</code></pre>

<p>通过简单的操作就完成创建了rpc网关服务community_gw。</p>

<p><br></p>

<h3 id="toc_17">编写community_gw服务的业务逻辑代码</h3>

<p>从上面图2中rpc网关代码鸡蛋模型解剖图看出，经过sponge剥离后的业务逻辑代码只包括蛋白和蛋黄两部分，编写业务逻辑代码都是围绕这两部分开展。</p>

<h4 id="toc_18">编写与proto文件相关的业务逻辑代码</h4>

<p>进入项目community-gw目录，打开终端，执行命令：</p>

<pre><code class="language-bash">make proto
</code></pre>

<p>这个命令是根据<code>community_gw/api/community_gw/v1</code>目录下的proto文件生成了api接口模板代码、注册路由代码、api接口错误码、swagger文档和相关的pb.go代码，也就是图2中的蛋白部分。</p>

<p>(1) <strong>api接口模板代码</strong>，在<code>community_gw/internal/service</code>目录下，文件名称与proto文件名一致，后缀名是<code>_logic.go</code>，名称分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/service/collect_gw_logic.go" rel="nofollow">collect_gw_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/service/comment_gw_logic.go" rel="nofollow">comment_gw_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/service/like_gw_logic.go" rel="nofollow">like_gw_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/service/post_gw_logic.go" rel="nofollow">post_gw_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/service/relation_gw_logic.go" rel="nofollow">relation_gw_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/service/user_gw_logic.go" rel="nofollow">user_gw_logic.go</a></p>

<p>在这些文件里面的方法函数与proto文件定义的rpc方法名一一对应，每个方法函数下有默认的使用示例，只需要简单调整就可以调用user、relation、creation服务端的rpc方法。上面那些文件代码是已经编写具体逻辑之后的代码。</p>

<p><br></p>

<p>(2) <strong>注册路由代码</strong>，在<code>community_gw/internal/routers</code>目录下，文件名称与proto文件名一致，后缀名是<code>_router.go</code>，名称分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/routers/collect_gw_router.go" rel="nofollow">collect_gw_router.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/routers/comment_gw_router.go" rel="nofollow">comment_gw_router.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/routers/like_gw_router.go" rel="nofollow">like_gw_router.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/routers/post_gw_router.go" rel="nofollow">post_gw_router.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/routers/relation_gw_router.go" rel="nofollow">relation_gw_router.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/routers/user_gw_router.go" rel="nofollow">user_gw_router.go</a></p>

<p>在这些文件里面的设置api接口的中间件，例如jwt鉴权，每个接口都已经存在中间件模板代码，只需要取消注释代码就可以使中间件生效，只需要取消注释代码就可以使中间件生效，支持路由分组和单独路由来设置gin中间件。</p>

<p><br></p>

<p>(3) <strong>api接口错误码</strong>，在<code>community_gw/internal/ecode</code>目录下，文件名称与proto文件名一致，后缀是<code>_rpc.go</code>，名称分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/ecode/collect_gw_rpc.go" rel="nofollow">collect_gw_rpc.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/ecode/comment_gw_rpc.go" rel="nofollow">comment_gw_rpc.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/ecode/like_gw_rpc.go" rel="nofollow">like_gw_rpc.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/ecode/post_gw_rpc.go" rel="nofollow">post_gw_rpc.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/ecode/relation_gw_rpc.go" rel="nofollow">relation_gw_rpc.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/internal/ecode/user_gw_rpc.go" rel="nofollow">user_gw_rpc.go</a></p>

<p>在这些文件里面的默认错误码变量与proto文件定义的rpc方法名一一对应，在这里添加或更改业务相关的错误码，注意错误码不能重复，否则会触发panic。</p>

<p>注： 如果调用的rpc方法本身包含了错误码，可以直接返回该错误码。</p>

<p><br></p>

<p>(4) <strong>swagger文档</strong>，在<code>community_gw/docs</code>目录下，名称为<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/8_community-cluster/community_gw/docs/apis.swagger.json" rel="nofollow">apis.swagger.json</a></p>

<p><br></p>

<p>如果在proto文件添加或更改了api接口，都需要再执行一次命令<code>make proto</code>即可，会自动把新生成代码自动合并到对应文件代码中，不用担心合并代码中会丢失已编写的业务逻辑代码问题，可以在<code>/tmp/sponge_merge_backup_code</code>目录下可以找到合并前的代码备份。</p>

<p><code>make proto</code>命令生成的代码是用来连接web框架代码和业务逻辑核心代码的桥梁，也就是图2中的蛋白部分，通过分层生成代码的好处是减少编写代码。</p>

<p><br></p>

<h3 id="toc_19">测试api接口</h3>

<p>编写了业务逻辑代码后，启动服务测试api接口，在第一次启动服务前，先打开配置文件(<code>community_gw/configs/community_gw.yml</code>)设置连接rpc服务配置信息，如下所示：</p>

<pre><code class="language-yaml"># grpc client settings, support for setting up multiple rpc clients
grpcClient:
  - name: &quot;user&quot;
    host: &quot;127.0.0.1&quot;
    port: 18282
    registryDiscoveryType: &quot;&quot;
    enableLoadBalance: false
  - name: &quot;relation&quot;
    host: &quot;127.0.0.1&quot;
    port: 28282
    registryDiscoveryType: &quot;&quot;
    enableLoadBalance: false
  - name: &quot;creation&quot;
    host: &quot;127.0.0.1&quot;
    port: 38282
    registryDiscoveryType: &quot;&quot;
    enableLoadBalance: false
</code></pre>

<p>执行命令编译启动服务：</p>

<pre><code class="language-bash"># 编译、运行服务
make run
</code></pre>

<p>在浏览器访问 <code>http://localhost:8080/apis/swagger/index.htm</code> ，进入swagger界面，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-gw-swagger.png" alt="community-gw-swagger" /></p>

<p>从图中看到有些api接口右边有一把锁标记，表示请求头会携带鉴权信息Authorization，服务端接收到请求是否做鉴权，由服务端决定，如果服务端需要做鉴权，可以在<code>community_gw/internal/routers</code>目录下后缀文件为<code>_router.go</code>文件中设置，也就是取消鉴权的注释代码，使api接口的鉴权中间件生效。</p>

<p><br></p>

<h2 id="toc_20">服务治理</h2>

<p>gRPC服务(user、relation、creation)和rpc网关服务(community-gw)都包含了丰富的服务治理插件(日志、限流、熔断、链路跟踪、服务注册与发现、指标采集、性能分析、资源统计、配置中心)，有些服务治理插件默认是关闭的，根据实际需要开启使用。</p>

<p>除了服务本身提供的治理插件，也可以使用自己的服务治理插件，添加自己的服务治理插件说明：</p>

<ul>
<li>对于gRPC服务(user、relation、creation)，在代码文件<code>服务名称/internal/server/grpc.go</code>里添加自己的插件，如果你的服务治理插件(拦截器)属于unary类型，添加到<code>unaryServerOptions</code>函数里面。如果你的服务治理插件(拦截器)属于stream类型，添加到<code>streamServerOptions</code>函数里面。</li>
<li>对于rpc网关服务community-gw，在代码文件<code>community-gw/internal/routers/routers.go</code>里添加自己的插件(gin中间件)。</li>
</ul>

<p><br></p>

<p>下面是默认的服务治理插件开启和设置说明，统一在各自服务配置文件<code>服务名称/configs/服务名称.yml</code>进行设置。</p>

<p><strong>日志</strong></p>

<p>日志插件(zap)默认是开启的，默认是输出到终端，默认输出日志格式是console，可以设置输出格式为json，设置日志保存到指定文件，日志文件切割和保留时间。</p>

<p>在配置文件里的字段<code>logger</code>设置：</p>

<pre><code class="language-yaml"># logger 设置
logger:
  level: &quot;info&quot;             # 输出日志级别 debug, info, warn, error，默认是debug
  format: &quot;console&quot;     # 输出格式，console或json，默认是console
  isSave: false           # false:输出到终端，true:输出到文件，默认是false
  logFileConfig:          # isSave=true时有效
    filename: &quot;out.log&quot;            # 文件名称，默认值out.log
    maxSize: 20                     # 最大文件大小(MB)，默认值10MB
    maxBackups: 50               # 保留旧文件的最大个数，默认值100个
    maxAge: 15                     # 保留旧文件的最大天数，默认值30天
    isCompression: true          # 是否压缩/归档旧文件，默认值false
</code></pre>

<p><br></p>

<p><strong>限流</strong></p>

<p>限流插件默认是关闭的，自适应限流，不需要设置其他参数。</p>

<p>在配置文件里的字段<code>enableLimit</code>设置：</p>

<pre><code class="language-yaml">  enableLimit: false    # 是否开启限流(自适应)，true:开启, false:关闭
</code></pre>

<p><br></p>

<p><strong>熔断</strong></p>

<p>熔断插件默认是关闭的，自适应熔断，支持自定义错误码(默认500和503)触发熔断，在<code>internal/routers/routers.go</code>设置。</p>

<p>在配置文件里的字段<code>enableCircuitBreaker</code>设置：</p>

<pre><code class="language-yaml">  enableCircuitBreaker: false    # 是否开启熔断(自适应)，true:开启, false:关闭
</code></pre>

<p><br></p>

<p><strong>链路跟踪</strong></p>

<p>链路跟踪插件默认是关闭的，链路跟踪依赖jaeger服务。</p>

<p>在配置文件里的字段<code>enableTrace</code>设置：</p>

<pre><code class="language-yaml">  enableTrace: false    # 是否开启追踪，true:启用，false:关闭，如果是true，必须设置jaeger配置。
  tracingSamplingRate: 1.0      # 链路跟踪采样率, 范围0~1.0浮点数, 0表示不采样, 1.0表示采样所有链路


# jaeger 设置
jaeger:
  agentHost: &quot;192.168.3.37&quot;
  agentPort: 6831
</code></pre>

<p>在jaeger界面上查看链路跟踪信息<a href="https://go-sponge.com/zh-cn/components?id=%f0%9f%8f%b7%e9%93%be%e8%b7%af%e8%b7%9f%e8%b8%aa" rel="nofollow">文档说明</a>。</p>

<p><br></p>

<p><strong>服务注册与发现</strong></p>

<p>服务注册与发现插件默认是关闭的，支持consul、etcd、nacos三种类型。</p>

<p>在配置文件里的字段<code>registryDiscoveryType</code>设置：</p>

<pre><code class="language-yaml">  registryDiscoveryType: &quot;&quot;    # 注册和发现类型：consul、etcd、nacos，如果为空表示关闭服务注册与发现。


# 根据字段registryDiscoveryType值来设置参数，例如使用consul作为服务发现，只需设置consul。
# consul 设置
consul:
  addr: &quot;192.168.3.37:8500&quot;

# etcd 设置
etcd:
  addrs: [&quot;192.168.3.37:2379&quot;]

# nacos 设置
nacosRd:
  ipAddr: &quot;192.168.3.37&quot;
  port: 8848
  namespaceID: &quot;3454d2b5-2455-4d0e-bf6d-e033b086bb4c&quot; # namespace id
</code></pre>

<p><br></p>

<p><strong>指标采集</strong></p>

<p>指标采集功能默认是开启的，提供给prometheus采集数据，默认路由是<code>/metrics</code>。</p>

<p>在配置文件里的字段<code>enableMetrics</code>设置：</p>

<pre><code class="language-yaml">  enableMetrics: true    # 是否开启指标采集，true：启用，false：关闭
</code></pre>

<p>使用prometheus和grafana采集指标和监控服务的<a href="https://go-sponge.com/zh-cn/components?id=%f0%9f%8f%b7%e7%9b%91%e6%8e%a7" rel="nofollow">文档说明</a>。</p>

<p><br></p>

<p><strong>性能分析</strong></p>

<p>性能分析插件默认是关闭的，采集profile的默认路由是<code>/debug/pprof</code>，除了支持go语言本身提供默认的profile分析，还支持io分析，路由是<code>/debug/pprof/profile-io</code>。</p>

<p>在配置文件里的字段<code>enableHTTPProfile</code>设置：</p>

<pre><code class="language-yaml">  enableHTTPProfile: false    # 是否开启性能分析，true：启用，false：关闭
</code></pre>

<p>通过路由采集profile进行性能分析方式，通常在开发或测试时使用，如果线上开启会有一点点性能损耗，因为程序后台一直定时记录profile相关信息。sponge生成的服务本身对此做了一些改进，平时停止采集profile，用户主动触发系统信号时才开启和关闭采集profile，采集profile保存到<code>/tmp/服务名称_profile目录</code>，默认采集为60秒，60秒后自动停止采集profile，如果只想采集30秒，发送第一次信号开始采集，大概30秒后发送第二次信号表示停止采集profile，类似开关一样。</p>

<p>这是采集profile操作步骤：</p>

<pre><code class="language-yaml"># 通过名称查看服务pid
ps aux | grep 服务名称

# 发送信号给服务
kill -trap pid值
</code></pre>

<p>注：只支持linux、darwin系统。</p>

<p><br></p>

<p><strong>资源统计</strong></p>

<p>资源统计插件默认是开启的，默认每分钟统计一次并输出到日志，资源统计了包括系统和服务本身这两部分的cpu和内存相关的数据。</p>

<p>资源统计还包含了自动触发采集profile功能，当连续3次统计本服务的CPU或内存平均值，CPU或内存平均值占用系统资源超过80%时，自动触发采集profile，默认采集为60秒，采集profile保存到<code>/tmp/服务名称_profile目录</code>，从而实现了自适应采集profile，比通过人工发送系统信号来采集profile又改进了一步。</p>

<p>在配置文件里的字段<code>enableHTTPProfile</code>设置：</p>

<pre><code class="language-yaml">  enableStat: true    # 是否开启资源统计，true:启用，false:关闭
</code></pre>

<p><br></p>

<p><strong>配置中心</strong></p>

<p>目前支持nacos作为配置中心，配置中心文件<code>configs/user_cc.yml</code>，配置内容如下：</p>

<pre><code class="language-yaml"># nacos 设置
nacos:
  ipAddr: &quot;192.168.3.37&quot;    # 服务地址
  port: 8848                      # 监听端口
  scheme: &quot;http&quot;                # 支持http和https
  contextPath: &quot;/nacos&quot;       # 路径
  namespaceID: &quot;3454d2b5-2455-4d0e-bf6d-e033b086bb4c&quot; # namespace id
  group: &quot;dev&quot;                    # 组名称: dev, prod, test
  dataID: &quot;community.yml&quot;  # 配置文件id
  format: &quot;yaml&quot;                 # 配置文件类型: json,yaml,toml
</code></pre>

<p>而服务的配置文件<code>configs/user.yml</code>复制到nacos界面上配置。使用nacos配置中心，启动服务命令需要指定配置中心文件，命令如下：</p>

<pre><code class="language-bash">./user -c configs/user_cc.yml -enable-cc
</code></pre>

<p>使用nacos作为配置中心的<a href="https://go-sponge.com/zh-cn/components?id=%f0%9f%8f%b7%e9%85%8d%e7%bd%ae%e4%b8%ad%e5%bf%83" rel="nofollow">文档说明</a>。</p>

<p><br></p>

<h2 id="toc_21">持续集成与部署</h2>

<p>sponge生成的gRPC和rpc网关服务包括了编译和部署脚本，编译支持二进制编译和docker镜像构建，部署支持二进制部署、docker部署、k8s部署三种方式，这些功能都统一集成在<code>Makefile</code>文件里，使用make命令就可以很方便的执行指定编译或部署服务。</p>

<p>除了使用make命令编译和部署，还支持自动化部署工具Jenkins，默认的Jenkins设置在文件<code>Jenkinsfile</code>，支持自动化部署到k8s，如果需要二进制或docker部署，需要对<code>Jenkinsfile</code>进行修改。</p>

<p>使用Jenkins持续集成和部署的<a href="https://go-sponge.com/zh-cn/deployment?id=%f0%9f%8f%b7%e6%8c%81%e7%bb%ad%e9%9b%86%e6%88%90%e9%83%a8%e7%bd%b2" rel="nofollow">文档说明</a>。</p>

<p><br></p>

<h2 id="toc_22">服务压测</h2>

<p>压测服务时使用的一些工具：</p>

<ul>
<li>http压测工具<a href="https://github.com/wg/wrk" rel="nofollow">wrk</a>或<a href="https://github.com/link1st/go-stress-testing" rel="nofollow">go-stress-testing</a>。</li>
<li>服务开启指标采集功能，使用prometheus采集服务指标和系统指标进行监控。</li>
<li>服务本身的自适应采集profile功能。</li>
</ul>

<p><br></p>

<p>压测指标：</p>

<ul>
<li><strong>并发度</strong>: 逐渐增加并发用户数，找到服务的最大并发度，确定服务能支持的最大用户量。</li>
<li><strong>响应时间</strong>: 关注并发用户数增加时，服务的平均响应时间和响应时间分布情况。确保即使在高并发下，响应时间也在可接受范围内。</li>
<li><strong>错误率</strong>: 观察并发增加时，服务出现错误或异常的概率。使用压测工具进行长时间并发测试，统计各并发级别下的错误数量和类型。</li>
<li><strong>吞吐量</strong>: 找到服务的最大吞吐量，确定服务在高并发下可以支持的最大请求量。这需要不断增加并发，直到找到吞吐量饱和点。</li>
<li><strong>资源利用率</strong>: 关注并发增加时，CPU、内存、磁盘I/O、网络等资源的利用率，找到服务的资源瓶颈。</li>
<li><strong>瓶颈检测</strong>: 通过观察高并发情况下服务的性能指标和资源利用率，找到系统和服务的硬件或软件瓶颈，以便进行优化。</li>
<li><strong>稳定性</strong>: 长时间高并发运行可以检测到服务存在的潜在问题，如内存泄露、连接泄露等，确保服务稳定运行。这需要较长时间的并发压测，观察服务运行指标。</li>
</ul>

<p>对服务进行压测，主要是为了评估其性能，确定能支持的最大并发和吞吐量，发现当前的瓶颈，并检测服务运行的稳定性，以便进行优化或容量规划。</p>

<p><br></p>

<h2 id="toc_23">总结</h2>

<p>本文介绍了把单体服务community-single拆分为微服务集群community-cluster的具体实践过程，微服务集群包括了用户服务(user)、关系服务(relation)和内容创作服务(creation)三个独立的服务，一个微服务入口的网关服务(community_gw)，这些服务代码(图1和图2中的蛋壳和蛋白部分)都是由工具sponge生成，核心业务逻辑代码是直接手动无缝移植，基本不需要重复编写代码，减少了大量工作量，从而提高了效率。</p>

<p>如果不是从单体服务拆分为微服务，而是项目一开始采用微服务集群，使用sponge开发的步骤也是一样，一开始采用微服务集群时，核心业务逻辑代码(图1和图2中的蛋黄部分)需要人工编写。</p>

<p>使用工具sponge从开发到部署<strong>gRPC服务</strong>具体流程如下：</p>

<ol>
<li>定义mysql表</li>
<li>在proto文件定义api接口</li>
<li>生成gRPC服务框架代码</li>
<li>根据proto文件生成业务逻辑相关代码</li>
<li>根据mysql表生成dao代码</li>
<li>在api接口模板文件中编写具体逻辑代码</li>
<li>在生成的rpc客户端代码中测试验证api接口</li>
<li>按需启用服务治理功能</li>
<li>持续集成与部署</li>
<li>服务压测</li>
</ol>

<p>开发一个完整的<strong>gRPC服务</strong>，真正需要人工编写代码的只有1、2、6这三个核心业务代码。</p>

<p>使用工具sponge从开发到部署<strong>rpc网关服务</strong>具体流程如下：</p>

<ol>
<li>在proto文件定义api接口</li>
<li>生成rpc网关服务框架代码</li>
<li>生成prc服务连接代码</li>
<li>根据proto文件生成业务逻辑相关代码</li>
<li>在api接口模板文件中编写具体逻辑代码</li>
<li>在swagger页面测试验证api接口</li>
<li>按需启用服务治理功能</li>
<li>持续集成与部署</li>
<li>服务压测</li>
</ol>

<p>开发一个完整的<strong>rpc网关服务</strong>，真正需要人工编写代码的只有1、5这两个核心业务代码。</p>

<p>使用工具sponge很容易开发一个完整的微服务集群，微服务集群的优点：</p>

<ul>
<li>高性能：基于 Protobuf 的高性能通信协议，同时具备高并发处理和低延迟的特点。</li>
<li>可扩展性：丰富的插件和组件机制，开发者可以根据实际需求定制和扩展框架功能。</li>
<li>高可靠性：提供了服务注册和发现、限流、熔断、链路、监控告警等功能，提升了微服务的可靠性。</li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/community-cluster.html">https://zhuyasen.com/post/community-cluster.html</a>，<a href="https://zhuyasen.com/post/community-cluster.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>使用开发框架sponge一天多开发完成一个简单版社区后端服务</title>
            <link>https://zhuyasen.com/post/community-single.html</link>
            <comments>https://zhuyasen.com/post/community-single.html#comments</comments>
            <guid>https://zhuyasen.com/post/community-single.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">community-single 介绍</h3>

<p>community-single是一个极简版社区的后端服务，主要包括用户的注册、登录、关注等功能，创作内容(文本、图片、视频)的发布、评论、点赞、收藏等功能，这些功能在各个社区平台、视频平台、直播平台等都比较常见，可以作为学习参考用，点击查看完整的<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single" rel="nofollow">项目代码</a>。</p>

<p>community-single项目一开始设计为单体web服务，整个服务由生成代码工具<a href="https://github.com/go-dev-frame/sponge" rel="nofollow">sponge</a>辅助完成，sponge生成web服务代码过程中剥离了业务逻辑与非业务逻辑两部分代码，这里的非业务逻辑代码指的是web服务框架代码，主要包括：</p>

<ul>
<li>经过封装的gin代码</li>
<li>服务治理(日志、限流、熔断、链路跟踪、服务注册与发现、指标采集、性能分析、配置中心、资源统计等)</li>
<li>编译构建和部署脚本(二进制、docker、k8s)</li>
<li>CI/CD(jenkins)</li>
</ul>

<p>除了web服务框架代码，其他都属于业务逻辑代码。</p>

<p>把一个完整web服务代码看作一个鸡蛋，<strong>蛋壳</strong>表示web服务框架代码，蛋白和蛋黄都表示业务逻辑代码，<strong>蛋黄</strong>是业务逻辑的核心(需要人工编写的代码)，例如定义mysql表、定义api接口、编写具体逻辑代码都属于蛋黄部分。<strong>蛋白</strong>是业务逻辑核心代码与web框架代码连接的桥梁(自动生成，不需要人工编写)，例如根据proto文件生成的注册路由代码、handler方法函数代码、参数校验代码、错误码、swagger文档等都属于蛋白部分。web服务鸡蛋模型剖析图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/web-http-pb-anatomy.png" alt="web-http-pb-anatomy" />
图1 web服务代码的组成结构图</p>

<p>因此开发一个完整web服务项目聚焦在了<strong>定义数据表</strong>、<strong>定义api接口</strong>、<strong>在模板代码中编写具体业务逻辑代码</strong>这3个节点上，也就是业务逻辑的核心代码(蛋黄)，其他代码(蛋壳和蛋白)是由sponge生成，可以帮助你少写很多代码，下面介绍从0开始到完成项目的开发过程。</p>

<p>开发过程依赖工具sponge，需要先安装sponge，点击查看<a href="https://github.com/go-dev-frame/sponge/blob/main/assets/install-cn.md#%E5%9C%A8linux%E6%88%96macos%E4%B8%8A%E5%AE%89%E8%A3%85sponge" rel="nofollow">安装说明</a>。</p>

<p><br>
<br></p>

<h3 id="toc_1">定义数据表和api接口</h3>

<p>根据业务需求，首先要定义数据表和api接口，这是业务逻辑代码核心(图1中的蛋黄部分)，后面需要根据数据表和api接口(IDL)来生成代码(图1中的蛋壳和蛋白两部分)。</p>

<h4 id="toc_2">定义数据表</h4>

<p>这是已经定义好的mysql表 <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/test/sql/community.sql" rel="nofollow">community.sql</a></p>

<p><br></p>

<h4 id="toc_3">定义api接口</h4>

<p>在proto文件定义api接口、输入输出参数、路由等，下面是已经定义好的api接口的proto文件：</p>

<ul>
<li><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/api/community/v1/user.proto" rel="nofollow">user.proto</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/api/community/v1/relation.proto" rel="nofollow">relation.proto</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/api/community/v1/like.proto" rel="nofollow">like.proto</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/api/community/v1/comment.proto" rel="nofollow">comment.proto</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/api/community/v1/collect.proto" rel="nofollow">collect.proto</a></li>
</ul>

<p><br></p>

<p>开发中不大可能一次性就定义好业务所需的mysql表和api接口，增加或更改是很常见的事，修改mysql表和proto文件后，如何同步更新到代码里，在下面的<strong>编写业务逻辑代码</strong>章节中介绍。</p>

<p><br>
<br></p>

<h3 id="toc_4">生成项目代码</h3>

<p>定义了数据表和api接口之后，然后在sponge的界面上根据proto文件生成web服务项目代码。进入sponge的UI界面，点击左边菜单栏【protobuf】&ndash;&gt; 【Web类型】&ndash;&gt;【创建web项目】，填写相关参数生成web项目代码，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-single-web.png" alt="community-single-web" /></p>

<p>解压代码，修改文件夹名称(例如community-single)，一个服务只需生成代码一次。 这就完成搭建了一个web服务的基本框架(图1中的蛋壳部分)，接着可以在web服务框架内编写业务逻辑代码了。</p>

<p><br>
<br></p>

<h3 id="toc_5">编写业务逻辑代码</h3>

<p>从上面图1中web服务代码鸡蛋模型解剖图看出，经过sponge剥离后的业务逻辑代码包括蛋白和蛋黄两部分，编写业务逻辑代码基本都是围绕这两部分开展。</p>

<h4 id="toc_6">编写与proto文件相关的业务逻辑代码</h4>

<p>进入项目community-single目录，打开终端，执行命令：</p>

<pre><code class="language-bash">make proto
</code></pre>

<p>这个命令是根据api/community/v1目录下的proto文件生成了接口模板代码、注册路由代码、api接口错误码、swagger文档这四个部分代码，也就是图1中的蛋白部分。</p>

<p><br></p>

<p>(1) <strong>生成的接口模板代码</strong>，在<code>internal/handler</code>目录下，文件名称与proto文件名一致，后缀名是<code>_logic.go</code>，名称分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/handler/collect_logic.go" rel="nofollow">collect_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/handler/comment_logic.go" rel="nofollow">comment_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/handler/like_logic.go" rel="nofollow">like_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/handler/post_logic.go" rel="nofollow">post_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/handler/relation_logic.go" rel="nofollow">relation_logic.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/handler/user_logic.go" rel="nofollow">user_logic.go</a></p>

<p>在这些文件里面的方法函数与proto文件定义的rpc方法名一一对应，默认每个方法函数下有简单的使用示例，只需在每个方法函数里面编写具体的逻辑代码，上面那些文件代码是已经编写过具体逻辑之后的代码。</p>

<p><br></p>

<p>(2) <strong>生成注册路由代码</strong>，在<code>internal/routers</code>目录下，文件名称与proto文件名一致，后缀名是<code>_handler.pb.go</code>，名称分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/routers/collect_handler.pb.go" rel="nofollow">collect_handler.pb.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/routers/comment_handler.pb.go" rel="nofollow">comment_handler.pb.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/routers/like_handler.pb.go" rel="nofollow">like_handler.pb.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/routers/post_handler.pb.go" rel="nofollow">post_handler.pb.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/routers/relation_handler.pb.go" rel="nofollow">relation_handler.pb.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/routers/user_handler.pb.go" rel="nofollow">user_handler.pb.go</a></p>

<p>在这些文件里面的设置api接口的中间件，例如jwt鉴权，每个接口都已经存在中间件模板代码，只需要取消注释代码就可以使中间件生效，支持路由分组和单独路由来设置中间件。</p>

<p><br></p>

<p>(3) <strong>生成接口错误码</strong>，在<code>internal/ecode</code>目录下，文件名称与proto文件名一致，后缀是<code>_http.go</code>，名称分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/ecode/collect_http.go" rel="nofollow">collect_http.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/ecode/comment_http.go" rel="nofollow">comment_http.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/ecode/like_http.go" rel="nofollow">like_http.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/ecode/post_http.go" rel="nofollow">post_http.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/ecode/relation_http.go" rel="nofollow">relation_http.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/ecode/user_http.go" rel="nofollow">user_http.go</a></p>

<p>在这些文件里面的默认错误码变量与proto文件定义的rpc方法名一一对应，在这里添加或更改业务相关的错误码，注意错误码不能重复，否则会触发panic。</p>

<p><br></p>

<p>(4) <strong>生成swagger文档</strong>，在<code>docs</code>目录下，名称为<a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/docs/apis.swagger.json" rel="nofollow">apis.swagger.json</a></p>

<p><br></p>

<p>如果在proto文件添加或更改了api接口，需要重新再执行一次命令<code>make proto</code>更新代码，会发现在<code>internal/handler</code>、<code>internal/routers</code>、<code>internal/ecode</code>目录下出现后缀名为日期时间的代码文件，打开文件，把新增或修改部分代码复制到同名文件代码中即可。复制完新增代码后，执行命令<code>make clean</code>清除这些日期后缀文件。</p>

<p><code>make proto</code>命令生成的代码是用来连接web框架代码和业务逻辑核心代码的桥梁，也就是蛋白部分，这种分层生成代码的好处是减少编写代码。</p>

<p><br></p>

<h4 id="toc_7">编写与mysql表相关的业务逻辑代码</h4>

<p>前面生成的web服务框架代码和根据proto文件生成的业务逻辑的部分代码，都还没有包括对mysql表的操作，因此需要根据mysql表生成dao(数据访问对象)代码，dao代码包括了对表的<strong>增删改查</strong>代码、缓存代码、model代码，这些代码属于图1中的蛋白部分。</p>

<p>进入sponge的UI界面，点击左边菜单栏【Public】&ndash;&gt; 【生成dao CRUD代码】，填写相关参数生成dao代码，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-single-dao.png" alt="community-single-dao" /></p>

<p>解压dao代码，把internal目录移动到community-single目录下，这样就完成添加了对mysql表的<strong>增删改查</strong>操作方法。当有新添加的mysql表时，需要再次指定新的mysql表生成dao代码。</p>

<p><br></p>

<p>指定mysql表生成的dao代码包括三个部分。</p>

<p>(1) <strong>生成model代码</strong>，在<code>internal/model</code>目录下，文件名称与mysql表名一致，分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/comment.go" rel="nofollow">comment.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/commentContent.go" rel="nofollow">commentContent.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/commentHot.go" rel="nofollow">commentHot.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/commentLatest.go" rel="nofollow">commentLatest.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/post.go" rel="nofollow">post.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/postHot.go" rel="nofollow">postHot.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/postLatest.go" rel="nofollow">postLatest.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/relationNum.go" rel="nofollow">relationNum.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/user.go" rel="nofollow">user.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/userCollect.go" rel="nofollow">userCollect.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/userComment.go" rel="nofollow">userComment.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/userFollower.go" rel="nofollow">userFollower.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/userFollowing.go" rel="nofollow">userFollowing.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/userLike.go" rel="nofollow">userLike.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/model/userPost.go" rel="nofollow">userPost.go</a></p>

<p>这是生成的对应gorm的go结构体代码。</p>

<p><br></p>

<p>(2) <strong>生成缓存代码</strong>，在<code>internal/cache</code>目录下文件，文件名称与mysql表名一致，分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/comment.go" rel="nofollow">comment.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/commentContent.go" rel="nofollow">commentContent.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/commentHot.go" rel="nofollow">commentHot.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/commentLatest.go" rel="nofollow">commentLatest.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/post.go" rel="nofollow">post.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/postHot.go" rel="nofollow">postHot.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/postLatest.go" rel="nofollow">postLatest.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/relationNum.go" rel="nofollow">relationNum.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/user.go" rel="nofollow">user.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/userCollect.go" rel="nofollow">userCollect.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/userComment.go" rel="nofollow">userComment.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/userFollower.go" rel="nofollow">userFollower.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/userFollowing.go" rel="nofollow">userFollowing.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/userLike.go" rel="nofollow">userLike.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/cache/userPost.go" rel="nofollow">userPost.go</a></p>

<p>编写业务代码过程中，为了提高性能，有可能使用到缓存，有时候对表的默认缓存(CRUD)不能满足要求，需要添加缓存代码，sponge支持一键生成缓存代码，点击左边菜单栏【Public】&ndash;&gt; 【生成cache代码】，填写参数生成代码，然后把解压的internal目录移动到community-single目录下，然后在业务逻辑中直接调用缓存接口。</p>

<p><br></p>

<p>(3) <strong>生成dao代码</strong>，在<code>internal/dao</code>目录下，文件名称与mysql表名一致，文件分别有：</p>

<p><a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/comment.go" rel="nofollow">comment.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/commentContent.go" rel="nofollow">commentContent.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/commentHot.go" rel="nofollow">commentHot.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/commentLatest.go" rel="nofollow">commentLatest.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/post.go" rel="nofollow">post.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/postHot.go" rel="nofollow">postHot.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/postLatest.go" rel="nofollow">postLatest.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/relationNum.go" rel="nofollow">relationNum.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/user.go" rel="nofollow">user.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/userCollect.go" rel="nofollow">userCollect.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/userComment.go" rel="nofollow">userComment.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/userFollower.go" rel="nofollow">userFollower.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/userFollowing.go" rel="nofollow">userFollowing.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/userLike.go" rel="nofollow">userLike.go</a>, <a href="https://github.com/go-dev-frame/sponge_examples/blob/main/7_community-single/internal/dao/userPost.go" rel="nofollow">userPost.go</a></p>

<p>编写业务代码过程中会涉及到操作mysql表，有时候对表的默认操作(CRUD)不能满足要求，这时需要人工编写自定义操作mysql表的函数方法与实现代码，例如comment.go、post.go等都包含少部分人工定义的操作msyql表的方法函数。</p>

<p><br></p>

<p>在开发过程中有时会修改或新增mysql表，基于mysql表生成的代码需要同步到项目代码中，分为两种情况处理：</p>

<ul>
<li>修改mysql表之后更新代码处理方式：只需根据修改后的表生成新model代码，替换旧的model代码。点击左边菜单栏【Public】&ndash;&gt; 【生成model代码】，填写参数，选择更改的mysql表，然后把解压的internal目录移动到community-single目录下，并确认替换。</li>
<li>新增mysql表之后处理方式：只需根据新增的表生成新的dao代码，添加到项目目录下。点击左边菜单栏【Public】&ndash;&gt; 【生成dao代码】，填写参数，选择新增的mysql表，然后把解压的internal目录移动到community-single目录下。</li>
</ul>

<p><br>
<br></p>

<h3 id="toc_8">测试api接口</h3>

<p>编写了业务逻辑代码后，启动服务测试api接口，在第一次启动服务前，先打开配置文件(<code>configs/community.yml</code>)设置mysql和redis地址，然后执行命令编译启动服务：</p>

<pre><code class="language-bash"># 编译、运行服务
make run
</code></pre>

<p>在浏览器访问 <code>http://localhost:8080/apis/swagger/index.htm</code> ，进入swagger界面，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/sponge/community-single-swagger2.png" alt="community-single-swagger" /></p>

<p>从图中看到有些api接口右边有一把锁标记，表示请求头会携带鉴权信息Authorization，服务端接收到请求是否做鉴权，由服务端决定，如果服务端需要做鉴权，可以在<code>internal/routers</code>目录下后缀文件为<code>_handler.pb.go</code>文件中设置，也就是取消鉴权的注释代码，使api接口的鉴权中间件生效。</p>

<p><br>
<br></p>

<h3 id="toc_9">服务治理</h3>

<p>生成的web服务代码中包含了丰富的服务治理插件，有些服务治理插件默认是关闭的，根据实际需要开启使用，统一在配置文件<code>configs/community.yml</code>进行设置。</p>

<p>除了web服务提供的服务治理插件，也可以使用自己的服务治理插件，建议在<code>internal/routers/routers.go</code>引入自己的服务治理插件。</p>

<p><br></p>

<h4 id="toc_10">日志</h4>

<p>日志插件(zap)默认是开启的，默认是输出到终端，默认输出日志格式是console，可以设置输出格式为json，设置日志保存到指定文件，日志文件切割和保留时间。</p>

<p>在配置文件里的字段<code>logger</code>设置：</p>

<pre><code class="language-yaml"># logger 设置
logger:
  level: &quot;info&quot;             # 输出日志级别 debug, info, warn, error，默认是debug
  format: &quot;console&quot;     # 输出格式，console或json，默认是console
  isSave: false           # false:输出到终端，true:输出到文件，默认是false
  logFileConfig:          # isSave=true时有效
    filename: &quot;out.log&quot;            # 文件名称，默认值out.log
    maxSize: 20                     # 最大文件大小(MB)，默认值10MB
    maxBackups: 50               # 保留旧文件的最大个数，默认值100个
    maxAge: 15                     # 保留旧文件的最大天数，默认值30天
    isCompression: true          # 是否压缩/归档旧文件，默认值false
</code></pre>

<p><br></p>

<h4 id="toc_11">限流</h4>

<p>限流插件默认是关闭的，自适应限流，不需要设置其他参数。</p>

<p>在配置文件里的字段<code>enableLimit</code>设置：</p>

<pre><code class="language-yaml">  enableLimit: false    # 是否开启限流(自适应)，true:开启, false:关闭
</code></pre>

<p><br></p>

<h4 id="toc_12">熔断</h4>

<p>熔断插件默认是关闭的，自适应熔断，支持自定义错误码(默认500和503)触发熔断，在<code>internal/routers/routers.go</code>设置。</p>

<p>在配置文件里的字段<code>enableCircuitBreaker</code>设置：</p>

<pre><code class="language-yaml">  enableCircuitBreaker: false    # 是否开启熔断(自适应)，true:开启, false:关闭
</code></pre>

<p><br></p>

<h4 id="toc_13">链路跟踪</h4>

<p>链路跟踪插件默认是关闭的，链路跟踪依赖jaeger服务。</p>

<p>在配置文件里的字段<code>enableTrace</code>设置：</p>

<pre><code class="language-yaml">  enableTrace: false    # 是否开启追踪，true:启用，false:关闭，如果是true，必须设置jaeger配置。
  tracingSamplingRate: 1.0      # 链路跟踪采样率, 范围0~1.0浮点数, 0表示不采样, 1.0表示采样所有链路


# jaeger 设置
jaeger:
  agentHost: &quot;192.168.3.37&quot;
  agentPort: 6831
</code></pre>

<p>在jaeger界面上查看链路跟踪信息<a href="https://go-sponge.com/zh-cn/service-governance?id=%e9%93%be%e8%b7%af%e8%b7%9f%e8%b8%aa" rel="nofollow">文档说明</a>。</p>

<p><br></p>

<h4 id="toc_14">服务注册与发现</h4>

<p>服务注册与发现插件默认是关闭的，支持consul、etcd、nacos三种类型。</p>

<p>在配置文件里的字段<code>registryDiscoveryType</code>设置：</p>

<pre><code class="language-yaml">  registryDiscoveryType: &quot;&quot;    # 注册和发现类型：consul、etcd、nacos，如果为空表示关闭服务注册与发现。


# 根据字段registryDiscoveryType值来设置参数，例如使用consul作为服务发现，只需设置consul。
# consul 设置
consul:
  addr: &quot;192.168.3.37:8500&quot;

# etcd 设置
etcd:
  addrs: [&quot;192.168.3.37:2379&quot;]

# nacos 设置
nacosRd:
  ipAddr: &quot;192.168.3.37&quot;
  port: 8848
  namespaceID: &quot;3454d2b5-2455-4d0e-bf6d-e033b086bb4c&quot; # namespace id
</code></pre>

<p><br></p>

<h4 id="toc_15">指标采集</h4>

<p>指标采集功能默认是开启的，提供给prometheus采集数据，默认路由是<code>/metrics</code>。</p>

<p>在配置文件里的字段<code>enableMetrics</code>设置：</p>

<pre><code class="language-yaml">  enableMetrics: true    # 是否开启指标采集，true：启用，false：关闭
</code></pre>

<p>使用prometheus和grafana采集指标和监控服务的<a href="https://go-sponge.com/zh-cn/service-governance?id=%e7%9b%91%e6%8e%a7" rel="nofollow">文档说明</a>。</p>

<p><br></p>

<h4 id="toc_16">性能分析</h4>

<p>性能分析插件默认是关闭的，采集profile的默认路由是<code>/debug/pprof</code>，除了支持go语言本身提供默认的profile分析，还支持io分析，路由是<code>/debug/pprof/profile-io</code>。</p>

<p>在配置文件里的字段<code>enableHTTPProfile</code>设置：</p>

<pre><code class="language-yaml">  enableHTTPProfile: false    # 是否开启性能分析，true：启用，false：关闭
</code></pre>

<p>通过路由采集profile进行性能分析方式，通常在开发或测试时使用，如果线上开启会有一点点性能损耗，因为程序后台一直定时记录profile相关信息。sponge生成的web服务对此做了一些改进，平时停止采集profile，用户主动触发系统信号时才开启和关闭采集profile，采集profile保存到<code>/tmp/服务名称_profile目录</code>，默认采集为60秒，60秒后自动停止采集profile，如果只想采集30秒，发送第一次信号开始采集，大概30秒后发送第二次信号表示停止采集profile，类似开关一样。</p>

<p>这是采集profile操作步骤：</p>

<pre><code class="language-yaml"># 通过名称查看服务pid
ps aux | grep 服务名称

# 发送信号给服务
kill -trap pid值
</code></pre>

<p>注：只支持linux、darwin系统。</p>

<p><br></p>

<h4 id="toc_17">资源统计</h4>

<p>资源统计插件默认是开启的，默认每分钟统计一次并输出到日志，资源统计了包括系统和服务本身这两部分的cpu和内存相关的数据，资源统计包含了自动触发采集profile功能，当连续3次统计本服务的CPU或内存平均值，CPU或内存平均值占用系统资源超过80%时，自动触发采集profile，默认采集为60秒，采集profile保存到<code>/tmp/服务名称_profile目录</code>，从而实现自适应采集profile，比通过人工发送系统信号来采集profile又改进了一步。</p>

<p>在配置文件里的字段<code>enableHTTPProfile</code>设置：</p>

<pre><code class="language-yaml">  enableStat: true    # 是否开启资源统计，true:启用，false:关闭
</code></pre>

<p><br></p>

<h4 id="toc_18">配置中心</h4>

<p>目前支持nacos作为配置中心，配置中心文件<code>configs/community_cc.yml</code>，配置内容如下：</p>

<pre><code class="language-yaml"># nacos 设置
nacos:
  ipAddr: &quot;192.168.3.37&quot;    # 服务地址
  port: 8848                      # 监听端口
  scheme: &quot;http&quot;                # 支持http和https
  contextPath: &quot;/nacos&quot;       # 路径
  namespaceID: &quot;3454d2b5-2455-4d0e-bf6d-e033b086bb4c&quot; # namespace id
  group: &quot;dev&quot;                    # 组名称: dev, prod, test
  dataID: &quot;community.yml&quot;  # 配置文件id
  format: &quot;yaml&quot;                 # 配置文件类型: json,yaml,toml
</code></pre>

<p>而服务的配置文件<code>configs/community.yml</code>复制到nacos界面上配置。使用nacos配置中心，启动服务命令需要指定配置中心文件，命令如下：</p>

<pre><code class="language-bash">./community -c configs/community_cc.yml -enable-cc
</code></pre>

<p>使用nacos作为配置中心的<a href="https://go-sponge.com/zh-cn/service-governance?id=%e9%85%8d%e7%bd%ae%e4%b8%ad%e5%bf%83" rel="nofollow">文档说明</a>。</p>

<p><br>
<br></p>

<h3 id="toc_19">持续集成与部署</h3>

<p>sponge生成的web服务包括了编译和部署脚本，编译支持二进制编译和docker镜像构建，部署支持二进制部署、docker部署、k8s部署三种方式，这些功能都统一集成在<code>Makefile</code>文件里，使用make命令就可以很方便的执行指定编译或部署服务。</p>

<p>除了使用make命令编译和部署，还支持自动化部署工具Jenkins，默认的Jenkins设置在文件<code>Jenkinsfile</code>，支持自动化部署到k8s，如果需要二进制或docker部署，需要对<code>Jenkinsfile</code>进行修改。</p>

<p>使用Jenkins持续集成和部署的<a href="https://go-sponge.com/zh-cn/cicd" rel="nofollow">文档说明</a>。</p>

<p><br>
<br></p>

<h3 id="toc_20">服务压测</h3>

<p>压测服务时使用的一些工具：</p>

<ul>
<li>http压测工具<a href="https://github.com/wg/wrk" rel="nofollow">wrk</a>或<a href="https://github.com/link1st/go-stress-testing" rel="nofollow">go-stress-testing</a>。</li>
<li>服务开启指标采集功能，使用prometheus采集服务指标和系统指标进行监控。</li>
<li>服务本身的自适应采集profile功能。</li>
</ul>

<p><br></p>

<p>压测指标：</p>

<ul>
<li><strong>并发度</strong>: 逐渐增加并发用户数，找到服务的最大并发度，确定服务能支持的最大用户量。</li>
<li><strong>响应时间</strong>: 关注并发用户数增加时，服务的平均响应时间和响应时间分布情况。确保即使在高并发下，响应时间也在可接受范围内。</li>
<li><strong>错误率</strong>: 观察并发增加时，服务出现错误或异常的概率。使用压测工具进行长时间并发测试，统计各并发级别下的错误数量和类型。</li>
<li><strong>吞吐量</strong>: 找到服务的最大吞吐量，确定服务在高并发下可以支持的最大请求量。这需要不断增加并发，直到找到吞吐量饱和点。</li>
<li><strong>资源利用率</strong>: 关注并发增加时，CPU、内存、磁盘I/O、网络等资源的利用率，找到服务的资源瓶颈。</li>
<li><strong>瓶颈检测</strong>: 通过观察高并发情况下服务的性能指标和资源利用率，找到系统和服务的硬件或软件瓶颈，以便进行优化。</li>
<li><strong>稳定性</strong>: 长时间高并发运行可以检测到服务存在的潜在问题，如内存泄露、连接泄露等，确保服务稳定运行。这需要较长时间的并发压测，观察服务运行指标。</li>
</ul>

<p>对服务进行压测，主要是为了评估其性能，确定能支持的最大并发和吞吐量，发现当前的瓶颈，并检测服务运行的稳定性，以便进行优化或容量规划。</p>

<p><br>
<br></p>

<h3 id="toc_21">总结</h3>

<p>community-single是使用工具sponge从开发到部署的实战项目示例，具体流程如下：</p>

<ol>
<li>定义mysql表</li>
<li>在proto文件定义api接口</li>
<li>根据proto文件生成web框架代码</li>
<li>根据proto文件生成业务逻辑相关代码</li>
<li>根据mysql表生成dao代码</li>
<li>在指定模板文件中编写具体逻辑代码</li>
<li>在swagger测试验证api接口</li>
<li>按需启用服务治理功能</li>
<li>持续集成与部署</li>
<li>服务压测</li>
</ol>

<p>看起来流程有点多，真正需要人工编写代码的只有1、2、6这三个核心业务流程，其他流程涉及到的代码或脚本由sponge生成，使用sponge剥离非业务逻辑代码和业务逻辑代码，让开发项目时只需要聚焦在业务逻辑的核心代码上，同时也使得项目代码变得规范统一，不同的程序员都可以迅速上手。再结合编程辅助工具Copilot或Codeium写代码，开发变得更高效、轻松。</p>

<p>community-single是单体web服务，随着需求增加，功能越来越复杂，使得代码维护和开发变得困难，可以拆分成多个微服务，web单体服务拆分成微服务过程，只换了蛋壳(web框架换成gRPC框架)和蛋白(http handler相关代码换成rpc service相关代码)，蛋黄(核心业务逻辑代码)不变，核心业务逻辑代码可以无缝的移植到微服务代码中。在下一篇文章介绍使用工具sponge辅助完成把community-single拆分为微服务集群。</p>
<p>本文链接：<a href="https://zhuyasen.com/post/community-single.html">https://zhuyasen.com/post/community-single.html</a>，<a href="https://zhuyasen.com/post/community-single.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>一个强大的Go开发框架sponge，以低代码方式开发项目</title>
            <link>https://zhuyasen.com/post/sponge.html</link>
            <comments>https://zhuyasen.com/post/sponge.html#comments</comments>
            <guid>https://zhuyasen.com/post/sponge.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">sponge 介绍</h3>

<p><strong>sponge</strong> 是一个强大的 <code>Go</code> 开发框架，其核心理念是通过解析 <code>SQL</code>、<code>Protobuf</code>、<code>JSON</code> 文件逆向生成模块化的代码，这些模块代码可灵活组合成多种类型的完整后端服务（<code>类似海绵细胞的特性，打散的海绵细胞能自动重新组合成新的海绵</code>）。sponge 拥有出色的项目工程化能力，提供一站式项目开发解决方案，涵盖代码生成、开发、测试、API 文档生成和部署。sponge 通过模块化架构与AI助手深度融合，大幅提升开发效率、降低技术门槛，助力开发者以&rdquo;低代码&rdquo;方式轻松构建稳定可靠的后端系统。</p>

<p>github地址： <a href="https://github.com/go-dev-frame/sponge" rel="nofollow">https://github.com/go-dev-frame/sponge</a></p>

<p><br></p>

<h3 id="toc_1">适用场景</h3>

<p>sponge 适用于快速构建多种类型的高性能后端服务，包括但不限于：</p>

<ul>
<li><code>RESTful API</code> 服务</li>
<li><code>Web</code> 服务 (基于Gin)</li>
<li><code>gRPC</code> 服务</li>
<li><code>HTTP+gRPC</code> 混合服务</li>
<li><code>gRPC Gateway API</code> 服务</li>
<li>云原生微服务</li>
</ul>

<p>此外，开发者还可以通过自定义模板，生成满足业务需求的各类代码。</p>

<p><br></p>

<h3 id="toc_2">核心优势</h3>

<ol>
<li><p><strong>一键生成完整后端服务代码</strong><br />
对于仅需 <code>CRUD API</code> 的 <code>Web</code> 或 <code>gRPC</code> 服务，无需编写任何 <code>Go</code> 代码。只需连接数据库(如 <code>MySQL</code>、<code>MongoDB</code>、<code>PostgreSQL</code>、<code>SQLite</code>)，即可一键生成完整后端服务代码，并轻松部署到 Linux 服务器、Docker 或 Kubernetes 上。</p></li>

<li><p><strong>高效开发通用服务</strong><br />
开发通用的 <code>Web</code>、<code>gRPC</code>、<code>HTTP+gRPC</code> 或 <code>gRPC Gateway</code> 服务，只需专注于以下三部分：</p>

<ul>
<li>数据库表的定义；</li>
<li>在 Protobuf 文件中定义 API 描述信息；</li>
<li>在生成的模板中，使用内置AI助手或人工编写业务逻辑代码。<br /></li>
</ul></li>
</ol>

<p>服务的框架代码和 CRUD API 代码均由 sponge 自动生成。</p>

<ol>
<li><p><strong>支持自定义模板，灵活扩展</strong><br />
sponge 支持通过自定义模板生成项目所需的多种代码类型，不局限于 <code>Go</code> 语言。例如：</p>

<ul>
<li>后端代码；</li>
<li>前端代码；</li>
<li>配置文件；</li>
<li>测试代码；</li>
<li>构建和部署脚本等。</li>
</ul></li>

<li><p><strong>在页面生成代码，简单易用</strong><br />
sponge 提供在页面生成代码，避免了复杂的命令行操作，只需在页面上简单的填写参数即可一键生成代码。</p></li>

<li><p><strong>与 AI 助手协同开发，形成开发闭环</strong><br />
sponge 与 内置的 AI 助手(DeepSeek、ChatGPT、Gemini)深度融合，形成一套完整的高效开发解决方案：</p>

<ul>
<li><strong>sponge</strong>：负责基础设施代码生成(服务框架、CRUD API 接口、自定义 API 接口代码(缺少业务逻辑)等)。</li>
<li><strong>AI助手</strong>：专注业务逻辑实现(表结构 DDL 设计、自定义 API 接口定义、业务逻辑实现代码)。</li>
</ul></li>
</ol>

<p><br></p>

<h3 id="toc_3">快速开始</h3>

<ol>
<li><p><strong>安装 sponge</strong>
支持在 windows、mac、linux 环境下安装 sponge，点击查看 <a href="https://github.com/go-dev-frame/sponge/blob/main/assets/install-cn.md" rel="nofollow"><strong>安装 sponge 说明</strong></a>。</p></li>

<li><p><strong>打开生成代码 UI 页面</strong>
安装完成后，执行命令打开 sponge UI 页面：</p></li>
</ol>

<pre><code class="language-bash">   sponge run
</code></pre>

<p>在本地浏览器访问 <code>http://localhost:24631</code>，在页面上操作生成代码，如下图所示：</p>

<p><img src="https://raw.githubusercontent.com/go-dev-frame/sponge/main/assets/sponge-ui.png" alt="sponge-ui" /></p>

<p><br></p>

<h3 id="toc_4">主要功能</h3>

<p>sponge包含丰富的组件(按需使用)：</p>

<ul>
<li>Web 框架 <a href="https://github.com/gin-gonic/gin" rel="nofollow">gin</a></li>
<li>RPC 框架 <a href="https://github.com/grpc/grpc-go" rel="nofollow">grpc</a></li>
<li>配置解析 <a href="https://github.com/spf13/viper" rel="nofollow">viper</a></li>
<li>日志 <a href="https://github.com/uber-go/zap" rel="nofollow">zap</a></li>
<li>数据库组件 <a href="https://github.com/go-gorm/gorm" rel="nofollow">gorm</a>, <a href="https://github.com/mongodb/mongo-go-driver" rel="nofollow">mongo-go-driver</a></li>
<li>缓存组件 <a href="https://github.com/go-redis/redis" rel="nofollow">go-redis</a>, <a href="https://github.com/dgraph-io/ristretto" rel="nofollow">ristretto</a></li>
<li>自动化api文档 <a href="https://github.com/swaggo/swag" rel="nofollow">swagger</a>, <a href="https://github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" rel="nofollow">protoc-gen-openapiv2</a></li>
<li>鉴权 <a href="https://github.com/golang-jwt/jwt" rel="nofollow">jwt</a></li>
<li>校验 <a href="https://github.com/go-playground/validator" rel="nofollow">validator</a></li>
<li>Websocket <a href="https://github.com/gorilla/websocket" rel="nofollow">gorilla/websocket</a></li>
<li>定时任务 <a href="https://github.com/robfig/cron" rel="nofollow">cron</a></li>
<li>消息队列组件 <a href="https://github.com/rabbitmq/amqp091-go" rel="nofollow">rabbitmq</a>, <a href="https://github.com/IBM/sarama" rel="nofollow">kafka</a></li>
<li>分布式事务管理器 <a href="https://github.com/dtm-labs/dtm" rel="nofollow">dtm</a></li>
<li>分布式锁 <a href="https://github.com/go-dev-frame/sponge/tree/main/pkg/dlock" rel="nofollow">dlock</a></li>
<li>自适应限流 <a href="https://github.com/go-dev-frame/sponge/tree/main/pkg/shield/ratelimit" rel="nofollow">ratelimit</a></li>
<li>自适应熔断 <a href="https://github.com/go-dev-frame/sponge/tree/main/pkg/shield/circuitbreaker" rel="nofollow">circuitbreaker</a></li>
<li>链路跟踪 <a href="https://github.com/open-telemetry/opentelemetry-go" rel="nofollow">opentelemetry</a></li>
<li>监控 <a href="https://github.com/prometheus/client_golang/prometheus" rel="nofollow">prometheus</a>, <a href="https://github.com/grafana/grafana" rel="nofollow">grafana</a></li>
<li>服务注册与发现 <a href="https://github.com/etcd-io/etcd" rel="nofollow">etcd</a>, <a href="https://github.com/hashicorp/consul" rel="nofollow">consul</a>, <a href="https://github.com/alibaba/nacos" rel="nofollow">nacos</a></li>
<li>自适应采集 <a href="https://go.dev/blog/pprof" rel="nofollow">profile</a></li>
<li>资源统计 <a href="https://github.com/shirou/gopsutil" rel="nofollow">gopsutil</a></li>
<li>配置中心 <a href="https://github.com/alibaba/nacos" rel="nofollow">nacos</a></li>
<li>代码质量检查 <a href="https://github.com/golangci/golangci-lint" rel="nofollow">golangci-lint</a></li>
<li>持续集成部署 CICD <a href="https://github.com/jenkinsci/jenkins" rel="nofollow">jenkins</a>, <a href="https://www.docker.com/" rel="nofollow">docker</a>, <a href="https://github.com/kubernetes/kubernetes" rel="nofollow">kubernetes</a></li>
<li>生成项目业务架构图 <a href="https://github.com/go-dev-frame/spograph" rel="nofollow">spograph</a></li>
<li>自定义模板生成代码 <a href="https://pkg.go.dev/text/template@go1.23.3" rel="nofollow">go template</a></li>
</ul>

<p><br></p>

<h3 id="toc_5">目录结构</h3>

<p>生成的服务代码目录结构遵循 <a href="https://github.com/golang-standards/project-layout" rel="nofollow">project-layout</a>。</p>

<p>这是生成的<code>单体应用单体仓库(monolith)</code>或<code>微服务多仓库(multi-repo)</code>代码目录结构：</p>

<pre><code class="language-bash">.
├── api            # protobuf文件和生成的*pb.go目录
├── assets         # 其他与资源库一起使用的资产(图片、logo等)目录
├── cmd            # 程序入口目录
├── configs        # 配置文件的目录
├── deployments    # 裸机、docker、k8s部署脚本目录
├── docs           # 设计文档和界面文档目录
├── internal       # 业务逻辑代码目录
│    ├── cache        # 基于业务包装的缓存目录
│    ├── config       # Go结构的配置文件目录
│    ├── dao          # 数据访问目录
│    ├── database     # 数据库目录
│    ├── ecode        # 自定义业务错误代码目录
│    ├── handler      # http的业务功能实现目录
│    ├── model        # 数据库模型目录
│    ├── routers      # http路由目录
│    ├── rpcclient    # 连接grpc服务的客户端目录
│    ├── server       # 服务入口，包括http、grpc等
│    ├── service      # grpc的业务功能实现目录
│    └── types        # http的请求和响应类型目录
├── pkg            # 外部应用程序可以使用的库目录
├── scripts        # 执行脚本目录
├── test           # 额外的外部测试程序和测试数据
├── third_party    # 依赖第三方protobuf文件或其他工具的目录
├── Makefile       # 开发、测试、部署相关的命令集合
├── go.mod         # go 模块依赖关系和版本控制文件
└── go.sum         # go 模块依赖项的密钥和校验文件
</code></pre>

<p><br></p>

<p>这是生成的<code>微服务单体仓库(mono-repo)</code>代码目录结构(也就是大仓库代码目录结构)：</p>

<pre><code class="language-bash">.
├── api
│    ├── server1       # 服务1的protobuf文件和生成的*pb.go目录
│    ├── server2       # 服务2的protobuf文件和生成的*pb.go目录
│    ├── server3       # 服务3的protobuf文件和生成的*pb.go目录
│    └── ...
├── server1        # 服务1的代码目录，与微服务多仓库(multi-repo)目录结构基本一样
├── server2        # 服务2的代码目录，与微服务多仓库(multi-repo)目录结构基本一样
├── server3        # 服务3的代码目录，与微服务多仓库(multi-repo)目录结构基本一样
├── ...
├── third_party    # 依赖的第三方protobuf文件
├── go.mod         # go 模块依赖关系和版本控制文件
└── go.sum         # go 模块依赖项的密钥和校验和文件
</code></pre>

<p><br></p>

<h3 id="toc_6">使用示例</h3>

<h4 id="toc_7">使用 sponge 创建服务示例</h4>

<ul>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/1_web-gin-CRUD" rel="nofollow">基于sql创建web服务(包括CRUD)</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/2_micro-grpc-CRUD" rel="nofollow">基于sql创建grpc服务(包括CRUD)</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/3_web-gin-protobuf" rel="nofollow">基于protobuf创建web服务</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/4_micro-grpc-protobuf" rel="nofollow">基于protobuf创建grpc服务</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/5_micro-gin-rpc-gateway" rel="nofollow">基于protobuf创建grpc网关服务</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/_10_micro-grpc-http-protobuf" rel="nofollow">基于protobuf创建grpc+http服务</a></li>
</ul>

<h4 id="toc_8">使用 sponge 开发完整项目示例</h4>

<ul>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/7_community-single" rel="nofollow">简单的社区web后端服务</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/8_community-cluster" rel="nofollow">简单的社区web后端服务拆分为微服务</a></li>
</ul>

<h4 id="toc_9">分布式事务示例</h4>

<ul>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/9_order-grpc-distributed-transaction" rel="nofollow">简单的分布式订单系统</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/_12_sponge-dtm-flashSale" rel="nofollow">秒杀抢购活动</a></li>
<li><a href="https://github.com/go-dev-frame/sponge_examples/tree/main/_14_eshop" rel="nofollow">电商系统</a></li>
</ul>

<p><br></p>

<h3 id="toc_10">总结</h3>

<p>sponge 是一个帮助你大幅提高开发效率、降低开发成本的开发框架，通过支持主流数据库、低代码开发和自动化功能，同时支持自定义的灵活扩展功能。如果您正在寻找一种方法来显著提高开发效率并缩短上线时间，那么sponge绝对值得一试。</p>

<p><br></p>
<p>本文链接：<a href="https://zhuyasen.com/post/sponge.html">https://zhuyasen.com/post/sponge.html</a>，<a href="https://zhuyasen.com/post/sponge.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>go test命令</title>
            <link>https://zhuyasen.com/post/gotest.html</link>
            <comments>https://zhuyasen.com/post/gotest.html#comments</comments>
            <guid>https://zhuyasen.com/post/gotest.html</guid>
            <description>
                <![CDATA[<blockquote>

<p><code>go test</code>命令只运行单元测试，添加<code>-bench=.</code>参数，go test同时执行单元测试和基准测试，当然可以通过正则过滤只运行基准测试，例如<code>-bench=^Benchmark</code>。添加<code>-conver</code>参数展示测试覆盖率。</p>

<p><br></p>

<h3 id="toc_0">测试代码</h3>

<p>测试5种字符串拼接效果，共两个文件splice.go和splice_test.go</p>

<p>splice.go文件内容如下：</p>

<pre><code class="language-go">package splice

import (
    &quot;bytes&quot;
    &quot;fmt&quot;
    &quot;strings&quot;
)

// SpliceWithPlus 使用+号拼接字符串
func SpliceWithPlus(s1 string, s2 string) string {
    return s1 + s2
}

// SpliceWithSprintf 使用fmt.Sprintf拼接字符串
func SpliceWithSprintf(s1 string, s2 string) string {
    return fmt.Sprintf(&quot;%s%s&quot;, s1, s2)
}

// SpliceWithJoin 使用strings.Join拼接字符串
func SpliceWithJoin(s1 string, s2 string) string {
    return strings.Join([]string{s1, s2}, &quot;&quot;)
}

// SpliceWithBuilder 使用strings.Builder拼接字符串
func SpliceWithBuilder(s1 string, s2 string) string {
    var builder strings.Builder
    builder.WriteString(s1)
    builder.WriteString(s2)
    return builder.String()
}

// SpliceWithBuilder 使用bytes.Buffer拼接字符串
func SpliceWithBuffer(s1 string, s2 string) string {
    var bf bytes.Buffer
    bf.WriteString(s1)
    bf.WriteString(s2)
    return bf.String()
}
</code></pre>

<p><br></p>

<p>测试文件splice_test.go内容如下：</p>

<pre><code class="language-go">package test_example

import &quot;testing&quot;

var (  
    count = 10
    s1    = strings.Repeat(&quot;1234567890&quot;, count)
    s2    = strings.Repeat(&quot;0987654321&quot;, count)
    want  = s1 + s2
)

func TestSpliceString(t *testing.T) {
    if got := SpliceWithPlus(s1, s2); got != want {
        t.Errorf(&quot;SpliceWithPlus() = %v, want %v&quot;, got, want)
    }
    if got := SpliceWithSprintf(s1, s2); got != want {
        t.Errorf(&quot;SpliceWithSprintf() = %v, want %v&quot;, got, want)
    }
    if got := SpliceWithJoin(s1, s2); got != want {
        t.Errorf(&quot;SpliceWithJoin() = %v, want %v&quot;, got, want)
    }
    if got := SpliceWithBuilder(s1, s2); got != want {
        t.Errorf(&quot;SpliceWithBuilder() = %v, want %v&quot;, got, want)
    }
    if got := SpliceWithBuffer(s1, s2); got != want {
        t.Errorf(&quot;SpliceWithBuffer() = %v, want %v&quot;, got, want)
    }
}

func BenchmarkSpliceWithPlus(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        SpliceWithPlus(s1, s2)
    }
}

func BenchmarkSpliceWithSprintf(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        SpliceWithSprintf(s1, s2)
    }
}

func BenchmarkSpliceWithJoin(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        SpliceWithJoin(s1, s2)
    }
}

func BenchmarkSpliceWithBuilder(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        SpliceWithBuilder(s1, s2)
    }
}

func BenchmarkSpliceWithBuffer(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        SpliceWithBuffer(s1, s2)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_1">单元测试</h3>

<p>单元测试常用参数：</p>

<ul>
<li><code>-v</code>：测试时显示详细信息，示例：<u>go test -v</u></li>
<li><code>-list</code>: 列出测试、基准测试函数，示例：<u>go test -list</u></li>
<li><code>-run</code>: 指定要运行的测试函数，示例：<u>go test -run TestSpliceString</u></li>
<li><code>-failfast</code>: 在第一个失败的测试处停止，退出测试，示例 <u>go test -failfast</u></li>
<li><code>-count=1</code>: 禁用缓存，示例：<u>go test -count=1</u></li>
<li><code>-cpu=4</code>: 限制cpu数量，把参数值传递给GOMAXPROCS，示例：<u>go test -cpu=4</u></li>
<li><code>-race</code>: 检测竞态条件，示例：<u>go test -race</u></li>
</ul>

<p><br></p>

<p>单元测试范围示例：</p>

<pre><code class="language-bash"># 执行当前下所有单元测试，包括子文件夹
go test ./...

# 执行当前目录下所有单元测试，不包括子文件夹
go test ./dir

# 执行指定文件单元测试，指定测试文件和被测试文件
go test splice.go splice_test.go

# 运行指定单元测试用例
go test -run TestSpliceString
</code></pre>

<p><br></p>

<h3 id="toc_2">基准测试</h3>

<p>基准测试框架对一个测试用例的默认测试时间是 1 秒。开始测试时，当以 Benchmark 开头的基准测试用例函数返回时还不到 1 秒，那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增，同时以递增后的值重新调用基准测试用例函数。</p>

<p>基准测试常用参数：</p>

<ul>
<li><code>-bench</code>: 基准测试必填参数，参数值是正则表达式，<u> -bench=.</u> 表示同时运行单元测试和基准测试；<u> -bench=^Benchmark </u> 表示执行当前目录下所有基准测试，也可以具体到某个函数的基准测试。</li>
<li><code>benchtime=3s</code>: 自定义测试时间，示例：<u>go test -bench=. -benchtime=3s</u></li>
<li><code>-benchmem</code>: 显示内存分配统计信息，示例：<u>go test -bench=. -benchmem</u></li>
<li><code>-cpu=4</code>: 限制线程数量，把参数值传递给GOMAXPROCS，示例：<u>go test -bench=. -cpu=4</u></li>
</ul>

<p>基准测试范围示例：</p>

<pre><code class="language-bash"># 执行当前目录下所有基准测试，包括子文件夹
go test -bench=. ./...

# 执行当前目录下的基准测试，不包括子文件夹
go test -bench=. ./dir

# 执行指定文件基准测试，指定测试文件和被测试文件
go test -bench=. splice.go splice_test.go

# 运行指定单元基准测试
go test -bench=BenchmarkSpliceWithPlus
</code></pre>

<p>修改splice_test.go文件的count变量值，分别为1、10、100，进行基准测试，结果如下：</p>

<pre><code class="language-bash">go version go1.17.2 windows/amd64

# count=1，10bytes 长度字符串拼接基准压测
$ go test -bench=. -benchmem

goos: windows
goarch: amd64
pkg: demo/test_example
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSpliceWithPlus-12              80041087                14.95 ns/op            0 B/op          0 allocs/op
BenchmarkSpliceWithSprintf-12            8744901               128.1 ns/op            56 B/op          3 allocs/op
BenchmarkSpliceWithJoin-12              33571974                35.15 ns/op           24 B/op          1 allocs/op
BenchmarkSpliceWithBuilder-12           20154652                59.78 ns/op           48 B/op          2 allocs/op
BenchmarkSpliceWithBuffer-12            19531503                62.91 ns/op           88 B/op          2 allocs/op
PASS
ok      demo/test_example       6.314s


# count=10，100bytes 长度字符串拼接基准压测
$ go test -bench=. -benchmem

goos: windows
goarch: amd64
pkg: demo/test_example
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSpliceWithPlus-12              15102146                66.62 ns/op          208 B/op          1 allocs/op
BenchmarkSpliceWithSprintf-12            6655336               181.0 ns/op           240 B/op          3 allocs/op
BenchmarkSpliceWithJoin-12              17094942                70.57 ns/op          208 B/op          1 allocs/op
BenchmarkSpliceWithBuilder-12           10660695               112.0 ns/op           336 B/op          2 allocs/op
BenchmarkSpliceWithBuffer-12             5528898               201.4 ns/op           640 B/op          3 allocs/op
PASS
ok      demo/test_example       6.610s


# count=100，1000bytes 长度字符串拼接基准压测
$ go test -bench=. -benchmem

goos: windows
goarch: amd64
pkg: demo/test_example
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSpliceWithPlus-12               2898604               387.1 ns/op          2048 B/op          1 allocs/op
BenchmarkSpliceWithSprintf-12            2218687               511.3 ns/op          2081 B/op          3 allocs/op
BenchmarkSpliceWithJoin-12               2922162               412.0 ns/op          2048 B/op          1 allocs/op
BenchmarkSpliceWithBuilder-12            2015064               607.7 ns/op          3072 B/op          2 allocs/op
BenchmarkSpliceWithBuffer-12              999949                1186 ns/op          6144 B/op          3 allocs/op
PASS
ok      demo/test_example       8.067s
</code></pre>

<p>基准压测函数后面数字12，表示执行基准测试使用的线程数量，可以通过<code>-cpu=4</code>参数来修改。</p>

<p>5种字符串拼接性能最好的是BenchmarkSpliceWithPlus，其次是BenchmarkSpliceWithJoin。</p>

<p><br></p>

<h4 id="toc_3">Benchstat工具</h4>

<p>单纯只压测一次结果不大准确，因此使用工具Benchstat 计算和比较有关基准的统计数据。</p>

<p>安装：</p>

<blockquote>
<p>go install golang.org/x/perf/cmd/benchstat@latest</p>
</blockquote>

<p>用法：</p>

<blockquote>
<p>benchstat [-delta-test name] [-geomean] [-html] [-sort order] old.txt [new.txt] [more.txt &hellip;]</p>
</blockquote>

<p>示例：</p>

<pre><code class="language-bash"># 第一次压测某个函数
go test -run=NONE -bench=BenchmarkSpliceWithSprintf -count=5 | tee -a old.txt

# 优化后(使用+号替换fmt.Sprintf拼接字符串)第二次压测某个函数
go test -run=NONE -bench=BenchmarkSpliceWithSprintf -count=5 | tee -a new.txt

# 查看压测结果
benchstat old.txt
benchstat new.txt

# 比较两次的统计结果
benchstat old.txt new.txt
# name                  old time/op  new time/op  delta
# SpliceWithSprintf-12   128ns ± 1%    33ns ± 0%  -74.63%  (p=0.000 n=14+5)
</code></pre>

<p>任何大于 5% 的值都表明某些样本不可靠，在这种情况下，您应该重新运行基准测试，尽可能保持环境稳定以提高可靠性。</p>

<p><br></p>

<h3 id="toc_4">测试覆盖率</h3>

<p>go test命令还支持展示测试覆盖率信息。</p>

<p>测试覆盖率常用参数：</p>

<ul>
<li><code>-cover</code>: 测试覆盖率必填参数，示例：<u>go test -v -cover</u></li>
<li><code>-coverprofile</code>: 导出单元测试覆盖率统计信息到文件，示例：<u>go test -coverprofile=cover.out</u></li>
<li><code>-html</code>: 在浏览器查看覆盖哪些代码，示例：<u>go tool cover -html=cover.out</u></li>
</ul>

<p>测试覆盖率范围示例：</p>

<pre><code class="language-bash"># 执行当前下所有单元测试，包括子文件夹，并展示覆盖率
go test -cover ./...

# 执行当前目录下所有单元测试，不包括子文件夹，并展示覆盖率
go test -cover ./dir

# 执行指定文件单元测试，指定测试文件和被测试文件，并展示覆盖率
go test -cover splice.go splice_test.go

# 运行指定单元测试用例，并展示覆盖率
go test -cover -run TestSpliceString
</code></pre>

<pre><code class="language-bash"># 在浏览器查看覆盖哪些代码
go test -coverprofile=cover.out &amp;&amp; go tool cover -html=cover.out
</code></pre>

<p><br></p>

<h3 id="toc_5">总结</h3>

<p>单元测试是项目开发环节必不可少的环节，是检验程序是否符合预期手段，测试代码有可能比实际业务逻辑代码还多，主要原因是测试包括各种测试case，这些case尽可能覆盖率所有业务代码，使用goland IDE写代码，按快捷键Alt+Insert可以自动化添加单元测试代码，只需要填写测试用例，大大减少写测试代码时间。基准测试是性能要求比较高的业务逻辑中必须测试环节，通过暴力压测方式检验程序性能达到什么水平，一般看完成操作耗时和内存分配情况来判断，也是程序性能调优的常用手段。</p>
<p>本文链接：<a href="https://zhuyasen.com/post/gotest.html">https://zhuyasen.com/post/gotest.html</a>，<a href="https://zhuyasen.com/post/gotest.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>go应用程序性能分析</title>
            <link>https://zhuyasen.com/post/pprof.html</link>
            <comments>https://zhuyasen.com/post/pprof.html#comments</comments>
            <guid>https://zhuyasen.com/post/pprof.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">1 介绍</h3>

<p><a href="https://github.com/google/pprof" rel="nofollow">pprof</a> 是一个可视化和分析数据的工具，pprof 读取 <a href="https://github.com/google/pprof/blob/master/proto/profile.proto" rel="nofollow">profile.proto</a> 格式的分析样本集合并生成报告以可视化和帮助分析数据，它可以生成文本和图形报告，是用来分析应用程序的性能重要工具，也可以作为其他用途，例如查看反汇编、和Linux perf工具结合使用。</p>

<p><br></p>

<h3 id="toc_1">2 安装</h3>

<p>golang本身自带pprof工具，pprof可视化时需要依赖<a href="https://graphviz.org/" rel="nofollow">Graphviz</a>工具。</p>

<p><br></p>

<p>linux环境下安装Graphviz：</p>

<blockquote>
<p>yum install -y graphviz</p>
</blockquote>

<p><br></p>

<p>windows环境安装Graphviz：</p>

<pre><code class="language-bash"># Graphviz下载地址： https://graphviz.org/download/
# (1) 安装，在界面默认是没有选择加入系统path，选择添加到系统path即可。(注：如果是默认安装，需要手动把`Graphviz/bin`目录放到系统path。)

# (2) 生成配置文件，进去 `Graphviz/bin` 安装目录，执行命令：
dot -c
</code></pre>

<p><br><br></p>

<h3 id="toc_2">3 使用模式</h3>

<h4 id="toc_3">3.1 报告生成</h4>

<p>pprof 生成指定格式的报告并退出，格式可以是文本或图形。</p>

<pre><code class="language-bash">pprof &lt;format&gt; [options] source
</code></pre>

<p><br></p>

<h4 id="toc_4">3.2 交互式终端使用</h4>

<p>pprof 启动一个交互式 shell，用户可以在其中键入命令，输入<code>help</code>获取命令帮助信息。</p>

<pre><code class="language-bash">pprof [options] source
</code></pre>

<p><br></p>

<h4 id="toc_5">3.3 网页界面</h4>

<p>pprof 可以指定端口上开启 HTTP 服务，在浏览器中访问该端口(例如 <a href="http://localhost:7778" rel="nofollow">http://localhost:7778</a> )，相对前两种模式，比较常用。</p>

<pre><code class="language-bash">pprof -http=[host]:[port] [options] source
</code></pre>

<p><br><br></p>

<h3 id="toc_6">4 一个go程序性能分析示例</h3>

<p>下面代码是分配栈对象和堆对象测试代码，可以调整<code>heapGSize</code>和<code>stackGSize</code>变量大小来调整栈对象和堆对象数量，随着goroutine数量增大，gc压力增大，GC阶段占CPU时间越来越大，CPU执行用户逻辑占比变小，说明程序性能下降。</p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;net/http&quot;
    _ &quot;net/http/pprof&quot;
    &quot;time&quot;
)

var chOut = make(chan string, 100)

func main() {
    // 开启pprof
    go func() {
        err := http.ListenAndServe(&quot;:7777&quot;, nil)
        if err != nil {
            panic(err)
        }
    }()

    // 20个goroutine创建堆对象
    heapGSize := 20
    for i := 0; i &lt; heapGSize; i++ {
        NO := i
        go func() {
            for {
                heapVar(NO)
                sleep()
            }
        }()
    }

    // 20个goroutine创建栈对象
    stackGSize := 20
    for i := 0; i &lt; stackGSize; i++ {
        NO := i
        go func() {
            for {
                stackVar(NO)
                sleep()
            }
        }()
    }

    go output(chOut)

    &lt;-time.After(time.Hour)
}

func heapVar(NO int) {
    for i := 0; i &lt; 2000; i++ {
        s := make([]int, 10240) // 10K
        s[0] = i
        if i == 0 { // 每次只把2000个堆对象中的第一个堆对象输出查看
            s[0] = time.Now().Second()
            chOut &lt;- fmt.Sprintf(&quot;heap%d:%d&quot;, NO, s[0])
        }
    }
}

type User struct {
    ID    int
    Name  string
    Age   int
    Email string
}

func stackVar(NO int) {
    for i := 0; i &lt; 100000; i++ {
        number := &amp;User{ID: i}
        if i == 0 {
            number.ID = time.Now().Second()
            chOut &lt;- fmt.Sprintf(&quot;stack%d:%d&quot;, NO, number.ID)
        }
    }
}

// 随机睡眠1~1.5秒
func sleep() {
    n := rand.Intn(500) + 1000
    time.Sleep(time.Millisecond * time.Duration(n))
}

// 打印输出
func output(ch &lt;-chan string) {
    maxSize := 100
    msgs := make([]string, maxSize, maxSize)
    var v string
    var count int
    for {
        select {
        case v = &lt;-ch:
            count++
            if count == maxSize {
                count = 0
                fmt.Printf(msgs[0] + &quot; &quot;) // 打印第0个位置消息，为了减少终端输出
            }
            msgs[count] = v
        }
    }
}
</code></pre>

<p>编译后运行测试程序，在浏览器打开 <code>http://localhost:7777/debug/pprof</code>  可以查看不同类型采集数据。</p>

<pre><code>count  Profile
15     allocs           // 过去所有内存分配的样本
0      block            // 同步阻塞
0      cmdline          // 当前程序的命令行调用
47     goroutine        // 当前所有goroutine的堆栈痕迹
15     heap             // 现场对象的内存分配样本。你可以指定gc GET参数，在进行堆采样之前运行GC
0      mutex            // 争夺锁的持有者
0      profile          // CPU概况。你可以在seconds GET参数中指定持续时间。在你得到概况文件后，使用go工具pprof命令来调查概况
16     threadcreate     // 操作系统创建新线程
0      trace            // 当前程序的执行跟踪。你可以在秒的GET参数中指定持续时间。在你得到跟踪文件后，使用go tool trace命令来调查该跟踪
</code></pre>

<p>页面只能查看这些数据简单分析，但可视化不够直观，使用<code>go tool pprof</code>更方便查看对应每个类型profile，在菜单【View】可以查看类型包括：</p>

<ul>
<li>TOP：排名</li>
<li>Graph：关系图</li>
<li>Flame Graph：火焰图</li>
<li>Peek：类似TOP，信息更详细</li>
<li>Source：源码</li>
</ul>

<p><br></p>

<h4 id="toc_7">4.1 CPU分析</h4>

<p>在实际中，如果发现程序的CPU使用率高时，可以使用<code>go tool pprof</code>采集CPU分析数据。下面命令是采集最近20秒(默认是30秒)CPU执行分析数据，http端口是7778。</p>

<blockquote>
<p>go tool pprof -http=:7778 -seconds=20 <a href="http://localhost:7777/debug/pprof/profile" rel="nofollow">http://localhost:7777/debug/pprof/profile</a></p>
</blockquote>

<p>等待20秒之后自动在浏览器展示，点击菜单【View】&ndash;&gt; 【Flame Graph】查看CPU火焰图，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%90%84%E5%87%BD%E6%95%B0%E5%8D%A0%E7%94%A8CPU%E7%81%AB%E7%84%B0%E5%9B%BE.jpg" alt="cpu火焰图" /></p>

<p>从图中可以看到<code>heapVar</code>和<code>stackVar</code>只占用1/3左右的CPU，2/3的CPU时间用来创建对象和回收对象了，说明有优化空间。</p>

<p><br></p>

<h4 id="toc_8">4.2 内存分析</h4>

<p>在实际中，如果发现程序的内存使用比较高时，可以使用<code>go tool pprof</code>采集内存分析数据，下面命令是查看堆分配情况，http端口是7779。</p>

<blockquote>
<p>go tool pprof -http=:7779 <a href="http://localhost:7777/debug/pprof/heap" rel="nofollow">http://localhost:7777/debug/pprof/heap</a></p>
</blockquote>

<p>执行命令之后自动在浏览器展示，点击菜单【SAMPLE】 &ndash;&gt; 【inuse_space】，然后点击菜单【View】&ndash;&gt; 【Flame Graph】查看内存火焰图，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%90%84%E5%87%BD%E6%95%B0%E5%8D%A0%E7%94%A8%E5%86%85%E5%AD%98%E7%81%AB%E7%84%B0%E5%9B%BE.jpg" alt="内存火焰图" /></p>

<p>从图中可以看到<code>heapVar</code>占用比较多，说明需要优化<code>heapVar</code>函数，其他系统底层调用，通常是用户程序调用导致，无法优化。</p>

<p><br></p>

<h4 id="toc_9">4.3 下载采集数据到本地分析</h4>

<p>除了直接在浏览器分析，还可以把采集的数据下载到本地后再分析，命令如下：</p>

<pre><code class="language-bash"># 下载到本地
wget &lt;url&gt; -O &lt;filename&gt;
# 下载的文件
go tool pprof -http=&lt;:prort&gt; &lt;filename&gt;
</code></pre>

<p><br></p>

<p>下载采集数据到本地之后，可以做优化前后对比，优化前测试程序的<code>heapVar</code>和<code>stackVar</code>变量值为20，优化后<code>heapVar</code>和<code>stackVar</code>变量值为1.</p>

<p><br></p>

<p><strong>CPU优化前后对比</strong></p>

<pre><code class="language-bash"># 下载优化前采集数据
wget http://localhost:7777/debug/pprof/profile -O cpu_before.out

# 下载优化后数据
wget http://localhost:7777/debug/pprof/profile -O cpu_after.out

# 比较优化前后
go tool pprof -http=:7778 --base cpu_before.out cpu_after.out
</code></pre>

<p>执行命令后，查看Graph图，绿色是优化节省的CPU资源，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E4%BC%98%E5%8C%96%E5%89%8D%E5%90%8ECPU%E4%BD%BF%E7%94%A8%E5%AF%B9%E6%AF%94.jpg" alt="优化前后CPU对比" /></p>

<p><br></p>

<p><strong>内存优化前后对比</strong></p>

<pre><code class="language-bash"># 下载优化前采集数据
wget http://localhost:7777/debug/pprof/heap -O heap_before.out

# 下载优化后数据
wget http://localhost:7777/debug/pprof/heap -O heap_after.out

# 优化前后比较
go tool pprof -http=:7779 --base heap_before.out heap_after.out
</code></pre>

<p>执行命令后，查看Graph图，绿色是优化节省的内存资源，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E4%BC%98%E5%8C%96%E5%89%8D%E5%90%8E%E5%86%85%E5%AD%98%E6%AF%94%E8%BE%83.jpg" alt="优化前后内存对比" /></p>

<p><br></p>

<p>注：切换到【VIEW】 &ndash;&gt; 【Source】源码界面，可以展示哪个函数节省了多少资源。</p>

<p><br><br></p>

<h3 id="toc_10">5 trace分析</h3>

<p>通过trace可以看到goroutine数量、线程数量、堆数量、GC、各个goroutine调度情况，可以查看各个线程当前时刻执行哪个goroutine，当前时刻各个goroutine在干什么，使用trace需要两个步骤：</p>

<p>(1) 下载trace数据到trace.out文件，只有1秒内trace数据</p>

<blockquote>
<p>wget <a href="http://localhost:7777/debug/pprof/trace" rel="nofollow">http://localhost:7777/debug/pprof/trace</a> -O trace.out</p>
</blockquote>

<p><br></p>

<p>(2) 查看trace信息，http端口为7780</p>

<blockquote>
<p>go tool trace -http=:7780 trace.out</p>
</blockquote>

<p>执行命令之后在浏览界面点击<code>View Trace</code>查看具体跟踪信息，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/trace%E7%95%8C%E9%9D%A2%E5%9B%BE.jpg" alt="trace界面图" /></p>

<p>从图中看出在1秒内，GC占用时间超过30%，说明这个程序是需要优化的。点击界面右边菜单<code>File Size Stats</code>，从统计中可以看到GC相关的事件数量非常多，例如 runtime.bgsweep、SWEEP、GC (dedicated)。</p>

<p><br></p>

<p>可以选择<code>zoom</code>进行放大看各个goroutine执行细节，在界面右上角搜索事件名称，例如STW、SWEEP等，下面是搜索STW的细节：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/trace%E7%95%8C%E9%9D%A2%E6%9F%A5%E7%9C%8BSTW.jpg" alt="STW图" /></p>

<p>从图中看到在8ms内出现了不少STW(stop the world)，在一个GC完整周期出现两次STW，其中绿色是sweep termination阶段的STW，红色是mark termination阶段的STW。</p>

<p><br></p>

<h3 id="toc_11">6 总结</h3>

<p>通过pprof作为go程序定位问题、性能优化的工具。建议生产环境的go服务开启pprof，正常情况下是不需要实时去采集内存、CPU数据来分析。因为不知道什么时候应该采集数据，建议写一个看门狗程序，检测CPU、内存或其他指标达到阈值时，自动采集保存现场数据，避免出现问题了，没有留下现场数据。</p>
<p>本文链接：<a href="https://zhuyasen.com/post/pprof.html">https://zhuyasen.com/post/pprof.html</a>，<a href="https://zhuyasen.com/post/pprof.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>channel原理和应用</title>
            <link>https://zhuyasen.com/post/channel.html</link>
            <comments>https://zhuyasen.com/post/channel.html#comments</comments>
            <guid>https://zhuyasen.com/post/channel.html</guid>
            <description>
                <![CDATA[<blockquote>

<p>channel 是一个数据管道，是 goroutine 之间数据通信桥梁，是线程安全的。channel分为有缓冲和无缓冲两种类型，其实无缓冲类型可以理解为有缓冲的一种特殊情况。</p>

<h3 id="toc_0">1 channel工作原理</h3>

<p>源码 go/src/runtime/chan.go</p>

<pre><code class="language-go">type hchan struct {
    qcount   uint           // 当前队列中剩余元素个数  
    dataqsiz uint           // 环形队列长度，即缓冲区的大小，即make（chan T，N），N.
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 每个元素的大小 
    closed   uint32         // 表示当前通道是否处于关闭状态。创建通道后，该字段设置为0，即通道打开; 通过调用close将其设置为1，通道关闭。 
    elemtype *_type         // 元素类型，用于数据传递过程中的赋值； 
    sendx    uint           // 环形缓冲区的状态字段，它指向环形队列当前发送索引
    recvx    uint           // 环形缓冲区的状态字段，它指向环形队列当前接收索引
    recvq    waitq          // 等待读消息的goroutine队列 
    sendq    waitq          // 等待写消息的goroutine队列 
    lock     mutex          // 互斥锁，为每个读写操作锁定通道，因为发送和接收必须是互斥操作
}
</code></pre>

<p>从结构体看核心字段是环形队列buf，而qcount、dataqsiz、sendx、recvx是维护buf状态字段，recvq和sendq是存放发送接收channel阻塞的goroutine队列，也是间接维护buf的，lock是整个结构体的锁，避免不同goroutine读写数据竞争，因此channel是并发安全的。</p>

<p><br></p>

<h4 id="toc_1">1.1 发送数据到channel</h4>

<table>
<thead>
<tr>
<th align="left">环形队列buf状态</th>
<th align="left">goroutine状态</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">buf未满</td>
<td align="left">有一个goroutine发送数据，不会阻塞，把发送的数据填充到环形队列buf空闲位置，按环形队列索引顺序填充数据</td>
</tr>

<tr>
<td align="left">buf已满</td>
<td align="left">有一个goroutine发送数据，环形队列buf没地方存放了，goroutine阻塞等待，把该goroutine的现场保存下来，存放到发送goroutine队列<code>sendq</code>，等环形队列buf被消费后有空闲的位置，从<code>sendq</code>队列(先进先出)唤醒goroutine恢复现场，把发送的数据填充到环形队列空闲位置</td>
</tr>
</tbody>
</table>

<p><img src="https://go-sponge.com/assets/images/blog/go/channel%E7%BC%93%E5%86%B2%E5%8C%BA%E6%9C%AA%E6%BB%A1%EF%BC%8C%E5%8F%91%E9%80%81%E6%95%B0%E6%8D%AE.jpg" alt="buf未满，发送数据" /></p>

<p><img src="https://go-sponge.com/assets/images/blog/go/channel%E7%BC%93%E5%86%B2%E5%8C%BA%E5%86%99%E6%BB%A1%E4%BA%861.jpg" alt="buf已满 " /></p>

<p><br></p>

<p>发送流程图:</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/channel%E5%8F%91%E9%80%81%E6%B5%81%E7%A8%8B%E5%9B%BE.jpg" alt="发送流程图" /></p>

<p><br></p>

<h4 id="toc_2">1.2 从channel接收数据</h4>

<table>
<thead>
<tr>
<th align="left">环形队列buf状态</th>
<th align="left">当前goroutine状态</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">buf有数据</td>
<td align="left">有一个goroutine接收数据，不会阻塞，按环形队列索引顺序消费数据，消费一个数据，环形队列buf就空出一个位置</td>
</tr>

<tr>
<td align="left">buf为空</td>
<td align="left">有一个goroutine接收数据，goroutine阻塞等待，把该goroutine的现场保存下来，存放到发送goroutine队列<code>recvq</code>，等环形队列buf有新的数据后，从<code>recvq</code>队列(先进先出)唤醒goroutine恢复现场，消费buf的数据</td>
</tr>
</tbody>
</table>

<p><img src="https://go-sponge.com/assets/images/blog/go/channel%E6%9C%89%E8%AF%BB%E6%9C%89%E5%86%99.jpg" alt="channel有读有写" /></p>

<p><img src="https://go-sponge.com/assets/images/blog/go/channel%E7%BC%93%E5%86%B2%E5%8C%BA%E4%B8%BA%E7%A9%BA%EF%BC%8C%E6%8E%A5%E6%94%B6%E4%BC%9A%E9%98%BB%E5%A1%9E.jpg" alt="buf为空" /></p>

<p><br></p>

<p>接收流程图：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/channel%E6%8E%A5%E6%94%B6%E6%B5%81%E7%A8%8B%E5%9B%BE.jpg" alt="接收流程图" /></p>

<p><br></p>

<h4 id="toc_3">1.3 goroutine挂起与唤醒</h4>

<p>goroutine挂起时调用gopark函数，唤醒调用goready函数，一般是成对出现。</p>

<ul>
<li>channel发送挂起，⼀定是由channel接收端(或close)唤醒。</li>
<li>channel接收挂起，⼀定是由channel发送(或close)唤醒。</li>
<li>当主动closechannel时，同时唤醒channel发送和接收队列的所有goroutine。</li>
</ul>

<p><br></p>

<h4 id="toc_4">1.4 channel操作方式</h4>

<p>操作 channel 一般有如下三种方式：</p>

<ul>
<li>读 &lt;-ch</li>
<li>写 ch&lt;-</li>
<li>关闭 close(ch)</li>
</ul>

<table>
<thead>
<tr>
<th align="left">操作</th>
<th align="left">nil的channel</th>
<th align="left">正常channel</th>
<th align="left">已关闭的channel</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">读 &lt;-ch</td>
<td align="left">阻塞</td>
<td align="left">成功或阻塞</td>
<td align="left">读到零值</td>
</tr>

<tr>
<td align="left">写 ch&lt;-</td>
<td align="left">阻塞</td>
<td align="left">成功或阻塞</td>
<td align="left">panic</td>
</tr>

<tr>
<td align="left">关闭 close(ch)</td>
<td align="left">panic</td>
<td align="left">成功</td>
<td align="left">panic</td>
</tr>
</tbody>
</table>

<p><br></p>

<p>可以使用for range接收管道数据</p>

<pre><code class="language-go">for v := range ch { // 一直循环等待读取数据，直到关闭通道退出循环
    fmt.Println(v)
}
</code></pre>

<p>可以使用select接收管道数据或发送数据到管道</p>

<pre><code class="language-go">var count int
for {
    select { // 可以用select作为接发送或接收选择器
    case v, ok := &lt;-ch1: // 当关闭通道后，如果有数据也会读出来
        if !ok { // 判断通道是否已经关闭
            fmt.Println(&quot;通道已经关闭&quot;)
            return
        }
        fmt.Println(v)

    case ch2&lt;-count: // 发送数据
    }
    count++
}
</code></pre>

<p><br></p>

<h3 id="toc_5">2 channel应用</h3>

<h4 id="toc_6">2.1 信息交流</h4>

<p>channel 的底层是一个循环队列，当队列的长度大于0的时候，可以在队列中缓存数据信息，向一个 goroutine存放数据，从一个 goroutine读取数据，就像水管的两头，这样就实现了goroutine之间的消息交流。</p>

<p>示例代码：</p>

<pre><code class="language-go">// InfoExchange 读取信息
func InfoExchange(ctx context.Context, in &lt;-chan interface{}) {
    for {
        select {
        case v, ok := &lt;-in:
            if !ok {
                fmt.Println(&quot;\n信息传递结束&quot;)
                return
            }
            fmt.Printf(&quot;%v &quot;, v)

        case &lt;-ctx.Done():
            return
        }
    }
}
</code></pre>

<p><br></p>

<p>测试代码：</p>

<pre><code class="language-go">func genInfo() &lt;-chan interface{} {
    in := make(chan interface{}, 5)

    go func() {
        defer close(in)
        for i := 0; i &lt; 10; i++ {
            in &lt;- i
            time.Sleep(time.Millisecond * 500)
        }
    }()

    return in
}

func TestInfoExchange(t *testing.T) {
    in := genInfo()

    delay := time.Second * 5
    ctx, _ := context.WithTimeout(context.Background(), delay)
    go InfoExchange(ctx, in)

    &lt;-time.After(delay)
}

/*
结果：

0 1 2 3 4 5 6 7 8 9 
信息传递结束
*/
</code></pre>

<p><br></p>

<h4 id="toc_7">2.2 数据传递</h4>

<p>数据传递类似游戏“击鼓传花”。鼓响时，花（或者其它物件）从一个人手里传到下一个人，数据就类似这里的花</p>

<p>示例代码：</p>

<pre><code class="language-go">// DataTransfer 数据传递，从chan读取数据，并传递给下一个chan
func DataTransfer(id int, n int, chans []chan interface{}) {
    for {
        token := &lt;-chans[id]
        fmt.Printf(&quot;id=%d, v=%v \n&quot;, id, token)

        chans[(id+1)%n] &lt;- token
        time.Sleep(time.Second)
    }
}
</code></pre>

<p><br></p>

<p>测试代码：</p>

<pre><code class="language-go">func TestStartTask(t *testing.T) {
    n := 4
    chans := []chan interface{}{}

    for i := 0; i &lt; n; i++ {
        chans = append(chans, make(chan interface{}, 1))
    }

    for i := 0; i &lt; n; i++ {
        go DataTransfer(i, n, chans)
    }

    // 初始化数据
    chans[0] &lt;- &quot;a&quot;
    chans[1] &lt;- &quot;b&quot;
    chans[2] &lt;- &quot;c&quot;
    chans[3] &lt;- &quot;d&quot;

    &lt;-time.After(time.Second * 5)
}

/*
结果：

id=3, v=d 
id=0, v=a 
id=1, v=b 
id=2, v=c 
id=1, v=a 
id=0, v=d 
id=3, v=c 
id=2, v=b 
...
*/
</code></pre>

<p><br></p>

<h4 id="toc_8">2.3 信号通知</h4>

<p>使用非缓冲channel的特性，当channel没有数据接收时会阻塞，直到有新的数据进来或者 channel 被关闭才会退出阻塞，因此可以作为信号通知。</p>

<p>示例代码：</p>

<pre><code class="language-go">quit := make(chan os.Signal)  
signal.Notify(quit, os.Interrupt)  
&lt;-quit
</code></pre>

<p><br></p>

<h4 id="toc_9">2.4 锁</h4>

<p>示例代码：</p>

<pre><code class="language-go">// Mutex 使用chan实现互斥锁
type Mutex struct {
    ch chan struct{}
}

// NewMutex 使用锁需要初始化
func NewMutex() *Mutex {
    mu := &amp;Mutex{make(chan struct{}, 1)}
    mu.ch &lt;- struct{}{}
    return mu
}

// Lock 请求锁，直到获取到
func (m *Mutex) Lock() {
    &lt;-m.ch
}

// Unlock 解锁
func (m *Mutex) Unlock() {
    select {
    case m.ch &lt;- struct{}{}:
    default:
        panic(&quot;unlock of unlocked mutex&quot;)
    }
}

// TryLock 尝试获取锁
func (m *Mutex) TryLock() bool {
    select {
    case &lt;-m.ch:
        return true
    default:
    }
    return false
}

// LockTimeout 加入一个超时的设置
func (m *Mutex) LockTimeout(timeout time.Duration) bool {
    timer := time.NewTimer(timeout)
    select {
    case &lt;-m.ch:
        timer.Stop()
        return true
    case &lt;-timer.C:
    }
    return false
}

// IsLocked 锁是否已被持有
func (m *Mutex) IsLocked() bool {
    return len(m.ch) == 0
}
</code></pre>

<p><br></p>

<p>测试代码：</p>

<pre><code class="language-go">func TestMutex_TryLock(t *testing.T) {
    m := NewMutex()

    for i := 0; i &lt; 5; i++ {
        go func(i int) {
            if m.TryLock() {
                fmt.Printf(&quot;NO %d get lock success\n&quot;, i)
            } else {
                fmt.Printf(&quot;NO %d get lock failed\n&quot;, i)
            }
        }(i)
    }

    time.Sleep(time.Millisecond)
}

/*
结果：

NO 4 get lock success
NO 2 get lock failed
NO 3 get lock failed
NO 0 get lock failed
NO 1 get lock failed
*/

func TestMutex_Lock(t *testing.T) {
    a := -1
    m := NewMutex()

    for i := 0; i &lt; 5; i++ {
        go func(i int) {
            for {
                m.Lock()
                a = i
                time.Sleep(time.Millisecond * 100)
                fmt.Println(&quot;a =&quot;, a)
                m.Unlock()
            }

        }(i)
    }

    time.Sleep(time.Second)
}


/*
结果：

a = 4
a = 2
a = 3
a = 0
a = 1
...
*/
</code></pre>

<p><br></p>

<h4 id="toc_10">2.5 任务编排</h4>

<h5 id="toc_11">2.5.1 or-Done 模式</h5>

<p>有n 个任务，其中任意一个完成就算完成，这叫or-Done 模式，</p>

<p>使用场景：用户查询请求，同时发两次给集群服务，取最快返回，使用冗余请求增加体验。</p>

<p>示例代码：</p>

<pre><code class="language-go">// OrDone 任意一个channel完成就退出
func OrDone(channels ...&lt;-chan interface{}) &lt;-chan interface{} { // &lt;1&gt;

    switch len(channels) {
    case 0: // &lt;2&gt;
        return nil
    case 1: // &lt;3&gt;
        return channels[0]
    }

    orDone := make(chan interface{})
    go func() { // &lt;4&gt;
        defer close(orDone)

        switch len(channels) {
        case 2: // &lt;5&gt;
            select {
            case &lt;-channels[0]:
            case &lt;-channels[1]:
            }
        default: // &lt;6&gt;
            select {
            case &lt;-channels[0]:
            case &lt;-channels[1]:
            case &lt;-channels[2]:
            case &lt;-OrDone(append(channels[3:], orDone)...): // &lt;6&gt;
            }
        }
    }()
    return orDone
}
</code></pre>

<p><br></p>

<p>测试：</p>

<pre><code class="language-go">func done(after time.Duration) &lt;-chan interface{} {
    c := make(chan interface{})
    go func() {
        defer close(c)
        fmt.Println(&quot;delay:&quot;, after)
        time.Sleep(after)
    }()
    return c
}

// 随机1~10秒
func randTime() time.Duration {
    n := time.Duration(rand.Int31n(10))
    return n * time.Second
}

func TestOrDone(t *testing.T) {

    &lt;-OrDone(
        done(randTime()),
        done(randTime()),
        done(randTime()),
        done(randTime()),
        done(randTime()),
        done(randTime()),
    )
}

/*
结果：

delay: 1s
delay: 1s
delay: 8s
delay: 7s
delay: 7s
delay: 9s
*/
</code></pre>

<p><br></p>

<h5 id="toc_12">2.5.2 扇入模式</h5>

<p>多个结果组合到一个channel中的过程叫扇入模式下，输入源有多个，输出目标只有一个。</p>

<p>示例代码：</p>

<pre><code class="language-go">// FanIn 扇入，多个channel组合到一个channel
func FanIn(chans ...&lt;-chan interface{}) &lt;-chan interface{} {
    switch len(chans) {
    case 0:
        c := make(chan interface{})
        close(c)
        return c
    case 1:
        return chans[0]
    case 2:
        return mergeTwo(chans[0], chans[1])
    default:
        m := len(chans) / 2
        return mergeTwo( // 对多个数据进行合并处理
            FanIn(chans[:m]...),
            FanIn(chans[m:]...))
    }
}

func mergeTwo(a, b &lt;-chan interface{}) &lt;-chan interface{} {
    c := make(chan interface{})
    go func() {
        defer close(c)
        for a != nil || b != nil { //只要还有可读的chan
            select {
            case v, ok := &lt;-a:
                if !ok { // a 已关闭，设置为nil
                    a = nil
                    continue
                }
                c &lt;- v
            case v, ok := &lt;-b:
                if !ok { // b 已关闭，设置为nil
                    b = nil
                    continue
                }
                c &lt;- v
            }
        }
    }()
    return c
}

</code></pre>

<p><br></p>

<p>测试：</p>

<pre><code class="language-go">func done(v int) &lt;-chan interface{} {
    in := make(chan interface{})
    go func() {
        defer close(in)
        in &lt;- v
        time.Sleep(time.Millisecond * 500)
    }()
    return in
}

func TestFanIn(t *testing.T) {
    out := FanIn(
        done(1),
        done(2),
        done(3),
    )

    for v := range out {
        fmt.Println(v)
    }
}

/*
结果：

1
3
2
*/
</code></pre>

<p><br></p>

<h5 id="toc_13">2.5.3 扇出模式</h5>

<p>扇出模式（Fan-Out）只有一个输入源，但是有多个输出目标。从源 channel 取出一个数据后，依次发送给多个目标 channel。发送的时候，既可以同步，也可以异步。</p>

<p>示例代码：</p>

<pre><code class="language-go">// FanOut 扇出，只有一个输入源，但是有多个输出目标
func FanOut(ch &lt;-chan interface{}, out []chan interface{}, async bool) {
    go func() {
        defer func() { //退出时关闭所有的输出chan
            for i := 0; i &lt; len(out); i++ {
                close(out[i])
            }
        }()

        for v := range ch { // 从输入chan中读取数据
            v := v
            for i := 0; i &lt; len(out); i++ {
                i := i
                if async { //异步
                    go func() {
                        out[i] &lt;- v // 放入到输出chan中，异步方式
                    }()
                } else {
                    out[i] &lt;- v // 放入到输出chan中，同步方式
                }
            }
        }
    }()
}

</code></pre>

<p><br></p>

<p>测试：</p>

<pre><code class="language-go">func TestFanOut(t *testing.T) {
    ch := make(chan interface{})
    chLister := []chan interface{}{make(chan interface{}), make(chan interface{}), make(chan interface{})}

    FanOut(ch, chLister, false)

    for i := 0; i &lt; 5; i++ {
        ch &lt;- i
        fmt.Println(&lt;-chLister[0], &lt;-chLister[1], &lt;-chLister[2])
        time.Sleep(time.Second)
    }
}

/*
结果：

0 0 0
1 1 1
2 2 2
3 3 3
4 4 4
*/
</code></pre>

<p><br></p>

<h5 id="toc_14">2.5.4 stream</h5>

<p>stream 是把 channel 当做流式管道的方式。</p>

<pre><code class="language-go">// AsStream 将一个 slice 转成流
func AsStream(done &lt;-chan struct{}, values ...interface{}) &lt;-chan interface{} {
    s := make(chan interface{}) //创建一个unbuffered的channel
    go func() {                 // 启动一个goroutine，往s中塞数据
        defer close(s)             // 退出时关闭chan
        for _, v := range values { // 遍历数组
            select {
            case &lt;-done:
                return
            case s &lt;- v: // 将数组元素塞入到chan中
            }
        }
    }()
    return s
}

// TakeN 获取流的前n个数据
func TakeN(done &lt;-chan struct{}, valueStream &lt;-chan interface{}, n int) &lt;-chan interface{} {
    takeStream := make(chan interface{}) // 创建输出流
    go func() {
        defer close(takeStream)
        for i := 0; i &lt; n; i++ { // 只读取前num个元素
            select {
            case &lt;-done:
                return
            case takeStream &lt;- &lt;-valueStream: //从输入流中读取元素
            }
        }
    }()
    return takeStream
}
</code></pre>

<p><br></p>

<h5 id="toc_15">2.5.5 map-reduce</h5>

<p>map-reduce 是一种面向大规模数据处理的并行计算模型和方法，但是这里要介绍的是一种单机版的 map-reduce 模式。</p>

<p>map-reduce 分为两个步骤，第一步是 map，将队列中的数据用 mapFn 函数处理；第二步是 reduce，将处理后的数据用 reduceFn 函数汇总。</p>

<pre><code class="language-go">// MapChan 处理mapFn处理数据
func MapChan(in &lt;-chan interface{}, mapFn func(interface{}) interface{}) &lt;-chan interface{} {
    out := make(chan interface{}) // 创建一个输出chan
    if in == nil {                // 异常检查
        close(out)
        return out
    }

    go func() { // 启动一个goroutine,实现map的主要逻辑
        defer close(out)
        for v := range in { // 从输入chan读取数据，执行业务操作，也就是map操作
            out &lt;- mapFn(v)
        }
    }()

    return out
}

// Reduce  reduceFn函数汇总
func Reduce(in &lt;-chan interface{}, reduceFn func(r, v interface{}) interface{}) interface{} {
    if in == nil { // 异常检查
        return nil
    }

    out := &lt;-in         // 先读取第一个元素
    for v := range in { // 实现reduce的主要逻辑
        out = reduceFn(out, v)
    }

    return out
}
</code></pre>

<p><br></p>

<p>测试：</p>

<pre><code class="language-go">// 需求：将一组数据中每个数据乘以10，最后计算总和。为此，我们需要实现 mapFn (乘 10) 和 reduceFn （求和）

// 生成一个数据流
func numStream(done &lt;-chan struct{}) &lt;-chan interface{} {
    s := make(chan interface{})
    values := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    go func() {
        defer close(s)
        for _, v := range values { // 从数组生成
            select {
            case &lt;-done:
                return
            case s &lt;- v:
            }
        }
    }()
    return s
}

func TestMapReduce(t *testing.T) {
    in := numStream(nil)

    // map操作: 乘以10
    mapFn := func(v interface{}) interface{} {
        return v.(int) * 10
    }

    // reduce操作: 对map的结果进行累加
    reduceFn := func(r, v interface{}) interface{} {
        return r.(int) + v.(int)
    }

    sum := Reduce(MapChan(in, mapFn), reduceFn) //返回累加结果
    fmt.Println(sum)
}
</code></pre>

<p><br></p>

<h4 id="toc_16">2.6 worker模式</h4>

<h5 id="toc_17">2.6.1 最简单的worker处理队列方式</h5>

<pre><code class="language-go">package main

import &quot;time&quot;

type Job int

func worker(jobChan &lt;-chan Job) {
    for job := range jobChan {
        // 顺序执行，缺点：如果处理过程中有等待或阻塞，会影响整个队列
        Process(job)

        // 并发执行，如果处理过程中有等待或阻塞，不会影响其他的job，
        // 缺点：并发处理job的goroutine数量不可控，每来一个新job就会启动一个goroutine，不建议这样处理。
        // 通常做法是开启有限个worker的goroutine来并行处理队列的job，而不是在process里并发执行。
        //go Process(job)
    }
}

func Process(job Job) {
    if job == 3 { // 等于3的这个job阻塞1s
        time.Sleep(time.Second)
    }
    println(&quot;job&quot;, job)
}

func main() {
    // make a channel with a capacity of 10.
    jobChan := make(chan Job, 10)

    // start the worker
    go worker(jobChan)

    // enqueue a job
    for i := 0; i &lt; 20; i++ {
        jobChan &lt;- Job(i)
    }

    time.Sleep(2 * time.Second)
}

/* 当次执行的结果如下：

(1) 顺序执行，在处理job 3时阻塞了一秒，其他job要等job 3处理完毕后再处理往下执行。
job 0
job 1
job 2
job 3
job 4
job 5
job 6
job 7
job 8
job 9
job 10
job 11
job 12
job 13
job 14
job 15
job 16
job 17
job 18
job 19

------------------------------------------------------

(2) 并发process执行，在处理job 3时阻塞了一秒，不影响其他的job处理。
job 2
job 10
job 0
job 6
job 4
job 5
job 9
job 7
job 1
job 8
job 13
job 11
job 12
job 19
job 17
job 18
job 14
job 15
job 16
job 3
*/
</code></pre>

<p><br></p>

<h5 id="toc_18">2.6.2 使用worker池处理队列</h5>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

type Job int

func worker(i int, jobChan &lt;-chan Job) {
    for job := range jobChan {
        Process(i, job)
    }
}

func Process(i int, job Job) {
    if job == 3 {
        time.Sleep(time.Second)
    }
    fmt.Printf(&quot;worker %2d process job %d\n&quot;, i, job)
}

func workPool(workerSize int, jobChan chan Job) {
    for i := 0; i &lt; workerSize; i++ {
        go func(i int) {
            worker(i, jobChan)
        }(i)
    }
}

func main() {
    jobChan := make(chan Job, 10)

    // 启动多个worker池并发处理队列的job，多个worker去抢队列job来处理，只有空闲的worker才能从队列中获取job
    workPool(5, jobChan)

    for i := 0; i &lt; 20; i++ {
        jobChan &lt;- Job(i)
    }

    time.Sleep(2 * time.Second)
}

/*当次执行结果如下：

可以看到worker 2处理job 3时阻塞一秒，worker 2在阻塞过程中没有去抢占队列新的job来处理。
worker  4 process job 0
worker  3 process job 4
worker  0 process job 2
worker  3 process job 6
worker  3 process job 8
worker  3 process job 9
worker  3 process job 10
worker  3 process job 11
worker  3 process job 12
worker  3 process job 13
worker  3 process job 14
worker  3 process job 15
worker  3 process job 16
worker  3 process job 17
worker  3 process job 18
worker  1 process job 1
worker  4 process job 5
worker  0 process job 7
worker  3 process job 19
worker  2 process job 3
*/

</code></pre>

<p><br></p>

<h5 id="toc_19">2.6.3 等待worker处理所有队列的job</h5>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;sync&quot;
    &quot;time&quot;
)

type Job int

func worker(n int, jobChan &lt;-chan Job, wg *sync.WaitGroup) {
    for job := range jobChan {
        Process(n, job, wg)
    }
}

func Process(n int, job Job, wg *sync.WaitGroup) {
    defer wg.Done()

    // 加上随机延时，方便查看打印效果
    size := randTime()
    time.Sleep(size * time.Millisecond)

    fmt.Printf(&quot;worker %2d process job %d, time %dms\n&quot;, n, job, size)
}

// 随机时间100~500
func randTime() time.Duration {
    rand.Seed(time.Now().UnixNano())
    return time.Duration(rand.Intn(400) + 100)
}

func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
    ch := make(chan struct{})

    go func() {
        wg.Wait()
        close(ch)
    }()

    select {
    case &lt;-ch:
        return true
    case &lt;-time.After(timeout):
        return false
    }
}

func workPool(workerNum int, jobChan chan Job, wg *sync.WaitGroup) {
    for i := 0; i &lt; workerNum; i++ {
        go func(i int) {
            worker(i, jobChan, wg)
        }(i)
    }
}

func main() {
    wg := &amp;sync.WaitGroup{}
    jobChan := make(chan Job, 10)

    workPool(5, jobChan, wg)

    for i := 0; i &lt; 20; i++ {
        wg.Add(1)
        jobChan &lt;- Job(i)
    }

    t := time.Now()

    // 等待所有job处理完毕才退出，不足：不管是顺序执行还是并发process方式处理job，如果其中有一个job阻塞了，会一直等待下去
    wg.Wait()

    // 带超时等待，如果超时了，直接忽略等待
    //ok := WaitTimeout(wg, 300*time.Millisecond)
    //if !ok {
    //  fmt.Printf(&quot;\n warning, process job timeout \n&quot;)
    //}

    fmt.Printf(&quot;\n handle queue time %v\n&quot;, time.Now().Sub(t))
}

/* 当次执行结果如下：

(1) 不带超时的wait
worker  2 process job 2, time 147ms
worker  0 process job 0, time 147ms
worker  1 process job 1, time 147ms
worker  3 process job 4, time 147ms
worker  4 process job 3, time 147ms
worker  1 process job 7, time 282ms
worker  4 process job 9, time 282ms
worker  0 process job 6, time 282ms
worker  3 process job 8, time 282ms
worker  2 process job 5, time 399ms
worker  1 process job 10, time 149ms
worker  0 process job 12, time 149ms
worker  3 process job 13, time 149ms
worker  4 process job 11, time 149ms
worker  2 process job 14, time 145ms
worker  0 process job 16, time 265ms
worker  1 process job 15, time 265ms
worker  3 process job 17, time 265ms
worker  4 process job 18, time 265ms
worker  2 process job 19, time 234ms

 handle queue time 786.8609ms

------------------------------------------------

(2) 有超时的wait
worker  3 process job 3, time 257ms
worker  1 process job 1, time 272ms
worker  0 process job 0, time 272ms
worker  4 process job 4, time 272ms
worker  2 process job 2, time 272ms
worker  4 process job 8, time 102ms
worker  3 process job 5, time 224ms

 warning, process job timeout

 handle queue time 300.3939ms
*/
</code></pre>

<p><br></p>

<h5 id="toc_20">2.6.4 使用context或channel停止worker</h5>

<p>有两种方式停止处理队列中的worker，分别是context和channel，多级函数传递或复杂点的控制建议使用context。</p>

<p><strong>(1) 使用context停止worker</strong></p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;

    &quot;golang.org/x/net/context&quot;
)

type Job int

func workPool(workerNum int, jobChan chan Job, ctx context.Context) {
    for i := 0; i &lt; workerNum; i++ {
        go func(i int) {
            worker(i, jobChan, ctx)
        }(i)
    }
}

// 通过context取消未完成的job
func worker(n int, jobChan &lt;-chan Job, ctx context.Context) {
    for {
        select {
        case job := &lt;-jobChan:
            Process(n, job)

        case &lt;-ctx.Done():
            fmt.Printf(&quot;cancel worker %d\n&quot;, n)
            return
        }
    }
}

func Process(n int, job Job) {
    size := randTime()
    time.Sleep(size * time.Millisecond)
    fmt.Printf(&quot;worker %2d process job %2d, time %dms\n&quot;, n, job, size)
}

// 随机时间100~500
func randTime() time.Duration {
    rand.Seed(time.Now().UnixNano())
    return time.Duration(rand.Intn(400) + 100)
}

func main() {
    jobChan := make(chan Job, 10)

    // 使用context控制worker是否停止，适合多级函数传递和控制，并且有超时取消
    ctx, cancel := context.WithCancel(context.Background())
    //ctx, cancel := context.WithTimeout(context.Background(), time.Second) // 可以设置延时的context
    workPool(5, jobChan, ctx)
    for i := 0; i &lt; 20; i++ {
        jobChan &lt;- Job(i)
    }

    cancel()
    time.Sleep(5 * time.Second)
}

/* 当次执行结果如下：
worker  2 process job  3, time 471ms
worker  3 process job  4, time 471ms
worker  1 process job  1, time 471ms
worker  4 process job  0, time 471ms
worker  0 process job  2, time 471ms
worker  3 process job  6, time 200ms
cancel worker 3
worker  4 process job  8, time 200ms
worker  2 process job  5, time 200ms
cancel worker 2
worker  0 process job  9, time 200ms
cancel worker 0 by context
worker  1 process job  7, time 200ms
cancel worker 1
worker  4 process job 10, time 310ms
cancel worker 4
*/
</code></pre>

<p><br></p>

<p><strong>(2)使用channel停止worker</strong></p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;
)

type Job int

func workPool(workerNum int, jobChan chan Job, ch chan struct{}) {
    for i := 0; i &lt; workerNum; i++ {
        go func(i int) {
            worker(i, jobChan, ch)
        }(i)
    }
}

// 通过channel取消未完成的job
func worker(n int, jobChan &lt;-chan Job, ch chan struct{}) {
    for {
        select {
        case job := &lt;-jobChan:
            Process(n, job)

        case &lt;-ch:
            fmt.Printf(&quot;cancel worker %d\n&quot;, n)
            return
        }
    }
}

func Process(n int, job Job) {
    size := randTime()
    time.Sleep(size * time.Millisecond)
    fmt.Printf(&quot;worker %2d process job %2d, time %dms\n&quot;, n, job, size)
}

// 随机时间100~500
func randTime() time.Duration {
    rand.Seed(time.Now().UnixNano())
    return time.Duration(rand.Intn(400) + 100)
}

func main() {
    jobChan := make(chan Job, 10)

    // 使用channel控制worker是否停止
    ch := make(chan struct{})
    workPool(5, jobChan, ch)
    for i := 0; i &lt; 20; i++ {
        jobChan &lt;- Job(i)
    }

    close(ch)
    time.Sleep(5 * time.Second)

}

/* 当次执行结果如下：
worker  1 process job  1, time 313ms
worker  3 process job  4, time 313ms
worker  2 process job  3, time 313ms
worker  0 process job  2, time 313ms
worker  4 process job  0, time 313ms
worker  3 process job  6, time 258ms
cancel worker 3
worker  2 process job  7, time 258ms
worker  4 process job  9, time 258ms
cancel worker 4
worker  0 process job  8, time 258ms
worker  1 process job  5, time 258ms
cancel worker 1
worker  0 process job 11, time 190ms
cancel worker 0
worker  2 process job 10, time 226ms
worker  2 process job 12, time 485ms
cancel worker 2
*/
</code></pre>

<p><br></p>

<h4 id="toc_21">2.7 关闭事件跟踪器</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;time&quot;
)

// Tracker 跟踪器  
type Tracker struct {  
   ch   chan string  
   stop chan struct{}  
}  
  
// NewTracker 实例化 func NewTracker() *Tracker {  
   return &amp;Tracker{  
      ch:   make(chan string, 20),  
      stop: make(chan struct{}),  
   }  
}  
  
// Event 触发事件  
func (t *Tracker) Event(ctx context.Context, data string) error {  
   select {  
   case t.ch &lt;- data:  
      return nil  
   case &lt;-ctx.Done():  
      return ctx.Err()  
   }  
}  
  
// Run 执行  
func (t *Tracker) Run() {  
   for data := range t.ch {  
      fmt.Println(data)  
      time.Sleep(time.Second * 10)  
   }  
   t.stop &lt;- struct{}{}  
}  
  
// Shutdown 关闭  
func (t *Tracker) Shutdown(ctx context.Context) {  
   close(t.ch)  
   select {  
   case &lt;-t.stop:  
      fmt.Println(&quot;正常结束&quot;)  
   case &lt;-ctx.Done():  
      fmt.Println(&quot;超时结束&quot;)  
   }  
}

func main() {  
   ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*2))  
   defer cancel() 
    
   tr := NewTracker()  
   go tr.Run()  
  
   tr.Event(ctx, &quot;data1&quot;)  
   tr.Event(ctx, &quot;data2&quot;)  
   tr.Event(ctx, &quot;data3&quot;)  
  
   tr.Shutdown(ctx)

/*
执行结果：

data1
超时结束
*/
}
</code></pre>

<p><br></p>

<h3 id="toc_22">3 注意事项</h3>

<p>未初始化的channel，读取里面的数据时，会造成死锁deadlock</p>

<pre><code class="language-go">var ch chan int
&lt;-ch  // 未初始化channel读数据会死锁`
</code></pre>

<p><br></p>

<p>未初始化的channel，往里面写数据时，会造成死锁deadlock</p>

<pre><code class="language-go">var ch chan int
ch&lt;-  // 未初始化channel写数据会死锁
</code></pre>

<p><br></p>

<p>未初始化的channel，关闭该channel时，会panic</p>

<pre><code class="language-go">var ch chan int
close(ch) // 关闭未初始化channel，触发panic
</code></pre>

<p><br></p>

<p>向已关闭的channel写数据，会pannic</p>

<pre><code class="language-go">var ch =make(chan int)
close(ch) 
ch&lt;-1 // channel已关闭，触发panic
</code></pre>
<p>本文链接：<a href="https://zhuyasen.com/post/channel.html">https://zhuyasen.com/post/channel.html</a>，<a href="https://zhuyasen.com/post/channel.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>go runtime</title>
            <link>https://zhuyasen.com/post/runtime.html</link>
            <comments>https://zhuyasen.com/post/runtime.html#comments</comments>
            <guid>https://zhuyasen.com/post/runtime.html</guid>
            <description>
                <![CDATA[<blockquote>

<p>#runtime #GMP #goroutine</p>

<p>go语言组成有两部分，一部分是用户程序代码，一部分是runtime，runtime作用是为了实现额外功能，在程序运行时自动加载/运行的的一些模块，runtime由4部分组成：</p>

<ul>
<li>Scheduler: 调度器管理所有的GMP，在后台执行调度循环。</li>
<li>Memory Management: 当代码需要内存时，负责内存分配工作。</li>
<li>Garbage Collector: 当内存不再需要时，负责回收内存。</li>
<li>Netpoll: 网络轮询负责管理网络FD相关的读写、就绪事件。</li>
</ul>

<p><br></p>

<h3 id="toc_0">调度器 Scheduler</h3>

<h4 id="toc_1">协程调度器GMP</h4>

<p>调度器本质是一个生产-消费流程，用户在程序中执行<code>go func{}</code>生成一个协程实体，提交到协程调度器，线程来执行(消费)。</p>

<ul>
<li>G: goroutine，一个计算任务。由需要执行的代码和其上下文组成，上下文包括：当前代码位置，栈顶、栈底地址，状态等。</li>
<li>M: machine，系统线程，执行实体，想要在CPU上执行代码，必须有线程，与C语⾔中的线程相同，通过系统调⽤clone来创建。</li>
<li>P: processor，虚拟处理器，M必须获得P才能执行代码，否则必须陷入休眠(后台监控线程除外)，你也可以将其理解为⼀种token ，有这个token，才有在物理CPU核心上执行的权力。</li>
</ul>

<p><br></p>

<p>协程框架图</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/GMP%E7%9A%84%E5%8D%8F%E7%A8%8B%E4%BA%A7%E7%94%9F.jpg" alt="协程产生框架图" /></p>

<p>上图左边是表示协程生产过程，包括虚拟处理器部分P和队列部分，每个P下面有runnext和local run quene，而global run quene是全局链表，所有P都可以共享，G执行优先级别最高是runnext，其次是本地队列，最后是全局队列。</p>

<ul>
<li>runnext: 下一个执行的G，类型是一个值。</li>
<li>local run quene：每个P自己队列，类型是数组，最大长度为256。</li>
<li>global run quene，全局队列，类型是链表，长度没有限制。</li>
</ul>

<pre><code>为什么队列要分为本地队列和全局队列？

为了在性能上达到更好目标，每个P执行自己的本地队列，不需要枷锁，而不同P之间频繁从全局队列获取G时要加锁的，队列分级就是避免频繁加锁，提高并发性能。


为什么最新创建的协程会被放到runnext去优先执行？

在计算机执行过程中，程序分为代码的局部性和数据的局部性，根据局部性原理，最近调用的代码，很大概率需要再一次调用，优先级更高，程序执行到当前时刻，变量和数据很大概率在当前CPU访问的cache里，因此访问效率也是最高的。刚刚创建的G很大概率是高优先级的G，因此放到runnext去优先执行。
</code></pre>

<p><br></p>

<p>上图的右边是协程的消费端，包括系统线程部分，工作的线程绑定P后一直调度循环，线程是按需创建的，空闲的线程在队列里，需要时再拿出来。</p>

<p><br></p>

<h4 id="toc_2">协程生产端</h4>

<p>新创建的协程加入队列的流程图</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%8D%8F%E7%A8%8B%E5%8A%A0%E5%85%A5%E9%98%9F%E5%88%97%E6%B5%81%E7%A8%8B.jpg" alt="协程加入队列的流程" /></p>

<p>使用go func()函数，通过newproc打包生产一个G，newproc里面做了申请栈、判断当前runnext、本地队列、全局队列是否需要对已存在的G进行转移，有三种情况：</p>

<ul>
<li>第一种情况：runnext为空，新创建的G直接放到runnext去执行。</li>
<li>第二种情况：runnext为不空，本地队列未满(最大256)，把runnext旧的G转移到本地队列，新创建的G放到runnext去执行。</li>
<li>第三种情况：runnext为不空，本地队列已满(最大256)，把runnext旧的G和和本地队列的一半G放到全局队列(全局队列时链表，理论是无限大)，新创建的G放到runnext去执行。</li>
</ul>

<p><br></p>

<h4 id="toc_3">协程消费端</h4>

<p>协程消费端框架图</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%8D%8F%E7%A8%8B%E6%B6%88%E8%B4%B9%E7%AB%AF.jpg" alt="协程消费流程" /></p>

<p><br></p>

<p>消费过程：</p>

<p>每循环调度一次schedtick值加1，每轮询60次本地队列，就去全局队列获取，目的是让全局队列的G也有机会被执行。</p>

<h5 id="toc_4">schedtick对60取模等于0的消费过程</h5>

<p>全局队列不为空，本地队列不为空情况，从全局队列获取一个G来执行。</p>

<p><br></p>

<h5 id="toc_5">schedtick对60取模不为0的消费过程</h5>

<ul>
<li>当runnext有G，直接从runnext获取G执行。</li>
<li>当runnext为空，本地队列有G，从本地队列中获取G执行</li>
<li>当runnext为空，本地队列为空，全局队列有G，从全局队列获一批G来执行，从全局队列获取G数量规则是从全局队列获取一半，如果数量超过128，最大值取128，获取的是全局队列的尾部。</li>
<li>当runnext为空，本地队列为空，全局队列为空，查询其他线程的本地队列是否有G，如果其他P的本地队列有G，就从其他P的本地队列偷取一半(后半部分)到本地队列执行，如果其他P的本地队列也为空，则挣扎一下再查询一遍，如果全局和其他P都为空，然后进入休眠状态。</li>
</ul>

<p><br></p>

<h4 id="toc_6">阻塞</h4>

<p>上面的goroutine都是正常执行，当goroutine出现阻塞怎么处理呢。有些阻塞可以被runtime拦截，有些阻塞不能被runtime拦截。</p>

<p><br></p>

<h5 id="toc_7">runtime可以拦截的阻塞</h5>

<p>常见会出现阻塞场景</p>

<p>(1) 调用time.Sleep函数</p>

<p>(2) 一直往channel写数据，另一端没来得及读取channel</p>

<p>(3) 一直读取channel数据，另一端没来得及写数据到channel</p>

<p>(4) 使用select，如果都没有出发channel，会阻塞</p>

<pre><code class="language-go">selct {
    case &lt;-c1:
        fmt.Println(&quot;c1 read&quot;)
    case &lt;-c2:
        fmt.Println(&quot;c2 read&quot;)    
}
</code></pre>

<p>(5) 锁，当资源被锁了，还没释放，另一个goroutine获取不到锁，出现阻塞</p>

<p>(6) 网络读写</p>

<pre><code class="language-go">var conn net.Conn
var buf = make([]byte, 1024)

// 读，没数据时阻塞
conn.Read(buf)

// 写，缓冲满时阻塞
conn.Write(buf)
</code></pre>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E9%98%BB%E5%A1%9E%E5%86%85%E9%83%A8%E7%BB%93%E6%9E%84.jpg" alt="阻塞内部结构" /></p>

<p><br></p>

<h5 id="toc_8">runtime 不能拦截的阻塞</h5>

<p>有些阻塞runtime不能被捕获到，例如cgo、系统调用，执行c代码或系统调用时，如果长时间运行需要剥离P执行，单独占用⼀个线程。</p>

<p><img src="pictures/在协程系统调用.gif" alt="在协程系统调用" /></p>

<p><br></p>

<h5 id="toc_9">阻塞处理</h5>

<p>如果一个8核处理器的8个线程同时都执行系统调用，而且都阻塞了，怎么办？</p>

<p>需要一个专有线程sysmon(system monitor)专门处理这个问题，sysmon线程拥有优高先级，而且不需要绑定P就可以执行。</p>

<p>sysmon主要功能有三个：</p>

<ul>
<li>checkdead: 检查所有线程是否都已经被阻塞了，如果是，则抛出异常，如果只是网络服务，这个检测不起作用，因为accept是正常运行的，不要被字面意思误解为可以检查死锁。</li>
<li>netpoll: 将g列表注入全局运行队列。</li>
<li>retake: 如果是syscall卡了很久，那就把p剥离(handoffp)，如果是用户g运行很久了(10ms)，那么发信号SIGURG抢占。</li>
</ul>

<p><br><br></p>

<h3 id="toc_10">内存管理 Memory Management</h3>

<p>内存管理的三个角色</p>

<table>
<thead>
<tr>
<th align="left">角色</th>
<th align="left">说明</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">Mutator</td>
<td align="left">fancy(花哨的) word for application ，其实就是你写的应用程序，它会不断地修改对象的引用关系，即对象图。</td>
</tr>

<tr>
<td align="left">Allocator</td>
<td align="left">内存分配器，负责管理从操作系统中分配出的内存空间，malloc  其实底层就有⼀个内存分配器的实现(glibc中)，tcmalloc是malloc多线程改进版。 Go中的实现类似tcmalloc 。</td>
</tr>

<tr>
<td align="left">Collector</td>
<td align="left">垃圾收集器，负责清理死对象，释放内存空间。</td>
</tr>
</tbody>
</table>

<p><br></p>

<p>内存管理概览</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%86%85%E9%83%A8%E7%BB%93%E6%9E%84%E5%9B%BE.jpg" alt="内存内部结构图" /></p>

<p><br></p>

<p>内存管理抽象</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E6%8A%BD%E8%B1%A1.jpg" alt="内存管理抽象" /></p>

<p><br></p>

<p>进程对应虚拟内存布局</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83.jpg" alt="进程对应虚拟内存布局" /></p>

<p><br></p>

<h4 id="toc_11">内存分配器类型</h4>

<p><strong>(1) 线性分配器(Bump/Sequential Allocator)</strong></p>

<p>Bump Sequential不会复用已经释放的内存，产生比较多内存碎片，基本不使用，Sequential Allocator可以复用已经释放内存，但是要额外维护一个free链表。</p>

<p><br></p>

<p><strong>(2) 空闲链表分配器(Free List Allocator)</strong></p>

<p>空闲链表分配器有first-fit、next-fit、best-fit、segregate-fit几种，go使用的是segregate-fit，减少内存碎片。</p>

<p><br></p>

<h4 id="toc_12">go语言内存分配</h4>

<p>执行malloc时</p>

<ul>
<li>分配内存小于128kb，brk只能通过调整  program break 位置推动堆增⻓</li>
<li>分配内存大于128kbmmap  可以从任意未分配位置映射内存</li>
</ul>

<p><br></p>

<p>内存分配器在  Go  语⾔中维护了⼀个多级结构：mcache &ndash;&gt; mcentral &ndash;&gt; mheap</p>

<table>
<thead>
<tr>
<th align="left">类型</th>
<th align="left">说明</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">mcache</td>
<td align="left">与P绑定，本地内存分配操作，不需要加锁。</td>
</tr>

<tr>
<td align="left">mcentral</td>
<td align="left">中⼼分配缓存，分配时需要上锁，不同spanClass使⽤不同的锁</td>
</tr>

<tr>
<td align="left">mheap</td>
<td align="left">全局唯⼀，从OS申请内存，并修改其内存定义结构时，需要加锁，是个全局锁。</td>
</tr>
</tbody>
</table>

<p>go的内存分类，预先分配好内存。</p>

<pre><code>// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align  
//     1          8        8192     1024           0     87.50%          8  
//     2         16        8192      512           0     43.75%         16  
//     3         24        8192      341           8     29.24%          8  
//     4         32        8192      256           0     21.88%         32  
//     5         48        8192      170          32     31.52%         16  
//     6         64        8192      128           0     23.44%         64  
//     7         80        8192      102          32     19.07%         16  
//     8         96        8192       85          32     15.95%         32  

...

//    66      28672       57344        2           0      4.91%       4096  
//    67      32768       32768        1           0     12.50%       8192
</code></pre>

<p><br></p>

<p>堆内存管理有Tiny alloc、Small alloc、Large alloc几种方式</p>

<ul>
<li>Tiny alloc分配内存</li>
</ul>

<p><br></p>

<ul>
<li>Small alloc分配内存</li>
</ul>

<p><img src="https://go-sponge.com/assets/images/blog/go/small_alloc.jpg" alt="Small alloc分配内存" /></p>

<p><br></p>

<ul>
<li>Large alloc分配内存</li>
</ul>

<p>⼤对象分配会直接越过mcache 、 mcentral ，直接从mheap进⾏相应数量的page分配，pageAlloc  结构经过多个版本的变化，从： freelist -&gt; treap -&gt; radix tree ，查找时间复杂度越来越低，结构越来越复杂。</p>

<p><br></p>

<p>Refill  流程：</p>

<ul>
<li>本地  mcache  没有时触发 (mcache.refill)</li>
<li>从  mcentral  ⾥的  non-empty  链表中找 (mcentral.cacheSpan)</li>
<li>尝试  sweep mcentral  的  empty ， insert sweeped -&gt; non-empty(mcentral.cacheSpan)</li>
<li>增⻓  mcentral ，尝试从  arena  获取内存 (mcentral.grow)</li>
<li>arena  如果还是没有，向操作系统申请 (mheap.alloc)</li>
</ul>

<p>最终还是会将申请到的mspan放在mcache中，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/mcache_refill%E8%BF%87%E7%A8%8B.jpg" alt="refill过程" /></p>

<p><br></p>

<p>mspan内部结构</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/mspan%E5%86%85%E9%83%A8%E7%BB%93%E6%9E%84.jpg" alt="mspan内部结构" /></p>

<p><br></p>

<p>go的变量分配在栈和堆是由编译器自动分配的，编译器如果能在编译期间确定变量的生命周期，就会在栈上分配，否则就是逃逸行为，需要在堆上分配内存。分配效率栈大于堆，空间大小堆大于栈。</p>

<p>常见变量逃逸场景：</p>

<ul>
<li>函数返回内部变量的指针</li>
<li>发送指针或带有指针的值到 channel 中</li>
<li>在一个切片上存储指针或带指针的值</li>
<li>slice 的背后数组被重新分配了，因为 append 时可能会超出其容量（cap）</li>
<li>在 interface 类型上调用方法</li>
<li>申请内存容量过大</li>
</ul>

<p>编译过程进行逃逸分析命令：</p>

<pre><code class="language-bash"># 示例
go build -gcflags=&quot;-m&quot; main.go

# 参数-m越多，打印信息越详细
go build -gcflags=&quot;-m -m&quot; main.go
</code></pre>

<p><br><br></p>

<h3 id="toc_13">垃圾回收 Garbage Collector</h3>

<p>内存垃圾类型分为语义垃圾和语法垃圾两种。</p>

<p><strong>语义垃圾</strong>(semantic garbage)，有的被称作内存泄露，语义垃圾指的是从语法上可达 ( 可以通过局部、全局变量引⽤得到 ) 的对象，但从语义上来讲他们是垃圾，垃圾回收器对此⽆能为⼒。</p>

<pre><code class="language-go">type a sturct {
    
}

s:=make([]*a, 10,10)
s=s[:5]

// 后面5个在堆上的内存语义上是应该回收，实际是一直占用内存的
</code></pre>

<p><br></p>

<p><strong>语法垃圾</strong>(syntactic garbage)，那些从语法上⽆法到达的对象，这些才是垃圾收集器主要的收集⽬标。</p>

<pre><code class="language-go">func fHeap()  {  
   s := make([]int, 10240)  
   fmt.Println(s)  
}

// 执行完函数，变量s内存会被回收
</code></pre>

<p><br></p>

<p>垃圾回收算法：</p>

<ul>
<li>引用计数 (Reference Counting) ：某个对象的根引用计数变为0时，其所有子节点均需被回收。</li>
<li>标记压缩 (Mark-Compact) ：将存活对象移动到⼀起，解决内存碎片问题。</li>
<li>复制算法 (Copying) ：将所有正在使⽤的对象从From复制到To空间，堆利用率只有⼀半。</li>
<li>标记清扫 (Mark-Sweep) ：解决不了内存碎片问题。需要与能尽量避免内存碎片的分配器使用，如tcmalloc，go使用的垃圾回收算法。</li>
</ul>

<p><br></p>

<p>触发gc条件：</p>

<ul>
<li>人工runtime.GC</li>
<li>需要分配内存时runtime.mallocgc</li>
<li>强制gc forcegchelper</li>
</ul>

<p><br></p>

<p>三色抽象:</p>

<table>
<thead>
<tr>
<th align="left">颜色</th>
<th align="left">说明</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">⿊</td>
<td align="left">已经扫描完毕，⼦节点扫描完毕，(gcmarkbits = 1，且在队列外)</td>
</tr>

<tr>
<td align="left">灰</td>
<td align="left">已经扫描完毕，⼦节点未扫描完毕。(gcmarkbits = 1，在队列内)</td>
</tr>

<tr>
<td align="left">⽩</td>
<td align="left">未扫描，collector不知道任何相关信息，标记结束后被回收的对象</td>
</tr>
</tbody>
</table>

<p><br></p>

<p>具体过程看 <a href="https://www.kancloud.cn/aceld/golang/1958308#GC_376" rel="nofollow">https://www.kancloud.cn/aceld/golang/1958308#GC_376</a></p>

<p><br></p>

<h4 id="toc_14">具体过程</h4>

<p>垃圾回收以<code>STW</code>作为界限可以分为5个阶段：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/gc%E8%BF%87%E7%A8%8B.jpg" alt="gc基本流程" /></p>

<p><br></p>

<table>
<thead>
<tr>
<th align="left">阶段</th>
<th align="left">说明</th>
<th align="left">赋值器状态</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">GCoff</td>
<td align="left">内存归还阶段，将内存依照策略归还给操作系统，写屏障关闭</td>
<td align="left">并发</td>
</tr>

<tr>
<td align="left">SweepTermination</td>
<td align="left">清扫终止阶段，为下一个阶段的并发标记做准备工作，启动写屏障</td>
<td align="left">STW</td>
</tr>

<tr>
<td align="left">Mark</td>
<td align="left">扫描标记阶段，与赋值器并发执行，写屏障开启</td>
<td align="left">并发</td>
</tr>

<tr>
<td align="left">MarkTermination</td>
<td align="left">标记终止阶段，保证一个周期内标记任务完成，停止写屏障</td>
<td align="left">STW</td>
</tr>

<tr>
<td align="left">GCoff</td>
<td align="left">内存清扫阶段，将需要回收的内存暂存，写屏障关闭</td>
<td align="left">并发</td>
</tr>
</tbody>
</table>

<p>写屏障是一个在并发垃圾回收器中才会出现的概念，垃圾回收器的正确性体现在：<strong>不应出现对象的丢失，也不应错误的回收还不需要回收的对象。</strong></p>

<p><br></p>

<p><strong>(1) 标记设置</strong></p>

<p>收集开始时，必须执行的第一个活动是打开写入屏障。写屏障的目的是允许收集器在收集期间保持堆上的数据完整性，因为收集器和应用程序 goroutine 将同时运行。为了打开 Write Barrier，必须停止运行的每个应用程序 goroutine，这个过程时间非常快，平均在10~30微秒内。</p>

<p><br></p>

<p><strong>(2) 标记</strong></p>

<p>一旦打开写屏障，收集器就会开始标记阶段，收集器做的第一件事就是为自己至少占用25%的可用CPU容量(如果有4个线程，一个用于执行GC)，这个阶段用户gc的goroutine和普通goroutine是并发执行的。如果收集内存对象速度赶不上新内存分配速度，收集器把原来执行应用程序 goroutine用来协助标记工作，这称为标记辅助。任何应用程序 Goroutine 被放置在 Mark Assist 中的时间量与它添加到堆内存中的数据量成正比，Mark Assist的作用是助于更快地完成收集。</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E6%94%B6%E9%9B%86%E5%86%85%E5%AD%98%E5%AF%B9%E8%B1%A1.jpg" alt="收集内存对象" /></p>

<p><br></p>

<p>并发标记，默认所有对象都是白色，使用三色标记法，优先扫描各个goroutine的栈对象，从根节点开始遍历所有对象，将可达的对象标记为黑色，再扫描标记堆对象。</p>

<p>并发扫描标记期间，其他goroutine在栈和堆有可能出现新建对象、对象引用指向变更等场景，有些场景会触发写屏障，写屏障只发生在堆的对象，栈对象的引用改变不会引起屏障触发，因为go是并发运行的，大部分的操作都发生在栈上，成千上万goroutine的栈都进行屏障保护会有性能问题。</p>

<p><strong>场景1：并发扫描标记期间其他goroutine在栈或堆上创建的新对象</strong></p>

<p>这些新建对象统一标记为黑色。</p>

<p><br></p>

<p><strong>场景2：并发扫描标记期间，一个栈对象(编号1)引用一个堆对象(编号7)</strong></p>

<p>因为对象1是在栈区，不启动写屏障，对象1标记为黑色，后面对象7被扫描到时标记为黑色。</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E6%A0%88%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E5%A0%86%E5%AF%B9%E8%B1%A1.jpg" alt="栈对象引用堆对象" /></p>

<p><br></p>

<p><strong>场景3：并发扫描标记期间，一个新建的栈对象(编号9)引用一个栈对象(编号3)，同时原来一个栈对象(编号2)删除引用对象(编号3)</strong></p>

<p>因为对象都是在栈区，不会触发写屏障，对象9标记为黑色，后面扫描到对象3时标记为黑色。</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E6%A0%88%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E6%A0%88%E5%AF%B9%E8%B1%A1.jpg" alt="栈对象引用栈对象" /></p>

<p><br></p>

<p><strong>场景4：并发扫描标记期间，一个堆对象(编号10)引用一个堆对象(编号7)</strong></p>

<p>因为是在堆区，会触发写屏障，对象10为黑色，此时对象7标记为灰色，下游对象6被保护，后面扫描到对象7和对象6会时会标记为黑色。</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%A0%86%E5%AF%B9%E8%B1%A1%E6%B7%BB%E5%8A%A0%E5%BC%95%E7%94%A8%E5%A0%86%E5%AF%B9%E8%B1%A1.jpg" alt="堆对象添加引用堆对象" /></p>

<p><br></p>

<p><strong>场景5：并发扫描标记期间，一个堆对象(编号4)删除引用堆对象(编号7)</strong></p>

<p>因为对象4是在堆区，会触发写屏障，此时对象7标记为灰色，最终标记为黑色。</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%A0%86%E5%AF%B9%E8%B1%A1%E5%88%A0%E9%99%A4%E5%BC%95%E7%94%A8%E5%A0%86%E5%AF%B9%E8%B1%A1.jpg" alt="堆对象删除引用堆对象" /></p>

<p><br></p>

<p>混合写屏障规则：</p>

<ul>
<li>GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描，无需STW)。</li>
<li>GC期间，任何在栈上创建的新对象，均为黑色。</li>
<li>被删除的对象标记为灰色。</li>
<li>被引用的对象标记为灰色。</li>
</ul>

<p>使用变形的<strong>弱三色不变式</strong></p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%BC%B1%E4%B8%89%E8%89%B2%E4%B8%8D%E5%8F%98%E5%BC%8F.jpg" alt="弱三色不变式" /></p>

<p><br></p>

<p><strong>(3) 标记终止</strong></p>

<p>标记工作完成后，下一阶段是标记终止。这阶段关闭Write Barrier，执行各种清理任务，计算下一个收集目标的时时间。在标记阶段发现自己处于紧密循环中的 Goroutines 也可能导致标记终止 STW 延迟延长，这个过程时间非常快，平均在60~90微秒内。</p>

<p><br></p>

<p><strong>(4) 扫除</strong></p>

<p>收集完成后会发生另一个活动叫扫除(sweeping)，扫除是指与堆内存中未标记为正在使用的值关联的内存被回收。当应用程序 Goroutine 尝试在堆内存中分配新值时，会发生此活动。</p>

<ul>
<li>gcStart &ndash;&gt;  gcBgMarkWorker &amp;&amp; gcRootPrepare，这时gcBgMarkWorker在休眠中  </li>
<li>schedule &ndash;&gt; findRunnableGCWorker，唤醒适宜数量的gcBgMarkWorker</li>
<li>gcBgMarkWorker &ndash;&gt; gcDrain &ndash;&gt; scanobject &ndash;&gt; greyobject(set mark bit and put to gcw)</li>
<li>在  gcBgMarkWorker  中调⽤  gcMarkDone  排空各种  wbBuf  后，使⽤分布式  termination 检查算法，进入gcMarkTermination &ndash;&gt; gcSweep  唤醒后台沉睡的  sweepg  和  scvg &ndash;&gt; sweep &ndash;&gt; wake bgsweep &amp;&amp; bgscavenge</li>
</ul>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.jpg" alt="垃圾回收" /></p>

<p><br></p>

<p>Golang各个版本垃圾回收区别：</p>

<ul>
<li><p>GoV1.3 普通标记清除法，整体过程需要启动STW，效率极低。</p></li>

<li><p>GoV1.5 三色标记法， 堆空间启动写屏障，栈空间不启动，全部扫描之后，需要重新扫描一次栈(需要STW)，效率普通</p></li>

<li><p>GoV1.8 三色标记法，混合写屏障机制， 栈空间不启动，堆空间启动，整个过程几乎不需要STW，效率较高。</p></li>
</ul>

<p><br></p>

<h4 id="toc_15">GC跟踪</h4>

<p>在运行任何 Go 应用程序时，可以通过<code>GODEBUG</code>在选项中包含环境变量来生成 GC 跟踪。<code>gctrace=1</code>每次发生收集时，运行时都会将 GC 跟踪信息写入<code>stderr</code>.</p>

<pre><code># 示例
GODEBUG=gctrace=1 ./app

gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7-&gt;11-&gt;6 MB, 10 MB goal, 12 P

各个值的含义：
// General
gc 65       : 自程序开始以来运行了65次GC
@6.068s     : 程序开始后的6秒
11%         : 到目前为止，有11%的可用CPU被用在了GC上

// Wall-Clock
0.058ms     : STW        : 标记开始，打开写屏障
1.2ms       : Concurrent : 标记时间
0.083ms     : STW        : 标记终止，写入障碍物关闭和清理

// CPU Time
0.70ms      : STW        : 标记开始
2.5ms       : Concurrent : 标记-辅助时间（GC与分配一致）
1.5ms       : Concurrent : 标记 - 背景GC时间
0ms         : Concurrent : 标记 - 闲置的GC时间
0.99ms      : STW        : 标记终止

// Memory
7MB         : 记开始前使用的堆内存
11MB        : 标记结束后使用中的堆内存
6MB         : 标记结束后，堆内存被标记为活的
10MB        : 标记结束后，堆内存的收集目标是使用中的

// Threads
12P         : 用于运行Goroutines的逻辑处理器或线程的数量
</code></pre>

<p><br></p>

<p>上面是在日志打印每次垃圾回收数据，不够直观，可以使用<code>go tool trace</code>命令，通过图形化界面查看程序生命周期内的所有协程执行过程(包括gc过程)。</p>

<p>(1) 首先在程序中插入跟踪程序代码：</p>

<pre><code class="language-go">// 在程序当前目录生成trace.out文件
type Trace struct {
    F *os.File
}

func (t *Trace) Start() {
    var err error
    t.F, err = os.Create(&quot;trace.out&quot;)
    if err != nil {
        panic(err)
    }

    err = trace.Start(t.F)
    if err != nil {
        panic(err)
    }
}

func (t *Trace) Stop() {
    trace.Stop()
    t.F.Close()
}

func main() {
    tr := &amp;Trace{}
    tr.Start()
    defer tr.Stop()
    
    // 你的程序
}
</code></pre>

<p><br></p>

<p>(2) 执行你的程序代码，等待程序正常结束，在程序当前目录下生成trace.out</p>

<p>(3) 查看程序跟踪信息</p>

<blockquote>
<p>go tool trace trace.out</p>
</blockquote>

<p>在浏览器显示支持跟踪类型</p>

<ul>
<li>View trace</li>
<li>Goroutine analysis</li>
<li>Network blocking profile (⬇)</li>
<li>Synchronization blocking profile (⬇)</li>
<li>Syscall blocking profile (⬇)</li>
<li>Scheduler latency profile (⬇)</li>
<li>User-defined tasks</li>
<li>User-defined regions</li>
<li>Minimum mutator utilization</li>
</ul>

<p><br></p>

<p>点击第一个View trace，选中小控制面板的zoom，在指定位置点击鼠标左键网上拖动放大细节，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/go/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E8%B7%9F%E8%B8%AA.jpg" alt="垃圾回收跟踪" /></p>

<p>从图中可以看出，在垃圾回收阶段，处理器1和处理器2是专门用来给收集器收集对象，其中处理器3也会辅助标记，使得更快的完成收集，并发收集对象过程中也有用户程序在执行，同时看到在垃圾回收这个过程出现两次STW。</p>

<p>在垃圾收集启动期间，运行时会调用 runtime.gcBgMarkStartWorkers 为全局每个处理器创建用于执行后台标记任务的 Goroutine，每一个 Goroutine 都会运行 runtime.gcBgMarkWorker，所有运行 runtime.gcBgMarkWorker 的Goroutine在启动后都会陷入休眠等待调度器的唤醒。一般情况下此函数不会占用这么多的 cpu，出现这种情况一般都是内存 gc 问题，如果分配对象的数量非常多，采集器来不及采集对象，就会唤醒runtime.gcBgMarkWorker的goroutine进行台标记。</p>

<p><br><br></p>

<p>参考：</p>

<ul>
<li>Go 中的垃圾回收 <a href="https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html" rel="nofollow">https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html</a></li>
<li>Golang三色标记+混合写屏障GC模式全分析 <a href="https://www.kancloud.cn/aceld/golang/1958308#GC_376" rel="nofollow">https://www.kancloud.cn/aceld/golang/1958308#GC_376</a></li>
<li>go tool trace <a href="https://making.pusher.com/go-tool-trace/" rel="nofollow">https://making.pusher.com/go-tool-trace/</a></li>
<li>揭秘 Golang 内存管理优化 <a href="https://cdmana.com/2021/10/20211031083312698S.html" rel="nofollow">https://cdmana.com/2021/10/20211031083312698S.html</a></li>
<li>垃圾收集器 <a href="https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/" rel="nofollow">https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/runtime.html">https://zhuyasen.com/post/runtime.html</a>，<a href="https://zhuyasen.com/post/runtime.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>go调试工具</title>
            <link>https://zhuyasen.com/post/delve.html</link>
            <comments>https://zhuyasen.com/post/delve.html#comments</comments>
            <guid>https://zhuyasen.com/post/delve.html</guid>
            <description>
                <![CDATA[<blockquote>

<h2 id="toc_0">go调试工具</h2>

<h3 id="toc_1">1 概念</h3>

<p>Delve是一个用于Go程序的源码级调试器，通过控制程序的执行与你的程序互动，评估变量，并提供线程/goroutine状态、CPU寄存器状态等信息，目标是为调试Go程序提供一个简单而强大的接口。</p>

<p>使用方法：</p>

<blockquote>
<p>dlv [command]</p>
</blockquote>

<pre><code>可用的命令：
  attach    连接到正在运行的进程并开始调试。
  connect   连接到一个无头调试服务器。
  core      检查一个核心转储。
  dap       [EXPERIMENTAL] 启动一个通过Debug Adaptor Protocol (DAP)通信的TCP服务器。
  debug     编译并开始调试当前目录下的主包，或指定的包。
  exec      执行一个预编译的二进制文件，并开始调试会话。
  help      关于任何命令的帮助
  run       已废弃的命令。使用'debug'代替。
  test      编译测试二进制文件并开始调试程序。
  trace     编译并开始追踪程序。
  version   打印版本。

可用标志：
      --accept-multiclient 允许无头服务器接受多个客户端连接。
      --api-version int 选择无头时的API版本。(默认为1)
      --backend string 后台选择（见'dlv help backend'）。(默认为 &quot;default&quot;)
      --build-flags string 构建标志，将被传递给编译器。
      --check-go-version 检查正在使用的Go的版本是否与Delve兼容。(默认为true)
      --headless 只运行调试服务器，在无头模式下。
      --init string 启动文件，由终端客户端执行。
  -l, --listen string 调试服务器监听地址。(默认为 &quot;127.0.0.1:0&quot;)
      --log 启用调试服务器的日志记录。
      --log-dest string 将日志写到指定的文件或文件描述符（见'dlv help log'）。
      --log-output string 逗号分隔的应该产生调试输出的组件列表(见'dlv help log')
      --only-same-user 只允许启动这个Delve实例的同一个用户的连接。(默认为true)
      --wd string 运行程序的工作目录。(默认为&quot;.&quot;)
</code></pre>

<p><br><br></p>

<h3 id="toc_2">2 debug和exec命令</h3>

<p>使用debug命令是从源码编译成二进制后进入调试会话，在本地目录下编译出来的<code>__debug_bin</code>临时文件，结束调试会话会自动删除临时文件<code>__debug_bin</code>，debug命令进入调试会话：</p>

<blockquote>
<p>dlv debug &ndash;check-go-version=false</p>
</blockquote>

<p>使用exec命令指定编译后的二进制文件进入调试会话，也就是比debug少了编译过程，exec 命令进入调试交互会话：</p>

<blockquote>
<p>dlv exec &lt;binary file&gt; &ndash;check-go-version=false</p>
</blockquote>

<p><br></p>

<p>调试会话命令说明：</p>

<pre><code>运行程序命令：
    call ------------------------ 恢复进程，注入一个函数调用（实验性的！！）。
    continue (alias: c) --------- 运行到断点或程序终止。
    next (alias: n) ------------- 跨越到下一个源代码行。
    restart (alias: r) ---------- 重新启动程序。
    step (alias: s) ------------- 单个程序的步骤。
    step-instruction (alias: si)  单步执行一条cpu指令。
    stepout (alias: so) --------- 走出当前函数。

操纵断点命令：
    break (alias: b) ------- 设置一个断点。
    breakpoints (alias: bp)  打印出活动断点的信息。
    clear ------------------ 删除断点。
    clearall --------------- 删除多个断点。
    condition (alias: cond)  设置断点条件。
    on --------------------- 当断点被击中时，执行一条命令。
    trace (alias: t) ------- 设置跟踪点。

查看程序变量和内存命令：
    args ----------------- 打印函数参数。
    display -------------- 每次程序停止时打印表达式的值。
    examinemem (alias: x)  检查内存。
    locals --------------- 打印本地变量。
    print (alias: p) ----- 评估一个表达式。
    regs ----------------- 打印CPU寄存器的内容。
    set ------------------ 改变一个变量的值。
    vars ----------------- 打印软件包变量。
    whatis --------------- 打印一个表达式的类型。

列出并在线程和goroutine之间切换命令：
    goroutine (alias: gr) -- 显示或改变当前的goroutine
    goroutines (alias: grs)  列出程序的goroutine。
    thread (alias: tr) ----- 切换到指定的线程。
    threads ---------------- 打印出每个被追踪的线程的信息。

查看调用栈和选择帧命令：
    deferred --------- 在一个延迟调用的背景下执行命令。
    down ------------- 将当前帧向下移动。
    frame ------------ 设置当前帧，或在不同的帧上执行命令。
    stack (alias: bt)  打印堆栈跟踪。
    up --------------- 将当前帧向上移动。

其他命令：
    config --------------------- 更改配置参数。
    disassemble (alias: disass)  反汇编程序。
    edit (alias: ed) ----------- 打开你在$DELVE_EDITOR或$EDITOR中的位置
    exit (alias: quit | q) ----- 退出调试器。
    funcs ---------------------- 打印函数的列表。
    help (alias: h) ------------ 打印帮助信息。
    libraries ------------------ 列出加载的动态库
    list (alias: ls | l) ------- 显示源代码。
    source --------------------- 执行一个包含delve命令列表的文件
    sources -------------------- 打印源文件的列表。
    types ---------------------- 打印类型列表
</code></pre>

<p><br></p>

<h3 id="toc_3">3 简单调试示例</h3>

<p>文件列表</p>

<pre><code>.
├── cal
│   └── cal.go
└── main.go
</code></pre>

<p><br></p>

<p>main.go代码</p>

<pre><code class="language-go">package main

import (
        &quot;demo/calculators/cal&quot;
        &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
        v := cal.Cal{12, 3}

        go func() {
                time.Sleep(time.Minute)
        }()

        fmt.Println(v.Add())
        fmt.Println(v.Sub())
        fmt.Println(v.Mul())
        fmt.Println(v.Div())

    time.Sleep(time.Second * 5)
}
</code></pre>

<p><br></p>

<p>cal.go代码</p>

<pre><code class="language-go">package cal

type Cal struct {
        X1 int
        X2 int
}

func (c *Cal)Add() int {
        return c.X1 +c.X2
}
func (c *Cal)Sub() int {
        return c.X1 -c.X2
}

func (c *Cal)Mul() int {
        return c.X1 *c.X2
}

func (c *Cal)Div() int {
        return c.X1 / c.X2
}
</code></pre>

<p><br></p>

<p>进入调试会话</p>

<blockquote>
<p>dlv debug main.go</p>
</blockquote>

<p><strong>(1) 打断点</strong></p>

<pre><code class="language-bash">Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x494b2f for main.main() ./main.go:9
(dlv) b main.go:16
Breakpoint 2 set at 0x494b6c for main.main() ./main.go:16
(dlv) b main.go:17
Breakpoint 3 set at 0x494bff for main.main() ./main.go:17
(dlv) b main.go:18
Breakpoint 4 set at 0x494c90 for main.main() ./main.go:18
(dlv) b main.go:19
Breakpoint 5 set at 0x494d25 for main.main() ./main.go:19
</code></pre>

<p>查看断点列表</p>

<pre><code class="language-bash">(dlv) bp
Breakpoint runtime-fatal-throw at 0x432f00 for runtime.fatalthrow() /usr/local/go/src/runtime/panic.go:1244 (0)
Breakpoint unrecovered-panic at 0x433000 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1271 (0)
        print runtime.curg._panic.arg
Breakpoint 1 at 0x494b2f for main.main() ./main.go:9 (0)
Breakpoint 2 at 0x494b6c for main.main() ./main.go:16 (0)
Breakpoint 3 at 0x494bff for main.main() ./main.go:17 (0)
Breakpoint 4 at 0x494c90 for main.main() ./main.go:18 (0)
Breakpoint 5 at 0x494d25 for main.main() ./main.go:19 (0)
</code></pre>

<p>清除断点使用 <code>clearall</code>命令</p>

<p><br></p>

<p><strong>(2) 执行到断点或结束位置</strong></p>

<pre><code class="language-bash">(dlv) c
&gt; main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x494b2f)
     4:         &quot;demo/calculators/cal&quot;
     5:         &quot;fmt&quot;
     6:     &quot;time&quot;
     7: )
     8:
=&gt;   9: func main() {
    10:         v := cal.Cal{12, 3}
    11:
    12:         go func() {
    13:                 time.Sleep(time.Minute)
    14:         }()
</code></pre>

<p><br></p>

<p><strong>(3) 执行下一个代码行</strong></p>

<pre><code class="language-bash">(dlv) n
&gt; main.main() ./main.go:12 (PC: 0x494b5e)
     7: )
     8:
     9: func main() {
    10:         v := cal.Cal{12, 3}
    11:
=&gt;  12:         go func() {
    13:                 time.Sleep(time.Minute)
    14:         }()
    15:
    16:         fmt.Println(v.Add())
    17:         fmt.Println(v.Sub())
</code></pre>

<p><br></p>

<p><strong>(4) 查看和修改变量</strong></p>

<pre><code class="language-bash">(dlv) locals
v = demo/calculators/cal.Cal {X1: 12, X2: 3}

(dlv) set v.X2=4

(dlv) locals
v = demo/calculators/cal.Cal {X1: 12, X2: 4}

(dlv) print v
demo/calculators/cal.Cal {X1: 12, X2: 4}
</code></pre>

<p><br></p>

<p><strong>(5) 查看调用栈信息</strong></p>

<pre><code class="language-bash">(dlv) bt
0  0x0000000000494bff in main.main
   at ./main.go:17
1  0x0000000000435273 in runtime.main
   at /usr/local/go/src/runtime/proc.go:255
2  0x000000000045f961 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:1581
</code></pre>

<p><br></p>

<p><strong>(5) 查看goroutine</strong></p>

<pre><code class="language-bash"># 当前goroutine
(dlv) goroutine
Thread 67537 at ./main.go:16
Goroutine 1:
        Runtime: ./main.go:16 main.main (0x494b6c)
        User: ./main.go:16 main.main (0x494b6c)
        Go: &lt;autogenerated&gt;:1 runtime.newproc (0x461e29)
        Start: /usr/local/go/src/runtime/proc.go:145 runtime.main (0x435080)

# 所有goroutine
(dlv) goroutines
* Goroutine 1 - User: ./main.go:16 main.main (0x494b6c) (thread 67537)
  Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x435692)
  Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x435692)
  Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x435692)
  Goroutine 5 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x435692)
  Goroutine 6 - User: ./main.go:12 main.main.func1 (0x494e00)
[6 goroutines]
</code></pre>

<p><br></p>

<p><strong>(6) 打印每个被追踪的线程的信息</strong></p>

<pre><code class="language-bash">(dlv) threads
* Thread 67537 at 0x494b6c ./main.go:16 main.main
  Thread 67667 at 0x46119d /usr/local/go/src/runtime/sys_linux_amd64.s:146 runtime.usleep
  Thread 67668 at 0x46119d /usr/local/go/src/runtime/sys_linux_amd64.s:146 runtime.usleep
  Thread 67669 at 0x461723 /usr/local/go/src/runtime/sys_linux_amd64.s:520 runtime.futex
  Thread 67670 at 0x461723 /usr/local/go/src/runtime/sys_linux_amd64.s:520 runtime.futex
</code></pre>

<p><br></p>

<p><strong>(7) 反汇编</strong></p>

<pre><code class="language-bash">(dlv) disassemble
TEXT main.main(SB) /home/vison/work/golang/project/src/demo/calculators/main.go
        main.go:9       0x494b20        4c8d642498                      lea r12, ptr [rsp-0x68]
        main.go:9       0x494b25        4d3b6610                        cmp r12, qword ptr [r14+0x10]
        main.go:9       0x494b29        0f86a8020000                    jbe 0x494dd7
        main.go:9       0x494b2f*       4881ece8000000                  sub rsp, 0xe8
        main.go:9       0x494b36        4889ac24e0000000                mov qword ptr [rsp+0xe0], rbp
        main.go:9       0x494b3e        488dac24e0000000                lea rbp, ptr [rsp+0xe0]
        main.go:10      0x494b46        440f117c2420                    movups xmmword ptr [rsp+0x20], xmm15
        main.go:10      0x494b4c        48c74424200c000000              mov qword ptr [rsp+0x20], 0xc
        main.go:10      0x494b55        48c744242803000000              mov qword ptr [rsp+0x28], 0x3
        main.go:12      0x494b5e        31c0                            xor eax, eax
        main.go:12      0x494b60        488d1d89dd0100                  lea rbx, ptr [rip+0x1dd89]
        main.go:12      0x494b67        e81484faff                      call $runtime.newproc
=&gt;      main.go:16      0x494b6c*       488d442420                      lea rax, ptr [rsp+0x20]
        main.go:16      0x494b71        e80ae5fcff                      call $demo/calculators/cal.(*Cal).Add
        main.go:16      0x494b76        4889442418                      mov qword ptr [rsp+0x18], rax
        main.go:16      0x494b7b        440f117c2470                    movups xmmword ptr [rsp+0x70], xmm15
        main.go:16      0x494b81        488d4c2470                      lea rcx, ptr [rsp+0x70]
        main.go:16      0x494b86        48894c2448                      mov qword ptr [rsp+0x48], rcx
        main.go:16      0x494b8b        488b442418                      mov rax, qword ptr [rsp+0x18]
        main.go:16      0x494b90        e80b52f7ff                      call $runtime.convT64
        main.go:16      0x494b95        4889442440                      mov qword ptr [rsp+0x40], rax
        main.go:16      0x494b9a        488b4c2448                      mov rcx, qword ptr [rsp+0x48]
        main.go:16      0x494b9f        8401                            test byte ptr [rcx], al
        main.go:16      0x494ba1        488d15f86d0000                  lea rdx, ptr [rip+0x6df8]
        main.go:16      0x494ba8        488911                          mov qword ptr [rcx], rdx
        main.go:16      0x494bab        488d7908                        lea rdi, ptr [rcx+0x8]
        main.go:16      0x494baf        833deab30c0000                  cmp dword ptr [runtime.writeBarrier], 0x0
        main.go:16      0x494bb6        7402                            jz 0x494bba
        main.go:16      0x494bb8        eb08                            jmp 0x494bc2
        main.go:16      0x494bba        48894108                        mov qword ptr [rcx+0x8], rax
        main.go:16      0x494bbe        6690                            data16 nop
        main.go:16      0x494bc0        eb07                            jmp 0x494bc9
        main.go:16      0x494bc2        e8d9adfcff                      call $runtime.gcWriteBarrier
        main.go:16      0x494bc7        eb00                            jmp 0x494bc9
        main.go:16      0x494bc9        488b442448                      mov rax, qword ptr [rsp+0x48]
        main.go:16      0x494bce        8400                            test byte ptr [rax], al
        main.go:16      0x494bd0        eb00                            jmp 0x494bd2
        main.go:16      0x494bd2        48898424b0000000                mov qword ptr [rsp+0xb0], rax
        main.go:16      0x494bda        48c78424b800000001000000        mov qword ptr [rsp+0xb8], 0x1
        main.go:16      0x494be6        48c78424c000000001000000        mov qword ptr [rsp+0xc0], 0x1
        main.go:16      0x494bf2        bb01000000                      mov ebx, 0x1
        main.go:16      0x494bf7        4889d9                          mov rcx, rbx
        main.go:16      0x494bfa        e8c1a8ffff                      call $fmt.Println
        main.go:17      0x494bff*       488d442420                      lea rax, ptr [rsp+0x20]
        main.go:17      0x494c04        e8b7e4fcff                      call $demo/calculators/cal.(*Cal).Sub
        main.go:17      0x494c09        4889442418                      mov qword ptr [rsp+0x18], rax
        main.go:17      0x494c0e        440f117c2470                    movups xmmword ptr [rsp+0x70], xmm15
        main.go:17      0x494c14        488d542470                      lea rdx, ptr [rsp+0x70]
        main.go:17      0x494c19        4889542438                      mov qword ptr [rsp+0x38], rdx
        main.go:17      0x494c1e        488b442418                      mov rax, qword ptr [rsp+0x18]
        main.go:17      0x494c23        e87851f7ff                      call $runtime.convT64
        main.go:17      0x494c28        4889442430                      mov qword ptr [rsp+0x30], rax
        main.go:17      0x494c2d        488b542438                      mov rdx, qword ptr [rsp+0x38]
        main.go:17      0x494c32        8402                            test byte ptr [rdx], al
        main.go:17      0x494c34        488d35656d0000                  lea rsi, ptr [rip+0x6d65]
        main.go:17      0x494c3b        488932                          mov qword ptr [rdx], rsi
        main.go:17      0x494c3e        488d7a08                        lea rdi, ptr [rdx+0x8]
        main.go:17      0x494c42        833d57b30c0000                  cmp dword ptr [runtime.writeBarrier], 0x0
        main.go:17      0x494c49        7402                            jz 0x494c4d
        main.go:17      0x494c4b        eb06                            jmp 0x494c53
        main.go:17      0x494c4d        48894208                        mov qword ptr [rdx+0x8], rax
        main.go:17      0x494c51        eb07                            jmp 0x494c5a
        main.go:17      0x494c53        e848adfcff                      call $runtime.gcWriteBarrier
        main.go:17      0x494c58        eb00                            jmp 0x494c5a
        main.go:17      0x494c5a        488b442438                      mov rax, qword ptr [rsp+0x38]
        main.go:17      0x494c5f        8400                            test byte ptr [rax], al
        main.go:17      0x494c61        eb00                            jmp 0x494c63
        main.go:17      0x494c63        4889842498000000                mov qword ptr [rsp+0x98], rax
        main.go:17      0x494c6b        48c78424a000000001000000        mov qword ptr [rsp+0xa0], 0x1
        main.go:17      0x494c77        48c78424a800000001000000        mov qword ptr [rsp+0xa8], 0x1
        main.go:17      0x494c83        bb01000000                      mov ebx, 0x1
        main.go:17      0x494c88        4889d9                          mov rcx, rbx
        main.go:17      0x494c8b        e830a8ffff                      call $fmt.Println
        main.go:18      0x494c90*       488d442420                      lea rax, ptr [rsp+0x20]
        main.go:18      0x494c95        e866e4fcff                      call $demo/calculators/cal.(*Cal).Mul
        main.go:18      0x494c9a        4889442418                      mov qword ptr [rsp+0x18], rax
        main.go:18      0x494c9f        440f117c2470                    movups xmmword ptr [rsp+0x70], xmm15
        main.go:18      0x494ca5        488d542470                      lea rdx, ptr [rsp+0x70]
        main.go:18      0x494caa        4889542468                      mov qword ptr [rsp+0x68], rdx
        main.go:18      0x494caf        488b442418                      mov rax, qword ptr [rsp+0x18]
        main.go:18      0x494cb4        e8e750f7ff                      call $runtime.convT64
        main.go:18      0x494cb9        4889442460                      mov qword ptr [rsp+0x60], rax
        main.go:18      0x494cbe        488b542468                      mov rdx, qword ptr [rsp+0x68]
        main.go:18      0x494cc3        8402                            test byte ptr [rdx], al
        main.go:18      0x494cc5        488d35d46c0000                  lea rsi, ptr [rip+0x6cd4]
        main.go:18      0x494ccc        488932                          mov qword ptr [rdx], rsi
        main.go:18      0x494ccf        488d7a08                        lea rdi, ptr [rdx+0x8]
        main.go:18      0x494cd3        833dc6b20c0000                  cmp dword ptr [runtime.writeBarrier], 0x0
        main.go:18      0x494cda        7402                            jz 0x494cde
        main.go:18      0x494cdc        eb06                            jmp 0x494ce4
        main.go:18      0x494cde        48894208                        mov qword ptr [rdx+0x8], rax
        main.go:18      0x494ce2        eb07                            jmp 0x494ceb
        main.go:18      0x494ce4        e8b7acfcff                      call $runtime.gcWriteBarrier
        main.go:18      0x494ce9        eb00                            jmp 0x494ceb
        main.go:18      0x494ceb        488b442468                      mov rax, qword ptr [rsp+0x68]
        main.go:18      0x494cf0        8400                            test byte ptr [rax], al
        main.go:18      0x494cf2        eb00                            jmp 0x494cf4
        main.go:18      0x494cf4        4889842480000000                mov qword ptr [rsp+0x80], rax
        main.go:18      0x494cfc        48c784248800000001000000        mov qword ptr [rsp+0x88], 0x1
        main.go:18      0x494d08        48c784249000000001000000        mov qword ptr [rsp+0x90], 0x1
        main.go:18      0x494d14        bb01000000                      mov ebx, 0x1
        main.go:18      0x494d19        4889d9                          mov rcx, rbx
        main.go:18      0x494d1c        0f1f4000                        nop dword ptr [rax], eax
        main.go:18      0x494d20        e89ba7ffff                      call $fmt.Println
        main.go:19      0x494d25*       488d442420                      lea rax, ptr [rsp+0x20]
        main.go:19      0x494d2a        e831e4fcff                      call $demo/calculators/cal.(*Cal).Div
        main.go:19      0x494d2f        4889442418                      mov qword ptr [rsp+0x18], rax
        main.go:19      0x494d34        440f117c2470                    movups xmmword ptr [rsp+0x70], xmm15
        main.go:19      0x494d3a        488d542470                      lea rdx, ptr [rsp+0x70]
        main.go:19      0x494d3f        4889542458                      mov qword ptr [rsp+0x58], rdx
        main.go:19      0x494d44        488b442418                      mov rax, qword ptr [rsp+0x18]
        main.go:19      0x494d49        e85250f7ff                      call $runtime.convT64
        main.go:19      0x494d4e        4889442450                      mov qword ptr [rsp+0x50], rax
        main.go:19      0x494d53        488b542458                      mov rdx, qword ptr [rsp+0x58]
        main.go:19      0x494d58        8402                            test byte ptr [rdx], al
        main.go:19      0x494d5a        488d353f6c0000                  lea rsi, ptr [rip+0x6c3f]
        main.go:19      0x494d61        488932                          mov qword ptr [rdx], rsi
        main.go:19      0x494d64        488d7a08                        lea rdi, ptr [rdx+0x8]
        main.go:19      0x494d68        833d31b20c0000                  cmp dword ptr [runtime.writeBarrier], 0x0
        main.go:19      0x494d6f        7402                            jz 0x494d73
        main.go:19      0x494d71        eb06                            jmp 0x494d79
        main.go:19      0x494d73        48894208                        mov qword ptr [rdx+0x8], rax
        main.go:19      0x494d77        eb09                            jmp 0x494d82
        main.go:19      0x494d79        e822acfcff                      call $runtime.gcWriteBarrier
        main.go:19      0x494d7e        6690                            data16 nop
        main.go:19      0x494d80        eb00                            jmp 0x494d82
        main.go:19      0x494d82        488b442458                      mov rax, qword ptr [rsp+0x58]
        main.go:19      0x494d87        8400                            test byte ptr [rax], al
        main.go:19      0x494d89        eb00                            jmp 0x494d8b
        main.go:19      0x494d8b        48898424c8000000                mov qword ptr [rsp+0xc8], rax
        main.go:19      0x494d93        48c78424d000000001000000        mov qword ptr [rsp+0xd0], 0x1
        main.go:19      0x494d9f        48c78424d800000001000000        mov qword ptr [rsp+0xd8], 0x1
        main.go:19      0x494dab        bb01000000                      mov ebx, 0x1
        main.go:19      0x494db0        4889d9                          mov rcx, rbx
        main.go:19      0x494db3        e808a7ffff                      call $fmt.Println
        main.go:21      0x494db8        48b800f2052a01000000            mov rax, 0x12a05f200
        main.go:21      0x494dc2        e8997afcff                      call $time.Sleep
        main.go:22      0x494dc7        488bac24e0000000                mov rbp, qword ptr [rsp+0xe0]
        main.go:22      0x494dcf        4881c4e8000000                  add rsp, 0xe8
        main.go:22      0x494dd6        c3                              ret
        main.go:9       0x494dd7        e8048cfcff                      call $runtime.morestack_noctxt
        main.go:9       0x494ddc        0f1f4000                        nop dword ptr [rax], eax
        main.go:9       0x494de0        e93bfdffff                      jmp $main.main
</code></pre>

<p><br></p>

<p>其他命令使用方法使用help查看。</p>
<p>本文链接：<a href="https://zhuyasen.com/post/delve.html">https://zhuyasen.com/post/delve.html</a>，<a href="https://zhuyasen.com/post/delve.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>cobra基础与实践</title>
            <link>https://zhuyasen.com/post/cobbra.html</link>
            <comments>https://zhuyasen.com/post/cobbra.html#comments</comments>
            <guid>https://zhuyasen.com/post/cobbra.html</guid>
            <description>
                <![CDATA[<blockquote>

<h2 id="toc_0">1 基本概念</h2>

<p>Cobra是Go的CLI框架，它包含一个用于创建强大的现代 CLI 应用程序的库和一个用于快速生成基于 Cobra 的应用程序和命令文件的工具。Cobra基于三个基本概念<code>commands</code>,<code>arguments</code>和<code>flags</code>，其中commands代表行为，arguments代表数值，flags代表对行为的改变，命令使用示例：</p>

<pre><code class="language-bash">APPNAME VERB NOUN --ADJECTIVE
# 或者
APPNAME COMMAND ARG --FLAG

# server是commands，port是flag 
hugo server --port=1313

# clone是commands，URL是arguments，brae是flags
git clone URL --bare
</code></pre>

<p><br></p>

<p>特点：
-   简单的基于子命令的 CLI：<code>app server</code>、<code>app fetch</code>等。
-   完全符合 POSIX 的标志（包括短版和长版）。
-   嵌套子命令。
-   全局、本地和级联标志。
-   <code>cobra init appname</code>使用&amp;轻松生成应用程序和命令<code>cobra add cmdname</code>。
-   明智的建议(<code>app srver</code>……你的意思是<code>app server</code>？).
-   命令和标志的自动帮助生成。
-   <code>-h</code>,<code>--help</code>等的自动帮助标志识别。
-   为您的应用程序自动生成 bash 自动完成功能。
-   为您的应用程序自动生成的手册页。
-   命令别名，这样您就可以在不破坏它们的情况下进行更改。
-   定义您自己的帮助、使用等的灵活性。
-   与<a href="http://github.com/spf13/viper" rel="nofollow">viper</a>的可选紧密集成。</p>

<p><br><br></p>

<h2 id="toc_1">2 安装cobra</h2>

<p>安装命令：</p>

<blockquote>
<p>go get -u github.com/spf13/cobra/cobra</p>
</blockquote>

<p>基本目录结构：</p>

<pre><code>▾ appName/
    ▾ cmd/
        root.go
        yourCommand1.go
        yourCommand2.go
      main.go
</code></pre>

<p><br></p>

<h3 id="toc_2">2.1 人工构建Cobra应用</h3>

<p>root.go文件内容如下：</p>

<pre><code class="language-go">package cmd  
  
import (  
   &quot;github.com/spf13/cobra&quot;
)  
  
var RootCmd = &amp;cobra.Command{  
   Use:                &quot;mpc&quot;,  
   Short:              &quot;Manage Prometheus configuration&quot;,  
   Long:               `Manage Prometheus configuration`,  
   DisableSuggestions: true,  
   Run: func(cmd *cobra.Command, args []string) {  
      cmd.Traverse(args)  
   },  
}  
</code></pre>

<p><br></p>

<p>man.go文件内容如下：</p>

<pre><code class="language-go">package main  
  
import (  
   &quot;mpc/cmd&quot;  
   &quot;os&quot;
)  

  
func main() {  
   if err := cmd.RootCmd.Execute(); err != nil {  
      fmt.Println(err)  
      os.Exit(1)  
   }  
}
</code></pre>

<p><br></p>

<h3 id="toc_3">2.2 使用生成器构建Cobra应用</h3>

<p>安装cobra后，在GOPATH文件夹<a href="http://github.com/spf13/cobra/cobra" rel="nofollow">github.com/spf13/cobra/cobra</a>下使用<code>go install</code>在<code>$GOPATH/bin</code>路径下生成<code>cobra.exe</code>可执行命令。</p>

<pre><code class="language-bash"># cobra -h

Usage:
  cobra [command]
 
Available Commands:
  add         向Cobra应用程序添加命令
  completion  完成为指定的shell生成自动完成脚本                 
  help        任何命令都需要帮助
  init        初始化Cobra应用程序                                             

Flags: 
  -a, --author string    作者姓名(默认为“您的姓名”)
      --config string    配置文件(默认值为$HOME/.cobra.yaml)
  -h, --help             cobra帮助
  -l, --license string   项目许可证名称
      --viper            使用Viper进行配置
                                                                                          
Use &quot;cobra [command] --help&quot; for more information about a command.
</code></pre>

<p><br></p>

<p>初始化根命令，在当前项目下生成cmd/root.go</p>

<blockquote>
<p>cobra init <your_app_name></p>
</blockquote>

<p>添加子命令，在当前项目下生成cmd/your_command.go</p>

<blockquote>
<p>cobra add <your_command></p>
</blockquote>

<p><br><br></p>

<h2 id="toc_4">3 使用规则</h2>

<p>cobra三大件是<code>commands</code>,<code>arguments</code>和<code>flags</code>，下面介绍三大件用法。</p>

<h3 id="toc_5">3.1 commands</h3>

<p>每个客户端命令都有一个根命令入口，其他子命令在根命令下延申，类似一颗树，例如根命令<code>kubectl</code>，其中一个子命令为<code>kubectl apply</code></p>

<p>cobra的根命令和子命令的简单示例：</p>

<pre><code class="language-go">// kubectl根命令
rootCMD := &amp;cobra.Command{  
   Use:           &quot;kubectl&quot;,  
   Short:         &quot;kubectl controls the Kubernetes cluster manager.&quot;,  
}

// 添加子命令
rootCMD.AddCommand(  
    applyCMD
)


// 一个子命令apply
applyCMD := &amp;cobra.Command{  
   Use:           &quot;apply&quot;,  
   Short:         &quot;Apply a configuration to a resource by filename or stdin.&quot;, 
   RunE: func(cmd *cobra.Command, args []string) error {  
      // 执行命令逻辑
   }, 
}
</code></pre>

<p>cobra.Command对象下有很多属性，下面是一些常用属性设置</p>

<p><br></p>

<p><strong>(1) 版本</strong></p>

<p>如果在root命令上设置了version字段，Cobra会添加一个顶级的<code>--version</code>标志。运行带有<code>-version</code>标志的应用程序将使用版本模板将版本打印到标准输出。</p>

<pre><code class="language-go">rootCmd.Version=&quot;0.0.1&quot;
// 或自定义版本
rootCmd.SetVersionTemplate(&quot;the version is 0.0.1&quot;)
</code></pre>

<p><br></p>

<p><strong>(2) 运行前和运行后钩子</strong></p>

<p>可以在命令的主运行函数之前或之后运行函数。PersistentPreRun和PreRun函数在运行之前执行，而PersistentPostRun和PostRun将在运行后执行。如果子函数不声明自己的函数，则它们将继承Persistent*Run函数，这些函数按以下顺序运行：</p>

<blockquote>
<p>PersistentPreRun &ndash;&gt;  PreRun &ndash;&gt;  Run &ndash;&gt;  PostRun &ndash;&gt;  PersistentPostRun</p>
</blockquote>

<p>带有错误返回执行顺序</p>

<blockquote>
<p>PersistentPreRunE &ndash;&gt;  PreRunE &ndash;&gt;  RunE &ndash;&gt;  PostRunE &ndash;&gt;  PersistentPostRunE</p>
</blockquote>

<p><br></p>

<p><strong>(3) 发生“未知命令”时的建议</strong></p>

<p>当发生“未知命令”错误时，Cobra将打印自动建议。这使得Cobra在发生拼写错误时的行为类似于git命令，如果不需要在命令中建议或调整字符串距离，可以通过属性禁止。</p>

<pre><code class="language-bash">command.DisableSuggestions = true
command.SuggestionsMinimumDistance = 1
</code></pre>

<p><br></p>

<p><strong>(4) 为命令生成文档</strong></p>

<p>Cobra可以基于子命令、标志等生成文档。请在<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fspf13%2Fcobra%2Fblob%2Fmaster%2Fdoc%2FREADME.md" rel="nofollow">docs generation文档</a>中阅读更多关于它的信息。</p>

<p><br></p>

<p><strong>(5) shell补全</strong></p>

<p>Cobra可以为以下shell生成shell完成文件：bash、zsh、fish、PowerShell。如果您在命令中添加更多信息，这些补全功能将非常强大和灵活。在<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fspf13%2Fcobra%2Fblob%2Fmaster%2Fshell_completions.md" rel="nofollow">Shell Completions</a>中阅读更多关于它的信息。</p>

<p><br></p>

<h3 id="toc_6">3.2 arguments</h3>

<p>命令可能有必须的参数，在cobra.Command对象属性下可以对参数进行处理，例如命令后面必须有且只有一个参数，示例代码如下：</p>

<pre><code class="language-go">rootCMD := &amp;cobra.Command{  
   Use:           &quot;kubectl&quot;,  
   Short:         &quot;kubectl controls the Kubernetes cluster manager.&quot;,
   Args:          cobra.ExactArgs(1),
}
</code></pre>

<p>cobra提供校验参数函数：
-   NoArgs: 如果存在任何位置参数，该命令将报告错误。
-   ArbitraryArgs: 该命令将接受任何args。
-   OnlyValidArgs: 如果有任何位置参数不在命令的ValidArgs字段中，则该命令将报告错误。
-   MinimumNArgs(int): 如果没有至少N个位置参数，则该命令将报告错误。
-   MaximumNArgs(int): 如果位置参数超过N个，则该命令将报告错误。
-   ExactArgs(int): 如果没有正好N个位置参数，则命令将报告错误。
-   ExactValidArgs(int): 如果没有正好N个位置参数，或者如果有任何位置参数不在命令的ValidArgs字段中，则该命令将报告错误
-   RangeArgs(min，max): 如果args的数目不在预期的最小和最大args数目之间，则命令将报告错误。</p>

<p><br></p>

<p>也可以在cobra.Command对象属性下的Run或RunE进行自定义校验，示例代码：</p>

<pre><code class="language-go">
var resourceNameArg string

applyCMD := &amp;cobra.Command{  
   Use:           &quot;get&quot;,  
   Short:         &quot;Display one or many resources.&quot;, 
   RunE: func(cmd *cobra.Command, args []string) error {
      if len(args) &lt; 1 {  
         return fmt.Errorf(&quot;You must specify the type of resource to get, eg: kubectl get pod&quot;)
      }
      resourceArg = args[0]  
      args = args[1:]  

      // 执行命令逻辑
   }, 
}
</code></pre>

<p><br></p>

<h3 id="toc_7">3.2 flags</h3>

<p>flags用来控制操作命令的操作方式。</p>

<p><strong>(1) 本地flags</strong></p>

<p>在本地分配一个flag，该flag只应用于该特定命令。</p>

<pre><code class="language-go">// flag s只在localCmd上起作用
localCmd.Flags().StringVarP(&amp;Source, &quot;source&quot;, &quot;s&quot;, &quot;&quot;, &quot;Source directory to read from&quot;)
</code></pre>

<p>默认情况下，Cobra只解析目标命令上的本地flag，而忽略父命令上的任何本地flags。通过启用Command.TraverseChildren，Cobra将在执行目标命令之前解析每个命令上的本地flags。</p>

<pre><code class="language-go">command := cobra.Command{
  Use: &quot;print [OPTIONS] [COMMANDS]&quot;,
  TraverseChildren: true,
}
</code></pre>

<p><br></p>

<p><strong>(2) 持久flags</strong></p>

<p>flag可以是持久的，这意味着该flag将可用于分配给它的命令以及该命令下的每个命令。对于全局flag，在根上指定一个标志作为持久标志。</p>

<pre><code class="language-go">// flag v将在rootCmd及以下的子命令上都生效
rootCmd.PersistentFlags().BoolVarP(&amp;Verbose, &quot;verbose&quot;, &quot;v&quot;, false, &quot;verbose output&quot;)
</code></pre>

<p><br></p>

<p><strong>(3) 必须的flags</strong></p>

<p>flags默认是可选的，如果希望命令在未设置flag时报告错误。</p>

<pre><code class="language-go">rootCmd.Flags().StringVarP(&amp;Region, &quot;region&quot;, &quot;r&quot;, &quot;&quot;, &quot;AWS region (required)&quot;)
rootCmd.MarkFlagRequired(&quot;region&quot;)
</code></pre>

<p><br></p>

<p><strong>(4)viper绑定flags</strong></p>

<pre><code class="language-go">var author string

func init() {
  rootCmd.PersistentFlags().StringVar(&amp;author, &quot;author&quot;, &quot;YOUR NAME&quot;, &quot;Author name for copyright attribution&quot;)
  viper.BindPFlag(&quot;author&quot;, rootCmd.PersistentFlags().Lookup(&quot;author&quot;))
}
</code></pre>

<p>在本例中，持久标志author与viper绑定。注意：当用户未提供&ndash;author标志时，变量author将不会设置为config中的值。</p>

<p><br><br></p>

<h2 id="toc_8">4 一个完整的cobra使用示例</h2>

<p>以一个管理prometheus的配置文件自动化运维工具<code>mpc</code>(<a href="https://github.com/zhufuyi/mpc" rel="nofollow">https://github.com/zhufuyi/mpc</a> )为例，<code>mpc</code>主要对prometheus.yaml文件的job、targets、labels三个对象增删改查，支持远程安装exporter，命令帮助信息如下：</p>

<pre><code class="language-bash">$ mpc
manage prometheus configuration, add,delete,update job

Usage:
  mpc [command]

Available Commands:
  add         Add job,targets,labels to prometheus configuration file
  completion  Generate the autocompletion script for the specified shell
  delete      Delete job,targets,labels in prometheus configuration file
  exec        Install and run service to one remote server
  execs       Install and run service to multiple remote servers
  get         Show job,targets,labels from prometheus configuration file
  help        Help about any command
  reload      Make the prometheus configuration effective
  replace     Replace job,targets,labels to prometheus configuration file
  resources   List of supported resources

Flags:
  -h, --help      help for mpc
  -v, --version   version for mpc

Use &quot;mpc [command] --help&quot; for more information about a command.
</code></pre>

<p><br></p>

<p>参考：</p>

<ul>
<li><a href="https://cobra.dev/" rel="nofollow">https://cobra.dev/</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/cobbra.html">https://zhuyasen.com/post/cobbra.html</a>，<a href="https://zhuyasen.com/post/cobbra.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>grpc基础与实践</title>
            <link>https://zhuyasen.com/post/grpc.html</link>
            <comments>https://zhuyasen.com/post/grpc.html#comments</comments>
            <guid>https://zhuyasen.com/post/grpc.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">1 grpc概述</h3>

<p><a href="https://github.com/grpc/grpc-go" rel="nofollow">grpc</a> 是一个高性能、开源的rpc框架，目前提供了多种语言版本，基于HTTP/2标准设计，拥有双向流、流控、头部压缩、单TCP连接上的多复用请求特性，接口描述语言使用protobuf。</p>

<p>在grpc中，一共有四种调用方式：</p>

<ul>
<li>一元RPC(unary RPC): 称为单次RPC，也就是一问一答RPC请求，是最基础最常用的调用方式。</li>
<li>服务端流式RPC(Server-side Streaming RPC): 是一个单向流，客户端发起一次普通RPC请求，服务端通过流式返回数据集。</li>
<li>客户端流式RPC(Client-side Streaming RPC): 是一个单向流，客户端通过流式发送数据集，服务端回复一次普通RPC请求。</li>
<li>双向流式RPC(Bidirectional Streaming RPC): 由客户端以流式发起请求，服务端同样以流式方式响应请求。一定有客户端发起，但交互方式(谁先谁后、一次发多少、相应多少、什么时候关闭)则由程序编写的方式来控制(可以结合协程)。</li>
</ul>

<p>Unary和Stream相比，因为省掉了中间每次建立连接的花费，所以效率上会提升一些。</p>

<p><br></p>

<p><strong>grpc调用流程：</strong></p>

<ul>
<li>客户端发起调用，即在程序中调用某个方法；</li>
<li>对请求信息使用protobuf进行对象序列化后发给服务端；</li>
<li>服务端接收请求后，解码请求信息，进行业务逻辑处理；</li>
<li>对处理结果使用protobuf进行对象序列化压缩后返回给客户端；</li>
<li>客户端接收到服务端响应后，解码结果。</li>
</ul>

<p><br></p>

<p><strong>grpc优点：</strong></p>

<ul>
<li>性能好，比json编解码数读快几十倍。</li>
<li>代码生成方便，使用proto工具自动生成对应语言代码。</li>
<li>支持多种流传输方式，支持一元RPC、服务端流式RPC、客户端流式RPC、双流向RPC共4中传输流。</li>
<li>有超时和取消处理机制，客户端和服务端在截止时间后对取消事件进行相关处理。</li>
</ul>

<p><br></p>

<p><strong>grpc缺点：</strong></p>

<ul>
<li>可读性差</li>
<li>不支持浏览器调用</li>
<li>外部组件支持性差</li>
</ul>

<p><br></p>

<p><strong>使用场景</strong></p>

<ul>
<li><p>unary(一元RPC)</p>

<ul>
<li>CRUD的api调用</li>
</ul></li>

<li><p>service-side streaming(服务端流方式)</p>

<ul>
<li>股票app：客户端向服务端发送一个股票代码，服务端就把该股票的实时数据源源不断的返回给客户端</li>
<li>app的在线push：client先发请求到server注册，然后server就可以发在线push了</li>
</ul></li>

<li><p>client-side rpc streaming(客户端流方式)</p>

<ul>
<li>物联网终端向服务器报送数据</li>
</ul></li>

<li><p>bi-side rpc streaming(双向流方式)</p>

<ul>
<li>聊天机器人</li>
<li>有状态的游戏服务器进行数据交换。比如LOL，王者荣耀等竞技游戏，client和server之间需要非常频繁地交换数据</li>
</ul></li>
</ul>

<p><br></p>

<h3 id="toc_1">2 grpc插件和使用命令</h3>

<h4 id="toc_2">2.1 安装插件</h4>

<p>把下载的可执行文件全部存放到<code>$GOPATH/bin</code>目录下。并且把proto依赖的包<a href="https://github.com/zhufuyi/grpc_examples/tree/main/include" rel="nofollow">include</a>存放到目录<code>$GOPATH/bin/include</code>下。</p>

<pre><code class="language-bash"># 各个插件版本
# protoc                    v3.20.1      命令
# protoc-gen-go             v1.28.0      protoc插件，根据proto文件生成*pb.go文件，是填充、序列化和检索消息类型代码。
# protoc-gen-gogofaster     v1.28.0      protoc插件，替换了protoc-gen-go插件，以提高编码和解码速度，还支持自定义标签。
# protoc-gen-go-grpc        v1.2.0       protoc插件，根据proto文件生成*_grpc.pb.go文件，是客户端和服务端的方法和接口代码。
# protoc-gen-grpc-gateway   v2.10.0      protoc插件，根据proto文件生成*pb.gw.go文件，是web的api代码。
# protoc-gen-openapiv2      v2.10.0      protoc插件，根据proto文件生成*swagger.json文件，是swagger-ui接口文档。
# protoc-gen-validate       v0.6.7       protoc插件，根据proto文件生成*pb.validate.go文件，是校验字段代码

# 下载protoc
wget https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.1

# 安装protoc-gen-go、protoc-gen-go-grpc、protoc-gen-validate插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
go install github.com/envoyproxy/protoc-gen-validate@v0.6.7

# 安装protoc-gen-grpc-gateway、protoc-gen-openapiv2插件，下载地址
wget https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v2.10.0/protoc-gen-grpc-gateway-v2.10.0-windows-x86_64.exe
wget https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v2.10.0/protoc-gen-openapiv2-v2.10.0-windows-x86_64.exe
</code></pre>

<p><br></p>

<h4 id="toc_3">2.2 protoc命令使用</h4>

<pre><code class="language-bash">outPath=&quot;${serviceName}pb&quot;  # 和proto文件的go_package名称一致，也就是文件夹名和包名一致  
mkdir -p ${outPath}  
  
# 生成pb.go和grpc.pb.go文件，
# pb.go文件是用于填充、序列化和检索消息类型的代码。
# _grpc.pb.go文件的客户端和服务器代码。
# 为了兼容旧版本protoc-gen-go生成代码，需要添加参数--go-grpc_opt=require_unimplemented_servers=false  
protoc --go_out=${outPath} --go_opt=paths=source_relative --go-grpc_out=${outPath} --go-grpc_opt=paths=source_relative *.proto  

# 生成pb.go和grpc.pb.go文件，使用protoc-gen-gogofaster插件，支持添加自定义tag，并且序列化和反序列化都比protoc-gen-go更快
protoc --gogofaster_out=${outPath} --gogofaster_opt=paths=source_relative --go-grpc_out=${outPath} --go-grpc_opt=paths=source_relative *.proto

# 生成*.pb.gw.go文件，web的api接口文件  
protoc --grpc-gateway_opt=paths=source_relative --grpc-gateway_out=${outPath} *.proto  

# 生成*.swagger.json文件  
protoc --openapiv2_opt=logtostderr=true --openapiv2_out=${outPath} *.proto

# 生成*.validate.go文件
protoc --validate_opt=paths=source_relative --validate_out=lang=go:${outPath} *.proto
</code></pre>

<p><br></p>

<p>旧版本protoc-gen-go生成代码命令：</p>

<blockquote>
<p>protoc &ndash;go_out=plugins=grpc:. *.proto</p>
</blockquote>

<p><br><br></p>

<h3 id="toc_4">3 protobuf简介</h3>

<p>protobuf是一种与语言无关、平台无关、可扩展的可序列化和结构化的数据描述语言(其IDL)，常用于通信协议、数据存储等，比json、XML更小，编码解码速度快得多。</p>

<p><strong>语法模板：</strong></p>

<p>最简单protobuf模板：</p>

<pre><code class="language-protobuf">syntax = &quot;proto3&quot;;

package helloworld;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}
</code></pre>

<p><br></p>

<p><strong>包括grpc-gateway和swagger文档模板</strong></p>

<pre><code class="language-protobuf">syntax = &quot;proto3&quot;;  
  
package proto;  
  
// 把google/api/annotations.proto和protoc-gen-openapiv2/options/annotations.proto文件存放在protoc的同级目录include下  
// protoc默认从同级目录include下查找  
import &quot;google/api/annotations.proto&quot;;  
import &quot;protoc-gen-openapiv2/options/annotations.proto&quot;;  
  
// 设置生成*go的包名  
option go_package = &quot;./accountpb&quot;;  
  
  
// 生成*.swagger.json文件的一些默认设置  
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {  
  info: {  
    version: &quot;2.0&quot;;  
  };  
  // 显示扩展文档  
  external_docs: {  
    url: &quot;https://baidu.com&quot;;  
    description: &quot;描述信息&quot;;  
  }  
  // 默认为HTTPS，根据实际需要设置  
  schemes: HTTP;  
};  
  
  
service Account {  
  rpc AddUser (User) returns (ID) {  
    // http设置  
    option (google.api.http) = {  
      post: &quot;/v1/addUser&quot;  
      body: &quot;*&quot;  
    };  
    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {  
      summary: &quot;添加用户&quot;,  
      description: &quot;add one User&quot;,  
      tags: &quot;addUser&quot;,  
    };  
  }  
  
  rpc GetUser (ID) returns (User) {  
    // http设置  
    option (google.api.http) = {  
      get: &quot;/v1/getUser&quot;  
    };  
    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {  
      summary: &quot;获取用户&quot;,  
      description: &quot;get one user&quot;,  
      tags: &quot;getUser&quot;,  
    };  
  }  
}  
  
message ID {  
  int64 id = 1;  
}  
  
message User {  
  int64 id = 1;  
  string name = 2;  
  string email = 3;  
}
</code></pre>

<p>注：service、rpc、message名称都是大写，而message里的字段是小写或下划线</p>

<p><br></p>

<p><strong>protobuf与go语言常见数据类型映射表：</strong></p>

<table>
<thead>
<tr>
<th align="left">proto</th>
<th align="left">go</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">bool</td>
<td align="left">bool</td>
</tr>

<tr>
<td align="left">string</td>
<td align="left">string</td>
</tr>

<tr>
<td align="left">bytes</td>
<td align="left">[]byte</td>
</tr>

<tr>
<td align="left">int32</td>
<td align="left">int32</td>
</tr>

<tr>
<td align="left">int64</td>
<td align="left">int64</td>
</tr>

<tr>
<td align="left">uint32</td>
<td align="left">uint32</td>
</tr>

<tr>
<td align="left">uint64</td>
<td align="left">uint64</td>
</tr>

<tr>
<td align="left">float</td>
<td align="left">float32</td>
</tr>

<tr>
<td align="left">double</td>
<td align="left">float64</td>
</tr>

<tr>
<td align="left">sint32, sfixed32</td>
<td align="left">int32</td>
</tr>

<tr>
<td align="left">sint64, sfixed64</td>
<td align="left">int64</td>
</tr>

<tr>
<td align="left">fixed32</td>
<td align="left">unit32</td>
</tr>

<tr>
<td align="left">fixed64</td>
<td align="left">unit64</td>
</tr>
</tbody>
</table>

<p><br></p>

<p><strong>复合类型映射表：</strong></p>

<p>(1) 数组类型</p>

<pre><code class="language-protobuf">message HelloRequest {
    repeated string name = 1；  // 等价go的[]string
}
</code></pre>

<p><br></p>

<p>(2) 嵌套类型</p>

<pre><code class="language-protobuf">message User {
    string name = 1；
}

message HelloRequest {
    repeated User users = 1；  // 等价go的[]User
}
</code></pre>

<p><br></p>

<p>(3) map</p>

<pre><code class="language-protobuf">message HelloRequest {
    map&lt;string, string&gt; names = 2；  // 等价go的map[string]striing
}
</code></pre>

<p><br></p>

<h3 id="toc_5">4 grpc使用示例</h3>

<h4 id="toc_6">4.1 一些调试grpc工具</h4>

<p><strong>(1) bloomrpc</strong></p>

<p>bloomRPC旨在为探索和查询 GRPC 服务提供最简单、最高效的开发人员体验，通过界面调试。</p>

<p>github: <a href="https://github.com/bloomrpc/bloomrpc" rel="nofollow">https://github.com/bloomrpc/bloomrpc</a></p>

<p><br></p>

<p>启动rpc服务端，导入proto文件，填写ip和端口，点击中间绿色按钮调用，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/bloomRPC.jpg" alt="bloomRPC" /></p>

<p><br></p>

<p><strong>(2) evans</strong></p>

<p><a href="https://github.com/ktr0731/evans" rel="nofollow">Evans</a> 是通过命令行容易调试 gRPC 客户端工具，命令中自带自动提示功能，使用非常方便</p>

<p><br></p>

<p><strong>(3) grpcurl</strong></p>

<p><a href="https://github.com/fullstorydev/grpcurl" rel="nofollow">grpcurl</a>是一个命令行工具，可让您与gRPC服务器进行交互，基本上是curl针对gRPC服务器的。</p>

<p><br></p>

<h4 id="toc_7">4.2 protobuf序列化和反序列化</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/tree/main/protobuf" rel="nofollow">protobuf示例代码</a></p>

<p><br></p>

<h4 id="toc_8">4.3 四种调用方式</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/tree/main/helloworld" rel="nofollow">helloworld示例代码</a></p>

<p><br></p>

<h4 id="toc_9">4.4 日志打印</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/logging" rel="nofollow">日志示例代码</a></p>

<p><br></p>

<h4 id="toc_10">4.5 元数据操作</h4>

<p>在HTTP/1.1中，通常通过Header来传递数据，对于grpc(HTTP/2)来说，很少直接用Header传递数据，一般使用metadata来传递和操作数据，metadata是一个map结构(map[string][]string)，共有两种创建方式：</p>

<ul>
<li>直接使用函数 metadata.New(map[string]string{})</li>
<li>直接调用函数 metadata.Pairs(key,value)，默认会把key转为小写，如果key相同，会追加到对应key的[]string上</li>
</ul>

<p>在grpc中，为了防止metadata从入站rpc直接转发到出站rpc情况，因此metadata分为传入和传出两种：</p>

<ul>
<li>metadata.NewIncomingContext: 创建一个附加了传入metadata的新上下文，仅供自身的grpc使用。</li>
<li>metadata.NewOutgoingContext: 创建一个附加了传出metadata的新上下文，仅供外部的grpc使用。</li>
</ul>

<p>在grpc中，metadata是存储在context的，context中的数据是在请求的Header中的，因此通过Header可以看到metadata数据。</p>

<p>设置自定义metadata信息示例：</p>

<pre><code class="language-go">key = &quot;authorization&quot;

// 创建 metadata 和 context
md := metadata.Pairs(key, &quot;Bearer eyJhb...ssw5c&quot;)
ctx := metadata.NewOutgoingContext(context.Background(), md)
</code></pre>

<p><br></p>

<p>读出自定义metadata信息示例：</p>

<pre><code class="language-go">key = &quot;authorization&quot;

// 使用metadata包读取key
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
    return status.Errorf(codes.DataLoss, &quot;failed to get metadata&quot;)
}
if authorization, ok := md[key]; ok {
    fmt.Printf(&quot;metadata: %s=%v\n&quot;, key, authorization)
} else {
    fmt.Printf(&quot;not found '%s' in metadata\n&quot;, key)
}


// 或使用封装好的包metautils读取key
authorization := metautils.ExtractIncoming(ctx).Get(key)
fmt.Println(color1)
</code></pre>

<p><br></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/metadata" rel="nofollow">元数据示例代码</a></p>

<p><br></p>

<h4 id="toc_11">4.6 拦截器</h4>

<p>grpc拦截器(Interceptor)可以在每一个RPC方法的前面或后面做统一的特殊处理，并且不直接侵入业务代码， 例如鉴权校验、超时控制、日志记录、链路跟踪等。拦截器的类型分为两种：</p>

<ul>
<li>一元拦截器(UnaryInterceptor)：拦截和处理一元RPC调用。</li>
<li>流拦截器(StreamInterceptor)：拦截和处理流式RPC调用。</li>
</ul>

<p>由于客户端和服务端有各自的一元拦截器和流拦截器， 因此，在gRPC中， 也可以说共有四种类型的拦截器。</p>

<ul>
<li>服务端一元拦截器(StreamServerInterceptor)</li>
<li>服务端流拦截器(StreamServerInterceptor)</li>
<li>客户端一元拦截器(UnaryClientInterceptor)</li>
<li>客户端流拦截器(StreamClientInterceptor)</li>
</ul>

<p>因为grpc拦截器类型不能重复，当需要多个拦截器时，借助go-grpc-middleware库来实现，安装库</p>

<blockquote>
<p>go get -u github.com/grpc-ecosystem/go-grpc-middleware</p>
</blockquote>

<p>go-grpc-middleware 拦截器分类：</p>

<p><a href="https://golangrepo.com/repo/grpc-ecosystem-go-grpc-middleware-go-network#auth" rel="nofollow">Auth</a></p>

<ul>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/auth" rel="nofollow"><code>grpc_auth</code></a> 一个可定制的（通过AuthFunc）的认证中间件</li>
</ul>

<p><a href="https://golangrepo.com/repo/grpc-ecosystem-go-grpc-middleware-go-network#logging" rel="nofollow">Logging</a></p>

<ul>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/tags" rel="nofollow"><code>grpc_ctxtags</code></a> - 一个为上下文添加标签图的库，数据由请求主体填充</li>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/logging/zap" rel="nofollow"><code>grpc_zap</code></a> 将zap日志库整合到gRPC处理程序中。</li>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/logging/logrus" rel="nofollow"><code>grpc_logrus</code></a> 将logrus日志库整合到gRPC处理程序中。</li>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/logging/kit" rel="nofollow"><code>grpc_kit</code></a> 将go-kit日志库整合到gRPC处理程序中。</li>
</ul>

<p><a href="https://golangrepo.com/repo/grpc-ecosystem-go-grpc-middleware-go-network#monitoring" rel="nofollow">Monitoring</a></p>

<p>grpc_prometheus ⚡ -  ⚡ - OpenTracing grpc_opentracing -</p>

<ul>
<li><a href="https://github.com/grpc-ecosystem/go-grpc-prometheus" rel="nofollow"><code>grpc_prometheus</code> ⚡</a> 普罗米修斯客户端和服务器端监控中间件 otgrpc</li>
<li><a href="https://github.com/grpc-ecosystem/grpc-opentracing/tree/master/go/otgrpc" rel="nofollow"><code>otgrpc</code> ⚡</a> - <a href="http://opentracing.io/" rel="nofollow">OpenTracing</a> 客户端和服务器端拦截器</li>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/tracing/opentracing" rel="nofollow"><code>grpc_opentracing</code></a> OpenTracing客户端和服务器端拦截器，支持流和处理程序返回的标记。</li>
</ul>

<p><a href="https://golangrepo.com/repo/grpc-ecosystem-go-grpc-middleware-go-network#client" rel="nofollow">Client</a></p>

<ul>
<li><a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/retry" rel="nofollow"><code>grpc_retry</code></a> 一个通用的gRPC响应代码重试机制</li>
</ul>

<p><a href="https://golangrepo.com/repo/grpc-ecosystem-go-grpc-middleware-go-network#server" rel="nofollow">Server</a>
grpc_validator -  grpc_recovery -  ratelimit -
-   <a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/validator" rel="nofollow"><code>grpc_validator</code></a> 来自.proto选项的codegen入站消息验证
-   <a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/recovery" rel="nofollow"><code>grpc_recovery</code></a> 将恐慌转化为gRPC错误
-   <a href="https://golangrepo.com/grpc-ecosystem/go-grpc-middleware/blob/master/ratelimit" rel="nofollow"><code>ratelimit</code></a> 由你自己的限制器限制grpc速率</p>

<p><br></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/interceptor" rel="nofollow">拦截器示例代码</a></p>

<p><br></p>

<h4 id="toc_12">4.7 keepalive</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/keepalive" rel="nofollow">keepalive示例代码</a></p>

<p><br></p>

<h4 id="toc_13">4.8 超时</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/timeout" rel="nofollow">超时示例代码</a></p>

<p><br></p>

<h4 id="toc_14">4.9 recovery</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/recovery" rel="nofollow">recovery示例代码</a></p>

<p><br></p>

<h4 id="toc_15">4.10 生成swagger接口文档</h4>

<p>(1) 安装插件 protoc-gen-openapiv2</p>

<pre><code class="language-bash"># windows环境
wget https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v2.10.0/protoc-gen-openapiv2-v2.10.0-windows-x86_64.exe

# 下载完成后改名为protoc-gen-openapiv2.exe,并移动到$GOPATH/bin/目录下
</code></pre>

<p><br></p>

<p>(2) 下载swagger UI文件</p>

<pre><code class="language-bash"># 下载swagger UI文件，然后解压
wget https://github.com/swagger-api/swagger-ui/archive/v3.37.0.zip

# 在$GOPATH/bin/include/目录下新建swagger目录，在$GOPATH/bin目录下有protoc文件
mkdir -p $GOPATH/bin/include/swagger

# 把swagger-ui里的dist目录下所有文件移动到$GOPATH/bin/include/swagger/
</code></pre>

<p><br></p>

<p>(3) 安装go-bindata和go-bindata-assetfs库</p>

<p>go-bindata工具主要为了将swagger-ui静态文件转为go代码，go-bindata-assetfs库是为了使外能够部访问swagger UI。</p>

<pre><code class="language-bash">go get -u github.com/go-bindata/go-bindata/...
go get -u github.com/elazarl/go-bindata-assetfs/...
</code></pre>

<p><br></p>

<p>(4)  将swagger静态资源转为go代码</p>

<pre><code class="language-bash"># 创建目录swagger-ui和swagger，其中swagger-ui存放dist目录下所有静态文件，swagger存放把静态文件转换后的go文件
mkdir -p pkg/swagger-ui pkg/swagger

# 转换为go
go-bindata --nocompress -pkg=swagger -o=pkg/swagger/data.go pkg/swagger-ui/...
</code></pre>

<p>把转换后的data.go文件复制到当前swagger目录下。</p>

<p><br></p>

<p>(5) 测试swagger UI服务</p>

<p>点击查看完整的<a href="https://github.com/zhufuyi/grpc_examples/blob/main/swagger-ui" rel="nofollow">swagger-ui示例代码</a></p>

<p>启动服务，在浏览器访问 <a href="http://127.0.0.1:8080/swagger-ui/" rel="nofollow">http://127.0.0.1:8080/swagger-ui/</a> ，把 <a href="http://127.0.0.1:8080/swagger/hello.swagger.json" rel="nofollow">http://127.0.0.1:8080/swagger/hello.swagger.json</a> 复制到swagger界面执行，就可以执行接口测试了。</p>

<p>注：hello.swagger.json中的schemes字段值为空，在swagger测试时默认使用https，导致本来http接口无法访问，所以需要在生成的*.swagger.json文档中手动加入schemes字段，这样可以选择https或http来测试接口。</p>

<pre><code class="language-json">  &quot;schemes&quot;:[
    &quot;https&quot;,
    &quot;http&quot;
  ],
</code></pre>

<p><br></p>

<h4 id="toc_16">4.11 validate</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/validate" rel="nofollow">validate示例代码</a></p>

<p><br></p>

<h4 id="toc_17">4.12 tag</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/tag" rel="nofollow">tag示例代码</a></p>

<p><br></p>

<h4 id="toc_18">4.13 TLS</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/security/tls" rel="nofollow">TLS示例代码</a></p>

<p><br></p>

<h4 id="toc_19">4.14 JWT</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/security/jwt_token" rel="nofollow">JWT示例代码</a></p>

<p><br></p>

<h4 id="toc_20">4.15 restful api 调用 grpc</h4>

<p>gRPC-Gateway 是 Google 协议缓冲区编译器 protoc 的 插件。它读取 protobuf 服务定义并生成一个反向代理服务器，该服务器将 RESTful HTTP API 转换为 gRPC。该服务器是根据 google.api.http 您的服务定义中的注释生成的。</p>

<p><img src="https://go-sponge.com/assets/images/blog/grpc-gateway.jpg" alt="grpc-gateway框架图" /></p>

<p><br></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/loadbalance/client_loadbalance" rel="nofollow">grpc-gateway示例代码</a></p>

<p><br></p>

<h4 id="toc_21">4.16 重试</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/retry" rel="nofollow">重试示例代码</a></p>

<p><br></p>

<h4 id="toc_22">4.17 限流</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/ratelimit" rel="nofollow">限流示例代码</a></p>

<p><br></p>

<h4 id="toc_23">4.18 注册与发现</h4>

<p>在分布式系统中，为了实现高可用，通常同一个服务会部署多个，为了使访问流量要均衡分散到多个服务上。</p>

<p>客户端要访问服务端，需要知道服务端ip地址和端口，如果服务数量比较少，并且服务不会频繁更改ip和端口，人还可以处理，如果服务数量多了，通过人工力量处理就非常麻烦了，需要动态获取服务端地址，也就是服务注册与发现，常见角色：</p>

<ul>
<li>注册中心：承担对服务信息进行注册、协调、管理等工作。</li>
<li>服务提供者(服务端): 暴露特定端口，并提供一个到多个的服务来允许外部访问。</li>
<li>服务消费者(客户端): 调用服务方。</li>
</ul>

<p>服务注册与发现原理：&rdquo;服务提供者&rdquo;在启动服务时会将自己的服务信息(ip地址、端口号、版本号等)注册到&rdquo;注册中心&rdquo;。&rdquo;服务消费者&rdquo;在进行调用时，会以约定命名标识(如服务名)到&rdquo;注册中心&rdquo;查询，发现当前哪些具体的服务可以调用。&rdquo;注册中心&rdquo;再根据约定的负载均衡算法进行调度，最终请求到服务提供者。</p>

<p>另外，当&rdquo;服务提供者&rdquo;出现问题时，或是当定期的&rdquo;心跳检测&rdquo;发现&rdquo;服务提供者&rdquo;无正确响应时，那么这个出现问题的服务就会被下线，并标识为不可用。即在启动时上报&rdquo;注册中心&rdquo;进行注册，把被检测到出问题的服务下线，以此来维护服务注册和发现。</p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/registerDiscovery" rel="nofollow">etcd服务注册与发现示例代码</a></p>

<p><br></p>

<h4 id="toc_24">4.19 负载均衡</h4>

<p>常见的负载均衡有客户端负载均衡和服务端负载均衡。</p>

<p>(1) 客户端负载均衡</p>

<p>客户端负载是指在调用时，由客户端到&rdquo;注册中心&rdquo;对服务提供者进行查询，并获取所需的服务清单。服务清单中包含各个服务的实际信息(如ip地址、端口号、集群命名空间等)。由客户端使用特定的负载均衡策略(如轮询)在服务清单中选择一个或多个服务进行调用。</p>

<ul>
<li>优点: 高性能、去中心化，并且不需要借助独立的外部负载均衡组件。</li>
<li>缺点: 实现成本较高， 要对不同语言的客户端实现各自对应的SDK及其负载均衡策略。</li>
</ul>

<p><br></p>

<p>(2) 服务端负载均衡</p>

<p>服务端负载，又被称为&rdquo;代理&rdquo;模式，在服务端侧搭设独立的负载均衡器，负载均衡器再根据给定的目标名称(如服务名)找到适合调用的服务实例，因此它具备负载均衡和反向代理两项功能。</p>

<ul>
<li>优点: 简单、透明，客户端不需要知道背后的逻辑，只需按给定的目标名称调用、访问即可，由服务端侧管理负载、均衡策略及代理</li>
<li>缺点: 外部的负载均衡器理论上可能成为性能瓶颈，会受到负载均衡器的吞吐率影响，并且与客户端负载相比，有可能出现更高的网络延迟。同时，必须要保持高可用，因为它是整个系统的 关键节点，一旦出现问题，影响非常大。</li>
</ul>

<p><br></p>

<p>(3) grpc官方设计思路</p>

<ul>
<li>客户端根据服务名称发起请求。</li>
<li>名称解析器解析服务名称并返回，服务名称解析成一个或多个ip地址，每个ip都会有标识，标识分为服务端地址、负载均衡地址、客户端使用的负载均衡策略。</li>
<li>客户端根据服务端类型选择相应的策略，如果grpc客户端获取的地址是负载均衡器地址，那么客户端将使用grpclb策略，否则使用服务配置请求的负载均衡策略；如果服务配置未请求负载均衡策略，则客户端默认选择第一个可用的服务端地址。</li>
<li>最后根据不同的策略进行实际调用。</li>
</ul>

<p>grpc默认支持两种负载均衡算法pick_first 和 round_robin。</p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/loadbalance/client_loadbalance" rel="nofollow">负载均衡示例代码</a></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/loadbalance/etcd_loadbalance" rel="nofollow">结合etcd做负载均衡示例代码</a></p>

<p><br></p>

<h4 id="toc_25">4.20 链路跟踪</h4>

<p>在微服务复制的分布式场景下，注入链路追踪是非常重要和必要的。做链路追踪的基本条件是注入追踪信息，而最简单的方法就是使用服务端和客户端拦截器组成完整的链路信息，具体如下：</p>

<ul>
<li>服务端拦截器：从metadata中提取链路信息， 将其设置并追加到服务端的调用上下文中。也就是说，如果发现本次调用并没有上一级的链路信息，那么它将会生成对应的父级信息，自己成为父级；如果发现本次调用存在既有的上一级链路信息，那么它将会根据上一级链路信息进行设置，成为其子级。</li>
<li>客户端拦截器：从调用的上下文中提取链路信息， 并将其作为metadata追加到rpc调用中。</li>
</ul>

<p>借助OpenTracing API和Jaeger Client两个go库实现与追踪系统对接。</p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/tracing/api2rpc" rel="nofollow">gin调用rpc的链路跟踪示例代码</a></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/tracing/rpc2rpc" rel="nofollow">rpc调用rpc的链路跟踪示例代码</a></p>

<p><br></p>

<h4 id="toc_26">4.21 熔断</h4>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/hystrix/withMetrics" rel="nofollow">熔断示例代码</a></p>

<p><br></p>

<h4 id="toc_27">4.22 prometheus监控</h4>

<p><strong>固定标签</strong></p>

<p>所有服务器端指标都以<code>grpc_server</code>名称开头。所有客户端指标都以<code>grpc_client</code>. 他们都有镜像概念，所有方法都包含相同的丰富标签：</p>

<ul>
<li><p><code>grpc_service</code>- <a href="http://www.grpc.io/docs/#defining-a-service" rel="nofollow">gRPC 服务</a>名称，它是 protobuf<code>package</code>和<code>grpc_service</code>部分名称的组合。例如 <code>package = mwitkow.testproto</code>和 <code>service TestService</code>组合的标签是<code>grpc_service=&quot;mwitkow.testproto.TestService&quot;</code></p></li>

<li><p><code>grpc_method</code>- 在 gRPC 服务上调用的方法的名称。例如  <code>grpc_method=&quot;Ping&quot;</code></p></li>

<li><p><code>grpc_type</code>- gRPC<a href="http://www.grpc.io/docs/guides/concepts.html#rpc-life-cycle" rel="nofollow">类型的请求</a>。区分两者非常重要，尤其是对于延迟测量。</p>

<ul>
<li><code>unary</code>是单请求单响应 RPC</li>
<li><code>client_stream</code>是一个多请求、单响应的 RPC</li>
<li><code>server_stream</code>是一个单请求、多响应的 RPC</li>
<li><code>bidi_stream</code>是一个多请求、多响应的 RPC</li>
</ul></li>
</ul>

<p>此外，对于已完成的 RPC，使用以下标签：</p>

<ul>
<li><p><code>grpc_code</code>- 人类可读的<a href="https://github.com/grpc/grpc-go/raw/master/codes/codes.go" rel="nofollow">gRPC 状态码</a>。所有状态的列表都很长，但这里有一些常见的：</p>

<ul>
<li><code>OK</code>- 表示 RPC 成功</li>
<li><code>IllegalArgument</code>- RPC 包含错误值</li>
<li><code>Internal</code>- 未向客户端披露服务器端错误</li>
</ul></li>
</ul>

<p><br></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/metrics/defaultMetrics" rel="nofollow">默认指标监控示例代码</a></p>

<p>点击查看<a href="https://github.com/zhufuyi/grpc_examples/blob/main/metrics/customizedMetrics" rel="nofollow">自定义指标监控示例代码</a></p>

<p><br></p>

<p>把client和server配置到prometheus之后，就可以在prometheus进行一些有用的查询了。</p>

<pre><code class="language-bash"># 1分钟请求率 qps
sum(rate(grpc_server_started_total{job=&quot;hello_grpc_server&quot;}[1m])) by (grpc_service)

# 一元请求错误率
sum(rate(grpc_server_handled_total{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;unary&quot;,grpc_code!=&quot;OK&quot;}[1m])) by (grpc_service)

# 一元请求错误百分比
sum(rate(grpc_server_handled_total{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;unary&quot;,grpc_code!=&quot;OK&quot;}[1m])) by (grpc_service) / sum(rate(grpc_server_started_total{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;unary&quot;}[1m])) by (grpc_service) * 100.0

# 平均响应流大小
sum(rate(grpc_server_msg_sent_total{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;server_stream&quot;}[10m])) by (grpc_service) / sum(rate(grpc_server_started_total{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;server_stream&quot;}[10m])) by (grpc_service)

# 一元请求的 99% 延迟
histogram_quantile(0.99, sum(rate(grpc_server_handling_seconds_bucket{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;unary&quot;}[5m])) by (grpc_service,le))

# 慢速一元查询的百分比 (&gt;250ms)
100.0 - (sum(rate(grpc_server_handling_seconds_bucket{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;unary&quot;,le=&quot;0.25&quot;}[5m])) by (grpc_service) / sum(rate(grpc_server_handling_seconds_count{job=&quot;hello_grpc_server&quot;,grpc_type=&quot;unary&quot;}[5m])) by (grpc_service)) * 100.0
</code></pre>

<p><br></p>

<h4 id="toc_28">4.23 grpc错误处理</h4>

<p>grpc返回字段有Code和Message两部分，官方定义的状态码如下：</p>

<table>
<thead>
<tr>
<th align="left">Code</th>
<th align="left">状态码</th>
<th align="left">说明</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">0</td>
<td align="left">OK</td>
<td align="left">成功</td>
</tr>

<tr>
<td align="left">1</td>
<td align="left">Canceled</td>
<td align="left">该操作被调用方取消</td>
</tr>

<tr>
<td align="left">2</td>
<td align="left">Unknown</td>
<td align="left">未知错误，如果不是grpc状态类型都统一归为未知错误，一般是用户自定义错误</td>
</tr>

<tr>
<td align="left">3</td>
<td align="left">InvalidArgument</td>
<td align="left">无效参数</td>
</tr>

<tr>
<td align="left">4</td>
<td align="left">DeadlineExceeded</td>
<td align="left">在操作完成之前超过了约定的最后期限</td>
</tr>

<tr>
<td align="left">5</td>
<td align="left">NotFound</td>
<td align="left">找不到</td>
</tr>

<tr>
<td align="left">6</td>
<td align="left">AlreadyExists</td>
<td align="left">已经存在</td>
</tr>

<tr>
<td align="left">7</td>
<td align="left">PermissionDenied</td>
<td align="left">权限不足</td>
</tr>

<tr>
<td align="left">8</td>
<td align="left">ResourceExhausted</td>
<td align="left">资源耗尽</td>
</tr>

<tr>
<td align="left">9</td>
<td align="left">FailedPrecondition</td>
<td align="left">该操作被拒绝，因为未处于执行该操作所需的态</td>
</tr>

<tr>
<td align="left">10</td>
<td align="left">Aborted</td>
<td align="left">该操作被中止</td>
</tr>

<tr>
<td align="left">11</td>
<td align="left">OutOfRange</td>
<td align="left">超出范围，尝试执行的操作超出了约定的有</td>
</tr>

<tr>
<td align="left">12</td>
<td align="left">Unimplemented</td>
<td align="left">未实现</td>
</tr>

<tr>
<td align="left">13</td>
<td align="left">Internal</td>
<td align="left">内部错误</td>
</tr>

<tr>
<td align="left">14</td>
<td align="left">Unavailable</td>
<td align="left">该服务当前不可用</td>
</tr>

<tr>
<td align="left">15</td>
<td align="left">DataLoss</td>
<td align="left">不可恢复的数据丢失或损坏</td>
</tr>

<tr>
<td align="left">16</td>
<td align="left">Unauthenticated</td>
<td align="left">身份验证元数据无效或凭据回调失败</td>
</tr>
</tbody>
</table>

<p><br></p>

<p>在grpc的状态信息中一共包含三个属性，分别是错误码(code)、错误消息(message)、错误信息详情(Details)，从any.proto文件引入detail字段，作为应用程序的错误码原型，重新封装grpc错误码和业务错误码。点击查看<a href="https://github.com/zhufuyi/grpc_examples/tree/main/pkg/errcode" rel="nofollow">重新封装grpc错误码</a>。</p>

<p>外部客户端可以直接调用 errcode.ToGRPCERROR(errcode.ERROR_LOGIN_DAIL)返回错误信息，而内部客户端获取错误详情代码如下：</p>

<pre><code class="language-go">    err := errcode.ToGRPCERROR(errcode.ERROR_LOGIN_DAIL)
    details := errcode.FromError(err).Details()
</code></pre>

<p><br><br></p>

<h3 id="toc_29">5 注意事项</h3>

<h4 id="toc_30">5.1 使用grpc-gateway注意事项</h4>

<p>(1) 生成swagger.json，默认使用HTTPS，但是rpc和web并没有设置TLS传输，请求会出错。</p>

<p>使用swagger-ui测试接口，返回错误 &ldquo;  TypeError: Failed to fetch&rdquo;，原因是使用https调用接口，而服务端web并没有开启https，调用会出错。</p>

<p>解决办法：</p>

<pre><code>设置swagger.json，把HTTPS改为HTTP
&quot;schemes&quot;: [  
  &quot;http&quot;  
]

或者修改proto文件，然后重新生成swagger.json
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {  
  // 默认为HTTPS，根据实际需要设置  
  schemes: HTTP;
}
</code></pre>

<p><br></p>

<p>(2) grpc-gateway的web服务注册路由<code>pb.Register***HandlerFromEndpoint(ctx, gwMux, grpcAddr, options)</code>的option和grpc服务<code>grpc.NewServer(options...)</code>的option设置要一致，要么同时使用TLS传输，要么同时取消TLS传输，否则报错类似<code>http: TLS handshake error from 127.0.0.1:14323: remote error: tls: unknown certificate</code></p>

<p>如果grpc使用了TLS传输，web服务建议也使用，web服务使用TLS监听服务，：<code>http.ListenAndServeTLS(webAddr, certfile.Path(&quot;server.crt&quot;), certfile.Path(&quot;server.key&quot;), mux)</code>，此时swagger-ui都是使用https访问。</p>

<p><br></p>

<h4 id="toc_31">5.2 注意ctx混淆使用</h4>

<p>(1) rpc的client端设置ctx，ctx的value通过header传递，server端接收到header，转为ctx。</p>

<pre><code class="language-go">// client端
md := metadata.Pairs(
  &quot;uid&quot;,&quot;100&quot;,
  &quot;authorization&quot;, token,
)  
ctx := metadata.NewOutgoingContext(context.Background(), md)


// 服务端读取可以使用grpc的metadata包读取，也可以用第三方封装方法读取github.com/grpc-ecosystem/go-grpc-middleware/util/metautils
metautils.ExtractIncoming(ctx).Get(&quot;uid&quot;)
metautils.ExtractIncoming(ctx).Get(&quot;authorization&quot;)
</code></pre>

<p><br></p>

<p>(2) ctx只在同一个服务内传递，新添加的kv和读取使用context的方法</p>

<pre><code class="language-go">// ctx设置value
newCtx := context.WithValue(ctx, &quot;tokenInfo&quot;, cc) // 后面方法可以通过ctx.Value(&quot;tokenInfo&quot;).(*jwt.CustomClaims)

// ctx读取key
tokenInfo, ok := ctx.Value(&quot;tokenInfo&quot;).(*auth.Token) // 从拦截器设置值读取
</code></pre>

<p><br></p>

<h4 id="toc_32">5.3 注意etcd版本问题</h4>

<p>etcd v3.5.0之后版本解决了grpc版本不兼容问题，etcd v3.5.0之后版本会优先使用代理地址代替etcd服务地址，使用时注意关闭代理。</p>
<p>本文链接：<a href="https://zhuyasen.com/post/grpc.html">https://zhuyasen.com/post/grpc.html</a>，<a href="https://zhuyasen.com/post/grpc.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>配置文件viper库</title>
            <link>https://zhuyasen.com/post/viper.html</link>
            <comments>https://zhuyasen.com/post/viper.html#comments</comments>
            <guid>https://zhuyasen.com/post/viper.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">1 viper概述</h3>

<p><a href="https://github.com/spf13/viper" rel="nofollow">viper</a>是Go应用程序的完整配置解决方案。在构建现代化应用程序的过程中，开发人员可以通过使用viper而不必考虑配置文件的格式问题，可以被认为是所有应用程序配置需求的注册表。它支持功能：</p>

<ul>
<li>设置默认值</li>
<li>从JSON，TOML，YAML，HCL和Java属性配置文件中读取</li>
<li>实时观看和重新读取配置文件(可选)</li>
<li>从环境变量中读取</li>
<li>从远程配置系统(etcd或Consul)读取，并观察变化</li>
<li>从命令行标志读取</li>
<li>从缓冲区读取</li>
<li>设置显式值</li>
</ul>

<p><br></p>

<p>viper提供的配置方式的优先级顺序如下(由高到低)：</p>

<ul>
<li>(1) 设置显示调用(explicit call to Set)</li>
<li>(2) 命令行标志(flag)</li>
<li>(3) 环境变量(env)</li>
<li>(4) 配置文件(config)</li>
<li>(5) 远程键/值存储(key/value store)</li>
<li>(6) 默认值(default)</li>
</ul>

<p>viper支持在运行时让应用程序实时读取配置文件，使应用程序一直使用最新修改的配置文件，从而不需要再重启服务。</p>

<p><br><br></p>

<h3 id="toc_1">2 解析常用配置文件示例</h3>

<p>示例展示读取yaml、toml、json配置到对象，并且实时监听更新配置文件变化。</p>

<p>文件目录如下：</p>

<pre><code class="language-bash">.
├── conf.go
├── conf_test.go
├── conf.json
├── conf.toml
└── conf.yaml
</code></pre>

<p><br></p>

<p>yaml格式配置文件conf.yaml内容如下：</p>

<pre><code class="language-yaml"># 服务名称
serverName: &quot;my server&quot;
# 监听地址
serverPort: 8080
# 运行模式，dev:开发环境，prod:正式环境
runMode: &quot;dev&quot;

# 是否开启性能分析功能，true:开启，false:关闭
isEnableProfile: true

# 输出日志级别 debug, info, warn, error
logLevel: &quot;debug&quot;

# Etcd集群地址
etcdEndpoints:
  - &quot;127.0.0.1:23791&quot;
  - &quot;127.0.0.1:23792&quot;
  - &quot;127.0.0.1:23793&quot;

# mysql配置
mysqlURL: &quot;root:123456@(127.0.0.1:3306)/user?charset=utf8&amp;parseTime=true&quot;

# mongodb配置
mongoURL: &quot;mongodb://test:123456@127.0.0.1:27017/test&quot;

# redis配置
redis:
  addr: &quot;127.0.0.1:6379&quot;
  password: &quot;123456&quot;
  db: 0

# 字典
servers:
  Beijing:
    addr: &quot;127.0.0.1&quot;
    port: &quot;20060&quot;
  Shanghai:
    addr: &quot;127.0.0.1&quot;
    port: &quot;20061&quot;
</code></pre>

<p><br></p>

<p>toml格式配置文件conf.toml内容如下：</p>

<pre><code class="language-toml"># 服务名称
serverName = &quot;my server&quot;
# 监听地址
serverPort = 8080
# 运行模式，dev:开发环境，prod:正式环境
runMode = &quot;dev&quot;

# 是否开启性能分析功能，true:开启，false:关闭
isEnableProfile = true

# 输出日志级别 debug, info, warn, error
logLevel = &quot;debug&quot;

# Etcd集群地址
etcdEndpoints = [&quot;127.0.0.1:23791&quot;, &quot;127.0.0.1:23792&quot;, &quot;127.0.0.1:23793&quot;]

# mysql配置
mysqlURL = &quot;root:123456@(127.0.0.1:3306)/user?charset=utf8&amp;parseTime=true&quot;

# mongodb配置
mongoURL = &quot;mongodb://test:123456@127.0.0.1:27017/test&quot;

# redis配置
[redis]
    addr = &quot;127.0.0.1:6379&quot;
    password = &quot;123456&quot;
    db = 0

# 字典
[servers]
    [servers.Beijing]
        addr = &quot;127.0.0.1&quot;
        port = &quot;20060&quot;
    [servers.Shanghai]
        addr = &quot;127.0.0.1&quot;
        port = &quot;20061&quot;
</code></pre>

<p><br></p>

<p>json格式配置文件conf.json内容如下：</p>

<pre><code class="language-json">{
    &quot;serverName&quot;:&quot;my server&quot;,
    &quot;serverPort&quot;:8080,
    &quot;runMode&quot;:&quot;dev&quot;,
    &quot;isEnableProfile&quot;:true,
    &quot;logLevel&quot;:&quot;debug&quot;,
    &quot;etcdEndpoints&quot;:[
        &quot;127.0.0.1:23791&quot;,
        &quot;127.0.0.1:23792&quot;,
        &quot;127.0.0.1:23793&quot;
    ],
    &quot;mysqlURL&quot;:&quot;root:123456@(127.0.0.1:3306)/user?charset=utf8&amp;parseTime=true&quot;,
    &quot;mongoURL&quot;:&quot;mongodb://test:123456@127.0.0.1:27017/test&quot;,
    &quot;redis&quot;:{
        &quot;addr&quot;:&quot;127.0.0.1:6379&quot;,
        &quot;password&quot;:&quot;123456&quot;,
        &quot;db&quot;:0
    },
    &quot;servers&quot;:{
        &quot;Beijing&quot;:{
            &quot;addr&quot;:&quot;127.0.0.1&quot;,
            &quot;port&quot;:&quot;20060&quot;
        },
        &quot;Shanghai&quot;:{
            &quot;addr&quot;:&quot;127.0.0.1&quot;,
            &quot;port&quot;:&quot;20061&quot;
        }
    }
}
</code></pre>

<p><br></p>

<p>解析配置文件conf.go内容如下：</p>

<pre><code class="language-go">package config

import (
    &quot;path&quot;
    &quot;strings&quot;

    &quot;github.com/spf13/viper&quot;
)

var conf = new(Conf)

// Conf 服务配置信息
type Conf struct {
    // 服务名称
    ServerName string `json:&quot;serverName&quot; toml:&quot;serverName&quot;`
    // 服务端口
    ServerPort int `json:&quot;serverPort&quot; toml:&quot;serverPort&quot;`
    // 运行模式
    RunMode string `json:&quot;runMode&quot; toml:&quot;runMode&quot;`

    // 是否开启go profile
    IsEnableProfile bool `json:&quot;isEnableProfile&quot; toml:&quot;isEnableProfile&quot;`

    // 输出日志级别
    LogLevel string `json:&quot;logLevel&quot; toml:&quot;logLevel&quot;`

    // Etcd集群地址
    EtcdEndpoints []string `json:&quot;etcdEndpoints&quot; toml:&quot;etcdEndpoints&quot;`

    // mysql配置
    MysqlURL string `json:&quot;mysqlURL&quot; toml:&quot;mysqlURL&quot;`

    // mongodb配置
    MongoURL string `json:&quot;mongoURL&quot; toml:&quot;mongoURL&quot;`

    // redis配置
    Redis *RedisConf `json:&quot;redis&quot; toml:&quot;redis&quot;`

    // log配置
    Servers map[string]*Servers `json:&quot;servers&quot; toml:&quot;servers&quot;`
}

// RedisConf Redis配置信息
type RedisConf struct {
    Addr     string `json:&quot;addr&quot; toml:&quot;addr&quot;`
    Password string `json:&quot;password&quot; toml:&quot;password&quot;`
    DB       int    `json:&quot;db&quot; toml:&quot;db&quot;`
}

// Servers 服务地址
type Servers struct {
    Addr string `json:&quot;addr&quot; toml:&quot;addr&quot;`
    Port string `json:&quot;port&quot; toml:&quot;port&quot;`
}

// Get 获取配置对象
func Get() *Conf {
    return conf
}

// ParseConfig 解析配置文件到对象，包括yaml、toml、json等文件
func ParseConfig(filePath string, fileName string) error {
    viper.AddConfigPath(filePath)                                  // 路径
    viper.SetConfigName(fileName)                                  // 名称
    viper.SetConfigType(strings.TrimLeft(path.Ext(fileName), &quot;.&quot;)) // 从文件名中获取配置类型

    err := viper.ReadInConfig()
    if err != nil {
        return err
    }

    err = viper.Unmarshal(conf)
    if err != nil {
        return err
    }

    // 监听配置文件更新
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        viper.Unmarshal(conf)
    })

    return nil
}
</code></pre>

<p><br></p>

<p>conf_test.go文件内容如下：</p>

<pre><code class="language-go">package config

import (
    &quot;testing&quot;

    &quot;github.com/k0kubun/pp&quot;
)

func TestParseYAML(t *testing.T) {
    err := ParseConfig(&quot;./&quot;, &quot;conf.yaml&quot;) // 解析yaml文件
    if err != nil {
        t.Error(err)
        return
    }

    pp.Println(Get())
}

func TestParseTOML(t *testing.T) {
    err := ParseConfig(&quot;./&quot;, &quot;conf.toml&quot;) // 解析toml文件
    if err != nil {
        t.Error(err)
        return
    }

    pp.Println(Get())
}

func TestParseJSON(t *testing.T) {
    err := ParseConfig(&quot;./&quot;, &quot;conf.json&quot;) // 解析json文件
    if err != nil {
        t.Error(err)
        return
    }

    pp.Println(Get())
}

// 测试更新配置文件
func TestWatch(t *testing.T) {
    err := ParseConfig(&quot;./&quot;, &quot;conf.yaml&quot;)
    if err != nil {
        t.Error(err)
        return
    }

    for i := 0; i &lt; 30; i++ {
        fmt.Println(&quot;port:&quot;, Get().ServerPort)
        time.Sleep(time.Second)
    }
}
</code></pre>

<p>经过测试，yaml、toml、json这三个文件解析结果都是一致的。</p>

<p><br></p>

<p>参考：<a href="https://blog.csdn.net/cs380637384/article/details/81217767" rel="nofollow">https://blog.csdn.net/cs380637384/article/details/81217767</a></p>
<p>本文链接：<a href="https://zhuyasen.com/post/viper.html">https://zhuyasen.com/post/viper.html</a>，<a href="https://zhuyasen.com/post/viper.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>TLS和SSL</title>
            <link>https://zhuyasen.com/post/certificate.html</link>
            <comments>https://zhuyasen.com/post/certificate.html#comments</comments>
            <guid>https://zhuyasen.com/post/certificate.html</guid>
            <description>
                <![CDATA[<blockquote>

<h4 id="toc_0">1.1 TLS/SSL基本概念</h4>

<p>SSL(Secure Socket Layer 安全套接层)是基于HTTPS下的一个协议加密层，起初是因为HTTP在传输数据时使用的是明文，是不安全的，为了解决这一隐患网景公司(Netscape)推出了SSL安全套接字协议层，SSL是基于HTTP标准并对TCP传输数据时进行加密，在HTTP和TCP之间，所以HPPTS是HTTP+SSL/TCP的简称。</p>

<p>TLS(Transport Layer Security)是传输层安全性协议，是IETF把SSL经过标准化的传输协议，可以看作是SSL的升级版，目的是保障互联网通信提供安全性和数据完整性。事实上我们现在用的都是TLS，但因为历史上习惯了SSL这个称呼。</p>

<p>目前应用最广泛的是TLS 1.0，但是主流浏览器都已经实现了TLS 1.2的支持。TLS 1.0通常被标示为SSL 3.1，TLS 1.1为SSL 3.2，TLS 1.2为SSL 3.3。</p>

<p>SSL/TLS有单向认证和双向认证两种方式：</p>

<ul>
<li>单向认证指的是只有一个对象校验对端的证书合法性，通常都是client来校验服务器的合法性，那么client需要一个ca.crt，服务器需要server.crt、server.key。</li>
<li>双向认证指的是相互校验，服务器需要校验每个client，client也需要校验服务器。server需要server.key 、server.crt 、ca.crt文件；client也需要client.key 、client.crt 、ca.crt文件。</li>
</ul>

<p><br></p>

<h4 id="toc_1">1.2 TLS/SSL握手通信机制</h4>

<p>TLS/SSL协议的基本过程：</p>

<ul>
<li>客户端向服务器端索要并验证公钥。</li>
<li>双方协商生成&rdquo;对话密钥&rdquo;。</li>
<li>双方采用&rdquo;对话密钥&rdquo;进行加密通信。</li>
</ul>

<p>客户端和服务器端在正式通信之前经过握手阶段，&rdquo;握手阶段&rdquo;涉及四次通信，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/929654_1_TLS%20handshake.png" alt="&quot;tls握手&quot;" /></p>

<p>&ldquo;握手阶段&rdquo;的所有通信都是明文的，握手完成之后是通信内容是经过秘钥加密的，握手过程说明如下：</p>

<p><br></p>

<p><strong>(1) 客户端发出请求(ClientHello)</strong></p>

<p>首先，客户端(通常是浏览器)先向服务器发出加密通信的请求，这被叫做ClientHello请求，在这一步客户端主要向服务器提供以下信息。</p>

<ul>
<li>支持的协议版本，比如TLS 1.0版。</li>
<li>一个客户端生成的随机数，稍后用于生成&rdquo;对话密钥&rdquo;。</li>
<li>支持的加密方法，比如RSA公钥加密。</li>
<li>支持的压缩方法。</li>
</ul>

<p>这里需要注意的是，客户端发送的信息之中不包括服务器的域名。也就是说，理论上服务器只能包含一个网站，否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。对于虚拟主机的用户来说，这当然很不方便。2006年，TLS协议加入了一个Server Name Indication扩展，允许客户端向服务器提供它所请求的域名。</p>

<p><br></p>

<p><strong>(2) 服务器回应(SeverHello)</strong></p>

<p>服务器收到客户端请求后，向客户端发出回应，这叫做SeverHello，服务器的回应包含以下内容：</p>

<ul>
<li>确认使用的加密通信协议版本，比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致，服务器关闭加密通信。</li>
<li>一个服务器生成的随机数，稍后用于生成&rdquo;对话密钥&rdquo;。</li>
<li>确认使用的加密方法，比如RSA公钥加密。</li>
<li>服务器证书。</li>
</ul>

<p>除了上面这些信息，如果服务器需要确认客户端的身份，就会再包含一项请求，要求客户端提供&rdquo;客户端证书&rdquo;。比如，金融机构往往只允许认证客户连入自己的网络，就会向正式客户提供USB密钥，里面就包含了一张客户端证书。</p>

<p><br></p>

<p><strong>(3) 客户端回应</strong></p>

<p>客户端收到服务器回应以后，首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期，就会向访问者显示一个警告，由其选择是否还要继续通信。如果证书没有问题，客户端就会从证书中取出服务器的公钥。然后，向服务器发送下面三项信息：</p>

<ul>
<li>一个随机数。该随机数用服务器公钥加密，防止被窃听。</li>
<li>编码改变通知，表示随后的信息都将用双方商定的加密方法和密钥发送。</li>
<li>客户端握手结束通知，表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值，用来供服务器校验。</li>
</ul>

<p>上面第一项的随机数，是整个握手阶段出现的第三个随机数，又称&rdquo;pre-master key&rdquo;。有了它以后，客户端和服务器就同时有了三个随机数，接着双方就用事先商定的加密方法，各自生成本次会话所用的同一把&rdquo;会话密钥&rdquo;。</p>

<p>注：如果前一步服务器要求客户端证书，客户端会在这一步发送证书及相关信息。</p>

<p><br></p>

<p><strong>(4) 服务器的最后回应</strong></p>

<p>服务器收到客户端的第三个随机数pre-master key之后，计算生成本次会话所用的&rdquo;会话密钥&rdquo;，向客户端最后发送下面信息：</p>

<ul>
<li>编码改变通知，表示随后的信息都将用双方商定的加密方法和密钥发送。</li>
<li>服务器握手结束通知，表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值，用来供客户端校验。</li>
</ul>

<p>整个握手阶段全部结束，接下来客户端与服务器进入加密通信，就完全是使用普通的HTTP协议，只不过用&rdquo;会话密钥&rdquo;加密内容。</p>

<p><br></p>

<h4 id="toc_2">1.3 HTTPS</h4>

<p>HTTPS(Hypertext Transfer Protocol over Secure Socket Layer)，是以安全为目标的HTTP通道，简单讲是HTTP的安全版，即HTTP和TCP之间加入TLS/SSL安全通信协议。</p>

<p>HTTPS和HTTP的区别：</p>

<ul>
<li>https协议需要到CA申请证书。</li>
<li>http是超文本传输协议，信息是明文传输；https 则是具有安全性的ssl加密传输协议。</li>
<li>http和https使用的是完全不同的连接方式，用的端口也不一样，前者是80，后者是443。</li>
<li>http的连接很简单，是无状态的；HTTPS协议是由TLS/SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议，比http协议安全。</li>
</ul>

<p><br><br></p>

<h3 id="toc_3">2 加密解密常用术语</h3>

<p>对于一份数据，通过一种算法，基于传入的密钥(一串由数字或字符组成的字符串，也称&rdquo;key&rdquo;)，将明文数据转换成了不可阅读的密文，这是&rdquo;加密&rdquo;，同样的，密文到达目的地后，需要再以相应的算法，配合一个密钥，将密文再解密成明文，这就是&rdquo;解密&rdquo;。</p>

<p><br></p>

<h4 id="toc_4">2.1 对称加密</h4>

<p>如果加密和解密使用的是同一个密钥，那么这就是&rdquo;对称密钥加解密&rdquo;，最常见的对称加密算法是DES。</p>

<p><br></p>

<h4 id="toc_5">2.2 非对称加解密</h4>

<p>如果加密和解密使用的是两个不同的密钥，那么这就是&rdquo;非对称密钥加解密&rdquo;，最常用的非对称加密算法是RSA。这两个不同的密钥一个叫作公开密钥(publickey)另一个叫私有密钥(privatekey)，公开密钥对外公开，而私有密钥则由自己保存，其实公钥和私钥并没有什么不同之处，公钥之所以成为公钥是因为它会被公开出来，产生任意份拷贝，供任何人获取，而只有服务主机持有唯一的一份私钥，这种分发模式实际上是Web站点多客户端(浏览器)与单一服务器的网络拓扑所决定的，多客户端意味着密钥能被复制和公开获取，单一服务器意味着密钥被严格控制，只能由本服务器持有，这实际上也是后面要提到的之所以能通过数据证书确定信任主机的重要原因之一。如果我们跳出web站点的拓扑环境，其实就没有什么公钥与私钥之分了，比如那些使用以密钥为身份认证的SSH主机，往往是为每一个用户单独生成一个私钥分发给他们自己保存，SSH主机会保存一份公钥，公钥私钥各有一份，都不会公开传播。</p>

<p>注：用公钥加密的数据，只能用私钥解密，公钥是无法解密的，同样用私钥加密的数据也只能用公钥来解密。</p>

<p><br></p>

<h4 id="toc_6">2.3 数字摘要</h4>

<p>在下载文件的时候经常会看到有的下载站点也提供下载文件的&rdquo;数字摘要&rdquo;，供下载者验证下载后的文件是否完整，或者说是否和服务器上的文件&rdquo;一模一样&rdquo;。其实，数字摘要就是采用单项Hash函数将需要加密的明文&rdquo;摘要&rdquo;成一串固定长度(128位)的密文，这一串密文又称为数字指纹，它有固定的长度，而且不同的明文摘要成密文，其结果总是不同的，同样的明文其摘要必定一致。 因此，&rdquo;数字摘要&rdquo;叫&rdquo;数字指纹&rdquo;可能会更贴切一些。&rdquo;数字摘要&rdquo;是https能确保数据完整性和防篡改的根本原因。</p>

<p><br></p>

<h4 id="toc_7">2.4 数字签名</h4>

<p>数字签名是水到渠成的技术，有了&rdquo;非对称密钥加解密&rdquo;和&rdquo;数字摘要&rdquo;两项技术之后，我们能做些什么呢？假如发送方想把一份报文发送给接收方，在发送报文前，发送方用一个哈希函数从报文文本中生成报文摘要，然后用自己的私人密钥对这个摘要进行加密，这个加密后的摘要将作为报文的&rdquo;签名&rdquo;和报文一起发送给接收方，接收方首先用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要，接着再用发送方的公用密钥来对报文附加的数字签名进行解密，如果这两个摘要相同、那么接收方就能确认报文是从发送方发送且没有被遗漏和修改过，这就是结合&rdquo;非对称密钥加解密&rdquo;和&rdquo;数字摘要&rdquo;技术所能做的事情，这也就是人们所说的&rdquo;数字签名&rdquo;技术。在这个过程中，对传送数据生成摘要并使用私钥进行加密的过程就是生成&rdquo;数字签名&rdquo;的过程，经过加密的数字摘要，就是人们所说的&rdquo;数字签名&rdquo;。</p>

<p>数字签名技术就是对&rdquo;非对称密钥加解密&rdquo;和&rdquo;数字摘要&rdquo;两项技术的应用，它将摘要信息用发送者的私钥加密，与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息，然后用HASH函数对收到的原文产生一个摘要信息，与解密的摘要信息对比。如果相同，则说明收到的信息是完整的，在传输过程中没有被修改，否则说明信息被修改过，因此数字签名能够验证信息的完整性。数字签名只能验证数据的完整性，数据本身是否加密不属于数字签名的控制范围。</p>

<p>综上所述，数字签名有两种功效：一是能确定消息确实是由发送方签名并发出来的，因为别人假冒不了发送方的签名。二是数字签名能确定消息的完整性。</p>

<p><br></p>

<h4 id="toc_8">2.5 数字证书</h4>

<p>数字证书是值得信赖的公钥，只从&rdquo;准确认证发送方身份&rdquo;和&rdquo;确保数据完整性&rdquo;两个安全方面来看，数字签名似乎已经完全做到了，还有漏洞握手过程中，用户收到的公钥是否真实可靠。传输过程会出现第三方劫持替换公钥，并串改信息可能。为了解决公钥是真实可靠问题，需要有一个权威的值得信赖的第三方机构(一般是由政府审核并授权的机构)来统一对外发放公钥，只有公钥在权威机构通过认证的，说明证书是真实可靠的。这种机构被称为证书权威机构(Certificate Authority，简称CA)，它们所发放的包含主机机构名称、公钥在内的文件就是人们所说的&rdquo;数字证书&rdquo;，数字证书包含内容：</p>

<ul>
<li>证书颁发机构的名称</li>
<li>证书本身的数字签名</li>
<li>证书持有者公钥</li>
<li>证书签名用到的Hash算法</li>
</ul>

<p>数字证书的颁发过程：用户首先产生自己的密钥对，并将公共密钥及部分个人身份信息传送给认证中心。认证中心在核实身份后，将执行一些必要的步骤，以确信请求确实由用户发送而来，然后，认证中心将发给用户一个数字证书，该证书内包含用户的个人信息和公钥信息，同时还附有认证中心的签名信息，用户拿到证书之后就可以进行相关的安全通信。数字证书各不相同，每种证书可提供不同级别的可信度。可以从证书发行机构获得您自己的数字证书。</p>

<p><br></p>

<p>浏览器默认都会内置CA根证书，其中根证书包含了CA的公钥，验证证书的有效性：</p>

<ul>
<li>如果证书颁发的机构是伪造的，浏览器不认识，直接认为是危险证书。</li>
<li>如果证书颁发的机构是确实存在，浏览器会根据CA名，找到对应内置的CA根证书、CA的公钥。用CA的公钥，对伪造的证书的摘要进行解密，发现解不了，认为是危险证书。</li>
<li>对于篡改的证书验证，使用CA的公钥对数字签名进行解密得到摘要A，然后再根据签名的Hash算法计算出证书的摘要B，对比A与B，若相等则正常，若不相等则是被篡改过的。</li>
<li>证书可在其过期前被吊销，通常情况是该证书的私钥已经失密。较新的浏览器如Chrome、Firefox、Opera和Internet Explorer都实现了在线证书状态协议(OCSP)以排除这种情形：浏览器将网站提供的证书的序列号通过OCSP发送给证书颁发机构，后者会告诉浏览器证书是否还是有效的。</li>
</ul>

<p><br><br></p>

<h3 id="toc_9">3 使用cfssl生成自签名证书</h3>

<p><a href="https://github.com/cloudflare/cfssl" rel="nofollow">cfssl</a>是用于生成颁发TLS/SSL证书的开源工具，cfssl不仅是分发证书的工具，也是证书颁发机构(CA)，对于使用HTTPS建立网站的任何人(从网站所有者到大型软件即服务公司)都是有用的。</p>

<h4 id="toc_10">3.1 安装</h4>

<p>根据系统环境，从github(<a href="https://github.com/cloudflare/cfssl/releases)下载对应版本，然后把可执行文件添加到环境变量中。" rel="nofollow">https://github.com/cloudflare/cfssl/releases)下载对应版本，然后把可执行文件添加到环境变量中。</a></p>

<p><br></p>

<h4 id="toc_11">3.2 生成自签名证书</h4>

<p><strong>(1) 生成根CA证书和私钥</strong></p>

<p>创建根CA证书和私钥的CSR(证书签名请求文件)配置文件ca-csr.json，内容如下：</p>

<pre><code class="language-json">{
    &quot;CN&quot;: &quot;myPlatform&quot;,
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;rsa&quot;,
        &quot;size&quot;: 2048
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;China&quot;,
            &quot;ST&quot;: &quot;Beijing&quot;,
            &quot;L&quot;: &quot;Beijing&quot;,
            &quot;O&quot;: &quot;OName&quot;,
            &quot;OU&quot;: &quot;OUName&quot;
        }
    ]
}
</code></pre>

<p>配置文件字段说明：</p>

<ul>
<li>CN：机构名称Comman Name，浏览器使用该字段验证网站是否合法</li>
<li>C: Country， 国家</li>
<li>ST: State，州，省</li>
<li>L: Locality，地区，城市</li>
<li>O: Organization Name，组织名称，公司名称</li>
<li>OU: Organization Unit Name，公司部门</li>
</ul>

<p><br></p>

<p>初始化CA，会生成3个文件，分别是ca.pem(ca证书文件)、ca-key.pem(ca私钥文件)、ca.csr(证书签名请求文件)，这些文件用于交叉签名或重新签</p>

<blockquote>
<p>cfssl gencert -initca config/ca-csr.json | cfssljson -bare ca -</p>
</blockquote>

<p><br></p>

<p><strong>(2) 颁发本地证书和私钥</strong></p>

<p>创建证书签名请求配置文件req-csr.json，内容如下：</p>

<pre><code class="language-json">{
    &quot;CN&quot;: &quot;myDomain&quot;,
    &quot;hosts&quot;: [
        &quot;localhost&quot;,
        &quot;127.0.0.1&quot;,
        &quot;domain_or_ip&quot;
    ],
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;rsa&quot;,
        &quot;size&quot;: 2048
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;China&quot;,
            &quot;ST&quot;: &quot;GuangDong&quot;,
            &quot;L&quot;: &quot;GuangZhou&quot;,
            &quot;O&quot;: &quot;myOName&quot;,
            &quot;OU&quot;: &quot;myOUName&quot;
        }
    ]
}
</code></pre>

<p>配置文件字段说明：</p>

<ul>
<li>hosts: 证书派发给哪些主机地址，ip或域名</li>
<li>CN：机构名称Comman Name，也可以是域名</li>
<li>C: Country， 国家</li>
<li>ST: State，州，省</li>
<li>L: Locality，地区，城市</li>
<li>O: Organization Name，组织名称，公司名称</li>
<li>OU: Organization Unit Name，公司部门</li>
</ul>

<p><br></p>

<p>生成证书策略配置文件ca-config.json，让CA软件知道颁发什么样的证书，内容如下：</p>

<pre><code class="language-json">{
    &quot;signing&quot;:{
        &quot;default&quot;:{
            &quot;expiry&quot;:&quot;43800h&quot;
        },
        &quot;profiles&quot;:{
            &quot;server&quot;:{
                &quot;expiry&quot;:&quot;43800h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;server auth&quot;
                ]
            },
            &quot;client&quot;:{
                &quot;expiry&quot;:&quot;43800h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                ]
            },
            &quot;peer&quot;:{
                &quot;expiry&quot;:&quot;43800h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;server auth&quot;,
                    &quot;client auth&quot;
                ]
            }
        }
    }
}
</code></pre>

<p>配置说明：</p>

<ul>
<li>有一个默认的配置default，或者根据需求设置profile下多种策略</li>
<li>signing: 表示证书可用于签名其它证书，生成的ca.pem证书中CA=TRUE</li>
<li>key encipherment: 表示证书可用于加密</li>
<li>server auth: 表示client可以用该CA对server提供的证书进行验证</li>
<li>client auth: 表示server可以用该CA对client提供的证书进行验证</li>
</ul>

<p><br></p>

<p>根据需求颁发不同类型的证书</p>

<pre><code class="language-bash"># 颁发服务端使用的证书，例如网站服务器
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=config/ca-config.json -profile=server config/req-csr.json | cfssljson -bare server

# 颁发双方都要身份验证的证书，例如etcd集群
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=config/ca-config.json -profile=peer config/req-csr.json | cfssljson -bare peer

# 颁发客户端证书
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=config/ca-config.json -profile=client config/req-csr.json | cfssljson -bare client
</code></pre>

<p>注：-profile参数值必须是配置ca-config.json的profiles下对应字段，如果配置文件里没有www字段，颁发证书时使用-profile=www，会报错{&ldquo;code&rdquo;:5100,&ldquo;message&rdquo;:&ldquo;Invalid policy: no key usage available&rdquo;}</p>

<p><br></p>

<p>查看证书信息</p>

<pre><code class="language-bash"># 查看cert(证书信息):
cfssl certinfo -cert myCertName.pem | jq
# 查看csr(证书签名请求)信息：
cfssl certinfo -csr myCertName.csr

# 也可以使用openssl命令查看证书和私钥信息
openssl x509 -text -noout  -in myCertName.pem
openssl rsa -text -noout -in ca-key.pem
#openssl req -text -noout -in ca.csr

# 验证派发的证书是否可用
openssl verify -CAfile ca.pem myCertName.pem
</code></pre>

<p><br><br></p>

<h3 id="toc_12">4 不同格式证书之间转换</h3>

<p>一般证书有三种格式：</p>

<ul>
<li>PEM(.pem) 前面命令生成的都是这种格式</li>
<li>DER(.cer .der) Windows 上常见</li>
<li>PKCS#12文件(.pfx .p12) Mac上常见</li>
</ul>

<pre><code class="language-bash"># PEM转换为CRT
openssl x509 -outform der -in ca.pem -out ca.crt

# PEM转换为DER
openssl x509 -outform der -in ca.pem -out ca.der

# DER转换为PEM
openssl x509 -inform der -in myserver.cer -out myserver.pem

# PEM转换为PKCS
openssl pkcs12 -export -out myserver.pfx -inkey myserver.key -in myserver.crt -certfile ca.crt

# PKCS转换为PEM
openssl pkcs12 -in myserver.pfx -out myserver2.pem -nodes
</code></pre>

<p><br><br></p>

<h3 id="toc_13">5 生成TLS/SSL证书在nginx使用示例</h3>

<p>此示例在linux环境下运行，把新生成的证书文件路径设置到nginx配置文件，使用docker启动nginx来测试。</p>

<h4 id="toc_14">5.1 创建生成证书的脚本文件</h4>

<p>脚本文件gen-server-cert.sh内容如下：</p>

<pre><code class="language-bash">#!/bin/bash


# -------------------------------- 参数判断 --------------------------------------------
params=$@

if [ $# -lt 1 ]; then
    echo &quot;param is empty&quot;
    echo &quot;usage:&quot;
    echo &quot;  \&quot;$0 zhuyasen.com\&quot; or \&quot;$0 192.168.3.100\&quot;&quot;
    exit
fi

# 用参数替换req-csr.json固定字段值domains_or_ips
hostFields=''
for val in $params; do
    hostFields=${hostFields}\\\&quot;$val\\\&quot;,
done
# 去掉最后一个逗号
hostFields=${hostFields%?}


# ---------------------------------- 创建认证中心(CA) ----------------------------------

# 创建存储证书目录和配置目录
mkdir -p certs
cd certs
mkdir -p config

# 创建根CA证书和私钥的CSR(证书签名请求文件)配置文件ca-csr.json
cat &gt; config/ca-csr.json &lt;&lt;EOF
{
    &quot;CN&quot;: &quot;myMechanism&quot;,
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;rsa&quot;,
        &quot;size&quot;: 2048
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;CN&quot;,
            &quot;ST&quot;: &quot;Beijing&quot;,
            &quot;L&quot;: &quot;Beijing&quot;,
            &quot;O&quot;: &quot;my organization&quot;,
            &quot;OU&quot;: &quot;my organization unit name&quot;
        }
    ]
}
EOF

# 生成CA证书和私钥，先判断ca-key.pem是否存在，如果存在则使用已存在的
if [[ ! -f &quot;ca-key.pem&quot; ]]; then
  echo &quot;generate new ca.pem and ca-key.pem&quot;
  cfssl gencert -initca config/ca-csr.json | cfssljson -bare ca -
else
  echo &quot;use existing ca-key.pem.&quot;
fi


# ---------------------------------- 派发证书  ----------------------------------

# 创建证书签名请求配置文件req-csr.json，这里填写申请组织信息、ip或域名
cat &gt; config/req-csr.json &lt;&lt;EOF
{
    &quot;CN&quot;: &quot;your-test-domain.com&quot;,
    &quot;hosts&quot;: [
        &quot;localhost&quot;,
        &quot;127.0.0.1&quot;,
        &quot;domains_or_ips&quot;
    ],
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;rsa&quot;,
        &quot;size&quot;: 2048
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;CN&quot;,
            &quot;ST&quot;: &quot;GuangDong&quot;,
            &quot;L&quot;: &quot;GuangZhou&quot;,
            &quot;O&quot;: &quot;communication&quot;,
            &quot;OU&quot;: &quot;cluster&quot;
        }
    ]
}
EOF


# 配置证书生成策略，让CA知道颁发什么样的证书。
cat &gt; config/ca-config.json &lt;&lt;EOF
{
    &quot;signing&quot;:{
        &quot;default&quot;:{
            &quot;expiry&quot;:&quot;87600h&quot;
        },
        &quot;profiles&quot;:{
            &quot;server&quot;:{
                &quot;expiry&quot;:&quot;8760h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;server auth&quot;
                ]
            },
            &quot;client&quot;:{
                &quot;expiry&quot;:&quot;8760h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;client auth&quot;
                ]
            },
            &quot;peer&quot;:{
                &quot;expiry&quot;:&quot;8760h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;server auth&quot;,
                    &quot;client auth&quot;
                ]
            }
        }
    }
}
EOF


tagName='\&quot;domains_or_ips\&quot;'
# 把domains_or_ips替换为输入的参数
sed -i &quot;s/$tagName/$hostFields/g&quot; config/req-csr.json

# 使用现有证书和私钥重新颁发新的证书，类型为服务端使用
for name in $params; do
  cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=config/ca-config.json -profile=server config/req-csr.json | cfssljson -bare $name
done

# 删除多余文件
ls | grep -v pem | xargs -i rm -rf {}
</code></pre>

<p><br></p>

<h4 id="toc_15">5.2 创建nginx默认配置文件</h4>

<p>nginx配置文件default.conf内容如下：</p>

<pre><code class="language-bash">server {
    server_name  localhost;

    # 证书
    listen 443 ssl;
    ssl_certificate /etc/nginx/certs/nginx.pem;
    ssl_certificate_key /etc/nginx/certs/nginx-key.pem;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;


    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

# 访问80端口是，重定向到443端口，重定地址是nginx所在宿主机的ip或域名
server{
    listen 80;
    server_name localhost;
    rewrite ^(.*)$  https://192.168.3.5 permanent;
}
</code></pre>

<p><br></p>

<h4 id="toc_16">5.3 创建启动nginx的docker启动脚本文件</h4>

<p>启动nginx脚本文件docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'
services:
  nginx:
    restart: always
    image: nginx
    container_name: nginx
    ports:
      - 80:80
      - 443:443
    volumes:
      - $PWD/conf.d:/etc/nginx/conf.d
      - $PWD/certs:/etc/nginx/certs
      #- $PWD/log:/var/log/nginx
</code></pre>

<p><br></p>

<p>上面准备的脚本文件目录：</p>

<pre><code class="language-bash">.
├── conf.d
│   └── default.conf
├── docker-compose.yml
└── gen-server-cert.sh
</code></pre>

<p><br></p>

<h4 id="toc_17">5.4 使用nginx检验证书是否可以使用</h4>

<pre><code class="language-bash"># 生成本地证书
./gen-server-cert.sh nginx

# 启动nginx
docker-compose up -d

# 在chrome浏览器打开 https://192.168.3.100，点击高级，继续前往，如果能访问，说明证书正常
# 把ca.pem改名为ca.crt，然后导入浏览器的受信任的根证书颁发机构，访问就不会警告了
</code></pre>

<p><br><br></p>

<p>参考：</p>

<ul>
<li><a href="https://github.com/cloudflare/cfssl" rel="nofollow">https://github.com/cloudflare/cfssl</a></li>
<li><a href="http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html" rel="nofollow">http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html</a></li>
<li><a href="https://laurence.blog.csdn.net/article/details/7585965" rel="nofollow">https://laurence.blog.csdn.net/article/details/7585965</a></li>
<li><a href="https://www.cnblogs.com/heart-runner/archive/2012/01/30/2332020.html" rel="nofollow">https://www.cnblogs.com/heart-runner/archive/2012/01/30/2332020.html</a></li>
<li><a href="https://blog.cloudflare.com/introducing-cfssl/" rel="nofollow">https://blog.cloudflare.com/introducing-cfssl/</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/certificate.html">https://zhuyasen.com/post/certificate.html</a>，<a href="https://zhuyasen.com/post/certificate.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>etcd基础与使用</title>
            <link>https://zhuyasen.com/post/etcd.html</link>
            <comments>https://zhuyasen.com/post/etcd.html#comments</comments>
            <guid>https://zhuyasen.com/post/etcd.html</guid>
            <description>
                <![CDATA[<blockquote>

<h2 id="toc_0">etcd基础与使用</h2>

<h3 id="toc_1">1 etcd简介</h3>

<p>etcd是一个高可用的分布式的键值对存储系统，常用做配置共享和服务发现，由CoreOS公司发起的一个开源项目，受到ZooKeeper与doozer启发而催生的项目，名称&rdquo;etcd&rdquo;源自两个想法，即Unix的&rdquo;/etc&rdquo;文件夹和&rdquo;d&rdquo;分布式系统。&rdquo;/etc&rdquo;文件夹是用于存储单个系统的配置数据的地方，而etcd用于存储大规模分布式的配置信息，etcd有如下特点：</p>

<ul>
<li>简单：基于HTTP+JSON的API，用curl就可以轻松使用。</li>
<li>可信：使用Raft算法充分实现了分布式。</li>
<li>安全：可选SSL客户认证机制。</li>
<li>快速：每个节点可支持上万QPS读写。</li>
</ul>

<p>etcd有V2和V3两个版本，V3版本供了更多功能并提高了性能，应用程序使用新的grpc API访问mvcc存储，mvcc存储区和旧存储区v2是分开且隔离的，写入存储v2不会影响mvcc存储，写入mvcc存储也不会影响存储v2。</p>

<p>API v2和API v3之间存在一些显着差异：</p>

<ul>
<li>事务：在v3中，etcd提供了多键条件事务。应用程序应使用事务代替Compare-And-Swap操作。</li>
<li>平面键空间：API v3中没有目录，只有键。例如，&rdquo;/a/b/c/&ldquo;是键。范围查询支持获取与给定前缀匹配的所有键。</li>
<li>紧凑的响应：delete操作不再返回以前的值。为了获得删除的值，可以使用事务原子地获取密钥，然后删除其值。</li>
<li>租约：替代v2 TTL；TTL绑定到租约，密钥附加到租约。TTL过期后，租约将被撤销，所有附加密钥也将被删除。</li>
</ul>

<p><br><br></p>

<h3 id="toc_2">2 etcd工作原理</h3>

<p>etcd集群本身是一个分布式系统，由多个节点相互通信构成整体对外服务，每个节点都存储了完整的数据，并且通过Raft协议保证每个节点维护的数据是一致的，在ETCD集群中任意时刻至多存在一个有效的主节点，由主节点处理所有来自客户端写操作，通过Raft协议保证写操作对状态机的改动会可靠的同步到其他节点，Raft协议如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_1_raft%E5%8D%8F%E8%AE%AE.jpg" alt="raft协议" /></p>

<p>Raft协议主要分为三个部分：选举，复制日志，安全性。</p>

<p><br></p>

<h4 id="toc_3">2.1 选举</h4>

<p>Raft协议是用于维护一组服务节点数据一致性的协议。这一组服务节点构成一个集群，并且有一个主节点来对外提供服务。当集群初始化，或者主节点挂掉后，面临一个选举问题。集群中每个节点，任意时刻处于Leader、Follower、Candidate这三个角色之一，选举特点如下：</p>

<ul>
<li>当集群初始化时候，每个节点都是Follower角色。</li>
<li>集群中存在至多1个有效的主节点，通过心跳与其他节点同步数据；</li>
<li>当Follower在一定时间内没有收到来自主节点的心跳，会将自己角色改变为Candidate，并发起一次选举投票。

<ul>
<li>当收到包括自己在内超过半数节点赞成后，选举成功。</li>
<li>当收到票数不足半数选举失败，或者选举超时。</li>
<li>若本轮未选出主节点，将进行下一轮选举(出现这种情况，是由于多个节点同时选举，所有节点均为获得过半选票)。</li>
</ul></li>
<li>Candidate节点收到来自主节点的信息后，会立即终止选举过程，进入Follower角色。</li>
</ul>

<p>为了避免陷入选举失败循环，每个节点未收到心跳发起选举的时间是一定范围内的随机值，这样能够避免2个节点同时发起选举。</p>

<p><br></p>

<h4 id="toc_4">2.2 复制日志</h4>

<p>日志复制是指主节点将每次操作形成日志条目，并持久化到本地磁盘，然后通过网络IO发送给其他节点。其他节点根据日志的逻辑时钟(TERM)和日志编号(INDEX)来判断是否将该日志记录持久化到本地。当主节点收到包括自己在内超过半数节点成功返回，那么认为该日志是可提交的(committed)，并将日志输入到状态机，将结果返回给客户端。</p>

<p>这里需要注意的是，每次选举都会形成一个唯一的TERM编号，相当于逻辑时钟，每一条日志都有全局唯一的编号。</p>

<p>主节点通过网络IO向其他节点追加日志。若某节点收到日志追加的消息，首先判断该日志的TERM是否过期，以及该日志条目的INDEX是否比当前以及提交的日志的INDEX跟早。若已过期，或者比提交的日志更早，那么就拒绝追加，并返回该节点当前的已提交的日志的编号。否则将日志追加，并返回成功。</p>

<p>当主节点收到其他节点关于日志追加的回复后，若发现有拒绝，则根据该节点返回的已提交日志编号，发生其编号下一条日志。</p>

<p>主节点向其他节点同步日志，还作了拥塞控制。主节点发现日志复制的目标节点拒绝了某次日志追加消息，将进入日志探测阶段，一条一条发送日志，直到目标节点接受日志，然后进入快速复制阶段，可进行批量日志追加。</p>

<p>按照日志复制的逻辑，我们可以看到，集群中慢节点不影响整个集群的性能。另外一个特点是，数据只从主节点复制到Follower节点，这样大大简化了逻辑流程。Raft日志复制路程如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_2_raft%E6%97%A5%E5%BF%97%E5%A4%8D%E5%88%B6.jpg" alt="raft复制日志" /></p>

<p><br></p>

<h4 id="toc_5">2.3 安全</h4>

<p><strong>选举</strong>和<strong>复制日志</strong>并不能保证节点间数据一致。当一个某个节点挂掉了，一段时间后再次重启，并刚好当选为主节点。而在其挂掉这段时间内，集群若有超过半数节点存活，集群会正常工作，那么会有日志提交，这些提交的日志无法传递给挂掉的节点。当挂掉的节点再次当选举节点，它将缺失部分已提交的日志。在这样场景下，按Raft协议，它将自己日志复制给其他节点，会将集群已经提交的日志给覆盖掉，这显然是不可接受的，对于出现这种问题解决办法：</p>

<ul>
<li><p>其他协议解决这个问题的办法是，新当选的主节点会询问其他节点，和自己数据对比，确定出集群已提交数据，然后将缺失的数据同步过来。这个方案有明显缺陷，增加了集群恢复服务的时间(集群在选举阶段不可服务)，并且增加了协议的复杂度。</p></li>

<li><p>Raft解决的办法是，在选举逻辑中，对能够成为主节点加以限制，确保选出的节点已定包含了集群已经提交的所有日志。如果新选出的主节点已经包含了集群所有提交的日志，那就不需要从和其他节点比对数据了，简化了流程，缩短了集群恢复服务的时间。</p></li>
</ul>

<p>为什么只要仍然有超过半数节点存活，一定能够选出包含所有日志数据的节点作为主节点呢？因为已经提交的日志必然被集群中超过半数节点持久化，显然前一个主节点提交的最后一条日志也被集群中大部分节点持久化。当主节点挂掉后，集群中仍有大部分节点存活，那这存活的节点中一定存在一个节点包含了已经提交的日志了，因此要求etcd集群节点数量为奇数(3，5，7，9……)。</p>

<p><br><br></p>

<h3 id="toc_6">3 ETCD应用场景</h3>

<h4 id="toc_7">3.1 服务发现</h4>

<p>ETCD服务发现示意图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_3_%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%E7%A4%BA%E6%84%8F%E5%9B%BE.jpg" alt="服务发现" /></p>

<p><br></p>

<p>服务发现是分布式系统中最常见的需要解决的问题之一，即在同一个分布式集群中的进程或服务，客户端通过名字就可以查找和连接服务端。要解决服务发现的问题，需要有下面三点：</p>

<ul>
<li>一个强一致性、高可用的服务存储目录。基于Raft算法的etcd天生就是这样一个强一致性高可用的服务存储目录。</li>
<li>一种注册服务和监控服务健康状态的机制。用户可以在etcd中注册服务，并且对注册的服务设置key TTL，定时保持服务的心跳以达到监控健康状态的效果。</li>
<li>一种查找和连接服务的机制。通过在etcd指定的主题快速找到服务地址。</li>
</ul>

<p><br></p>

<p><strong>(1) 在微服务中使用etcd服务发现</strong></p>

<p>随着Docker容器的流行，多种微服务共同协作，构成一个相对功能强大的组织架构。使用etcd服务发现机制，在etcd中注册某个服务名字的目录，在该目录下存储可用的服务节点的IP。服务使用者从etcd目录下查找可用的服务节点IP来连接和调用，达到透明化的动态添加这些服务目的，示意图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_4_%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%8D%8F%E5%90%8C%E5%B7%A5%E4%BD%9C.jpg" alt="服务发现" /></p>

<p><br></p>

<p><strong>(2) 在PaaS平台中使用etcd服务发现</strong></p>

<p>PaaS平台中的应用一般都有多个实例，通过域名不仅可以透明的对多个实例进行访问，而且还可以做到负载均衡。但是应用的某个实例随时都有可能故障重启，这时就需要动态的配置域名解析(路由)信息,通过etcd的服务发现功能就可以轻松解决这个动态配置的问题，实现多实例与实例故障重启透明化目的，示意图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_5_%E4%BA%91%E6%9C%8D%E5%8A%A1%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0.jpg" alt="服务发现" /></p>

<p><br></p>

<h4 id="toc_8">3.2 发布订阅消息</h4>

<p>etcd的发布订阅消息示意图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_6_%E8%AE%A2%E9%98%85%E6%B6%88%E8%B4%B9.jpg" alt="edct发布订阅" /></p>

<p>在分布式系统中，消息发布与订阅最适合使用用在组件之间通信。使用etcd发布订阅功能可以实现一个配置共享中心，数据提供者在配置中心发布消息，消息消费者订阅他们关心的主题，一旦主题有新消息发布，就会实时通知订阅者，通过这种方式可以做到分布式系统配置的集中式管理与动态更新。</p>

<p>etcd发布订阅最典型应用在kubernetes上，其他场景应用：</p>

<ul>
<li>app或服务用到的一些配置信息放到etcd上进行集中管理。在启动的时候主动从etcd获取一次配置信息，在etcd节点上注册一个Watcher并等待，以后每次配置有更新的时候，etcd都会实时通知订阅者，以此达到获取最新配置信息的目的。</li>
<li>分布式搜索服务中，索引的元信息和服务器集群机器的节点状态存放在etcd中，供各个客户端订阅使用。使用etcd的key TTL功能可以确保机器状态是实时更新的。</li>
<li>分布式日志收集系统。 这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用(或主题)来分配收集任务单元，因此可以在etcd上创建一个以应用(主题)命名的目录，并将这个应用(主题相关)的所有机器ip，以子目录的形式存储到目录上，然后设置一个etcd递归的Watcher，递归式的监控应用(主题)目录下所有信息的变动。这样就实现了机器IP(消息)变动的时候，能够实时通知到收集器调整任务分配。</li>
<li>系统中信息需要动态自动获取与人工干预修改信息请求内容的情况。只需要要将信息存放到指定的etcd目录中，etcd的这些目录就可以通过HTTP的接口在外部访问。</li>
</ul>

<p><br></p>

<h4 id="toc_9">3.3 负载均衡</h4>

<p>etcd的负载均衡示意图如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_7_%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1.jpg" alt="etcd负载均衡" /></p>

<p>etcd本身分布式架构存储的信息访问支持负载均衡，etcd集群化以后，每个etcd的核心节点都可以处理用户的请求。所以把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择。
etcd可以监控一个集群中多个节点的状态，利用etcd维护一个负载均衡节点表，当有一个请求发过来后，可以轮询式的把请求转发给存活着的节点。</p>

<p>分布式系统中，为了保证服务的高可用以及数据的一致性，通常都会把数据和服务部署多份，以此达到对等服务，即使其中的某一个服务失效了，也不影响使用。由此带来的坏处是数据写入性能下降，而好处则是数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据，所以用户的访问流量就可以分流到不同的机器上。</p>

<p><br></p>

<h4 id="toc_10">3.4 分布式通知与协调</h4>

<p>这里说到的分布式通知与协调，与消息发布和订阅有些相似。都用到了etcd中Watche机制，通过注册与异步通知机制，实现分布式环境下不同系统之间 的通知与协调，从而对数据变更做到实时处理。实现方式：不同系统都在etcd上对同一个目录进行注册，同时设置Watcher观测该目录的变化(如果对子目录的变化也有需要，可以设置递归模式)，当某个系统更新了etcd的目录，那么设置了Watcher的系统就会收到通知，并作出相应处理。</p>

<p>使用etcd完成分布式协同工作原理如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_8_%E9%80%9A%E7%9F%A5%E4%B8%8E%E5%8D%8F%E8%B0%83.jpg" alt="etcd通知与协调" /></p>

<ul>
<li><strong>通过etcd进行低耦合的心跳检测</strong>。检测系统和被检测系统通过etcd上某个目录关联而非直接关联起来，这样可以大大减少系统的耦合性。</li>
<li><strong>通过etcd完成系统调度</strong>。某系统有控制台和推送系统两部分组成，控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作，实际上是修改了etcd上某些目录节点的状态，而etcd就把这些变化通知给注册了Watcher的推送系统客户端，推送系统再作出相应的推送任务。</li>
<li><strong>通过etcd完成工作汇报</strong>。大部分类似的任务分发系统，子任务启动后，到etcd来注册一个临时工作目录，并且定时将自己的进度进行汇报(将进度写入到这个临时目录)，这样任务管理者就能够实时知道任务进度。</li>
</ul>

<p><br></p>

<h4 id="toc_11">3.5 分布式锁</h4>

<p>因为etcd使用Raft算法保持了数据的强一致性，某次操作存储到集群中的值必然是全局一致的，所以很容易实现分布式锁，锁有两种使用方式:</p>

<ul>
<li>保持独占，即所有获取锁的用户最终只有一个可以得到。etcd为此提供了一套实现分布式锁原子操作CAS(CompareAndSwap)的API。通过设置prevExist值，可以保证在多个节点同时去创建某个目录时只有一个成功，而创建成功的用户就可以认为是获得了锁。</li>
<li>控制时序，即所有想要获得锁的用户都会被安排执行，但是获得锁的顺序也是全局唯一的，同时决定了执行顺序。etcd为此也提供了一套API(自动创建有序键)，对一个目录建值时指定为POST动作，这样etcd会自动在目录下生成一个当前最大的值为键，存储这个新的值(客户端编号)。同时还可以使用API按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序，而这些键中存储的值可以是代表客户端的编号。</li>
</ul>

<p>从etcd获取的分布式锁如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/765756_9_%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81.jpg" alt="分布式锁" /></p>

<p><br></p>

<h4 id="toc_12">3.6 分布式队列</h4>

<p>分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似，创建一个先进先出的队列，保证顺序。另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点，condition可以表示信息如下：</p>

<ul>
<li><p>condition可以表示队列大小。比如一个大的任务需要很多小任务就绪的情况下才能执行，每次有一个小任务就绪，就给这个condition数字加1，直到达到大任务规定的数字，再开始执行队列里的一系列小任务，最终执行大任务，如下图所示：
<img src="https://go-sponge.com/assets/images/blog/765756_10_%E5%88%86%E5%B8%83%E5%BC%8F%E9%98%9F%E5%88%97.jpg" alt="分布式队列" /></p></li>

<li><p>condition可以表示某个任务在不在队列。这个任务可以是所有排序任务的首个执行程序，也可以是拓扑结构中没有依赖的点。通常必须执行这些任务后才能执行队列中的其他任务。</p></li>

<li><p>condition还可以表示其它的一类开始执行任务的通知。可以由控制程序指定，当condition出现变化时，开始执行队列任务。</p></li>
</ul>

<p><br></p>

<h4 id="toc_13">3.7 集群监控</h4>

<p>使用etcd来实现集群的实时性的监控，可以第一时间检测到各节点的健康状态，以完成集群的监控要求。etcd本身就有自带检点健康监控功能，实现起来也比较简单。</p>

<ul>
<li>使用Watcher机制，当某个节点消失或有变动时，Watcher会第一时间发现并告知用户。</li>
<li>节点可以设置TTL key，比如每隔30s发送一次心跳使代表该机器存活的节点继续存在，否则节点消失。</li>
</ul>

<p><br></p>

<h4 id="toc_14">3.8 Leader竞选</h4>

<p>使用分布式锁，可以完成Leader竞选。这种场景通常是一些长时间CPU计算或者使用IO操作的机器，只需要竞选出的Leader计算或处理一次，就可以把结果复制给其他的Follower，从而避免重复劳动，节省计算资源。</p>

<p>可使用在搜索系统中建立全量索引。如果每个机器都进行一遍索引的建立，不但耗时而且建立索引的一致性不能保证。通过在etcd的CAS机制同时创建一个节点，创建成功的机器作为Leader，进行索引计算，然后把计算结果分发到其它节点。</p>

<p><br><br></p>

<h3 id="toc_15">4 安装</h3>

<h4 id="toc_16">4.1 在docker安装单机版</h4>

<p>使用docker-compose.yml脚本如下：</p>

<pre><code class="language-yaml">version: &quot;3&quot;
  
services:
  etcd:
    image: quay.io/coreos/etcd
    container_name: etcd-single
    restart: always
    ports:
      - 2379:2379
      - 2380:2380
    volumes:
      - $PWD/etcd-data:/etcd-data
    environment:
      - ETCDCTL_API=3
    command:
      - /usr/local/bin/etcd
      - --data-dir=/etcd-data
      - --name=etcd-single
      - --listen-peer-urls=http://0.0.0.0:2380
      - --listen-client-urls=http://0.0.0.0:2379
      - --initial-advertise-peer-urls=http://0.0.0.0:2380
      - --advertise-client-urls=http://0.0.0.0:2379
      #- --initial-cluster=etcd-single=http://0.0.0.0:2380 # 不指定参数，让etcd自动生成
</code></pre>

<pre><code class="language-bash"># 启动etcd服务
docker-compose up -d

# 把容器里的etcdctl客户端复制到本地使用
docker exec -t etcd-single cp /usr/local/bin/etcdctl /etcd-data
sudo mv /etcd-data/etcdctl /usr/local/bin/

# 让etcdctl使用v3版本，和服务端对应
echo 'export ETCDCTL_API=3' &gt;&gt; ~/.bashrc
source  ~/.bashrc

# 查看版本
etcdctl version

# 查看成员
etcdctl member list
</code></pre>

<p><br></p>

<h4 id="toc_17">4.2 在一台主机上安装docker集群版</h4>

<pre><code class="language-yaml">version: '3'

services:
  etcd1:
    image: quay.io/coreos/etcd
    container_name: etcd1
    restart: always
    environment:
      - ETCDCTL_API=3
    command:
      - etcd
      - --name=etcd1
      - --data-dir=/etcd-data
      - --advertise-client-urls=http://0.0.0.0:2379
      - --listen-client-urls=http://0.0.0.0:2379
      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-cluster-token=etcd-cluster
      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
    ports:
      - 23791:2379
      - 23801:2380
    volumes:
      - $PWD/etcd1-data:/etcd-data
    networks:
      - etcd-net

  etcd2:
    image: quay.io/coreos/etcd
    container_name: etcd2
    restart: always
    environment:
      - ETCDCTL_API=3
    command:
      - etcd
      - --name=etcd2
      - --data-dir=/etcd-data
      - --advertise-client-urls=http://0.0.0.0:2379
      - --listen-client-urls=http://0.0.0.0:2379
      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-cluster-token=etcd-cluster
      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
    ports:
      - 23792:2379
      - 23802:2380
    volumes:
      - $PWD/etcd2-data:/etcd-data
    networks:
      - etcd-net

  etcd3:
    image: quay.io/coreos/etcd
    container_name: etcd3
    restart: always
    environment:
      - ETCDCTL_API=3
    command:
      - etcd
      - --name=etcd3
      - --data-dir=/etcd-data
      - --advertise-client-urls=http://0.0.0.0:2379
      - --listen-client-urls=http://0.0.0.0:2379
      - --listen-peer-urls=http://0.0.0.0:2380
      - --initial-cluster-token=etcd-cluster
      - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
      - --initial-cluster-state=new
    ports:
      - 23793:2379
      - 23803:2380
    volumes:
      - $PWD/etcd3-data:/etcd-data
    networks:
      - etcd-net

networks:
  etcd-net:
</code></pre>

<pre><code class="language-bash"># 启动etcd集群
docker-compose up -d

# 查看集群成员列表
etcdctl --endpoints=http://127.0.0.1:23791 member list

# 查看成员状态
etcdctl --write-out=table --endpoints=http://127.0.0.1:23791 endpoint status
</code></pre>

<p><br></p>

<h4 id="toc_18">4.3 在多个节点安装etcd集群</h4>

<p>一共三个节点，IP地址分别是192.168.3.101、192.168.3.102、192.168.3.103。</p>

<p>在192.168.3.101节点创建docker-compose.yml文件，内容如下：</p>

<pre><code class="language-yaml">version: &quot;3.7&quot;
  
services:
  etcd:
    image: quay.io/coreos/etcd
    container_name: my-etcd
    restart: always
    environment:
      - ETCDCTL_API=3
    command:
      - etcd
      # 成员
      - --name=etcd1
      - --data-dir=/etcd-data
      - --listen-peer-urls=http://0.0.0.0:2380
      - --listen-client-urls=http://0.0.0.0:2379
      # 集群
      - --initial-advertise-peer-urls=http://192.168.3.101:2380
      - --advertise-client-urls=http://192.168.3.101:2379
      - --initial-cluster-token=cluster-token
      - --initial-cluster=etcd1=http://192.168.3.101:2380,etcd2=http://192.168.3.102:2380,etcd3=http://192.168.3.103:2380
      - --initial-cluster-state=new
    volumes:
      - $PWD/etcd-data:/etcd-data
    ports:
      - 2379:2379
      - 2380:2380
    network_mode: &quot;host&quot;
    stdin_open: true
    tty: true
</code></pre>

<p><br></p>

<p>在192.168.3.102节点创建docker-compose.yml文件，内容如下：</p>

<pre><code class="language-yaml">version: &quot;3.7&quot;
  
services:
  etcd:
    image: quay.io/coreos/etcd
    container_name: my-etcd
    restart: always
    environment:
      - ETCDCTL_API=3
    command:
      - etcd
      # 成员
      - --name=etcd2
      - --data-dir=/etcd-data
      - --listen-peer-urls=http://0.0.0.0:2380
      - --listen-client-urls=http://0.0.0.0:2379
      # 集群
      - --initial-advertise-peer-urls=http://192.168.3.102:2380
      - --advertise-client-urls=http://192.168.3.102:2379
      - --initial-cluster-token=cluster-token
      - --initial-cluster=etcd1=http://192.168.3.101:2380,etcd2=http://192.168.3.102:2380,etcd3=http://192.168.3.103:2380
      - --initial-cluster-state=new
    volumes:
      - $PWD/etcd-data:/etcd-data
    ports:
      - 2379:2379
      - 2380:2380
    network_mode: &quot;host&quot;
    stdin_open: true
    tty: true
</code></pre>

<p><br></p>

<p>在192.168.3.103节点创建docker-compose.yml文件，内容如下：</p>

<pre><code class="language-yaml">version: &quot;3.7&quot;
  
services:
  etcd:
    image: quay.io/coreos/etcd
    container_name: my-etcd
    restart: always
    environment:
      - ETCDCTL_API=3
    command:
      - etcd
      # 成员
      - --name=etcd3
      - --data-dir=/etcd-data
      - --listen-peer-urls=http://0.0.0.0:2380
      - --listen-client-urls=http://0.0.0.0:2379
      # 集群
      - --initial-advertise-peer-urls=http://192.168.3.103:2380
      - --advertise-client-urls=http://192.168.3.103:2379
      - --initial-cluster-token=cluster-token
      - --initial-cluster=etcd1=http://192.168.3.101:2380,etcd2=http://192.168.3.102:2380,etcd3=http://192.168.3.103:2380
      - --initial-cluster-state=new
    volumes:
      - $PWD/etcd-data:/etcd-data
    ports:
      - 2379:2379
      - 2380:2380
    network_mode: &quot;host&quot;
    stdin_open: true
    tty: true
</code></pre>

<p><br></p>

<p>三个节点的etcd运行脚本不一样的地方只有三个启动参数，分别是&ndash;name、&ndash;initial-advertise-peer-urls、&ndash;advertise-client-urls。</p>

<pre><code class="language-bash"># 分别在3个节点启动etcd
docker-compose up -d

# 查看集群成员列表
export ENDPOINTS=192.168.3.101:2379,192.168.3.102:2379,192.168.3.103:2379
etcdctl --endpoints=$ENDPOINTS member list

# 查看成员状态
etcdctl --write-out=table --endpoints=$ENDPOINTS endpoint status
</code></pre>

<p><br></p>

<h4 id="toc_19">4.4 TLS加密通信</h4>

<p>如果集群需要使用TLS协议进行的加密通信，又要验证其身份，需要添加自签名证书，生成自签名证书脚本文件gen-peer-certs.sh如下：</p>

<pre><code class="language-bash">#!/bin/bash


# -------------------------------- 参数判断 --------------------------------------------
params=$@

if [ $# -lt 1 ]; then
    echo &quot;param is empty&quot;
    echo &quot;usage: $0 [domain ...] | [ip ...]&quot;
    echo &quot;eg: $0 zhuyasen.com 192.168.3.100&quot;
    echo &quot;&quot;
    exit
fi

# 用参数替换req-csr.json固定字段值domains_or_ips
hostFields=''
for val in $params; do
    hostFields=${hostFields}\\\&quot;$val\\\&quot;,
done
# 去掉最后一个逗号
hostFields=${hostFields%?}


# ---------------------------------- 创建认证中心(CA) ----------------------------------
# 创建存储证书目录和配置目录
mkdir -p certs
cd certs
mkdir -p config

# 创建根CA证书和私钥的CSR(证书签名请求文件)配置文件ca-csr.json
cat &gt; config/ca-csr.json &lt;&lt;EOF
{
    &quot;CN&quot;: &quot;myMechanism&quot;,
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;rsa&quot;,
        &quot;size&quot;: 2048
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;CN&quot;,
            &quot;ST&quot;: &quot;Beijing&quot;,
            &quot;L&quot;: &quot;Beijing&quot;,
            &quot;O&quot;: &quot;my organization&quot;,
            &quot;OU&quot;: &quot;my organization unit name&quot;
        }
    ]
}
EOF

# 生成CA证书和私钥，先判断ca-key.pem是否存在，如果存在则使用已存在的
if [[ ! -f &quot;ca-key.pem&quot; ]]; then
  echo &quot;generate new ca.pem and ca-key.pem&quot;
  cfssl gencert -initca config/ca-csr.json | cfssljson -bare ca -
else
  echo &quot;use existing ca-key.pem.&quot;
fi


# ---------------------------------- 派发证书  ----------------------------------
# 创建证书签名请求配置文件req-csr.json，这里填写申请组织信息、ip或域名，注：当集群使用tls认证，并且只使用ip访问时，必须添加本地ip进来，外部访问才能通过鉴权，否则只能本地访问
cat &gt; config/req-csr.json &lt;&lt;EOF
{
    &quot;CN&quot;: &quot;etcd&quot;,
    &quot;hosts&quot;: [
        &quot;localhost&quot;,
        &quot;127.0.0.1&quot;,
        &quot;domains_or_ips&quot;
    ],
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;rsa&quot;,
        &quot;size&quot;: 2048
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;CN&quot;,
            &quot;ST&quot;: &quot;GuangDong&quot;,
            &quot;L&quot;: &quot;GuangZhou&quot;,
            &quot;O&quot;: &quot;communication&quot;,
            &quot;OU&quot;: &quot;cluster&quot;
        }
    ]
}
EOF


# 配置证书生成策略，让CA知道颁发什么样的证书。
cat &gt; config/ca-config.json &lt;&lt;EOF
{
    &quot;signing&quot;:{
        &quot;default&quot;:{
            &quot;expiry&quot;:&quot;87600h&quot;
        },
        &quot;profiles&quot;:{
            &quot;peer&quot;:{
                &quot;expiry&quot;:&quot;8760h&quot;,
                &quot;usages&quot;:[
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;server auth&quot;,
                    &quot;client auth&quot;
                ]
            }
        }
    }
}
EOF


tagName='\&quot;domains_or_ips\&quot;'
# 把domains_or_ips替换为输入的参数
sed -i &quot;s/$tagName/$hostFields/g&quot; config/req-csr.json

# 使用现有证书和私钥重新颁发新的证书，类型为服务端使用
for name in $params; do
  cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=config/ca-config.json -profile=peer config/req-csr.json | cfssljson -bare $name
  cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=config/ca-config.json -profile=peer config/req-csr.json | cfssljson -bare peer-$name
done

# 删除多余文件
ls | grep -v pem | xargs -i rm -rf {}
</code></pre>

<p><br></p>

<pre><code class="language-bash"># 执行脚本生成证书，参数为各个节点ip地址，在当前certs目录下
./gen-peer-certs.sh 192.168.3.101 192.168.3.102 192.168.3.103

# 验证生成的证书是否有效
cd certs
openssl verify -CAfile ca.pem 192.168.3.101.pem
openssl verify -CAfile ca.pem peer-192.168.3.101.pem

# 在docker-compose.yml添加共享目录，把证书复制到容器里
    volumes:
      - $PWD/certs:/etcd-certs

# 在docker-compose.yml的etcd启动命令中添加证书参数，每个节点都需要添加
    command:
      # client端证书
      - --client-cert-auth
      - --trusted-ca-file=/etcd-certs/ca.pem
      - --cert-file=/etcd-certs/192.168.3.xxx.pem
      - --key-file=/etcd-certs/192.168.3.xxx-key.pem
      # peer端证书
      - --peer-client-cert-auth
      - --peer-trusted-ca-file=/etcd-certs/ca.pem
      - --peer-cert-file=/etcd-certs/peer-192.168.3.xxx.pem
      - --peer-key-file=/etcd-certs/peer-192.168.3.xxx-key.pem

# 连接客户端需要认证参数
--cacert --cert --key这三个参数都不能缺少，其中--cert --key这两个参数可以是任意一个节点的客户端证书和私钥

# 示例
etcdctl --endpoints=$ENDPOINTS \
--cacert=ca.pem \
--cert=192.168.3.101.pem \
--key=192.168.3.101-key.pem \
member list

# 为了避免每次都输入一大串参数，在~/.bashrc添加下面别名
export ETCDCTL_API=3
HOST_1=192.168.3.101:23791
HOST_2=192.168.3.102:23792
HOST_3=192.168.3.103:23793
ENDPOINTS=$HOST_1,$HOST_2,$HOST_3
alias etcdctl=&quot;etcdctl --endpoints=$ENDPOINTS&quot;
certPath=/home/zhuyasen/work/etcd/etcd-cluster-local/certs
alias etcdctlcert=&quot;etcdctl --endpoints=$ENDPOINTS --cacert=${certPath}/ca.pem --cert=${certPath}/192.168.3.101.pem --key=${certPath}/192.168.3.101-key.pem&quot;

# 刷新生效
source ~/.bashrc

# 使用
etcdctlcert member list
</code></pre>

<p>注：如果是在本地一台主机使用docker搭建的需要tls鉴权认证的etcd集群，局域网内其他主机想要通过tls鉴权访问集群，必须把etcd集群所在主机的ip地址填写到req-csr.json配置文件的hosts字段下，否则会报错authentication handshake failed: x509: certificate is valid for 127.0.0.1, not 192.168.3.5(集群所在主机的ip地址)</p>

<p><br></p>

<h4 id="toc_20">4.5 etcd集群性能压测</h4>

<p>定义性能的两个因素是：延迟和吞吐量，延迟是完成操作所花费的时间。吞吐量是一段时间内完成的全部操作。</p>

<pre><code class="language-bash"># 下载压测工具(需要代理)，需要先安装go才可以下载编译
set https_proxy=http://127.0.0.1:10809
set http_proxy=http://127.0.0.1:10809
go get -v go.etcd.io/etcd/tools/benchmark

# 进去目录$GOPATH/src/go.etcd.io/etcd/tools/benchmark
go build
sudo mv benchmark /usr/local/bin
</code></pre>

<p><br></p>

<p>设置环境变量</p>

<pre><code class="language-bash">set HOST_1=192.168.3.101:2379
set HOST_2=192.168.3.102:2379
set HOST_3=192.168.3.103:2379

# 获取主节点(IS LEADER为true)
etcdctl endpoint status --endpoints=${HOST_1},${HOST_2},${HOST_3}
# 得知主节点为HOST_3
</code></pre>

<p><br></p>

<p><strong>(1) 写入压测</strong></p>

<pre><code class="language-bash"># 压测写入主节点(多用户)
benchmark --endpoints=${HOST_1} --target-leader  --conns=100 --clients=1000 \
    put --key-size=8 --sequential-keys --total=100000 --val-size=256
# 结果: 吞吐量65985.5875 req/s，平均延时14.8毫秒

# 压测写入所有成员(多用户)
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=100 --clients=1000 \
    put --key-size=8 --sequential-keys --total=100000 --val-size=256
# 结果: 吞吐量62264.1271 req/s，平均延时15.7毫秒
</code></pre>

<p><br></p>

<p><strong>(2) 读取压测</strong></p>

<pre><code class="language-bash"># 线性化(linearizabe)读取数据
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=100 --clients=1000 range foo --consistency=l --total=100000
# 结果: 吞吐量103923.3802 req/s，平均延时9毫秒

# 串行化(serializabe)读取数据
benchmark --endpoints=${HOST_1},${HOST_2},${HOST_3} --conns=100 --clients=1000 range foo --consistency=s --total=100000
# 结果: 吞吐量115904.776 req/s，平均延时8.1毫秒
</code></pre>

<p><br><br></p>

<h3 id="toc_21">5 etcdctl常用命令</h3>

<p>etcdctl是一个命令行的客户端，它提供了简洁的命令，可理解为命令工具集，可以方便我们在对服务进行测试或者手动修改数据库内容。etcdctl与kubectl、systemctl命令原理及操作类似。</p>

<p>用法：</p>

<blockquote>
<p>etcdctl [global options] command [command options][args…]</p>
</blockquote>

<p>安装etcdctl：</p>

<pre><code class="language-bash"># 方式一：从github官网下载 https://github.com/etcd-io/etcd/releases

# 方式二：从运行的docker中复制到本地
sudo docker cp etcd容器ID或名称:/usr/local/bin/etcdctl /usr/local/bin
</code></pre>

<p><br></p>

<p>etcd 在键的组织上采用了层次化的空间结构(类似于文件系统中目录的概念)，数据库操作围绕对键值和目录的 CRUD <a href="符合 REST 风格的一套操作：Create, Read, Update, Delete" rel="nofollow">增删改查</a>完整生命周期的管理。</p>

<p>具体的命令选项参数可以通过 etcdctl command &ndash;help来获取相关帮助，下面都是V3版本命令。</p>

<p>指定etcd集群，在~/.bashrc添加下面内容</p>

<pre><code class="language-bash">export ETCDCTL_API=3
HOST_1=127.0.0.1:23791
HOST_2=127.0.0.1:23792
HOST_3=127.0.0.1:23793
ENDPOINTS=$HOST_1,$HOST_2,$HOST_3
# 覆盖etcdctl命令，如果需要使用原生命令，可以在命令开头加一个\反斜线，例如：\etcdctl xxxx xxxx
alias etcdctl=&quot;etcdctl --endpoints=$ENDPOINTS&quot;
alias etcdctljson=&quot;etcdctl --endpoints=$ENDPOINTS --write-out=json&quot;
alias etcdctltable=&quot;etcdctl --endpoints=$ENDPOINTS --write-out=table&quot;
</code></pre>

<p>刷新生效：</p>

<blockquote>
<p>source ~/.bashrc</p>
</blockquote>

<p><br></p>

<p>KV API的操作有下面保证：</p>

<ul>
<li><strong>原子性</strong>，所有API请求都是原子请求，一个操作要么完全完成，要么根本不完成。对于监视请求，由一个操作生成的所有事件将在一个监视响应中，Watch从不观察单个操作的部分事件。</li>
<li><strong>耐用性</strong>，任何完成的操作都是持久的，所有可访问的数据也是持久数据，读取将永远不会返回尚未持久化的数据。</li>
<li><strong>严格的可序列化性</strong> ，这是分布式事务数据库系统的最强隔离保证，读操作将永远不会观察到任何中间数据。</li>
</ul>

<p><br></p>

<h4 id="toc_22">5.1 增删改查数据相关命令</h4>

<p><strong>增加和修改，如果存在则替换</strong></p>

<pre><code class="language-bash">etcdctl put &lt;键名&gt; &lt;键值&gt; [选项]

# 示例
etcdctl put key &quot;Hello ETCD&quot;
etcdctl put key1 &quot;Hello ETCD 1&quot;
etcdctl put leaseKey &quot;alive value&quot; --lease=12f775cb02d34d94 # 有生命周期的key
</code></pre>

<p><br></p>

<p><strong>查询</strong></p>

<pre><code class="language-bash">etcdctl get &lt;键名&gt; [选项]

# 示例：
etcdctl get key
etcdctl get key --prefix # 相同前缀查找
etcdctl get / --prefix --keys-only # 只获取/开始的所有key，不包括值
</code></pre>

<p><br></p>

<p><strong>删除</strong></p>

<pre><code class="language-bash">etcdctl del &lt;键名&gt; [选项]

# 示例：
etcdctl del key
etcdctl get key --prefix # 相同前缀删除
</code></pre>

<p><br></p>

<h4 id="toc_23">5.2 集群状态相关命令</h4>

<p><strong>查看集群状态</strong></p>

<pre><code class="language-bash">etcdctl endpoint status --write-out=table

# +-----------------+------------------+---------+---------+-----------+-----------+------------+
# |    ENDPOINT     |        ID        | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
# +-----------------+------------------+---------+---------+-----------+-----------+------------+
# | 127.0.0.1:23791 | ade526d28b1f92f7 |   3.3.8 |   22 MB |     false |        10 |       9177 |
# | 127.0.0.1:23792 | d282ac2ce600c1ce |   3.3.8 |   22 MB |     false |        10 |       9177 |
# | 127.0.0.1:23793 | bd388e7810915853 |   3.3.8 |   22 MB |      true |        10 |       9177 |
# +-----------------+------------------+---------+---------+-----------+-----------+------------+
</code></pre>

<p><br></p>

<p><strong>查看集群健康状态</strong></p>

<pre><code class="language-bash">etcdctl endpoint health

# 127.0.0.1:23793 is healthy: successfully committed proposal: took = 583.669µs
# 127.0.0.1:23792 is healthy: successfully committed proposal: took = 710.885µs
# 127.0.0.1:23791 is healthy: successfully committed proposal: took = 734.486µs
</code></pre>

<p><br></p>

<h4 id="toc_24">5.3 集群成员操作相关命令</h4>

<p><strong>查看集群成员列表</strong></p>

<pre><code class="language-bash">etcdctl member list --write-out=table

# +------------------+---------+-------+-------------------+---------------------+
# |        ID        | STATUS  | NAME  |    PEER ADDRS     |    CLIENT ADDRS     |
# +------------------+---------+-------+-------------------+---------------------+
# | ade526d28b1f92f7 | started | etcd1 | http://etcd1:2380 | http://0.0.0.0:2379 |
# | bd388e7810915853 | started | etcd3 | http://etcd3:2380 | http://0.0.0.0:2379 |
# | d282ac2ce600c1ce | started | etcd2 | http://etcd2:2380 | http://0.0.0.0:2379 |
# +------------------+---------+-------+-------------------+---------------------+
</code></pre>

<p><br></p>

<p><strong>添加成员</strong></p>

<pre><code class="language-bash">etcdctl member add &lt;成员名称&gt; [--peer-urls=节点地址]

# 示例：将目标节点etcd4添加到集群
etcdctl member add etcd4 http://192.168.3.104:2380
# 启动目标集群时需要设置启动参数如下
etcd --name=etcd4 --data-dir=/etcd-data \
  --listen-peer-urls=http://192.168.3.104:2380 \
  --listen-client-urls=http://192.168.3.104:2379 \
  --initial-advertise-peer-urls=http://192.168.3.104:2380 \
  --advertise-client-urls=http://192.168.3.104:2379 \
  --initial-cluster=etcd1=http://192.168.3.101:2380,etcd2=http://192.168.3.102:2380,etcd3=http://192.168.3.103:2380,etcd4=http://192.168.3.104:2380 \
  --initial-cluster-state=existing
</code></pre>

<p><br></p>

<p><strong>更新成员</strong></p>

<p>新成员必须启动的，类似添加成员</p>

<pre><code class="language-bash">etcdctl member update &lt;成员id&gt; [--peer-urls=节点地址]

  # 示例：
  etcdctl member update ade526d28b1f92f7 --peer-urls=http://192.168.3.111:2380
</code></pre>

<p><br></p>

<p><strong>删除成员</strong></p>

<pre><code class="language-bash">etcdctl member remove &lt;成员id&gt;

# 示例
etcdctl member remove ade526d28b1f92f7
</code></pre>

<h4 id="toc_25">5.4 租约相关命令</h4>

<p>租约具有生命周期，需要为租约授予一个TTL(time to live)，将租约绑定到一个key上，则key的生命周期与租约一致，可续租，可撤销租约。</p>

<blockquote>
<p>etcdctl lease <subcommand></p>
</blockquote>

<p>etcdctl lease 子命令有：</p>

<ul>
<li>grant: 添加新租约</li>
<li>revoke: 删除租约</li>
<li>list: 列出所有租约</li>
<li>timetolive: 获取租约详情信息</li>
<li>keep-alive: 保持租约有效(续签)</li>
</ul>

<p><br></p>

<pre><code class="language-bash"># 生成一份新租约
etcdctl lease grant 600

# 查看租约列表
etcdctl lease list

# 查看租约的剩余生命时长，可以使用json输出得到字段值
etcdctl lease timetolive 12f775cb02d34d94
etcdctl lease timetolive 12f775cb02d34d94 --keys # 查看已绑定的key

# 撤销租约，绑定租约的key也会自动失效
etcdctl lease revoke 12f775cb02d34d94

# 续租
etcdctl lease keep-alive 12f775cb02d34d94 # 持续续租，无过期(阻塞)
etcdctl lease keep-alive 12f775cb02d34d94 --once # 将保持存活时间重置为其原始值并立即退出

# key绑定租约
etcdctl put leaseKey &quot;alive value&quot; --lease=12f775cb02d34d94
</code></pre>

<p><br></p>

<h4 id="toc_26">5.5 watch命令</h4>

<p>watch是监听键或前缀发生改变的事件流。</p>

<pre><code class="language-bash"># 对某个key监听操作，当/key1发生改变时，会返回最新值
etcdctl watch /key1

# 监听key前缀
etcdctl watch /key --prefix

# 监听到改变后执行相关操作
etcdctl watch /key1 -- etcdctl member list
</code></pre>

<h4 id="toc_27">5.6 事务txn</h4>

<p>txn从标准输入读取多个etcd请求，并将它们作为单个原子事务应用。 交易由条件清单组成，如果所有条件都为真，则应用请求列表；如果任何条件为假，则不应用请求列表。</p>

<blockquote>
<p>etcdctl txn [options]</p>
</blockquote>

<pre><code class="language-bash"># 设置key值
etcdctl put name zhangsan
etcdctl put age 22

# 交互式事务
etcdctl txn -i
# ---------------- 进入终端交互式 ----------------
compares:
value(&quot;age&quot;) &gt; &quot;18&quot; # 条件清单1
value(&quot;name&quot;) = &quot;zhangsan&quot; # 条件清单2

success requests (get, put, del):
put result true  # 所有条件成立执行命令

failure requests (get, put, del):
put result false  # 至少有一个条件清单不成立执行命令

SUCCESS

OK
# ---------------- 结束终端交互式 ----------------

# 查看事务执行结果
etcdctl get result
  # result
  # true
</code></pre>

<p><br></p>

<h4 id="toc_28">5.7 分布式锁</h4>

<p>分布式锁，多个客户端同时抢锁，抢到锁可以操作，其他没有获取到锁的会等待阻塞状态，等锁释放之后才可以获取到锁。</p>

<blockquote>
<p>etcdctl lock <lockname> [options] [exec-command arg1 arg2 &hellip;]</p>
</blockquote>

<pre><code class="language-bash"># 在第一个终端
etcdctl lock mutexKey
  # mutexKey/326963a02758b52d
​
# 在第二终端
etcdctl lock mutexKey
​
# 当第一个终端结束了，第二个终端会显示
mutexKey/326963a02758b531
</code></pre>

<p>注：只有当正常退出且释放锁后，lock命令的退出码是0，否则这个锁会一直被占用直到过期。</p>

<p><br></p>

<h4 id="toc_29">5.7 备份和恢复命令</h4>

<pre><code class="language-bash"># 快照
etcdctl snapshot save backup.db

# 查看快照文件信息
etcdctl snapshot status backup.db --write-out=table

# 恢复快照
etcdctl snapshot restore backup.db \
--name=etcd1 \
--data-dir=xxx \
--initial-advertise-peer-urls=xxx \
--initial-cluster=xxx \
--initial-cluster-token=xxx
</code></pre>

<p><br></p>

<h4 id="toc_30">5.8 查看警报命令</h4>

<p>如果内部出现问题，会触发警报，可以通过命令查看警报引起原因。</p>

<pre><code class="language-bash"># 查看所有警报
etcdctl alarm list

# 解除所有警报
etcdctl alarm disarm
</code></pre>

<p><br></p>

<h4 id="toc_31">5.9 用户和角色相关命令</h4>

<p>etcd默认是没有开启访问控制的，如果开启外网访问etcd的话就需要考虑访问控制的问题，etcd提供了两种访问控制的方式：</p>

<ul>
<li>基于身份验证的访问控制</li>
<li>基于证书的访问控制</li>
</ul>

<p>etcd有一个特殊用户root和一个特殊角色root：</p>

<ul>
<li><strong>root用户</strong>：root用户是etcd的超级管理员，拥有etcd的所有权限，在开启角色认证之前为们必须要先建立好root用户。</li>
<li><strong>root角色</strong>：具有该root角色的用户既具有全局读写访问权限，具有更新集群的身份验证配置的权限。此外，该root角色还授予常规集群维护的特权，包括修改集群成员资格，对存储进行碎片整理以及拍摄快照。</li>
</ul>

<p>注：root用户必须拥有root角色之后，root用户才允许在操作etcd的所有东西。</p>

<p><br></p>

<p><strong>etcd的权限资源：</strong></p>

<ul>
<li><strong>Users</strong>: user用来设置身份认证(user：passwd)，一个用户可以拥有多个角色，每个角色被分配一定的权限(只读、只写、可读写)，用户分为root用户和非root用户。</li>
<li><strong>Roles</strong>: 角色用来关联权限，角色主要三类：root角色。默认创建root用户时即创建了root角色，该角色拥有所有权限；guest角色，默认自动创建，主要用于非认证使用。普通角色，由root用户创建角色，并分配指定权限。</li>
<li><strong>Permissions</strong>: 权限分为只读、只写、可读写三种权限，权限即对指定目录或key的读写权限。</li>
</ul>

<p>注意：如果没有指定任何验证方式，即没显示指定以什么用户进行访问，那么默认会设定为 guest 角色。默认情况下 guest 也是具有全局访问权限的。</p>

<p><br></p>

<p><strong>管理用户的子命令</strong></p>

<blockquote>
<p>etcdctl user <subcommand></p>
</blockquote>

<p>etcdctl user 子命令有：</p>

<ul>
<li>add: 添加新用户</li>
<li>delete: 删除用户</li>
<li>get: 获取用户的详细信息</li>
<li>list: 列出所有用户</li>
<li>passwd: 修改用户密码</li>
<li>grant-role: 授予用户角色</li>
<li>revoke-role: 撤销用户的角色</li>
</ul>

<p><br></p>

<p><strong>管理角色的子命令</strong></p>

<blockquote>
<p>etcdctl role <subcommand></p>
</blockquote>

<p>etcdctl role 子命令有：</p>

<ul>
<li>add: 添加新角色</li>
<li>delete: 删除角色</li>
<li>get: 获取角色的详细信息</li>
<li>list: 列出所有角色</li>
<li>grant-permission: 把key操作权限授予给一个角色</li>
<li>revoke-permission: 从角色中撤销key操作权限</li>
</ul>

<p><br></p>

<p><strong>开启root身份验证</strong></p>

<p>开启了身份验证之后，所有etcdctl命令操作都需要指定用户参数&ndash;user，参数值为<strong>用户名:密码</strong>，类似开启了证书访问控制之后，所有etcdctl命令需要添加证书参数&ndash;cacert.</p>

<pre><code class="language-bash"># 创建root后，root用户默认拥有类似linux一样超级管理员权限，添加用户root后默认还有root角色
etcdctl user add root
  # Password of root: 123456
  # Type password of root again for confirmation: 123456

# 开启身份验证，如果取消把enable改为disable
etcdctl auth enable

# 操作时必须指定用户，否则会报错
etcdctl put key &quot;hello etcd&quot; --user=root:123456
etcdctl get key --user=root:123456
</code></pre>

<p><br></p>

<p><strong>新用户和角色授权</strong></p>

<p>开启了root身份验证之后，就可以对普通用户和角色操作了。</p>

<p>(1) 用户增删改查</p>

<pre><code class="language-bash"># 添加新用户zhangsan
etcdctl user add zhangsan --user=root:123456
  # Password of root: 123456
  # Type password of root again for confirmation: 123456

# 获取用户的详细信息
etcdctl user get zhangsan --user=root:123456

# 查看所有用户
etcdctl user list --user=root:123456

# 修改用户密码
etcdctl user passwd zhangsan --user=root:123456

# 删除用户
etcdctl user delete zhangsan --user=root:123456
</code></pre>

<p><br></p>

<p>(2) 角色增删改查</p>

<pre><code class="language-bash"># 添加新角色redis
etcdctl role add redis --user=root:123456

# 获取角色的详细信息
etcdctl role get redis --user=root:123456

# 查看所有角色
etcdctl role list --user=root:123456

# 删除角色
etcdctl role delete redis --user=root:123456
</code></pre>

<p><br></p>

<p>(3) 绑定和授权</p>

<p>有了新用户和新角色之后，还需要把用户和角色绑定在一起，确定授权权限之后，新用户才可以对key有对应操作权限</p>

<pre><code class="language-bash"># 授予角色redis权限，可以设置只读(read)、只写(write)、读写(readwrite)
etcdctl role grant-permission redis readwrite redisKey/ --user=root:123456
# 或授予key前缀 etcdctl role grant-permission redis readwrite redisKey/  --prefix=true  --user=root:123456

# 用户zhangsan绑定redis角色，获得操作权限
etcdctl user grant-role zhangsan redis --user=root:123456


# 下面是用户zhangsan在授权前后操作redisKey/
  xxxx@pc:~$ etcdctl put redisKey/ &quot;hello redis&quot; --user=root:123456
    # OK
  xxxx@pc:~$ etcdctl get redisKey/ --user=root:123456
    # redisKey/
    # hello redis
  xxxx@pc:~$ etcdctl get redisKey/ --user=zhangsan:123456
    # Error: etcdserver: permission denied
  xxxx@pc:~$ etcdctl role grant-permission redis readwrite redisKey/ --user=root:123456
    # Role redis updated
  xxxx@pc:~$ etcdctl get redisKey/ --user=zhangsan:123456
    # Error: etcdserver: permission denied
  xxxx@pc:~$ etcdctl user grant-role zhangsan redis --user=root:123456
    # Role redis is granted to user zhangsan
  xxxx@pc:~$ etcdctl get redisKey/ --user=zhangsan:123456
    # redisKey/
    # hello redis


# 撤回角色redis对redisKey/的操作权限
etcdctl role revoke-permission redis redisKey/ --user=root:123456

# 解绑用户zhangsan和角色redis，也就是用户zhangsan操作权限(redis角色的权限)被收回
etcdctl user revoke-role zhangsan redis --user=root:123456
</code></pre>

<p><br><br></p>

<h3 id="toc_32">6 etcd的go客户端</h3>

<h4 id="toc_33">6.1 安装</h4>

<p>不要直接使用 go get -u go.etcd.io/etcd
命令安装etcd客户端，可会遇到些奇怪问题，直接从github下载稳定版本 <a href="https://github.com/etcd-io/etcd/archive/v3.4.13.zip" rel="nofollow">https://github.com/etcd-io/etcd/archive/v3.4.13.zip</a></p>

<pre><code class="language-bash"># 在$GOPATH/src下创建目录go.etcd.io

# 解压文件v3.4.13.zip，并把目录名称改为etcd，然后把整个etcd目录移动到$GOPATH/src/go.etcd.io/目录下即可
</code></pre>

<p><br></p>

<h4 id="toc_34">6.2 连接etcd服务</h4>

<p><strong>(1) 简单连接</strong></p>

<pre><code class="language-go">func InitETCD(endPoints []string) (*clientv3.Client, error) {
    // 配置
    config := clientv3.Config{
        Endpoints:   endPoints,
        DialTimeout: 10 * time.Second,
    }

    // 连接
    return clientv3.New(config)
}

/* 调用
    cli, err := InitETCD([]string{&quot;192.168.3.5:2379&quot;})
    if err != nil {
        panic(err)
    }
*/
</code></pre>

<p><br></p>

<p><strong>(2) 有tls身份验证连接</strong></p>

<pre><code class="language-go">// InitETCDWithTLS 连接需要认证的etcd
func InitETCDWithTLS(endPoints []string, caFile, certFile, keyFile string) (*clientv3.Client, error) {
    tlsInfo := transport.TLSInfo{
        TrustedCAFile: caFile,
        CertFile:      certFile,
        KeyFile:       keyFile,
    }
    tlsConfig, err := tlsInfo.ClientConfig()
    if err != nil {
        return nil, err
    }

    // 配置etcd
    config := clientv3.Config{
    Endpoints: endPoints,
        DialTimeout: 5 * time.Second,
        TLS:       tlsConfig,
    }

    return clientv3.New(config)
}

/*
    endPoints := []string{&quot;192.168.3.5:23791&quot;, &quot;192.168.3.5:23792&quot;, &quot;192.168.3.5:23793&quot;}
    caFile := &quot;D:\\certs\\ca.pem&quot;
    certFile := &quot;D:\\certs\\etcd1.pem&quot;
  keyFile := &quot;D:\\certs\\etcd1-key.pem&quot;
  
    cli, err := InitETCDWithTLS(endPoints, caFile, certFile, keyFile)
    if err != nil {
        panic(err)
    }
*/
</code></pre>

<p><br></p>

<h4 id="toc_35">6.3 增删改查数据</h4>

<p><strong>(1) 增加和修改</strong></p>

<pre><code class="language-go">    putResp, err := cli.KV.Put(context.Background(), &quot;/user/zhangsan&quot;, &quot;v5&quot;)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(&quot;global version is&quot;, putResp.Header.Revision)


  // 增加或修改时返回上一个版本值和版本号
    putResp, err := cli.KV.Put(context.Background(), &quot;/user/zhangsan&quot;, &quot;v6&quot;, clientv3.WithPrevKV())
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(&quot;global version is&quot;, putResp.Header.Revision)
    if putResp.PrevKv != nil &amp;&amp; string(putResp.PrevKv.Value) != &quot;&quot; {
        fmt.Printf(&quot;preVal=%s, preVersion=%d\n&quot;,
            string(putResp.PrevKv.Value), // 上一个版本值
            putResp.PrevKv.Version,       // 上一个历史版本号
        )
    }
</code></pre>

<p><strong>(2) 查询</strong></p>

<pre><code class="language-go">  key:=&quot;job1&quot;

  // ops为空时，只查询单个key
    ops := []clientv3.OpOption{
        // 前缀查询
        //clientv3.WithPrefix(),

        // 范围查询
        //clientv3.WithRange(&quot;/job4&quot;),

        // 查询结果排序，查询结果可以按Key、Value、CreateRevision(key创建的版本)、Version(key历史版本数量)、ModRevision(key对应最新版本)排序，可以升序(Ascend)或降序(Descend)。
        //clientv3.WithSort(clientv3.SortByVersion, clientv3.SortDescend),

        // 获取数量
    //clientv3.WithCountOnly(), // 只返回数量，不返回kv，可以判断指定key是否存在，可以用来统计key数量

        // 实现翻页功能
        //clientv3.WithFromKey(),
        //clientv3.WithLimit(3),
    }

    getResp, err := cli.KV.Get(context.Background(), key, ops...)
    if err != nil {
        fmt.Println(err)
        return
    }
    for i, v := range getResp.Kvs {
        fmt.Printf(&quot;(%d) %s -&gt; %s, historySize=%d, createRev=%d, modRev=%d,headRev=%d\n&quot;,
            i,
            string(v.Key),
            string(v.Value),
            v.Version,               // 本key一共有多少个历史版本
            v.CreateRevision,        // 第一次创建所在版本号
            v.ModRevision,           // 最新更改版本号
            getResp.Header.Revision, // 全局最大版本号
        )
    }
</code></pre>

<p><br></p>

<p><strong>(3) 删除</strong></p>

<pre><code class="language-go">  // ops为空时，只删除个key
    ops := []clientv3.OpOption{
    // 删除匹配前缀所有的key
  //clientv3.WithPrefix(),
  }

    delResp, err := cli.KV.Delete(context.Background(), key, ops...)
    if err != nil {
        fmt.Println(err)
        return
  }
  
    fmt.Sprintln(&quot;deleted count: &quot;, delResp.Deleted) // 删除了多少个key
    fmt.Sprintln(&quot;PrevKvs : &quot;, delResp.PrevKvs)      // 删除了哪些key
  }
</code></pre>

<p><br></p>

<h4 id="toc_36">6.4 事务txn</h4>

<pre><code class="language-go">    key := &quot;num&quot;

    _, err := cli.KV.Put(context.Background(), key, &quot;05&quot;)
    if err != nil {
        panic(err)
    }

    // 链式操作
    txnResp, err := cli.KV.Txn(context.Background()).
        If(clientv3.Compare(clientv3.Value(key), &quot;&gt;&quot;, &quot;10&quot;)). // 条件，可以多个clientv3.Cmp
        Then(clientv3.OpPut(key, &quot;10&quot;)).                      // 所有条件成立，执行的操作，可以多个clientv3.Op
        Else(clientv3.OpPut(key, &quot;0&quot;)).                       // 至少一个条件不成立，执行的操作，可以多个clientv3.Op
        Commit()
    if err != nil {
        panic(err)
    }

    if txnResp.Succeeded {
        fmt.Println(&quot;txn success&quot;)
    } else {
        fmt.Println(&quot;txn failed&quot;)
    }
</code></pre>

<p><br></p>

<h4 id="toc_37">6.5 租约lease</h4>

<p><strong>(1) 生成租约</strong></p>

<pre><code class="language-go">    // 生成一个新的租约(单位秒)
    grantResp, err := cli.Grant(context.Background(), 300)
    if err != nil {
        panic(err)
    }
    fmt.Println(&quot;lease id is&quot;, grantResp.ID, &quot;or&quot;, strconv.FormatInt(int64(grantResp.ID), 16))

    // 把新的租约绑定到key，租约过期后自动删除
    _, err = cli.KV.Put(context.Background(), &quot;foo&quot;, &quot;bar&quot;, clientv3.WithLease(grantResp.ID))
    if err != nil {
        panic(err)
    }

    // 只按原来租约时间续签一次
    kao, kaerr := cli.KeepAliveOnce(context.Background(), grantResp.ID)
    if kaerr != nil {
        panic(err)
    }
    fmt.Println(&quot;ttl:&quot;, kao.TTL)

    // 持续续签租约，直到程序运行结束
    kaResp, err := cli.KeepAlive(context.Background(), grantResp.ID)
    if err != nil {
        panic(err)
    }
    ka := &lt;-kaResp // 通过管道获取返回信息，缓冲管道
    fmt.Println(ka.ID, ka.TTL)

    // 撤销租约，会使绑定租约的key立即删除
    time.Sleep(time.Second * 30)
    _, err = cli.Revoke(context.Background(), grantResp.ID)
    if err != nil {
        panic(err)
    }
  fmt.Printf(&quot;revoke lease(%d) success&quot;, grantResp.ID)
  
    // 查看租约还有多久过期，从而知道绑定的key什么时候过期
    ttlResp, err := cli.TimeToLive(context.Background(), grantResp.ID, clientv3.WithAttachedKeys())
    if err != nil {
        panic(err)
    }
    fmt.Println(
        ttlResp.GrantedTTL, // 租约总时长(秒)
        ttlResp.TTL,        // 剩余时长(秒)
        string(bytes.Join(ttlResp.Keys, []byte(&quot;,&quot;))), // 绑定的key
    )
</code></pre>

<p><br></p>

<h4 id="toc_38">6.7 监听watch</h4>

<p>watch监听put和delete事件，可以使用ctx取消监听。</p>

<pre><code class="language-go">    // 监听一个key
    go func() {
        wChan := cli.Watch(context.Background(), &quot;/user/zhangsan&quot;) // 对key监听
        for wResp := range wChan {                                 // 监听key值是否有变化(一直阻塞)，也可以使用for select来获取管道信息，结合ctx来控制是否退出监听
            for _, event := range wResp.Events { // 查看事件，根据事件类型(修改、删除)做出相应处理
                fmt.Printf(&quot;%s %q : %q\n&quot;, event.Type, event.Kv.Key, event.Kv.Value)
            }
        }
    }()

    // 监听key前缀
    go func() {
        ctx, _ := context.WithCancel(context.Background())
        wChan := cli.Watch(ctx, &quot;/user/&quot;, clientv3.WithPrefix()) // 对适配前缀所有key监听
        for {
            select {
            case wResp := &lt;-wChan:
                for _, event := range wResp.Events { // 查看事件，根据事件类型(修改、删除)做出相应处理
                    fmt.Printf(&quot;%s %q : %q\n&quot;, event.Type, event.Kv.Key, event.Kv.Value)
                }
            case &lt;-ctx.Done():
                fmt.Println(ctx.Err())
                return
            }
        }
    }()

    // 不管key有没有更新，etcd会每个10分钟发送一次通知事件
    go func() {
        wChan := cli.Watch(context.Background(), &quot;foo&quot;, clientv3.WithProgressNotify())
        for wResp := range wChan {
            for _, event := range wResp.Events {
                fmt.Printf(&quot;%s %q : %q\n&quot;, event.Type, event.Kv.Key, event.Kv.Value)
            }
            fmt.Printf(&quot;wResp.Header.Revision: %d\n&quot;, wResp.Header.Revision)
            fmt.Println(&quot;wResp.IsProgressNotify:&quot;, wResp.IsProgressNotify())
        }
    }()

    // key范围监听，不包括最大值
    go func() {
        wChan := cli.Watch(context.Background(), &quot;job1&quot;, clientv3.WithRange(&quot;job3&quot;))
        for wResp := range wChan {
            for _, event := range wResp.Events {
                fmt.Printf(&quot;%s %q : %q\n&quot;, event.Type, event.Kv.Key, event.Kv.Value)
            }
        }
    }()
</code></pre>

<p><br></p>

<h4 id="toc_39">6.8 实现分布式锁示例</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;time&quot;

    &quot;go.etcd.io/etcd/clientv3&quot;
)

func main() {
    cli, err := InitETCD([]string{&quot;192.168.3.5:2379&quot;})
    if err != nil {
        panic(err)
    }

    handle := func() {
        fmt.Println(&quot;处理业务中...&quot;)
        time.Sleep(10 * time.Second)
        fmt.Println(&quot;处理业务完毕&quot;)
    }

    DistributedLock(cli, handle)
}

// InitETCD 连接etcd
func InitETCD(endPoints []string) (*clientv3.Client, error) {
    // 配置
    config := clientv3.Config{
        Endpoints:   endPoints,
        DialTimeout: 10 * time.Second,
    }

    // 连接
    return clientv3.New(config)
}

// DistributedLock 分布式锁
func DistributedLock(cli *clientv3.Client, handle func()) {
    // 生成一个新的租约(单位秒)
    grantResp, err := cli.Grant(context.Background(), 5)
    if err != nil {
        return
    }
    fmt.Println(&quot;new lease id is&quot;, grantResp.ID)

    ctx, cancelFunc := context.WithCancel(context.Background())
    // 处理完业务后结束租约
    defer func() {
        cli.Revoke(context.Background(), grantResp.ID)
        cancelFunc()
    }()

    // 持续续租，直到处理完业务
    err = keepAlive(ctx, cli, grantResp.ID)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 新建事务
    txn := cli.KV.Txn(context.Background())
    key := &quot;/lock/mutex&quot;
    // 判断key是否存在，不存在说明成功抢到锁
    txn.If(clientv3.Compare(clientv3.CreateRevision(key), &quot;=&quot;, 0)).
        Then(clientv3.OpPut(key, &quot;ok&quot;, clientv3.WithLease(grantResp.ID))). // 创建key并绑定租约
        Else(clientv3.OpGet(key))                                          // 否则抢锁失败
    txnResp, err := txn.Commit() // 提交事务
    if err != nil {
        fmt.Println(err)
        return
    }

    // 判断是否抢到了锁，true:抢锁成功，false:抢锁失败
    if !txnResp.Succeeded {
        fmt.Printf(&quot;锁(%s)已被占用:&quot;, key)
        return
    }

    // 处理业务
    handle()
}

// 持续续租
func keepAlive(ctx context.Context, cli *clientv3.Client, leaseID clientv3.LeaseID) error {
    // 自动续租
    keepRespChan, err := cli.KeepAlive(ctx, leaseID)
    if err != nil {
        return err
    }

    // 处理续约应答
    go func(ctx context.Context) {
        for {
            select {
            case _, ok := &lt;-keepRespChan:
                if !ok {
                    return
                }
            case &lt;-ctx.Done():
                return
            }
        }
    }(ctx)

    return nil
}
</code></pre>

<p><br><br></p>

<p>参考：</p>

<ul>
<li>etcd官方文档 <a href="https://etcd.io/docs/v3.4.0/" rel="nofollow">https://etcd.io/docs/v3.4.0/</a></li>
<li>etcd应用场景 <a href="https://www.jianshu.com/p/abedeea7044e" rel="nofollow">https://www.jianshu.com/p/abedeea7044e</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/etcd.html">https://zhuyasen.com/post/etcd.html</a>，<a href="https://zhuyasen.com/post/etcd.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>网格交易策略</title>
            <link>https://zhuyasen.com/post/gridStrategy.html</link>
            <comments>https://zhuyasen.com/post/gridStrategy.html#comments</comments>
            <guid>https://zhuyasen.com/post/gridStrategy.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">1 网格交易历史来源</h3>

<p>网格交易的思路来源于信息论之父香农，上世纪40年代的某一天，香农在黑板上给大家演示了他的投资理论：</p>

<p>在任何一个价位，用资金的50%买入资产作为起始仓位，当价格上涨一定幅度就卖出一部分仓位套现，当价格下跌一定幅度就买入一部分仓位补仓，保持仓位和现金的比例始终为50%:50%，香农始终采用了半仓的持仓方式，保持每年复利29%，直到50岁得了老年痴呆症，才没能延续辉煌。</p>

<p><br><br></p>

<h3 id="toc_1">2 网格策略</h3>

<p>根据香农仓位和现金1:1交易思路拓展为n:1，n表示网格数量，1表示一份投资金额，把投资金额平均分布在n个网格上，随着行情在网格范围内波动，行情下跌时逐步加仓，行情上涨时逐步减仓，持仓会跟着行情动态变化，通过低买高卖赚取利润。</p>

<p>如下表一个10个格子的网格策略列表，价格排列为等比数列，每格投资金额一样，从表格中可以看出当前网格卖出数量和下一格买入数量是相同的(例如序号1的卖出数量0.265和序号2的买入数量0.265是一样的)，也就是低价买入，高价卖出相同数量仓位来赚取利润，如果行情在网格价格范围内振荡越频繁，收益就越高。</p>

<table>
<thead>
<tr>
<th align="left">网格序号</th>
<th align="left">买卖价格</th>
<th align="left">买入数量</th>
<th align="left">卖出数量</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">0</td>
<td align="left">400</td>
<td align="left">0</td>
<td align="left">0.257</td>
</tr>

<tr>
<td align="left">1</td>
<td align="left">389</td>
<td align="left">0.257</td>
<td align="left">0.265</td>
</tr>

<tr>
<td align="left">2</td>
<td align="left">378</td>
<td align="left">0.265</td>
<td align="left">0.272</td>
</tr>

<tr>
<td align="left">3</td>
<td align="left">367</td>
<td align="left">0.272</td>
<td align="left">0.28</td>
</tr>

<tr>
<td align="left">4</td>
<td align="left">357</td>
<td align="left">0.28</td>
<td align="left">0.288</td>
</tr>

<tr>
<td align="left">5</td>
<td align="left">347</td>
<td align="left">0.288</td>
<td align="left">0.297</td>
</tr>

<tr>
<td align="left">6</td>
<td align="left">337</td>
<td align="left">0.297</td>
<td align="left">0.305</td>
</tr>

<tr>
<td align="left">7</td>
<td align="left">327</td>
<td align="left">0.305</td>
<td align="left">0.315</td>
</tr>

<tr>
<td align="left">8</td>
<td align="left">318</td>
<td align="left">0.315</td>
<td align="left">0.324</td>
</tr>

<tr>
<td align="left">9</td>
<td align="left">309</td>
<td align="left">0.324</td>
<td align="left">0.333</td>
</tr>

<tr>
<td align="left">10</td>
<td align="left">300</td>
<td align="left">0.333</td>
<td align="left">0</td>
</tr>
</tbody>
</table>

<p><br><br></p>

<h3 id="toc_2">3 网格类型</h3>

<h4 id="toc_3">3.1 普通网格交易</h4>

<p>普通网格交易是一种动态调仓位的自动化交易策略，设定网格上限、下限和网格数量，根据网格上下限按一定方式(等比、等差)分为N个格，把总投资金额平均分散到每个格子，在低价买入高价卖出来赚取利润，如下图所示。</p>

<p><img src="https://go-sponge.com/assets/images/blog/gridStrategy-001.png" alt="普通网格交易" /></p>

<p><strong>适用场景</strong></p>

<p>只适合横盘振荡行情，不适合大涨或大跌行情，当行情上涨或下跌超过网格范围后，网格就会在等待状态，需要等到行情重新回到网格范围内才会交易。</p>

<p><strong>优点</strong></p>

<ul>
<li>行情在网格价格范围内不需要判断行情方向就可以实现盈利。</li>
<li>启动网格后自动化交易。</li>
</ul>

<p><strong>缺点</strong></p>

<ul>
<li>相比持币待涨方式，资金利用率低，收益率也低。</li>
<li>行情大跌的时候很快满仓造成亏损比较大，行情大涨时很快清仓完，导致盈利低下。</li>
<li>需要使用者判断当前行情是否在心理上的中低价位，如果不挑选时刻盲目进场，入场时行情价格刚好在高位，很容易造成亏损。</li>
</ul>

<p><br></p>

<h4 id="toc_4">3.2 趋势网格交易</h4>

<p>趋势网格是在普通网格交易基础上，添加网格自动跟随行情往上移动功能，以入场价为起点，当行情上涨超过网格最大值时，自动生成新网格，使得网格可以继续交易赚取利润，不需要人工干预，当行情下跌超过当前网格最小值后，网格会在等待状态，等到行情回到当前网格范围内才可以继续交易，如下图所示。</p>

<p><img src="https://go-sponge.com/assets/images/blog/gridStrategy-002.png" alt="趋势网格" /></p>

<p><strong>适用场景</strong></p>

<p>只适合振荡上涨行情，因此需要用户选择恰当时机结束趋势网格来实现利润。因为行情持续下跌时的浮动盈亏会抵消到原来振荡上涨时赚取的利润，甚至亏损。</p>

<p><strong>优点</strong></p>

<ul>
<li>行情在振荡或上涨情况下都可以实现盈利。</li>
<li>启动网格后自动化交易。</li>
</ul>

<p><strong>缺点</strong></p>

<ul>
<li>相比持币待涨方式，资金利用率低，收益率也低。</li>
<li>行情大跌的时候很快满仓造成亏损比较大。</li>
<li>行情下跌超出当前网格范围，造成网格会在等待状态，无法继续交易。</li>
<li>需要使用者判断当前行情是否在心理上的中低价位，如果不挑选时刻盲目进场，进场时行情价格刚好在高位，很容易造成亏损。</li>
<li>需要使用者判断当前行情是否在心理上为高位，结束网格来实现利润。</li>
</ul>

<p><br></p>

<h4 id="toc_5">3.3 无限网格</h4>

<p>无限网格和趋势网格交易类似，网格只有下限，没有上限，只需设置网格下限值和每格收益率，生成一个网格，当行情超过网格最大值时，买入一定仓位，自动挂更高的卖单，只要行情一直涨，都会跟随行情上涨下去。</p>

<p><img src="https://go-sponge.com/assets/images/blog/gridStrategy-003.png" alt="无限网格" /></p>

<p><strong>适用场景</strong></p>

<p>无限网格适合波动比较大、整体上涨的行情，适合连续执行半年以上。设置心理底价后直接进场，当行情下跌后分批抄底，当行情上涨至少也可以赚取利润。</p>

<p><strong>优点</strong></p>

<ul>
<li>在波动比较大的行情下也不容易进入等待状态，可以持续套利盈利。</li>
<li>设置好参数运行后，不需要再关注行情，让其长期自动交易。</li>
</ul>

<p><strong>缺点</strong></p>

<ul>
<li>相对于网格交易，资金利用率更低，收益率也更低。</li>
<li>需要使用者判断当前行情是否在心理上的中低价位，如果不挑选时刻盲目进场，进场时行情价格刚好在高位，很容易造成亏损。</li>
</ul>

<p><br><br></p>

<h3 id="toc_6">4 计算特定条件的最优网格</h3>

<p>网格最理想情况是碰到反复振荡行情，遇到振荡行情，如果网格参数设置不合理，同样无法获得收益，甚至亏损。如果设置网格数少，并且网格间隔太大，虽然每格收益率大，但受制于行情振荡幅度，交易机会很小，收益也会少；如果网格数比较多，并且网格间隔太小，虽然交易机会比较多，但每次低买高卖的利润都不够手续费，造成亏损。因此设置好的网格参数不是随便设置网格数量、最小价格和最大价格就可以有收益了，需要平衡每格收益率和交易机会，与投资金额、买卖最小金额、手续费、当前行情价格、目标收益率有关，根据已知条件抽象出一条计算最优网格参数题目，如下所示：</p>

<p>已知品种ethusdt的当前价格380，投资总金额totalSum=100，每次买入或卖出最小金额limitVolume=10，每次成功的买入或卖出订单手续费为0.1%，网格在低价买入，然后高价卖出称为一次套利arbitrage，套利减去买和卖手续费后得到利润profit，每格收益率profitRate=profit÷arbitrage×100%，要求profitRate&gt;0.1%，计算出最优的网格参数：</p>

<ul>
<li>(1) 网格数量num，其中5&lt;=num&lt;=60；</li>
<li>(2) 网格平均间距intervalPrice，网格价格间隔越小，在行情震荡时出现套利机会越多；</li>
<li>(3) 最优网格对应的最低价格minPrice、最高价格maxPrice、网格价格分布序列。</li>
</ul>

<p><em>注：profitRate越小，并且intervalPrice也越小时得到的网格参数认为最优。</em></p>

<p>根据已知条件计算得出最优网格参数如下：</p>

<ul>
<li>网格数num: 10</li>
<li>网格最低价格minPrice: 370.88 USDT</li>
<li>网格最高价格maxPrice: 401.28 USDT</li>
<li>网格平均间距intervalPrice: 3.04 USDT</li>
</ul>

<p>网格序列如下：</p>

<table>
<thead>
<tr>
<th align="left">网格序号</th>
<th align="left">买卖价格</th>
<th align="left">买入数量</th>
<th align="left">卖出数量</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">0</td>
<td align="left">401.28</td>
<td align="left">0</td>
<td align="left">0.025117</td>
</tr>

<tr>
<td align="left">1</td>
<td align="left">398.13</td>
<td align="left">0.025117</td>
<td align="left">0.025316</td>
</tr>

<tr>
<td align="left">2</td>
<td align="left">395.01</td>
<td align="left">0.025316</td>
<td align="left">0.025516</td>
</tr>

<tr>
<td align="left">3</td>
<td align="left">391.91</td>
<td align="left">0.025516</td>
<td align="left">0.025718</td>
</tr>

<tr>
<td align="left">4</td>
<td align="left">388.83</td>
<td align="left">0.025718</td>
<td align="left">0.025922</td>
</tr>

<tr>
<td align="left">5</td>
<td align="left">385.78</td>
<td align="left">0.025922</td>
<td align="left">0.026127</td>
</tr>

<tr>
<td align="left">6</td>
<td align="left">382.75</td>
<td align="left">0.026127</td>
<td align="left">0.026333</td>
</tr>

<tr>
<td align="left">7</td>
<td align="left">379.75</td>
<td align="left">0.026333</td>
<td align="left">0.026541</td>
</tr>

<tr>
<td align="left">8</td>
<td align="left">376.77</td>
<td align="left">0.026541</td>
<td align="left">0.026751</td>
</tr>

<tr>
<td align="left">9</td>
<td align="left">373.81</td>
<td align="left">0.026751</td>
<td align="left">0.026963</td>
</tr>

<tr>
<td align="left">10</td>
<td align="left">370.88</td>
<td align="left">0.026963</td>
<td align="left">0</td>
</tr>
</tbody>
</table>

<p>根据投入金额和每格收益率即可计算出网格参数(最小价格、最大价格、网格数)，通过程序方式计算出来的网格参数可以适合各种不同手续费的交易所，因此可以做到自动化生成网格策略的效果。</p>

<p><br><br></p>

<h3 id="toc_7">5 总结</h3>

<p>普通网格交易、趋势网格、无限网格适应不同行情类型，各有优缺点，不能说哪种更好，如果行情在某个范围内频繁振荡，使用普通网格交易、趋势网格最适合，收益更高，如果行情长期出现大幅涨跌情况，选择无限网格更适合，无论哪种网格策略，选择进场的时机(行情在低位)非常重要。</p>

<p>世界上还没有完美的交易策略可以长期应对不可预测的行情，网格策略可以应对某种特定行情的一种交易策略，因为行情不可预测，通过广撒网方式去捕捉行情，网格交易需要人工判断哪条河有鱼(行情)，才能把网撒出去，不能随便撒网，因为撒网需要成本的，随便撒网大多数结果是竹篮打水一场空(亏损)。</p>
<p>本文链接：<a href="https://zhuyasen.com/post/gridStrategy.html">https://zhuyasen.com/post/gridStrategy.html</a>，<a href="https://zhuyasen.com/post/gridStrategy.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>根据服务名称查看golang程序的profile信息</title>
            <link>https://zhuyasen.com/post/goprofile.html</link>
            <comments>https://zhuyasen.com/post/goprofile.html#comments</comments>
            <guid>https://zhuyasen.com/post/goprofile.html</guid>
            <description>
                <![CDATA[<blockquote>

<p>go语言本身带有runtime/pprof包，使用pprof可以查看程序profile信息(例如cpu、内存、goroutine等)。</p>

<p>一个项目中可能有很多服务，这些服务部署在k8s集群或不同节点，如果想查看某个服务的profile信息(前提是开启profile功能)，通常需要找到该服务对应节点ip和端口，如果服务部署在k8s集群，可以通过端口映射、端口转发、ingress等方式获取服务的profile信息，有点麻烦，特别是服务多了之后，不容易管理和查看，为了方便管理，希望只需要知道服务名称就可以获取到对应服务的profile信息，不需要知道ip和端口，通过服务名称就可以查看该服务的profile信息。</p>

<p>具体实现步骤：</p>

<ul>
<li>(1) 使用自定义的路由(/goprofile/your-server-name)替换默认路由(/debug/pprof)；</li>
<li>(2) 使用nginx反向代理，根据路由转发请求到不同服务，然后使用负载均衡器转发请求到nginx服务。</li>
</ul>

<p><br></p>

<h3 id="toc_0">1 在服务程序中获取profile信息</h3>

<p>无论是web服务还是非服务，都可以做成通过http获取服务的profile信息，如果你的服务是web服务，刚好使用了gin框架，只需添加简单的几行代码即可，具体示例如下：</p>

<pre><code class="language-golang">package main

import (
    &quot;github.com/gin-contrib/pprof&quot;
    &quot;github.com/gin-gonic/gin&quot;
)

var enableProfile bool

func init() {
    flag.BoolVar(&amp;enableProfile, &quot;enableProfile&quot;, &quot;&quot;, &quot;is enable go profile&quot;)
    flag.Parse()
}

func main() {
  r := gin.Default()
  if enableProfile {
    // 使用服务名称替换默认路由
    pprof.Register(r,&quot;/goprofile/&quot;+&quot;your-server-name&quot;)
  }
  r.Run(&quot;:10060&quot;)
}
</code></pre>

<p><br></p>

<p>如果程序非gin框架程序，也可以通过gin伪造一个web服务出来放到goroutine去执行即可，具体示例如下：</p>

<pre><code class="language-golang">package main

import (
    &quot;github.com/gin-contrib/pprof&quot;
    &quot;github.com/gin-gonic/gin&quot;
)

var enableProfile bool

func init() {
    flag.BoolVar(&amp;enableProfile, &quot;enableProfile&quot;, &quot;&quot;, &quot;is enable go profile&quot;)
    flag.Parse()
}

func profile() {
  r := gin.Default()
  // 使用服务名称替换默认路由
  pprof.Register(r,&quot;/goprofile/&quot;+&quot;your-server-name&quot;)
  r.Run(&quot;:10060&quot;)
}

func main() {
  if enableProfile {
    go profile()
  }
  
  // run your code
  select{}
}
</code></pre>

<p>启动服务时开启profile功能：./your-app -enableProfile=true，开启profile功能后，在浏览器打开 http://<host-ip>:10060/goprofile/your-server-name 可以查看profile信息。</p>

<p><br><br></p>

<h3 id="toc_1">2 使用nginx做路由转发</h3>

<p>新建一个nginx配置default.conf，文件内容如下：</p>

<pre><code>server {
    listen       80;
    server_name  localhost;
    #charset koi8-r;
    #access_log  logs/host.access.log  main;

    location / {
        root   html;
        index  index.html index.htm;
    }
    
    location /goprofile/your-server-name1 {
        proxy_pass http://your-server-name1.com:8080/goprofile/your-server-name1;
    }

    location /goprofile/your-server-name2 {
        proxy_pass http://your-server-name2.com:8081/goprofile/your-server-name2;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}
</code></pre>

<p>把default.conf文件复制到/etc/nginx/conf.d/目录下，启动nginx服务，使用nginx代理方式可以通过服务名称访问对应profile信息。</p>

<p>使用反向代理遇到的301重定向问题，例如在浏览器访问<a href="http://xxx.com/abc，会重定向到http://xxx.com/abc/，也就是当请求URL后面没有/，nginx目录中没有对应的文件，就会自动进行" rel="nofollow">http://xxx.com/abc，会重定向到http://xxx.com/abc/，也就是当请求URL后面没有/，nginx目录中没有对应的文件，就会自动进行</a> 301并加上/。</p>

<p>解决方式：</p>

<p>在nginx的配置文件中，加上<strong>port_in_redirect off;</strong> 如果是nginx 版本号大于1.11.8，可以考虑使用<strong>absolute_redirect off;</strong></p>

<p>注意：
在用chrome的时候，一定要先清除缓存在测试，chrome会自动将301缓存在本地。</p>

<p><br><br></p>

<h3 id="toc_2">3 在k8s获取golang程序的profile信息的完整示例</h3>

<p><strong>(1) golang程序开启profile功能</strong></p>

<p>首先添加获取golang程序的profile信息功能，然后使用参数方式开启和关闭profile功能，默认是关闭状态，例如开启profile功能：./your-app -enableProfile</p>

<p><br></p>

<p><strong>(2) 在k8s部署nginx代理</strong></p>

<p>nginx配置文件go-profile-proxy-configmap.yml文件内容如下：</p>

<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: go-profile-proxy-config
data:
  default.conf: |-
    server {
        listen       80;
        server_name  localhost;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }
        
        location /goprofile/wq-account {
            proxy_pass http://wq-account-svc:80/goprofile/wq-account;
        }

        location /goprofile/wq-pcc {
            proxy_pass http://wq-pcc-svc:80/goprofile/wq-pcc;
        }

        location /goprofile/wq-monitor-msg-collect {
            proxy_pass http://wq-monitor-msg-collect-svc:80/goprofile/wq-monitor-msg-collect;
        }
        
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
</code></pre>

<p><br></p>

<p>部署nginx服务脚本文件go-profile-proxy-deployment.yml内容如下：</p>

<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-profile-proxy-dm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-profile-proxy
  template:
    metadata:
      name: go-profile-proxy-pod
      labels:
        app: go-profile-proxy
    spec:
      containers:
      - name: go-profile-proxy
        image: nginx:1.15
        imagePullPolicy: IfNotPresent
        ports:
        - name: app-port
          containerPort: 80
        volumeMounts:
        - name: go-profile-proxy-vl
          mountPath: /etc/nginx/conf.d/
          readOnly: true

      volumes:
      - name: go-profile-proxy-vl
        configMap:
          name: go-profile-proxy-config
</code></pre>

<p><br></p>

<p>暴露nginx服务端口，部署脚本go-profile-proxy-svc.yml文件内容如下：</p>

<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: go-profile-proxy-svc
spec:
  selector:
    app: go-profile-proxy
  type: NodePort
  ports: 
  - name: app-svc-port
    port: 80
    targetPort: 80
    nodePort: 32280
</code></pre>

<p><br></p>

<p>在k8s启动nginx代理服务：</p>

<pre><code class="language-bash">kubectl apply -f go-profile-proxy-configmap.yml
kubectl apply -f go-profile-proxy-deployment.yml
kubectl apply -f go-profile-proxy-svc.yml
</code></pre>

<p>在浏览器打开 http://<hostIP>:32280/goprofile/your-server-name 就可以查看profile信息了。</p>

<p><br><br></p>

<h3 id="toc_3">4 go程序性能分析</h3>

<p><strong>(1) 查看服务的prifile概况</strong></p>

<p>例如wq-pcc服务已开启了profile功能，这里使用负载均衡器域名和端口<a href="http://xxxxxx.elb.ap-northeast-1.amazonaws.com:32280转发请求到nginx服务，如果想查看wq-pcc服务的profile信息，在浏览器打开http://xxxxxx.elb.ap-northeast-1.amazonaws.com:32280/goprofile/wq-pcc即可，如下图所示：" rel="nofollow">http://xxxxxx.elb.ap-northeast-1.amazonaws.com:32280转发请求到nginx服务，如果想查看wq-pcc服务的profile信息，在浏览器打开http://xxxxxx.elb.ap-northeast-1.amazonaws.com:32280/goprofile/wq-pcc即可，如下图所示：</a></p>

<p><img src="https://go-sponge.com/assets/images/blog/276888_1_%E6%A0%B9%E6%8D%AE%E6%9C%8D%E5%8A%A1%E6%9F%A5%E7%9C%8Bprofileg%E6%A6%82%E5%86%B5.jpg" alt="查看profile概况" /></p>

<p><br></p>

<p><strong>(2) 具体分析服务的性能</strong></p>

<p>收集服务wq-pcc在40秒内的cpu和内存信息，在这段时间内进行暴力压测，尽量让cpu占用性能产生数据，如果不指定时间，默认收集30秒。</p>

<pre><code class="language-bash">go tool pprof --seconds 40 http://xxxxxx.elb.ap-northeast-1.amazonaws.com:32280/goprofile/wq-pcc/profile
go tool pprof --seconds 40 http://xxxxxx.elb.ap-northeast-1.amazonaws.com:32280/goprofile/wq-pcc/heap
</code></pre>

<p>收集完毕后使用top或web命令查看信息详情，使用web命令要求本地安装<a href="https://graphviz.gitlab.io/download/" rel="nofollow">Graphviz</a>工具，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/276888_2_%E9%87%87%E9%9B%86cpu%E4%BF%A1%E6%81%AF.jpg" alt="采集cpu信息" /></p>

<p><br></p>

<p>如果本地安装了FlameGraph和go-torch，可以查看火焰图，生成火焰图方法：</p>

<pre><code class="language-bash"># 采集30秒的cpu数据
go-torch go-torch http://dev-k8s-server-b6a745217e8dd924.elb.ap-northeast-1.amazonaws.com:32280/goprofile/wq-pcc/profile -t 30
# 30s后go-torch完成采样，输出以下信息：Writing svg to torch.svg (torch.svg文件保存在安装路径$GOPATH/github.com/brendangregg/FlameGraph目录下)

在浏览器打开file:///Users/any/work/go/src/github.com/brendangregg/FlameGraph/torch.svg

火焰图的y轴表示cpu调用方法的先后，x轴表示在每个采样调用时间内，方法所占的时间百分比，越宽代表占据cpu时间越多。
</code></pre>

<p><img src="https://go-sponge.com/assets/images/blog/696682_1_%E7%81%AB%E7%84%B0%E5%9B%BE%E7%A4%BA%E4%BE%8B.jpg" alt="火焰图" /></p>

<p>在win10中无法生成svg，出现错误：flamegraph.pl: %1 is not a valid Win32 application，暂时未有解决方法，在mac或linux可以正常生成svg文件。</p>

<p>更多详细性能分析教程查看：<a href="https://blog.csdn.net/wangdenghui2005/article/details/99119941" rel="nofollow">https://blog.csdn.net/wangdenghui2005/article/details/99119941</a></p>
<p>本文链接：<a href="https://zhuyasen.com/post/goprofile.html">https://zhuyasen.com/post/goprofile.html</a>，<a href="https://zhuyasen.com/post/goprofile.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>go语言开发规范</title>
            <link>https://zhuyasen.com/post/devspec.html</link>
            <comments>https://zhuyasen.com/post/devspec.html#comments</comments>
            <guid>https://zhuyasen.com/post/devspec.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">1 代码格式化</h3>

<p>go默认已经有了gofmt工具，但是建议使用goimport工具，这个在gofmt的基础上增加了自动删除和引入包，目前IDE基本都支持goimports，安装goimports：</p>

<blockquote>
<p>go get golang.org/x/tools/cmd/goimports</p>
</blockquote>

<p>对import的包进行分组管理，用换行符分割，而且标准库作为分组的第一组。如果你的包引入了三种类型的包，有标准库包、程序内部包、第三方包，建议采用如下方式进行组织你的包：</p>

<pre><code class="language-go">import (
    &quot;fmt&quot;
    &quot;os&quot;

    &quot;kmg/a&quot;
    &quot;kmg/b&quot;

    &quot;code.google.com/a&quot;
    &quot;github.com/b&quot;
)
</code></pre>

<p>在项目中不要使用相对路径引入包：</p>

<pre><code class="language-go">// 错误示例
import &quot;../net&quot;

// 正确的做法
import &quot;github.com/repo/proj/src/net&quot;
</code></pre>

<p><br><br></p>

<h3 id="toc_1">2 注释</h3>

<p>代码注释有两种方式：</p>

<ul>
<li>行注释：//</li>
<li>块注释：/* …… */</li>
</ul>

<p>如果想在每个文件中的头部加上版权注释，需要在版权注释和package注释前面加一个空行，否则版权注释会作为package的注释，如下所示：</p>

<pre><code class="language-go">// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

/*
Package net provides a portable interface for network I/O, including
TCP/IP, UDP, domain name resolution, and Unix domain sockets.
......
*/
package net
</code></pre>

<p>注：使用//注释时，在//之后应该加一个空格。</p>

<p><br></p>

<p>package里导出的变量和函数前面必须写注释，如果不添加注释，golint检查无法通过，如下所示：</p>

<pre><code class="language-go">const (
    // StatusRunning 运行状态
    StatusRunning = 1
)

// Send 发送消息
func Send(content []byte){
    ...
}
</code></pre>

<p><br><br></p>

<h3 id="toc_2">3 一些大小的约定</h3>

<ul>
<li>单个文件代码行数建议不超过500行。</li>
<li>单个函数长度不超过100行。</li>
<li>函数两个要求：单一职责、要短小。</li>
<li>单行语句不能过长，如不能拆分需要分行写，一行最多120个字符。</li>
<li>函数中缩进嵌套必须小于等于3层，禁止出现以下这种锯齿形的函数，应通过尽早通过return等方法重构。</li>
</ul>

<p>错误的示范：</p>

<pre><code class="language-go">if ... {
    if ... {
        if ... {
            
        } else {
            if ... {
                
            }
        }
    } else {
        
    }
}
</code></pre>

<ul>
<li>保持函数内部实现的组织粒度是相近的。</li>
</ul>

<p>建议把下面代码整理下：</p>

<pre><code class="language-go">func main() {
    initLog()
    
    //这一段代码的组织粒度，明显与其他的不均衡
    orm.DefaultTimeLoc = time.UTC
    sqlDriver := beego.AppConfig.String(&quot;sqldriver&quot;)
    dataSource := beego.AppConfig.String(&quot;datasource&quot;)
    modelregister.InitDataBase(sqlDriver, dataSource)
    
    Run()
}
</code></pre>

<p>改为：</p>

<pre><code class="language-go"> func main() {
    initLog()

    initORM()  //修改后，函数的组织粒度保持一致 

    Run()
}
</code></pre>

<p><br><br></p>

<h3 id="toc_3">4 命名规则</h3>

<p><strong>(1) 局部变量命名规则</strong></p>

<p>局部变量名称一般遵循驼峰法，但遇到特有名词时，需要遵循以下规则：</p>

<ul>
<li>如果变量为私有，且特有名词为首个单词，则使用小写，如apiClient。</li>
<li>其它情况都应当使用该名词原有的写法，如 APIClient、repoID、UserID。</li>
<li>错误示例：UrlArray，应该写成urlArray或者URLArray。</li>
</ul>

<p>如果变量类型为bool类型，则名称应以Is、Has、Can或Allow开头，例如：isExist、hasConflict、canManage、allowGitHook。</p>

<p>在相对简单的环境(对象数量少、针对性强)中，可以将一些名称由完整单词简写为单个字母，例如user简写为u。</p>

<p><br></p>

<p><strong>(2) 全局变量命名规则</strong></p>

<p>全局变量名称一般遵循驼峰法，不要使用简写，做到见其名知其义。</p>

<p><br></p>

<p><strong>(3) 包的命名规则</strong></p>

<p>包的命名使用小写，尽量不要使用下划线或者混合大小写，包名应该用单数的形式，比如util、model，而不是utils、models。</p>

<p><br></p>

<p><strong>(4) 函数命名规则</strong></p>

<p>使用驼峰式命名，名字可以长但是得把功能描述清楚，函数名应当是动词或动词短语，如postPayment、deletePage、save等，也可以在名词前面加上get、set、is前缀。</p>

<p><br></p>

<p><strong>(5) 结构体命名规则</strong></p>

<p>结构体名应该是名词或名词短语，如Custome、WikiPage、Account、AddressParser，避免使用Manager、Processor、Data、Info这样的名称，结构体名称不应该是动词。</p>

<p>带mutex的struct的接收者receivers必须是带指针的接收者，具体示例：</p>

<pre><code class="language-go">type foo struct {
    mutex sync.Mutex
    ...
}

// 这里的接收者必须是指针，保证只对同一个锁操作，达到对同一个资源操作的互斥效果。
func (f *foo) Write (content []byte) error {
    f.mutex.Lock()
    defer f.mutex.Unlock()
    
    ...
}
</code></pre>

<p><br></p>

<p><strong>(6) 接口命名规则</strong></p>

<p>单个函数的接口名以er作为后缀，如Reader、Writer。接口的实现则去掉er。</p>

<pre><code class="language-go">type Reader interface {
    Read(p []byte) (n int, err error)
}
</code></pre>

<p>两个函数的接口名综合两个函数名，后面加er：</p>

<pre><code class="language-go">type WriteFlusher interface {
    Write([]byte) (int, error)
    Flush() error
}
</code></pre>

<p>三个以上函数的接口名，抽象这个接口的功能，类似于结构体名：</p>

<pre><code class="language-go">type Car interface {
    Start([]byte)
    Stop() error
    Recover()
}
</code></pre>

<p><br></p>

<p><strong>(7) 函数接收者命名规则</strong></p>

<p>Receiver的名称应该缩写，一般使用一个或者两个字符作为Receiver的名称，如下所示：</p>

<pre><code class="language-go">func (f foo) method1() {
    ...
}

func (f *foo) method2() {
    ...
}
</code></pre>

<p><br></p>

<p><strong>(8) 常量命名</strong></p>

<p>使用驼峰式命名，如果是枚举类型的常量，需要先创建相应类型，例如：</p>

<pre><code class="language-go">type Scheme string

const (
    HTTP  Scheme = &quot;http&quot;
    HTTPS Scheme = &quot;https&quot;
)
</code></pre>

<p>常量名称容易混淆的情况下，为了更好地区分枚举类型，可以使用完整的前缀：</p>

<pre><code class="language-go">type Status string

const (
    StatusRunning Status = 1
    StatusStop Status = 2
)
</code></pre>

<p><br></p>

<p>不要写类似于下面这种代码，如果没有注释，不知到1代表什么意义。</p>

<pre><code class="language-go">if status == 1 {
    ...
}
</code></pre>

<p>应该先定义常量，多个同类型的常量方便统一维护，应该改为：</p>

<pre><code class="language-go">const (
    // 运行状态
    StatusRunning = 1
)

if status == StatusRunning {
    ...
}
</code></pre>

<p><br></p>

<p><strong>(9) 单元测试文件和函数命名</strong></p>

<ul>
<li>单元测试文件名命名必须在文件名后面加上_test，表示该文件为单元测试文件，例如example_test.go。</li>
<li>测试用例的函数名称必须以Test开头，例如TestExample。</li>
<li>如果测试的函数是某个对象的方法，命名方式为Test+对象名_方法名，例如TestAccount_Insert。</li>
</ul>

<p><br><br></p>

<h3 id="toc_4">5 error处理</h3>

<p>为了编写强壮的代码，不要忽略错误，也不要使用panic抛出异常，而是要处理每一个错误，尽管代码写起来可能有些繁琐。</p>

<p>error处理不要写成下面这种形式，一旦有错误发生，尽可能return返回。</p>

<pre><code class="language-go">if err != nil {
    // error handling
} else {
    // normal code
}
</code></pre>

<p>而是写成下面这种形式：</p>

<pre><code class="language-go">if err != nil {
    // 如果是最顶层函数，处理错误。
    // 如果不是最顶层函数，可以在原来错误基础上添加新的错误说明，然后返回。
    return
}

// normal code
</code></pre>

<p>error的错误描述如果是英文必须为小写，不需要标点结尾。</p>

<p><br></p>

<p>go语言自带的包没有打印堆栈信息，多级错误返回情况下，比较难以判断返回错误的根因是在哪一个环节产生的，使用 github.com/pkg/errors 可以包装错误信息，如下所示：</p>

<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;io/ioutil&quot;

    &quot;github.com/pkg/errors&quot;
)

func main() {
    err := test1(&quot;hello.txt&quot;)
    if err != nil {
        // 无打印堆栈信息
        fmt.Println(&quot;1&quot;, err)

        // 只获取根因(无包装信息)
        fmt.Println(&quot;2&quot;, errors.Cause(err))

        // 使用%+v打印堆栈信息
        fmt.Printf(&quot;3 %+v\n&quot;, err)
        return
    }
}

func test1(file string) error {
    if err := test2(file); err != nil {
        // 因为下层错误已经包装过数据，无需重复包装
        return err
    }
    return nil
}

func test2(file string) error {
    content, err := ioutil.ReadFile(file)
    if err != nil {
        // 把错误的根因和消息包装后返回。
        return errors.Wrap(err, &quot;read file error&quot;)
    }

    fmt.Println(string(content))

    return nil
}
</code></pre>

<p><br><br></p>

<h3 id="toc_5">6 string和slice</h3>

<p><strong>(1) 判断字符串为空</strong></p>

<p>不要使用：</p>

<pre><code class="language-go">if len(str) == 0 {
    ...
}
</code></pre>

<p>而是使用：</p>

<pre><code class="language-go">if str == &quot;&quot; {
    ...
}
</code></pre>

<p><br></p>

<p><strong>(2) 判断slice为非空</strong></p>

<p>不要使用：</p>

<pre><code class="language-go">if slice != nil &amp;&amp; len(slice) &gt; 0 {
    ...
}
</code></pre>

<p>而是使用：</p>

<pre><code class="language-go">if len(slice) &gt; 0 {
    ...
}
</code></pre>

<p><br></p>

<p><strong>(3) byte/string slice的相等性比较</strong></p>

<p>不要使用：</p>

<pre><code class="language-go">bytes.Compare(s1, s2) == 0
bytes.Compare(s1, s2) != 0
</code></pre>

<p>而是使用：</p>

<pre><code class="language-go">bytes.Equal(s1, s2) == 0
bytes.Equal(s1, s2) != 0
</code></pre>

<p><br></p>

<p><strong>(4) 检测是否包含字串</strong></p>

<p>不要使用 strings.IndexRune(s1, &lsquo;x&rsquo;) &gt; -1及其类似的方法IndexAny、Index检查字符串包含，
而是使用strings.ContainsRune、strings.ContainsAny、strings.Contains来检查。</p>

<p><br></p>

<p><strong>(5) 复制slice</strong></p>

<p>不要使用遍历方式：</p>

<pre><code class="language-go">var b1, b2 []byte
for i, v := range b1 { 
    b2[i] = v
}
或
for i := range b1 { 
    b2[i] = b1[i]
}
</code></pre>

<p>而是使用：</p>

<pre><code class="language-go">copy(b2, b1)
</code></pre>

<p><br></p>

<p><strong>(6) 把一个slice追加到另一个slice后面</strong></p>

<p>不要使用遍历方式：</p>

<pre><code class="language-go">var a, b []int
for _, v := range a {
    b = append(b, v)
}
</code></pre>

<p>而是使用：</p>

<pre><code class="language-go">var a, b []int
b = append(b, a...)
</code></pre>

<p><br><br></p>

<h3 id="toc_6">7 布尔值判断</h3>

<p>判断真假不要使用：</p>

<pre><code class="language-go">if b == true {
    ...
}
if b == false {
    ...
}
</code></pre>

<p>而是使用：</p>

<pre><code class="language-go">if b {
    ...
}
if !b {
    ...
}
</code></pre>

<p><br><br></p>

<h3 id="toc_7">8 参数传递</h3>

<ul>
<li>参数比较多时(7个以上)，建议把参数放到结构体里，通过结构体传参。</li>
<li>对于大量数据的struct使用指针传参。</li>
<li>对于map、slice、chan这些参数不需要传递指针，因为map、slice、chan是引用类型。</li>
</ul>

<p><br></p>

<h3 id="toc_8">9 闭包使用</h3>

<p>在循环或者goroutine中使用闭包，必须使用显式的变量调用。</p>

<p>典型的闭包错误使用方式：</p>

<pre><code class="language-go">func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i &lt; 5; i++ {
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}

// 执行结果是55555，这显然不是我们想要的结果(01234)
</code></pre>

<p>正确的使用方式：</p>

<pre><code class="language-go">func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i &lt; 5; i++ {
        // 显式传参进去
        go func(j int) {
            fmt.Println(j)
            wg.Done()
        }(i)
    }
    wg.Wait()
}
</code></pre>

<p><br><br></p>

<h3 id="toc_9">10 单元测试</h3>

<p><strong>(1) 无依赖的功能测试</strong></p>

<p>单元测试的原则，就是测试的函数方法，不要受到所依赖环境的影响，比如网络访问等。</p>

<p>以下面一个简单的计算器代码为例：</p>

<pre><code class="language-go">package calculator

import (
    &quot;fmt&quot;
    &quot;strconv&quot;
)

const (
    ErrorDivision = &quot;error: the dividend cannot be 0.&quot;
    ErrorOp       = &quot;errorr: unknown op type, only support operations +-*/&quot;
)

type Calculator struct {
    x1 int
    x2 int
    op string
}

func (c *Calculator) String() string {
    return fmt.Sprintf(&quot;%d%s%d=&quot;, c.x1, c.op, c.x2)
}

// Run 加减乘除计算
func (c *Calculator) Run() string {
    switch c.op {
    case &quot;+&quot;:
        return strconv.Itoa(c.x1 + c.x2)
    case &quot;-&quot;:
        return strconv.Itoa(c.x1 - c.x2)
    case &quot;*&quot;:
        return strconv.Itoa(c.x1 * c.x2)
    case &quot;/&quot;:
        if c.x2 == 0 {
            return ErrorDivision
        }
        return strconv.Itoa(c.x1 / c.x2)
    default:
        return ErrorOp
    }
}
</code></pre>

<p><br></p>

<p>测试代码：</p>

<pre><code class="language-go">package calculator

import (
    &quot;reflect&quot;
    &quot;testing&quot;

    &quot;github.com/google/go-cmp/cmp&quot;
)

// 测试示例1，有可能会这样写测试，逐个实例化后判断
func TestCalculator_Run1(t *testing.T) {
    got := (&amp;Calculator{10, 2, &quot;+&quot;}).Run()
    expected := &quot;12&quot;
    if got != expected {
        t.Errorf(&quot;got: %v, expected: %v&quot;, got, expected)
    }

    got = (&amp;Calculator{10, 2, &quot;-&quot;}).Run()
    expected = &quot;8&quot;
    if got != expected {
        t.Errorf(&quot;got: %v, expected: %v&quot;, got, expected)
    }

    got = (&amp;Calculator{10, 2, &quot;*&quot;}).Run()
    expected = &quot;20&quot;
    if got != expected {
        t.Errorf(&quot;got: %v, expected: %v&quot;, got, expected)
    }

    got = (&amp;Calculator{10, 2, &quot;/&quot;}).Run()
    expected = &quot;5&quot;
    if got != expected {
        t.Errorf(&quot;got: %v, expected: %v&quot;, got, expected)
    }
}

// 测试示例2，第1种测试太啰嗦了，可以这样优化，看起来更简洁
func TestCalculator_Run2(t *testing.T) {
    // 列举测试数据
    tests := []struct {
        input    *Calculator
        expected string
    }{
        {&amp;Calculator{10, 2, &quot;+&quot;}, &quot;12&quot;},
        {&amp;Calculator{10, 2, &quot;-&quot;}, &quot;8&quot;},
        {&amp;Calculator{10, 2, &quot;*&quot;}, &quot;20&quot;},
        {&amp;Calculator{10, 2, &quot;/&quot;}, &quot;5&quot;},
        {&amp;Calculator{10, 0, &quot;/&quot;}, ErrorDivision},
        {&amp;Calculator{10, 2, &quot;$&quot;}, ErrorOp},
    }

    // 判断
    for _, v := range tests {
        got := v.input.Run()
        expected := v.expected
        if !reflect.DeepEqual(got, expected) {
            t.Errorf(&quot;got: %v, expected: %v&quot;, got, expected)
        }
    }
}

// 测试示例3，第2种测试写法虽然很简洁，但是当某个输入判断不通过时，而且测试数据多的时候，不好区分是哪个输入测试失败，可以再优化一下
func TestCalculator_Run3(t *testing.T) {
    // 列举测试数据
    tests := map[string]struct {
        input    *Calculator
        expected string
    }{
        &quot;加法&quot;:    {&amp;Calculator{10, 2, &quot;+&quot;}, &quot;12&quot;},
        &quot;减法&quot;:    {&amp;Calculator{10, 2, &quot;-&quot;}, &quot;8&quot;},
        &quot;乘法&quot;:    {&amp;Calculator{10, 2, &quot;*&quot;}, &quot;20&quot;},
        &quot;除法&quot;:    {&amp;Calculator{10, 2, &quot;/&quot;}, &quot;5&quot;},
        &quot;被除数为0&quot;: {&amp;Calculator{10, 0, &quot;/&quot;}, ErrorDivision},
        &quot;非法操作符&quot;: {&amp;Calculator{10, 2, &quot;$&quot;}, ErrorOp},
    }

    // 判断
    for key, v := range tests {
        got := v.input.Run()
        expected := v.expected
        if !reflect.DeepEqual(got, expected) {
            t.Errorf(&quot;%s: got: %v, expected: %v&quot;, key, got, expected)
        }
    }
}

// 测试示例4，第3种测试方法可以很快定位哪个输入测试失败，如果比较对象的元素很多的时候，虽然最后可以判断出结果不一样，
// 但是不一样在哪里，没有指出来，所有引入一个强大的比较对象的包go-cmp，类似git diff比较不同。
func TestCalculator_Run4(t *testing.T) {
    // 列举测试数据
    tests := map[string]struct {
        input    *Calculator
        expected string
    }{
        &quot;加法&quot;:    {&amp;Calculator{10, 2, &quot;+&quot;}, &quot;12&quot;},
        &quot;减法&quot;:    {&amp;Calculator{10, 2, &quot;-&quot;}, &quot;8&quot;},
        &quot;乘法&quot;:    {&amp;Calculator{10, 2, &quot;*&quot;}, &quot;20&quot;},
        &quot;除法&quot;:    {&amp;Calculator{10, 2, &quot;/&quot;}, &quot;5&quot;},
        &quot;被除数为0&quot;: {&amp;Calculator{10, 0, &quot;/&quot;}, ErrorDivision},
        &quot;非法操作符&quot;: {&amp;Calculator{10, 2, &quot;$&quot;}, ErrorOp},
    }

    // 判断
    for key, v := range tests {
        got := v.input.Run()
        expected := v.expected
        if result := cmp.Diff(got, expected); result != &quot;&quot; {
            t.Error(key, result)
        }
    }
}
</code></pre>

<p><br></p>

<p><strong>(2) mock单元测试</strong></p>

<p>在开发过程中往往需要配合单元测试，但是很多时候，单元测试需要依赖一些比较复杂的准备工作，比如需要依赖数据库环境，需要依赖网络环境，单元测试就变成了一件非常麻烦的事情。</p>

<p>mock对象就是为了解决依赖环境的问题，mock(模拟)对象能够模拟实际依赖对象的功能，同时又不需要非常复杂的准备工作，你需要做的，仅仅就是定义对象接口，然后实现它，再交给测试对象去使用。</p>

<p>安装go mock工具：</p>

<pre><code class="language-bash">go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
</code></pre>

<p>在$GOPATH/src目录下新建一个项目hello，新建一个hello.go文件，内容如下：</p>

<pre><code class="language-go">package hello

type Talker interface {
    SayHello(word string) (response string)
}

</code></pre>

<p><br></p>

<p>新建persion.go文件，在文件里定义一个Persion结构体，并实现Talker接口，persion.go文件内容如下：</p>

<pre><code class="language-go">package hello

import &quot;fmt&quot;

type Person struct {
    name string
}

func NewPerson(name string) *Person {
    return &amp;Person{
        name: name,
    }
}

func (p *Person) SayHello(name string) (word string) {
    return fmt.Sprintf(&quot;hello %s, welcome come to our shop, my name is %s&quot;, name, p.name)
}
</code></pre>

<p>假设商店有一个迎宾员，实现了Talker接口，迎宾员能够自动向客人说SayHello，新建shop.go文件内容如下：</p>

<pre><code class="language-go">package hello

type Shop struct {
    Usher Talker
}

func NewShop(t Talker) *Shop {
    return &amp;Shop{
        Usher: t,
    }
}

func (c *Shop) Meeting(guestName string) string {
    return c.Usher.SayHello(guestName)
}
</code></pre>

<p>使用mockgen工具模拟Shop对象：</p>

<pre><code class="language-bash"># 新建文件夹
mkdir mock_hello

# mock对象
mockgen -source=hello.go &gt; mock_hello/mock_hello.go
</code></pre>

<p>使用这个mock对象，新建一个测试文件shop_test.go文件：</p>

<pre><code class="language-go">package hello

import (
    &quot;testing&quot;
    &quot;hello/mock_hello&quot;

    &quot;github.com/golang/mock/gomock&quot;
)

func TestShop_Meeting(t *testing.T) {
    ctl := gomock.NewController(t)
    mock_talker := mock_hello.NewMockTalker(ctl)
    mock_talker.EXPECT().SayHello(gomock.Eq(&quot;张三&quot;)).Return(&quot;你好张三，欢迎光临。&quot;)

    shop := NewShop(mock_talker)
    t.Log(shop.Meeting(&quot;张三&quot;))
    //t.Log(shop.Meeting(&quot;李四&quot;))
}
</code></pre>

<p>mock对象的SayHello可以接受的参数有gomock.Eq(x interface{})和gomock.Any()，前一个要求测试参数必须相等，第二个允许传入任意参数。</p>

<p><br><br></p>

<h3 id="toc_10">11 README文件</h3>

<p>每个文件夹下都应该有一个README文件，该文件是对当前目录下所有文件的一个概述和主要方法描述，并给出一些相应的链接地址，包含代码所在地、引用文档所在地、API文档所在地。</p>

<p>README文件不仅是对自己代码的一个梳理，更是让别人在接手你的代码时能帮助快速上手的有效资料。所以每一个写好README文档的程序员绝对都是一个负责任的好程序员。</p>

<p><br><br></p>

<h3 id="toc_11">12 合理规划项目的目录</h3>

<p>合理规划目录，一个目录中只包含一个包(实现一个模块的功能)，如果模块功能复杂考虑拆分子模块，或者拆分目录。</p>

<p>不要把不同功能模块放到一个包下：</p>

<pre><code>project
├─  config.go
├─  controller.go
├─  filter.go
├─  flash.go
└─  log.go
</code></pre>

<p>而是把各个模块功能分到不同目录：</p>

<pre><code>project  
├─cache  
│  │  cache.go  
│  │  conv.go  
│  │        
│  └─redis  
│          redis.go  
├─config  
│  │  config.go  
│  │  fake.go  
│  │  ini.go  
│  └─yaml  
│     yaml.go  
└─log  
      conn.go  
      console.go  
      log.go  
</code></pre>

<p><br><br></p>

<h3 id="toc_12">13 channel和goroutine</h3>

<p><strong>(1) channel</strong></p>

<p>在任何情况下，不要在读取channel数据端关闭channel，因为发送端在不知情况下继续发送数据到该channel时会造成panic。要停止使用channel正确做法是在channel发送端关闭，接收端可以检测到channel是否已关闭。</p>

<p>关于使用写入channel超时处理，有可能会下面这样写：</p>

<pre><code class="language-go">ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for{
    ...
    
    select {
    case dataChannel &lt;- msg:
    case &lt;-ticker.C:
        // 超时处理
    }
}
</code></pre>

<p>上面这样会有个小问题，当写入的channel和超时的channel同时触发的时候(当然这个情况概率是比较小的)，select会随机选择执行一个分支，如果select选择了触发超时分支，如果处理不当会造成该数据缺失了，为了避免这个问题，做一些修改，如下面代码所示：</p>

<pre><code class="language-go">ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for{
    ...
    
    select {
    case dataChannel &lt;- msg:
    default: // forbid block
    }
    
    select {
    case &lt;-ticker.C:
        // 超时处理
    default: // forbid block
    }
}
</code></pre>

<p><br></p>

<p><strong>(2) goroutine</strong></p>

<p>如果启动的goroutine是用来做任务的，建议要写成可手动结束的goroutine，防止goroutine泄漏：</p>

<pre><code class="language-go">func worker(ctx context.Context, jobChan &lt;-chan Job) {
    select {
    case job &lt;- jobChan:
        Process(job)
    case &lt;-ctx.Done():
        // 结束worker
        return
    }
}
</code></pre>

<p>如果不受限制的启动新goroutine，有可能会消耗完系统资源，建议使用goroutine池，使用有限的goroutine去做共同任务：</p>

<pre><code class="language-go">func workPool(ctx context.Context, jobChan chan Job) {
    for i := 0; i &lt; 10000; i++ {
        go func() {
            worker(ctx, jobChan)
        }()
    }
}
</code></pre>

<p><br><br></p>

<p>参考：</p>

<ul>
<li><a href="https://www.jianshu.com/p/ea7dfe61f705" rel="nofollow">https://www.jianshu.com/p/ea7dfe61f705</a></li>
<li><a href="https://colobu.com/2017/02/07/write-idiomatic-golang-codes" rel="nofollow">https://colobu.com/2017/02/07/write-idiomatic-golang-codes</a></li>
<li><a href="https://www.cnblogs.com/Survivalist/articles/10596110.html" rel="nofollow">https://www.cnblogs.com/Survivalist/articles/10596110.html</a></li>
<li><a href="https://www.jb51.net/article/151392.html" rel="nofollow">https://www.jb51.net/article/151392.html</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/devspec.html">https://zhuyasen.com/post/devspec.html</a>，<a href="https://zhuyasen.com/post/devspec.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>prometheus基础和使用</title>
            <link>https://zhuyasen.com/post/prometheus.html</link>
            <comments>https://zhuyasen.com/post/prometheus.html#comments</comments>
            <guid>https://zhuyasen.com/post/prometheus.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">1 prometheus介绍</h3>

<p>Prometheus是一个云原生计算基础项目，是一个系统和服务监控系统。它以给定的时间间隔从配置的目标收集指标，评估规则表达式，显示结果，并且如果观察到某些条件为真，则可以触发警报。</p>

<p>prometheus的主要区别特征是：</p>

<ul>
<li>一个多维数据模型（时间序列由指标名称定义和设置键/值尺寸）</li>
<li>一个灵活的查询语言来利用这一维度</li>
<li>不依赖于分布式存储; 单个服务器节点是自治的</li>
<li>时间序列集合通过HTTP 上的拉模型进行</li>
<li>通过中间网关支持推送时间序列</li>
<li>通过服务发现或静态配置发现目标</li>
<li>多种图形和仪表板支持模式</li>
<li>支持分层和水平联合</li>
</ul>

<p><img src="https://go-sponge.com/assets/images/blog/279024-0-prometheus-architecture.jpg" alt="architecture" /></p>

<p><br><br></p>

<h3 id="toc_1">2 prometheus安装</h3>

<h4 id="toc_2">2.1 在docker安装prometheus</h4>

<p>prometheus的配置文件prometheus.yml内容如下：</p>

<pre><code class="language-yaml"># my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - &quot;first_rules.yml&quot;
  # - &quot;second_rules.yml&quot;

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=&lt;job_name&gt;` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']
</code></pre>

<p><br></p>

<p>docker-compose.yml的内容如下：</p>

<pre><code class="language-yaml">version: &quot;3&quot;

services:
  prometheus:
    container_name: prometheus
    image: prom/prometheus:v2.11.1
    ports:
      - 9090:9090
    command:
      - &quot;--config.file=/etc/prometheus/prometheus.yml&quot;
      - &quot;--storage.tsdb.path=/prometheus&quot;
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prom-data:/prometheus

volumes:
  prom-data:
    driver: local
</code></pre>

<p><br></p>

<p>启动prometheus：</p>

<blockquote>
<p>docker-compose up -d</p>
</blockquote>

<p>启动后在浏览器打开 <a href="http://192.168.101.88:9090" rel="nofollow">http://192.168.101.88:9090</a> ，进入prometheus界面，只是没有数据而已。</p>

<p><br></p>

<h4 id="toc_3">2.2 采集node节点资源、容器信息、grafana数据可视化</h4>

<p>一般不建议使用docker安装node-exporter，如果在docker安装node-exporter需要把节点信息映射到容器中。</p>

<p>prometheus的配置文件prometheus.yml内容如下：</p>

<pre><code class="language-yaml"># my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      #- alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - &quot;first_rules.yml&quot;
  #- &quot;/etc/prometheus/rules/*.yml&quot;

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=&lt;job_name&gt;` to any timeseries scraped from this config.
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090','cadvisor:8080','node-exporter:9100']
</code></pre>

<p><br></p>

<p>grafana的配置文件datasource.yaml内容如下：</p>

<pre><code class="language-yaml"># config file version
apiVersion: 1

# list of datasources that should be deleted from the database
deleteDatasources:
  - name: Graphite
    orgId: 1

datasources:
- name: Prometheus
  type: prometheus
  access: proxy
  isDefault: true
  url: http://prometheus:9090
  # don't use this in prod
  editable: true
</code></pre>

<p><br></p>

<p>docker-compose.yml文件内容如下：</p>

<pre><code class="language-yaml">version: &quot;3&quot;

services:
  prometheus:
    container_name: prometheus
    restart: always
    image: prom/prometheus:v2.11.1
    ports:
      - 9090:9090
    command:
      - &quot;--config.file=/etc/prometheus/prometheus.yml&quot;
      - &quot;--storage.tsdb.path=/prometheus&quot;
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
      - prom-data:/prometheus
    networks:
      - prom-net

  # node节点资源信息
  node-exporter:
    container_name: node-exporter
    restart: always
    image: prom/node-exporter:latest
    ports:
      - 9100:9100
    networks:
      - prom-net

  # 容器相关信息
  cadvisor:
    container_name: cadvisor
    restart: always
    image: google/cadvisor:latest
    ports:
      - 9101:8080
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    networks:
      - prom-net

  # 数据可视化
  grafana:
    container_name: grafana
    image: grafana/grafana:6.2.5
    restart: always
    ports:
      - &quot;3003:3000&quot;
    volumes:
      - ./grafana/datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yaml
      - grafana-data:/var/lib/grafana
    networks:
      - prom-net
    #environment:
    #  - GF_SECURITY_ADMIN_PASSWORD=123456

volumes:
  prom-data:
    driver: local
  grafana-data:
    driver: local

networks: 
  prom-net:
    driver: bridge
</code></pre>

<p><br></p>

<p>启动prometheus：</p>

<blockquote>
<p>docker-compose up -d</p>
</blockquote>

<p>启动后在浏览器打开 <a href="http://192.168.101.88:9090" rel="nofollow">http://192.168.101.88:9090</a> ，进入prometheus界面，点击菜单status下Targets查看已经生效的Targets，如下图所示：
<img src="https://go-sponge.com/assets/images/blog/279024-2-targets-UI.jpg" alt="targets" /></p>

<p><br></p>

<p>输入关键字prometheus_http_requests_total可以查询请求prometheus数据，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/279024-1-prometheus-search.jpg" alt="targets" /></p>

<p><br></p>

<p>在浏览器打开 <a href="http://192.168.101.88:3003" rel="nofollow">http://192.168.101.88:3003</a> ，默认登陆账号和密码都是admin，由于grafana配置文件已经设置了prometheus的数据源，不需要再添加数据源，如果没有配置数据源，先要添加数据源。</p>

<p>在浏览器打开 <a href="https://grafana.com/grafana/dashboards" rel="nofollow">https://grafana.com/grafana/dashboards</a> ，搜索相应的dashboard，使用grafana编号为1860和8919能够满足node-export(系统)的数据可视化了，使用grafana编号893(容器)能够满足cadvisor的数据可视化，具体使用方法进入grafana界面，鼠标移动到左边菜单栏的+号，选择import，输入编号1860，鼠标点击任意其它地方，选择prometheus数据源后导入即可，最后效果图如下：</p>

<p><img src="https://go-sponge.com/assets/images/blog/279024-3-prometheus-host-monitor-UI.jpg" alt="targets" /></p>

<p><br></p>

<p><strong>grafana免密码登录设置</strong></p>

<p>修改配置文件，允许匿名登录enabled = true，匿名登录角色改为Admin</p>

<pre><code>[auth.anonymous]
# enable anonymous access
enabled = true

# specify role for unauthenticated users
org_role = Admin
</code></pre>

<p><br><br></p>

<h3 id="toc_4">3 PromQL的基础知识</h3>

<p>指标(Metric)的通用格式：</p>

<pre><code>&lt;metric name&gt;{&lt;label name&gt;=&lt;label value&gt;, ...}
</code></pre>

<p>指标的名称(metric name)可以反映被监控样本的含义（比如，http_requests<em>total - 表示当前系统接收到的HTTP请求总量）。指标名称只能由ASCII字符、数字、下划线以及冒号组成并必须符合正则表达式[a-zA-Z</em>:][a-zA-Z0-9_:]*。</p>

<p>标签(label)反映了当前样本的特征维度，通过这些维度Prometheus可以对样本数据进行过滤，聚合等。标签的名称只能由ASCII字符、数字以及下划线组成并满足正则表达式[a-zA-Z<em>][a-zA-Z0-9</em>]*。</p>

<p>其中以__作为前缀的标签，是系统保留的关键字，只能在系统内部使用。标签的值则可以包含任何Unicode编码的字符。在Prometheus的底层实现中指标名称实际上是以 __name__=<metric name>的形式保存在数据库中的，因此以下两种方式均表示的同一条time-series：</p>

<pre><code class="language-bash">api_http_requests_total{method=&quot;POST&quot;, handler=&quot;/messages&quot;}

{__name__=&quot;api_http_requests_total&quot;，method=&quot;POST&quot;, handler=&quot;/messages&quot;}
</code></pre>

<p>示例：</p>

<pre><code class="language-bash">node_cpu{cpu=&quot;cpu0&quot;,mode=&quot;idle&quot;} 362812.7890625
node_load1 3.0703125

node_cpu和node_load1表明了当前指标的名称、大括号中的标签则反映了当前样本的一些特征和维度、浮点数则是该监控样本的具体值。
</code></pre>

<p><br></p>

<h4 id="toc_5">3.1 Metrics类型</h4>

<p>Prometheus定义了4中不同的指标类型(metric type)：</p>

<ul>
<li>Counter（计数器）</li>
<li>Gauge（仪表盘）</li>
<li>Histogram（直方图）</li>
<li>Summary（摘要）</li>
</ul>

<p>在Exporter返回的样本数据中，其注释中也包含了该样本的类型。例如：</p>

<pre><code># HELP node_cpu Seconds the cpus spent in each mode.
# TYPE node_cpu counter
node_cpu{cpu=&quot;cpu0&quot;,mode=&quot;idle&quot;} 362812.7890625
</code></pre>

<p><strong>(1) Counter：只增不减的计数器</strong></p>

<p>Counter类型的指标其工作方式和计数器一样，只增不减（除非系统发生重置）。常见的监控指标，如http_requests_total，node_cpu都是Counter类型的监控指标。 一般在定义Counter类型指标的名称时推荐使用_total作为后缀。</p>

<p>Counter是一个简单但有强大的工具，例如我们可以在应用程序中记录某些事件发生的次数，通过以时序的形式存储这些数据，我们可以轻松的了解该事件产生速率的变化。PromQL内置的聚合操作和函数可以用户对这些数据进行进一步的分析：</p>

<pre><code class="language-bash"># 通过rate()函数获取HTTP请求量的增长率：
rate(http_requests_total[5m])

# 查询当前系统中，访问量前10的HTTP地址：
topk(10, http_requests_total)
</code></pre>

<p><br></p>

<p><strong>(2) Gauge：可增可减的仪表盘</strong></p>

<p>与Counter不同，Gauge类型的指标侧重于反应系统的当前状态。因此这类指标的样本数据可增可减。常见指标如：node_memory_MemFree（主机当前空闲的内容大小）、node_memory_MemAvailable（可用内存大小）都是Gauge类型的监控指标。</p>

<p>通过Gauge指标，用户可以直接查看系统的当前状态：node_memory_MemFree对于Gauge类型的监控指标，通过PromQL内置函数delta()可以获取样本在一段时间返回内的变化情况。还可以使用deriv()计算样本的线性回归模型，甚至是直接使用predict_linear()对数据的变化趋势进行预测。</p>

<pre><code class="language-bash"># 计算CPU温度在两个小时内的差异：
delta(cpu_temp_celsius{host=&quot;zeus&quot;}[2h])

# 预测系统磁盘空间在4个小时之后的剩余情况：
predict_linear(node_filesystem_free{job=&quot;node&quot;}[1h], 4 * 3600)
</code></pre>

<p><br></p>

<p><strong>(3) 使用Histogram和Summary分析数据分布情况</strong></p>

<p>除了Counter和Gauge类型的监控指标以外，Prometheus还定义分别定义Histogram和Summary的指标类型。Histogram和Summary主用用于统计和分析样本的分布情况。</p>

<p>在大多数情况下人们都倾向于使用某些量化指标的平均值，例如CPU的平均使用率、页面的平均响应时间。这种方式的问题很明显，以系统API调用的平均响应时间为例：如果大多数API请求都维持在100ms的响应时间范围内，而个别请求的响应时间需要5s，那么就会导致某些WEB页面的响应时间落到中位数的情况，而这种现象被称为长尾问题。例如，统计延迟在0~10ms之间的请求数有多少而10~20ms之间的请求数又有多少，通过这种方式可以快速分析系统慢的原因。Histogram和Summary都是为了能够解决这样问题的存在，通过Histogram和Summary类型的监控指标，我们可以快速了解监控样本的分布情况。</p>

<pre><code class="language-bash"># HELP prometheus_tsdb_wal_fsync_duration_seconds Duration of WAL fsync.
# TYPE prometheus_tsdb_wal_fsync_duration_seconds summary
prometheus_tsdb_wal_fsync_duration_seconds{quantile=&quot;0.5&quot;} 0.012352463
prometheus_tsdb_wal_fsync_duration_seconds{quantile=&quot;0.9&quot;} 0.014458005
prometheus_tsdb_wal_fsync_duration_seconds{quantile=&quot;0.99&quot;} 0.017316173
prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002
prometheus_tsdb_wal_fsync_duration_seconds_count 216

从上面的样本中可以得知当前Prometheus Server进行wal_fsync操作的总次数为216次，耗时2.888716127000002s。其中中位数（quantile=0.5）的耗时为0.012352463，9分位数（quantile=0.9）的耗时为0.014458005s。
</code></pre>

<p><br></p>

<h4 id="toc_6">3.4 PromQL基础</h4>

<p>Prometheus通过指标名称（metrics name）以及对应的一组标签（labelset）唯一定义一条时间序列。指标名称反映了监控样本的基本标识，而label则在这个基本特征上为采集到的数据提供了多种特征维度。用户可以基于这些特征维度过滤，聚合，统计从而产生新的计算后的一条时间序列。</p>

<p>PromQL是Prometheus内置的数据查询语言，其提供对时间序列数据丰富的查询，聚合以及逻辑运算能力的支持。并且被广泛应用在Prometheus的日常应用当中，包括对数据查询、可视化、告警处理当中。可以这么说，PromQL是Prometheus所有应用场景的基础，理解和掌握PromQL是Prometheus入门的第一课。</p>

<p><br></p>

<p><strong>(1) 查询时间序列</strong></p>

<p>当Prometheus通过Exporter采集到相应的监控指标样本数据后，我们就可以通过PromQL对监控样本数据进行查询。</p>

<p>当我们直接使用监控指标名称查询时，可以查询该指标下的所有时间序列。如：</p>

<pre><code class="language-bash">prometheus_http_requests_total
# 等价于
prometheus_http_requests_total{}
</code></pre>

<p><br></p>

<p>PromQL还支持用户根据时间序列的标签匹配模式来对时间序列进行过滤，目前主要支持两种匹配模式：完全匹配(=)和排除匹配(!=)。</p>

<p>通过使用label=value可以选择那些标签满足表达式定义的时间序列；
反之使用label!=value则可以根据标签匹配排除时间序列；</p>

<pre><code class="language-bash"># 查询所有prometheus_http_requests_total时间序列中满足标签instance为localhost:9090的时间序列，则可以使用如下表达式：
prometheus_http_requests_total{instance=&quot;localhost:9090&quot;}

# 反之使用instance!=&quot;localhost:9090&quot;则可以排除这些时间序列：
prometheus_http_requests_total{instance!=&quot;localhost:9090&quot;}
</code></pre>

<p><br></p>

<p>除了使用完全匹配的方式对时间序列进行过滤以外，PromQL还可以支持使用正则表达式作为匹配条件，多个表达式之间使用|进行分离：</p>

<p>使用label=~regx表示选择那些标签符合正则表达式定义的时间序列；
反之使用label!~regx进行排除；</p>

<pre><code class="language-bash"># 如果想查询多个环节下的时间序列序列可以使用如下表达式：
http_requests_total{environment=~&quot;staging|testing|development&quot;,method!=&quot;GET&quot;}
</code></pre>

<p><br></p>

<p><strong>(2) 范围查询</strong></p>

<p>直接通过类似于PromQL表达式http_requests_total查询时间序列时，返回值中只会包含该时间序列中的最新的一个样本值，这样的返回结果我们称之为<strong>瞬时向量</strong>。而相应的这样的表达式称之为<strong>瞬时向量表达式</strong>。</p>

<p>而如果我们想过去一段时间范围内的样本数据时，我们则需要使用<strong>区间向量表达式</strong>。区间向量表达式和瞬时向量表达式之间的差异在于在区间向量表达式中我们需要定义时间选择的范围，时间范围通过时间范围选择器[]进行定义。例如，通过以下表达式可以选择最近5分钟内的所有样本数据：</p>

<pre><code>http_requests_total{}[5m]

s - 秒
m - 分钟
h - 小时
d - 天
w - 周
y - 年
</code></pre>

<p><br></p>

<p><strong>(3) 时间位移操作</strong></p>

<p>在瞬时向量表达式或者区间向量表达式中，都是以当前时间为基准：</p>

<pre><code class="language-bash">http_requests_total{} # 瞬时向量表达式，选择当前最新的数据
http_requests_total{}[5m] # 区间向量表达式，选择以当前时间为基准，5分钟内的数据
而如果我们想查询，5分钟前的瞬时样本数据，或昨天一天的区间内的样本数据呢? 这个时候我们就可以使用位移操作，位移操作的关键字为offset。
</code></pre>

<p>可以使用offset时间位移操作：</p>

<pre><code class="language-bash">http_requests_total{} offset 5m
http_requests_total{}[1d] offset 1d
</code></pre>

<p><br></p>

<p><strong>(4) 聚合操作</strong></p>

<p>如果描述样本特征的标签(label)在并非唯一的情况下，通过PromQL查询数据，会返回多条满足这些特征维度的时间序列。而PromQL提供的聚合操作可以用来对这些时间序列进行处理，形成一条新的时间序列：</p>

<pre><code class="language-bash"># 查询系统所有http请求的总量
sum(http_requests_total)

# 按照mode计算主机CPU的平均使用时间
avg(node_cpu) by (mode)

# 按照主机查询各个主机的CPU使用率
sum(sum(irate(node_cpu{mode!='idle'}[5m]))  / sum(irate(node_cpu[5m]))) by (instance)
</code></pre>

<p><br></p>

<p><strong>(5) 合法的PromQL</strong></p>

<pre><code class="language-bash">http_requests_total # 合法
http_requests_total{} # 合法
{method=&quot;get&quot;} # 合法
{__name__=~&quot;http_requests_total&quot;} # 合法
{__name__=~&quot;node_disk_bytes_read|node_disk_bytes_written&quot;} # 合法

而如下表达式，则不合法：
{job=~&quot;.*&quot;} # 不合法
</code></pre>

<p><br></p>

<h4 id="toc_7">3.5 PromQL操作符</h4>

<p>使用PromQL除了能够方便的按照查询和过滤时间序列以外，PromQL还支持丰富的操作符，用户可以使用这些操作符对进一步的对事件序列进行二次加工。这些操作符包括：数学运算符，逻辑运算符，布尔运算符等等</p>

<p><strong>(1) 数学运算符</strong></p>

<p>PromQL支持的所有数学运算符如下所示：</p>

<pre><code class="language-bash">+ (加法)
- (减法)
* (乘法)
/ (除法)
% (求余)
^ (幂运算)
</code></pre>

<p>例如查看内存使用率：</p>

<blockquote>
<p>(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes</p>
</blockquote>

<p><strong>(2) 逻辑运行</strong></p>

<p>目前，Prometheus支持以下布尔运算符如下：</p>

<pre><code class="language-bash">== (相等)
!= (不相等)
&gt; (大于)
&lt; (小于)
&gt;= (大于等于)
&lt;= (小于等于)
</code></pre>

<p>例如查看内存使用率超过0.707的数据：</p>

<pre><code>(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes &gt; 0.707
</code></pre>

<p><br></p>

<p><strong>(3) 布尔运算</strong></p>

<p>布尔运算符的默认行为是对时序数据进行过滤。而在其它的情况下我们可能需要的是真正的布尔结果。例如，只需要知道当前模块的HTTP请求量是否&gt;=1000，如果大于等于1000则返回1（true）否则返回0（false）。这时可以使用bool修饰符改变布尔运算的默认行为。</p>

<pre><code>prometheus_http_requests_total &gt; bool 5000
</code></pre>

<p><br></p>

<p><strong>(4) 使用集合运算符</strong></p>

<p>使用瞬时向量表达式能够获取到一个包含多个时间序列的集合，我们称为瞬时向量。 通过集合运算，可以在两个瞬时向量与瞬时向量之间进行相应的集合操作。目前，Prometheus支持以下集合运算符：</p>

<pre><code class="language-bash">and (并且)
or (或者)
unless (排除)
vector1 and vector2 会产生
</code></pre>

<p><br></p>

<p><strong>(5) 操作符优先级</strong></p>

<p>在PromQL操作符中优先级由高到低依次为：</p>

<pre><code class="language-bash">^
*, /, %
+, -
==, !=, &lt;=, &lt;, &gt;=, &gt;
and, unless
or
</code></pre>

<p><br></p>

<h4 id="toc_8">3.6 PromQL聚合操作</h4>

<p>Prometheus还提供了下列内置的聚合操作符，这些操作符作用域瞬时向量。可以将瞬时表达式返回的样本数据进行聚合，形成一个新的时间序列。</p>

<pre><code class="language-bash">rate（时间内变化率，指定时间范围内所有数据点，适合缓慢变化的计数器）
irate（时间内变化率，指定时间范围内的最近两个数据点来算速率，适合快速变化的计数器）
sum (求和)
min (最小值)
max (最大值)
avg (平均值)
stddev (标准差)
stdvar (标准差异)
count (计数)
count_values (对value进行计数)
bottomk (后n条时序)
topk (前n条时序)
quantile (分布统计)
</code></pre>

<p>使用聚合操作的语法如下：</p>

<pre><code>&lt;aggr-op&gt;([parameter,] &lt;vector expression&gt;) [without|by (&lt;label list&gt;)]

aggr-op: 聚合操作符
parameter: 参数(可选)
vector expression: 矢量式
without: 用于从计算结果中移除列举的标签(维度)，而保留其它标签
by: 向量中只保留列出的标签(维度)，其余标签则移除，必须指明标签列表
</code></pre>

<p>示例：</p>

<blockquote>
<p>sum(prometheus_http_requests_total) without (instance)</p>
</blockquote>

<p>等价于</p>

<blockquote>
<p>sum(prometheus_http_requests_total) by (code,handler,job,method)</p>
</blockquote>

<p>例如获取HTTP请求数前5位的时序样本数据：</p>

<blockquote>
<p>topk(5, prometheus_http_requests_total)</p>
</blockquote>

<p>例如找到当前样本数据中的中位数(0&lt;v&lt;1)：</p>

<blockquote>
<p>quantile(0.5, prometheus_http_requests_total)</p>
</blockquote>

<p><br></p>

<p>常用top10统计promQL语句示例：</p>

<pre><code class="language-promql"># CPU 使用率 top10
label_replace(topk(10,(100 - avg(irate(node_cpu_seconds_total{mode=&quot;idle&quot;}[5m]))by (instance) * 100)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# 内存使用率 top10
label_replace(topk(10,((1 - (node_memory_MemAvailable_bytes{} / (node_memory_MemTotal_bytes{})))* 100)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# 磁盘使用率 top10
label_replace(topk(10,((1 - (node_memory_MemAvailable_bytes{} / (node_memory_MemTotal_bytes{})))* 100))by (instance),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# 磁盘IO总线利用率 top10
label_replace(topk(10,(avg(irate(node_disk_io_time_seconds_total{}[1m])) by(instance)* 100)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# 网络下载 top10
label_replace(topk(10,(sum(irate(node_network_receive_bytes_total{device!~&quot;tap.*|veth.*|br.*|docker.*|virbr*|lo*&quot;}[1m])*8) by (instance))),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# 网络上传 top10
label_replace(topk(10,(sum(irate(node_network_transmit_bytes_total{device!~&quot;tap.*|veth.*|br.*|docker.*|virbr*|lo*&quot;}[1m])*8) by (instance))),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# TCP 网络包错误率 top10
label_replace(topk(10,(avg(irate(node_netstat_Tcp_InErrs{}[1m])) by (instance) / avg(irate(node_netstat_Tcp_InSegs{}[1m])) by (instance))),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# TCP 建立连接数 top10
label_replace(topk(10,avg(node_netstat_Tcp_CurrEstab{}) by (instance)) ,&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# TCP 等待断开连接 top10
label_replace(topk(10,avg(node_sockstat_TCP_tw{}) by (instance)) ,&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# 1分钟，5分钟，15分钟CPU平均负载 top10
label_replace(topk(10,avg(node_load1{}) by (instance)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)
label_replace(topk(10,avg(node_load5{}) by (instance)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)
label_replace(topk(10,avg(node_load15{}) by (instance)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# CPU 上下文切换平均次数 top10
label_replace(topk(10,avg(irate(node_context_switches_total{}[5m]))by (instance)),&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)

# swap 交换分区使用 top10
label_replace(topk(10,avg(node_memory_SwapTotal_bytes{}-node_memory_SwapFree_bytes{}) by (instance)) ,&quot;ip&quot;,&quot;$1&quot;,&quot;instance&quot;,&quot;(.*):.*&quot;)
</code></pre>

<p><br></p>

<h4 id="toc_9">3.7 PromQL子查询</h4>

<p>常用的子查询：</p>

<pre><code class="language-bash">avg_over_time()  # 指定间隔内所有点的平均值。
min_over_time()  # 指定间隔中所有点的最小值。
max_over_time()  # 指定间隔内所有点的最大值。
sum_over_time()  # 指定时间间隔内所有值的总和。
</code></pre>

<p><br></p>

<p><strong>avg_over_time</strong> 示例：</p>

<pre><code class="language-bash"># 查询一天空闲空间的平均值
avg_over_time(node_filesystem_files_free[1d])
</code></pre>

<p><strong>min_over_time</strong> 示例：</p>

<pre><code class="language-bash"># 一天 空闲空间的最大值
max_over_time(node_filesystem_files_free[1d])
</code></pre>

<p><strong>max_over_time</strong> 示例：</p>

<pre><code class="language-bash"># 统计prometheus上/metrics页面在5分钟内区间向量的平均值的点，在1个小时中每个点的值。
max_over_time(rate(prometheus_http_requests_total[5m])[1h:1m])
 
# rate(prometheus_http_requests_total[5m])[1h:1m]  
# 它将五分钟的数据聚合成一个瞬时向量。
# [1h就像范围向量选择器一样，它定义了相对于查询求值时间的范围大小。
# :1m]要使用的间隔值。如果没有定义，它默认为全局计算区间。
</code></pre>

<p><strong>sum_over_time</strong> 示例：</p>

<pre><code class="language-bash"># 统计prometheus上/metrics页面在5分钟内区间向量值的点总和，在1个小时中每个点的值。
sum_over_time(rate(prometheus_http_requests_total[5m])[1h:1m])
</code></pre>

<p><strong>请求数量总和</strong>：</p>

<pre><code class="language-bash"># 最近10分钟请求数量总和
sum(max_over_time(prometheus_http_requests_total{}[10m]) - min_over_time(prometheus_http_requests_total{}[10m]))
</code></pre>

<p><br></p>

<h4 id="toc_10">3.8 逻辑运算（与、或、非）</h4>

<pre><code class="language-bash">and      # 与
or       # 或
unless   # 非
</code></pre>

<p><br></p>

<p><strong>and</strong> 示例:</p>

<pre><code class="language-bash"># 同时满足多个条件
node_filesystem_size_bytes{fstype!=&quot;tmpfs&quot;} and node_filesystem_size_bytes != 0 and node_filesystem_size_bytes{mountpoint=&quot;/root-disk&quot;}
</code></pre>

<p><strong>or</strong> 示例:</p>

<pre><code class="language-bash"># 至少满足一个条件
node_filesystem_avail_bytes &gt; 200000 or node_filesystem_avail_bytes &lt; 2500000
</code></pre>

<p><strong>unless</strong> 示例:</p>

<pre><code class="language-bash"># 忽略标签为{instance=&quot;192.168.1.21:9100&quot;,job=&quot;node&quot;}数据
up{instance=&quot;192.168.1.20:9100&quot;,job=&quot;node&quot;} unless up{instance=&quot;192.168.1.21:9100&quot;,job=&quot;node&quot;}
 
# 当标签相同时输出数据
up{instance=&quot;192.168.1.20:9100&quot;,job=&quot;node&quot;} unless up{instance=&quot;192.168.1.20:9100&quot;,job=&quot;node&quot;}
</code></pre>

<p><br></p>

<h4 id="toc_11">3.9 4个黄金指标和USE方法</h4>

<p>监控内容对应的Exporter：</p>

<table>
<thead>
<tr>
<th>级别</th>
<th>监控什么</th>
<th>Exporter</th>
</tr>
</thead>

<tbody>
<tr>
<td>网络</td>
<td>网络协议：http、dns、tcp、icmp；网络硬件：路由器，交换机等</td>
<td>BlockBox Exporter;SNMP Exporter</td>
</tr>

<tr>
<td>主机</td>
<td>资源用量</td>
<td>node exporter</td>
</tr>

<tr>
<td>容器</td>
<td>资源用量</td>
<td>cAdvisor</td>
</tr>

<tr>
<td>应用(包括Library)</td>
<td>延迟，错误，QPS，内部状态等</td>
<td>代码中集成Prmometheus Client</td>
</tr>

<tr>
<td>中间件状态</td>
<td>资源用量，以及服务状态</td>
<td>代码中集成Prmometheus Client</td>
</tr>

<tr>
<td>编排工具</td>
<td>集群资源用量，调度等</td>
<td>Kubernetes Components</td>
</tr>
</tbody>
</table>

<p><br></p>

<p><strong>(1) 4个黄金指标</strong></p>

<p>Four Golden Signals是Google针对大量分布式监控的经验总结，4个黄金指标可以在服务级别帮助衡量终端用户体验、服务中断、业务影响等层面的问题，主要关注与以下四种类型的指标：延迟，通讯量，错误以及饱和度。</p>

<ul>
<li>延迟：服务请求所需时间。</li>
</ul>

<p>记录用户所有请求所需的时间，重点是要区分成功请求的延迟时间和失败请求的延迟时间。 例如在数据库或者其他关键祸端服务异常触发HTTP 500的情况下，用户也可能会很快得到请求失败的响应内容，如果不加区分计算这些请求的延迟，可能导致计算结果与实际结果产生巨大的差异。除此以外，在微服务中通常提倡“快速失败”，开发人员需要特别注意这些延迟较大的错误，因为这些缓慢的错误会明显影响系统的性能，因此追踪这些错误的延迟也是非常重要的。</p>

<ul>
<li>通讯量：监控当前系统的流量，用于衡量服务的容量需求。</li>
</ul>

<p>流量对于不同类型的系统而言可能代表不同的含义。例如，在HTTP REST API中, 流量通常是每秒HTTP请求数；</p>

<ul>
<li>错误：监控当前系统所有发生的错误请求，衡量当前系统错误发生的速率。</li>
</ul>

<p>对于失败而言有些是显式的(比如, HTTP 500错误)，而有些是隐式(比如，HTTP响应200，但实际业务流程依然是失败的)。</p>

<p>对于一些显式的错误如HTTP 500可以通过在负载均衡器(如Nginx)上进行捕获，而对于一些系统内部的异常，则可能需要直接从服务中添加钩子统计并进行获取。</p>

<ul>
<li>饱和度：衡量当前服务的饱和度。</li>
</ul>

<p>主要强调最能影响服务状态的受限制的资源。 例如，如果系统主要受内存影响，那就主要关注系统的内存状态，如果系统主要受限与磁盘I/O，那就主要观测磁盘I/O的状态。因为通常情况下，当这些资源达到饱和后，服务的性能会明显下降。同时还可以利用饱和度对系统做出预测，比如，“磁盘是否可能在4个小时候就满了”。</p>

<p><br></p>

<p><strong>(2) RED方法</strong></p>

<p>RED方法是Weave Cloud在基于Google的“4个黄金指标”的原则下结合Prometheus以及Kubernetes容器实践，细化和总结的方法论，特别适合于云原生应用以及微服务架构应用的监控和度量。主要关注以下三种关键指标：</p>

<ul>
<li>(请求)速率：服务每秒接收的请求数。</li>
<li>(请求)错误：每秒失败的请求数。</li>
<li>(请求)耗时：每个请求的耗时。</li>
</ul>

<p>在“4大黄金信号”的原则下，RED方法可以有效的帮助用户衡量云原生以及微服务应用下的用户体验问题。</p>

<p><br></p>

<p><strong>(3) USE方法</strong></p>

<p>USE方法全称&rdquo;Utilization Saturation and Errors Method&rdquo;，主要用于分析系统性能问题，可以指导用户快速识别资源瓶颈以及错误的方法。正如USE方法的名字所表示的含义，USE方法主要关注与资源的：使用率(Utilization)、饱和度(Saturation)以及错误(Errors)。</p>

<ul>
<li>使用率：关注系统资源的使用情况。 这里的资源主要包括但不限于：CPU，内存，网络，磁盘等等。100%的使用率通常是系统性能瓶颈的标志。</li>
<li>饱和度：例如CPU的平均运行排队长度，这里主要是针对资源的饱和度(注意，不同于4大黄金信号)。任何资源在某种程度上的饱和都可能导致系统性能的下降。</li>
<li>错误：错误计数。例如：“网卡在数据包传输过程中检测到的以太网网络冲突了14次”。</li>
</ul>

<p><br><br></p>

<h3 id="toc_12">4 告警</h3>

<p>告警能力在Prometheus的架构中被划分成两个独立的部分。通过在Prometheus中定义AlertRule（告警规则），Prometheus会周期性的对告警规则进行计算，如果满足告警触发条件就会向Alertmanager发送告警信息。</p>

<p>在Prometheus中一条告警规则主要由以下几部分组成：</p>

<ul>
<li>告警名称：用户需要为告警规则命名，当然对于命名而言，需要能够直接表达出该告警的主要内容。</li>
<li>告警规则：告警规则实际上主要由PromQL进行定义，其实际意义是当表达式（PromQL）查询结果持续多长时间（During）后出发告警。</li>
</ul>

<p>Alertmanager特性：</p>

<ul>
<li>分组：分组机制可以将详细的告警信息合并成一个通知，避免一次性接受大量的告警通知，而无法对问题进行快速定位。</li>
<li>抑制：抑制是指当某一告警发出后，可以停止重复发送由此告警引发的其它告警的机制。</li>
<li>静默：提供了一个简单的机制可以快速根据标签对告警进行静默处理。如果接收到的告警符合静默的配置，Alertmanager则不会发送告警通知。</li>
</ul>

<p><br></p>

<h4 id="toc_13">4.1 定义告警规则</h4>

<p>一条典型的告警规则如下所示：</p>

<pre><code class="language-yaml">groups:
- name: example
  rules:
  - alert: HighErrorRate
    expr: job:request_latency_seconds:mean5m{job=&quot;myjob&quot;} &gt; 0.5
    for: 10m
    labels:
      severity: page
    annotations:
      summary: High request latency
      description: description info
</code></pre>

<ul>
<li>alert：告警规则的名称。</li>
<li>expr：基于PromQL表达式告警触发条件，用于计算是否有时间序列满足该条件。</li>
<li>for：评估等待时间，可选参数。用于表示只有当触发条件持续一段时间后才发送告警。在等待期间新产生告警的状态为pending。</li>
<li>labels：自定义标签，允许用户指定要附加到告警上的一组附加标签。</li>
<li>annotations：用于指定一组附加信息，比如用于描述告警详细信息的文字等，annotations的内容在告警产生时会一同作为参数发送到Alertmanager。</li>
</ul>

<p><br></p>

<p>使用promtool工具检查告警语法：</p>

<blockquote>
<p>promtool check rules /path/to/example.rules.yml</p>
</blockquote>

<p>promtool工具下载地址： <a href="https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/" rel="nofollow">https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/</a></p>

<p><br></p>

<h4 id="toc_14">4.2 alertmanager配置详解</h4>

<p>Alertmanager主要负责对Prometheus产生的告警进行统一处理，因此在Alertmanager配置中一般会包含以下几个主要部分：</p>

<ul>
<li>全局配置（global）：用于定义一些全局的公共参数，如全局的SMTP配置，Slack配置等内容；</li>
<li>模板（templates）：用于定义告警通知时的模板，如HTML模板，邮件模板等；</li>
<li>告警路由（route）：根据标签匹配，确定当前告警应该如何处理；</li>
<li>接收人（receivers）：接收人是一个抽象的概念，它可以是一个邮箱也可以是微信，Slack或者Webhook等，接收人一般配合告警路由使用；</li>
<li>抑制规则（inhibit_rules）：合理设置抑制规则可以减少垃圾告警的产生</li>
</ul>

<p>其完整配置格式如下：</p>

<pre><code class="language-yaml">global:
  [ resolve_timeout: &lt;duration&gt; | default = 5m ]
  [ smtp_from: &lt;tmpl_string&gt; ] 
  [ smtp_smarthost: &lt;string&gt; ] 
  [ smtp_hello: &lt;string&gt; | default = &quot;localhost&quot; ]
  [ smtp_auth_username: &lt;string&gt; ]
  [ smtp_auth_password: &lt;secret&gt; ]
  [ smtp_auth_identity: &lt;string&gt; ]
  [ smtp_auth_secret: &lt;secret&gt; ]
  [ smtp_require_tls: &lt;bool&gt; | default = true ]
  [ slack_api_url: &lt;secret&gt; ]
  [ victorops_api_key: &lt;secret&gt; ]
  [ victorops_api_url: &lt;string&gt; | default = &quot;https://alert.victorops.com/integrations/generic/20131114/alert/&quot; ]
  [ pagerduty_url: &lt;string&gt; | default = &quot;https://events.pagerduty.com/v2/enqueue&quot; ]
  [ opsgenie_api_key: &lt;secret&gt; ]
  [ opsgenie_api_url: &lt;string&gt; | default = &quot;https://api.opsgenie.com/&quot; ]
  [ hipchat_api_url: &lt;string&gt; | default = &quot;https://api.hipchat.com/&quot; ]
  [ hipchat_auth_token: &lt;secret&gt; ]
  [ wechat_api_url: &lt;string&gt; | default = &quot;https://qyapi.weixin.qq.com/cgi-bin/&quot; ]
  [ wechat_api_secret: &lt;secret&gt; ]
  [ wechat_api_corp_id: &lt;string&gt; ]
  [ http_config: &lt;http_config&gt; ]

templates:
  [ - &lt;filepath&gt; ... ]

route: &lt;route&gt;

receivers:
  - &lt;receiver&gt; ...

inhibit_rules:
  [ - &lt;inhibit_rule&gt; ... ]
</code></pre>

<p>在全局配置中需要注意的是resolve_timeout，该参数定义了当Alertmanager持续多长时间未接收到告警后标记告警状态为resolved（已解决）。该参数的定义可能会影响到告警恢复通知的接收时间，读者可根据自己的实际场景进行定义，其默认值为5分钟。</p>

<p><br></p>

<p><strong>(1) route告警路由</strong></p>

<p>alertmanager配置中的route是基于标签的告警路由，对于不同级别的告警，我们可能会有不同的处理方式，在route中可以定义更多的子Route，这些Route通过标签匹配告警的处理方式，告警的匹配有两种方式可以选择：</p>

<ul>
<li>方式一：基于字符串验证，通过设置match规则判断当前告警中是否存在标签labelname并且其值等于labelvalue。</li>
<li>方式二：基于正则表达式，通过设置match_re验证当前告警标签的值是否满足正则表达式的内容。</li>
</ul>

<p>alertmanager配置示例如下：</p>

<pre><code class="language-yaml">route:
  receiver: 'default-receiver'
  group_by: [cluster, alertname]
  group_wait: 30s
  group_interval: 10m
  repeat_interval: 1h
  
  routes:
  - receiver: 'database-pager'
    # 这里没有group_by，继承顶级的group_by
    group_wait: 10s
    match_re:
      service: mysql|cassandra
  - receiver: 'frontend-pager'
    group_by: [product, environment]
    match:
      team: frontend
</code></pre>

<p><br></p>

<p>使用命令检查告警规则是否合法</p>

<blockquote>
<p>promtool check rules /path/to/example.rules.yml</p>
</blockquote>

<p><br></p>

<p><strong>(2) receiver告警接收器发送通知</strong></p>

<p>每一个receiver具有一个全局唯一的名称，并且对应一个或者多个通知方式：</p>

<pre><code class="language-yaml">name: &lt;string&gt;
email_configs:
  [ - &lt;email_config&gt;, ... ]
webhook_configs:
  [ - &lt;webhook_config&gt;, ... ]
hipchat_configs:
  [ - &lt;hipchat_config&gt;, ... ]
pagerduty_configs:
  [ - &lt;pagerduty_config&gt;, ... ]
pushover_configs:
  [ - &lt;pushover_config&gt;, ... ]
slack_configs:
  [ - &lt;slack_config&gt;, ... ]
opsgenie_configs:
  [ - &lt;opsgenie_config&gt;, ... ]
victorops_configs:
  [ - &lt;victorops_config&gt;, ... ]
</code></pre>

<p>目前官方内置的第三方通知集成包括：邮件、 即时通讯软件（如Slack、Hipchat）、移动应用消息推送(如Pushover)和自动化运维工具（例如：Pagerduty、Opsgenie、Victorops）。Alertmanager的通知方式中还可以支持Webhook，通过这种方式开发者可以实现更多个性化的扩展支持。</p>

<p><br></p>

<h4 id="toc_15">4.3 告警发钉钉实例</h4>

<p>如果只是告警，不把告警消息发出去，不需要安装alertmanager和prometheus-webhook-dingtalk服务，编写告警规则，然后在prometheus配置中导入告警规则文件即可。</p>

<pre><code class="language-yaml">rule_files:
  # - &quot;first_rules.yml&quot;
  - &quot;/etc/prometheus/rules/*.yml&quot;
</code></pre>

<p>然后在浏览器访问 <a href="http://192.168.101.88:9090/alerts" rel="nofollow">http://192.168.101.88:9090/alerts</a> 可以看到有没有告警消息。</p>

<p>为了实现告警发钉钉消息，需要安装alertmanager和prometheus-webhook-dingtalk两个服务，下面是一个在docker上实现prometheus采集和告警发钉钉脚本。</p>

<p>(1) 添加告警规则配置，./rules/hoststats-alert.yml文件内容如下：</p>

<pre><code class="language-yaml">groups:
- name: hostStatsAlert
  rules:
  - alert: hostCpuUsageAlert
    expr: sum(avg without (cpu)(irate(node_cpu_seconds_total{mode!='idle'}[5m]))) by (instance) &gt; 0.5
    for: 1m
    labels:
      severity: page
    annotations:
      summary: &quot;Instance {{ $labels.instance }} CPU usgae high&quot;
      description: &quot;{{ $labels.instance }} CPU usage above 50% (current value: {{ $value }})&quot;
  - alert: hostMemUsageAlert
    expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes &gt; 0.8
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: &quot;Instance {{ $labels.instance }} MEM usgae high&quot;
      description: &quot;{{ $labels.instance }} MEM usage above 80% (current value: {{ $value }})&quot;
</code></pre>

<p><br></p>

<p>(2) 添加alertmanager配置，文件./config/alertmanager.yml内容如下：</p>

<pre><code class="language-yaml">global:
  resolve_timeout: 5m

route:
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 1h
  receiver: 'dingding-webhook1'
  routes:
    - match:
        severity: critical
      receiver: dingding-webhook1
    - match:
        severity: page
      receiver: dingding-webhook2

receivers:
  # 对应prometheus-webhook-dingtalk服务监听地址，改服务启动时需要添加webhook1、webhook2对应的钉钉地址参数。
- name: dingding-webhook1
  webhook_configs:
  - url: http://prometheus-webhook-dingtalk:8060/dingtalk/webhook1/send 
- name: dingding-webhook2
  webhook_configs:
  - url: http://prometheus-webhook-dingtalk:8060/dingtalk/webhook2/send
</code></pre>

<p><br></p>

<p>(3) 修改prometheus配置，添加规则文件和报警管理器，文件./config/prometheus.yml的内容如下：</p>

<pre><code class="language-yaml"># my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - &quot;first_rules.yml&quot;
  - &quot;/etc/prometheus/rules/*.yml&quot;

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=&lt;job_name&gt;` to any timeseries scraped from this config.
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090','cadvisor:8080','node-exporter:9100']
</code></pre>

<p><br></p>

<p>修改docker-compose.yml配置，添加alertmanager和prometheus-webhook-dingtalk服务，docker-compose.yml文件内容如下：</p>

<pre><code class="language-bash">version: &quot;3&quot;

services:
  prometheus:
    container_name: prometheus
    restart: always
    image: prom/prometheus:v2.11.1
    ports:
      - 9090:9090
    command:
      - &quot;--config.file=/etc/prometheus/prometheus.yml&quot;
      - &quot;--storage.tsdb.path=/prometheus&quot;
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./rules:/etc/prometheus/rules
      - prom-data:/prometheus
    networks:
      - prom-net

  node-exporter:
    container_name: node-exporter
    restart: always
    image: prom/node-exporter:latest
    ports:
      - 9100:9100
    networks:
      - prom-net

  cadvisor:
    container_name: cadvisor
    restart: always
    image: google/cadvisor:latest
    ports:
      - 9101:8080
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    networks:
      - prom-net

  grafana:
    container_name: grafana
    image: grafana/grafana:6.2.5
    restart: always
    ports:
      - &quot;3003:3000&quot;
    volumes:
      - ./grafana/datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yaml
      - grafana-data:/var/lib/grafana
    networks:
      - prom-net
    #environment:
    #  - GF_SECURITY_ADMIN_PASSWORD=123456

  # prometheus的告警信息发送到alertmanager服务，alertmanager可以转发告警信息到邮件、微信、钉钉等
  alertmanager:
    container_name: alertmanager
    restart: always
    image: prom/alertmanager:latest
    command:
      - &quot;--config.file=/etc/alertmanager/alertmanager.yml&quot;
      - &quot;--storage.path=/alertmanager&quot;
    volumes:
      - ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml
      - alertmanager-data:/alertmanager
    ports:
      - 9093:9093
    networks:
      - prom-net

  # 发送告警信息到钉钉
  prometheus-webhook-dingtalk:
    container_name: prometheus-webhook-dingtalk
    restart: always
    image: timonwong/prometheus-webhook-dingtalk:latest
    command:
      - &quot;--ding.profile=webhook1=https://oapi.dingtalk.com/robot/send?access_token=f6ac4c35e3aedd9a3d34b9d6950d8e0d0891c4d6837b1596a738e3b9d77e932b&quot;
      - &quot;--ding.profile=webhook2=https://oapi.dingtalk.com/robot/send?access_token=fcc0038d1d09712449a1ba129e0e11748e1bc63f547d49140db35079e29c3973&quot;
    ports:
      - 8060:8060
    networks:
      - prom-net

volumes:
  prom-data:
    driver: local
  grafana-data:
    driver: local
  alertmanager-data:
    driver: local

networks: 
  prom-net:
    driver: bridge
</code></pre>

<p><br></p>

<p>启动服务：</p>

<blockquote>
<p>docker-compose up -d</p>
</blockquote>

<p>在浏览器打开 <a href="http://192.168.101.88:9090/rules" rel="nofollow">http://192.168.101.88:9090/rules</a> ，查看告警文件是否生效，如下图所示：
<img src="https://go-sponge.com/assets/images/blog/279024-5-rules-UI.jpg" alt="rules" /></p>

<p><br></p>

<p>点击菜单Alerts，查看是否有告警触发，未触发时背景是绿色 0 active，触发后有两种状态pending和firing，pending状态的背景为黄色 1 active，firing状态背景变为红色 1 active，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/279024-4-prometheus-alert-UI.jpg" alt="alerts" /></p>

<p><br></p>

<p>测试让其触发告警：</p>

<pre><code class="language-bash"># 启动测试容器
docker run --rm -it busybox sh

# 执行耗cpu命令
cat /dev/zero&gt;/dev/null

# 注意：如果一个容器不够，可以启动多几个
# 因为rules文件里设置for为1分钟，说明如果1分钟后告警条件持续满足，则会实际触发告警并且告警状态为FIRING。
</code></pre>

<p>对于已经pending或者firing的告警，prometheus也会将它们存储到时间序列ALERTS{}中，可以通过表达式，查询告警结果如下：</p>

<pre><code>ALERTS{alertname=&quot;hostMemUsageAlert&quot;,alertstate=&quot;firing&quot;,instance=&quot;node-exporter:9100&quot;,job=&quot;prometheus&quot;,severity=&quot;critical&quot;}
</code></pre>

<p>样本值为1表示当前告警处于活动状态（pending或者firing），当告警从活动状态转换为非活动状态时，样本值则为0。</p>

<p><br></p>

<p>在浏览器访问alertmanager服务界面( <a href="http://192.168.101.88:9093/#/alerts" rel="nofollow">http://192.168.101.88:9093/#/alerts</a> )，如果prometheus触发(firing)了告警，会显示告警消息记录，同时也会把告警消息转发给prometheus-webhook-dingtalk服务，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/279024-6-alerte-UI.jpg" alt="alerts" /></p>

<p>当触发告警时，alertmanager服务通过http推送(POST)告警信息给prometheus-webhook-dingtalk服务，推送告警消息的url格式为 <a href="http://xxxxx:8060/dingtalk/webhook1/send" rel="nofollow">http://xxxxx:8060/dingtalk/webhook1/send</a> ，其中webhook1是钉钉地址对应的名称，该名称在prometheus-webhook-dingtalk服务启动时设置，docker-compose.yml中prometheus-webhook-dingtalk服务启动参数如下：</p>

<pre><code class="language-yaml">    command:
      - &quot;--ding.profile=webhook1=https://oapi.dingtalk.com/robot/send?access_token=xxxxxx&quot;
</code></pre>

<p>prometheus-webhook-dingtalk服务接收到消息后从url解析出钉钉地址对应的名称，根据名称获得钉钉地址，然后把告警消息推送到钉钉，如下图所示：</p>

<p><img src="https://go-sponge.com/assets/images/blog/279024-7-send-dingding-example.jpg" alt="dinding-msg" /></p>

<p><br><br></p>

<h3 id="toc_16">5 exporter</h3>

<p>广义上讲所有可以向Prometheus提供监控样本数据的程序都可以被称为一个Exporter。而Exporter的一个实例称为target。</p>

<p><strong>(1) 常用Exporter</strong></p>

<table>
<thead>
<tr>
<th>范围</th>
<th>常用Exporter</th>
</tr>
</thead>

<tbody>
<tr>
<td>数据库</td>
<td>MySQL Exporter, Redis Exporter, MongoDB Exporter, MSSQL Exporter等</td>
</tr>

<tr>
<td>硬件</td>
<td>Apcupsd Exporter，IoT Edison Exporter， IPMI Exporter, Node Exporter等</td>
</tr>

<tr>
<td>消息队列</td>
<td>Beanstalkd Exporter, Kafka Exporter, NSQ Exporter, RabbitMQ Exporter等</td>
</tr>

<tr>
<td>存储</td>
<td>Ceph Exporter, Gluster Exporter, HDFS Exporter, ScaleIO Exporter等</td>
</tr>

<tr>
<td>HTTP服务</td>
<td>Apache Exporter, HAProxy Exporter, Nginx Exporter等</td>
</tr>

<tr>
<td>API服务</td>
<td>AWS ECS Exporter， Docker Cloud Exporter, Docker Hub Exporter, GitHub Exporter等</td>
</tr>

<tr>
<td>日志</td>
<td>Fluentd Exporter, Grok Exporter等</td>
</tr>

<tr>
<td>监控系统</td>
<td>Collectd Exporter, Graphite Exporter, InfluxDB Exporter, Nagios Exporter, SNMP Exporter等</td>
</tr>

<tr>
<td>其它</td>
<td>Blockbox Exporter, JIRA Exporter, Jenkins Exporter， Confluence Exporter等</td>
</tr>
</tbody>
</table>

<p><br></p>

<p><strong>(2) Exporter的运行方式</strong></p>

<p>从Exporter的运行方式上来讲，又可以分为：</p>

<ul>
<li>独立使用的</li>
</ul>

<p>以已经使用过的Node Exporter为例，由于操作系统本身并不直接支持Prometheus，同时用户也无法通过直接从操作系统层面上提供对Prometheus的支持。因此，用户只能通过独立运行一个程序的方式，通过操作系统提供的相关接口，将系统的运行状态数据转换为可供Prometheus读取的监控数据。 除了Node Exporter以外，比如MySQL Exporter、Redis Exporter等都是通过这种方式实现的。 这些Exporter程序扮演了一个中间代理人的角色。</p>

<ul>
<li>集成到应用中的</li>
</ul>

<p>为了能够更好的监控系统的内部运行状态，有些开源项目如Kubernetes，ETCD等直接在代码中使用了Prometheus的Client Library，提供了对Prometheus的直接支持。这种方式打破的监控的界限，让应用程序可以直接将内部的运行状态暴露给Prometheus，适合于一些需要更多自定义监控指标需求的项目。</p>

<p><br></p>

<p><strong>(3) Exporter规范</strong></p>

<pre><code class="language-bash"># HELP node_cpu Seconds the cpus spent in each mode.
# TYPE node_cpu counter
node_cpu{cpu=&quot;cpu0&quot;,mode=&quot;idle&quot;} 362812.7890625
# HELP node_load1 1m load average.
# TYPE node_load1 gauge
node_load1 3.0703125
</code></pre>

<p>如果当前行以# HELP开始，Prometheus将会按照以下规则对内容进行解析，得到当前的指标名称以及相应的说明信息。</p>

<p>如果当前行以# TYPE开始，Prometheus会按照以下规则对内容进行解析，得到当前的指标名称以及指标类型，如果没有明确的指标类型需要返回为untyped。</p>

<p>除了# 开头的所有行都会被视为是监控样本数据。</p>

<p><br></p>

<h4 id="toc_17">5.1 linux系统监控</h4>

<p><strong>(1) 启动node-exporter</strong></p>

<p>使用docker启动，docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'

services:
  node-exporter:
    image: prom/node-exporter:v1.2.2
    container_name: node-exporter
    command:
      - '--path.rootfs=/host'
    network_mode: host
    pid: host
    restart: always
    volumes:
      - '/:/host:ro,rslave'
</code></pre>

<p><br></p>

<p><strong>(2) 配置prometheus</strong></p>

<p>在Prometheus配置文件添加job，内容如下：</p>

<pre><code class="language-yaml">  - job_name: 'node-exporter'
    scrape_interval: 15s
    static_configs:
      - targets: ['192.168.111.128:9100']
        labels:
          project: &quot;电商&quot;
          env: &quot;dev&quot;
</code></pre>

<p>重载prometheus配置，使配置生效</p>

<blockquote>
<p>curl -X POST <a href="http://192.168.111.128:9090/-/reload" rel="nofollow">http://192.168.111.128:9090/-/reload</a></p>
</blockquote>

<p>注：启动prometheus时必须添加参数&ndash;web.enable-lifecycle，表示开启prometheus重载配置功能。</p>

<p><br></p>

<p><strong>(3) 导入grafana模板</strong></p>

<p>打开grafana，点击import，输入编号12377，其中数字编号是官网 <a href="https://grafana.com/grafana/dashboards" rel="nofollow">https://grafana.com/grafana/dashboards</a> 的一个模板编号，确定之后，grafana会自动从grafana官网下载模板json文件。</p>

<p><br></p>

<h4 id="toc_18">5.2 cadvisor容器监控</h4>

<p>cadvisor是Google开源的一款用于展示和分析容器运行状态的可视化工具。通过在主机上运行CAdvisor用户可以轻松的获取到当前主机上容器的运行统计信息，并以图表的形式向用户展示。</p>

<p><strong>(1) 启动cadvisor容器</strong></p>

<p>使用docker启动，docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'

services:
    cadvisor-exporter:
        image: gcr.io/cadvisor/cadvisor:v0.37.5
        container_name: cadvisor
        # 设置容器权限为root
        privileged: true
        volumes:
            - /:/rootfs:ro
            - /var/run:/var/run:ro
            - /sys:/sys:ro
            - /var/lib/docker/:/var/lib/docker:ro
            - /dev/disk/:/dev/disk:ro
        ports:
            - 9192:8080
        restart: always
</code></pre>

<p><br></p>

<p>注：如果启动容器cadvisor时出现下面错误：</p>

<pre><code class="language-bash">Failed to start container manager: inotify_add_watch /sys/fs/cgroup/cpuacct,cpu: no such file or directory
</code></pre>

<p>解决办法：在管理员模式下执行</p>

<pre><code class="language-bash">mount -o remount,rw '/sys/fs/cgroup'
ln -s /sys/fs/cgroup/cpu,cpuacct /sys/fs/cgroup/cpuacct,cpu
</code></pre>

<p><br></p>

<p><strong>(2) 配置Prometheus</strong></p>

<p>在prometheus的配置文件scrape_configs下添加配置：</p>

<pre><code class="language-yaml">  - job_name: 'cadvisor'
    scrape_interval: 60s
    static_configs:
    - targets: ['192.168.11.128:9192']
</code></pre>

<p>重载prometheus配置，使配置生效</p>

<blockquote>
<p>curl -X POST <a href="http://192.168.111.128:9090/-/reload" rel="nofollow">http://192.168.111.128:9090/-/reload</a></p>
</blockquote>

<p>注：启动prometheus时必须添加参数&ndash;web.enable-lifecycle，表示开启prometheus重载配置功能。</p>

<p><br></p>

<p><strong>(3) 导入grafana模板</strong></p>

<p>打开grafana，点击import，输入编号14282，其中数字编号是官网 <a href="https://grafana.com/grafana/dashboards" rel="nofollow">https://grafana.com/grafana/dashboards</a> 的模板编号，确定之后，grafana会自动从grafana官网下载模板json文件。</p>

<p>使用14282模板时，需要做一下调整，可以结合node-exporter在一个界面展示。</p>

<pre><code class="language-bash"># 修改变量host的Query字段值
label_values(cadvisor_version_info{}, instance)

# 根据实际情况是否过滤显示端口，字段Regex值
/([^:]+):.*/

# 如果过滤端口，每个dashboard都需要添加
instance=~&quot;$host:.*&quot;
</code></pre>

<p><br></p>

<p><strong>(4) 一些常使用的监控</strong></p>

<p>下面表格中列举了一些cadvisor中获取到的典型监控指标：</p>

<table>
<thead>
<tr>
<th>指标名称</th>
<th>类型</th>
<th>含义</th>
</tr>
</thead>

<tbody>
<tr>
<td>container_cpu_load_average_10s</td>
<td>gauge</td>
<td>过去10秒容器CPU的平均负载</td>
</tr>

<tr>
<td>container_cpu_usage_seconds_total</td>
<td>counter</td>
<td>容器在每个CPU内核上的累积占用时间 (单位：秒)</td>
</tr>

<tr>
<td>container_cpu_system_seconds_total</td>
<td>counter</td>
<td>System CPU累积占用时间（单位：秒）</td>
</tr>

<tr>
<td>container_cpu_user_seconds_total</td>
<td>counter</td>
<td>User CPU累积占用时间（单位：秒）</td>
</tr>

<tr>
<td>container_fs_usage_bytes</td>
<td>gauge</td>
<td>容器中文件系统的使用量(单位：字节)</td>
</tr>

<tr>
<td>container_fs_limit_bytes</td>
<td>gauge</td>
<td>容器可以使用的文件系统总量(单位：字节)</td>
</tr>

<tr>
<td>container_fs_reads_bytes_total</td>
<td>counter</td>
<td>容器累积读取数据的总量(单位：字节)</td>
</tr>

<tr>
<td>container_fs_writes_bytes_total</td>
<td>counter</td>
<td>容器累积写入数据的总量(单位：字节)</td>
</tr>

<tr>
<td>container_memory_max_usage_bytes</td>
<td>gauge</td>
<td>容器的最大内存使用量（单位：字节）</td>
</tr>

<tr>
<td>container_memory_usage_bytes</td>
<td>gauge</td>
<td>容器当前的内存使用量（单位：字节</td>
</tr>

<tr>
<td>container_spec_memory_limit_bytes</td>
<td>gauge</td>
<td>容器的内存使用量限制</td>
</tr>

<tr>
<td>machine_memory_bytes</td>
<td>gauge</td>
<td>当前主机的内存总量</td>
</tr>

<tr>
<td>container_network_receive_bytes_total</td>
<td>counter</td>
<td>容器网络累积接收数据总量（单位：字节）</td>
</tr>

<tr>
<td>container_network_transmit_bytes_total</td>
<td>counter</td>
<td>容器网络累积传输数据总量（单位：字节）</td>
</tr>
</tbody>
</table>

<p><br></p>

<p>获取监控值的表达式：</p>

<pre><code class="language-bash"># 计算容器的CPU使用率
sum(irate(container_cpu_usage_seconds_total{image!=&quot;&quot;}[1m])) without (cpu)

# 查询容器内存使用量(字节)
container_memory_usage_bytes{image!=&quot;&quot;}

# 查询容器网络接收量速率(字节/秒)
sum(rate(container_network_receive_bytes_total{image!=&quot;&quot;}[1m])) without (interface)

# 查询容器网络传输量速率(字节/秒)
sum(rate(container_network_transmit_bytes_total{image!=&quot;&quot;}[1m])) without (interface)

# 查询容器文件系统读取速率(字节/秒)
sum(rate(container_fs_reads_bytes_total{image!=&quot;&quot;}[1m])) without (device)

# 查询容器文件系统写入速率(字节/秒)
sum(rate(container_fs_writes_bytes_total{image!=&quot;&quot;}[1m])) without (device)
</code></pre>

<p><br></p>

<h4 id="toc_19">5.3 mysql监控</h4>

<p>通过mysql exporter实现对mysql数据库的性能以及资源利用率进行监控和度量。</p>

<p><strong>(1) 启动mysql-exporter容器</strong></p>

<p>使用docker启动，docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'

services:
    mysql-exporter:
        image: prom/mysqld-exporter:v0.13.0
        restart: always
        ports:
            - 9104:9104
        environment:
            DATA_SOURCE_NAME: &quot;root:123456@(192.168.111.128:3306)/&quot;
</code></pre>

<p><br></p>

<p><strong>(2) 配置Prometheus</strong></p>

<p>在prometheus的配置文件scrape_configs下添加配置：</p>

<pre><code class="language-yaml">  # mysql资源export
  - job_name: 'mysql-exporter'
    static_configs:
    - targets: ['192.168.111.128:9104']
      #labels:
        #project: 'dbaas'
        #environment: 'dev'
</code></pre>

<p>重载prometheus配置，使配置生效</p>

<blockquote>
<p>curl -X POST <a href="http://192.168.111.128:9090/-/reload" rel="nofollow">http://192.168.111.128:9090/-/reload</a></p>
</blockquote>

<p>注：启动prometheus时必须添加参数&ndash;web.enable-lifecycle，表示开启prometheus重载配置功能。</p>

<p>可以在prometheus UI中查看到当前所有的Target状态，如果为up状态，说明mysql-exporter与prometheus集成成功。</p>

<p><br></p>

<p><strong>(3) 导入grafana</strong></p>

<p>把dashboard id(单机版536或集群版537)导入到grafana的dashboard。</p>

<p><br></p>

<p><strong>(4) 一些mysql常使用的监控指标</strong></p>

<ul>
<li><strong>监控数据库吞吐量</strong></li>
</ul>

<p>对于数据库而言，最重要的工作就是实现对数据的增、删、改、查。为了衡量数据库服务器当前的吞吐量变化情况。在mysql内部通过一个名为Questions的计数器，当客户端发送一个查询语句后，其值就会加1，对应mysql语句：</p>

<blockquote>
<p>SHOW GLOBAL STATUS LIKE &ldquo;Questions&rdquo;;</p>
</blockquote>

<p>一般还可以从监控读操作和写操作的执行情况进行判断。通过mysql全局状态中的Com_select可以查询到当前服务器执行查询语句的总次数：相应的，也可以通过Com_insert、Com_update以及Com_delete的总量衡量当前服务器写操作的总次数，通过以下指令查询当前mysql实例insert语句的执行次数总量：</p>

<blockquote>
<p>SHOW GLOBAL STATUS LIKE &ldquo;Com_insert&rdquo;;</p>
</blockquote>

<p>通过以上监控指标，以及实际监控的场景，我们可以利用PromQL快速建立多个监控项。</p>

<pre><code class="language-bash"># 查看当前mysql实例查询速率的变化情况，查询数量的突变往往暗示着可能发生了某些严重的问题，因此用于用户应该关注并且设置响应的告警规则，以及时获取该指标的变化情况
rate(mysql_global_status_questions[2m])
    
# 查看当前MySQL实例写操作速率的变化情况
sum(rate(mysql_global_status_commands_total{command=~&quot;insert|update|delete&quot;}[2m])) without (command)
</code></pre>

<p><br></p>

<ul>
<li><strong>监控连接情况</strong></li>
</ul>

<p>在MySQL中通过全局设置max_connections限制了当前服务器允许的最大客户端连接数量。一旦可用连接数被用尽，新的客户端连接都会被直接拒绝。用户可以通过以下指令查看当前MySQL服务的max_connections配置：</p>

<blockquote>
<p>SHOW VARIABLES LIKE &lsquo;max_connections&rsquo;;</p>
</blockquote>

<p>mysql_global_variables_max_connections： 允许的最大连接数；</p>

<p>mysql_global_status_threads_connected： 当前打开的连接；</p>

<p>mysql_global_status_threads_running：当前正在使用的连接数；</p>

<p>mysql_global_status_aborted_connects：当前拒绝的连接数；</p>

<p>mysql_global_status_connection_errors_total{error=&ldquo;max_connections&rdquo;}：由于超出最大连接数导致的错误；</p>

<p>mysql_global_status_connection_errors_total{error=&ldquo;internal&rdquo;}：由于系统内部导致的错误；</p>

<p>通过以上监控指标，以及实际监控的场景，我们可以利用PromQL快速建立多个监控项。</p>

<pre><code class="language-bash"># 查询当前剩余的可用连接数
mysql_global_variables_max_connections - mysql_global_status_threads_connected

# 查询当前MySQL实例连接拒绝数
mysql_global_status_aborted_connects
</code></pre>

<p><br></p>

<ul>
<li><strong>监控缓冲池使用情况</strong></li>
</ul>

<p>mysql默认的存储引擎InnoDB使用了一片称为缓冲池的内存区域，用于缓存数据表以及索引的数据。 当缓冲池的资源使用超出限制后，可能会导致数据库性能的下降，同时很多查询命令会直接在磁盘中执行，导致磁盘I/O不断攀升。</p>

<p>在mysql查看当前缓冲池中的内存页的总页数：</p>

<blockquote>
<p>SHOW GLOBAL STATUS LIKE &ldquo;Innodb_buffer_pool_pages_total&rdquo;;</p>
</blockquote>

<p>在mysql查看正常从缓冲池读取数据的请求数量：</p>

<blockquote>
<p>SHOW GLOBAL STATUS LIKE &ldquo;Innodb_buffer_pool_read_requests&rdquo;;</p>
</blockquote>

<p>当缓冲池无法满足时，mysql只能从磁盘中读取数据，如果Innodb_buffer_pool_reads的值开始增加，可能意味着数据库的性能有问题，查看从磁盘读取数据的请求数量：</p>

<blockquote>
<p>SHOW GLOBAL STATUS LIKE &ldquo;Innodb_buffer_pool_reads&rdquo;;</p>
</blockquote>

<p>通过以上监控指标，以及实际监控的场景，我们可以利用PromQL快速建立多个监控项。</p>

<pre><code class="language-bash"># 通过以下PromQL可以得到各个MySQL实例的缓冲池利用率:
(sum(mysql_global_status_buffer_pool_pages) by (instance) - sum(mysql_global_status_buffer_pool_pages{state=&quot;free&quot;}) by (instance)) / sum(mysql_global_status_buffer_pool_pages) by (instance)

# 计算2分钟内磁盘读取请求次数的增长率的变化情况：
rate(mysql_global_status_innodb_buffer_pool_reads[2m])
</code></pre>

<p><br></p>

<ul>
<li><strong>查询性能</strong></li>
</ul>

<p>MySQL还提供了一个Slow_queries的计数器，当查询的执行时间超过long_query_time的值后，计数器就会+1，其默认值为10秒。</p>

<p>在mysql查看慢查询命令：</p>

<blockquote>
<p>SHOW VARIABLES LIKE &lsquo;long_query_time&rsquo;;</p>
</blockquote>

<p>在mysql查看慢查询的数量：</p>

<blockquote>
<p>SHOW GLOBAL STATUS LIKE &ldquo;Slow_queries&rdquo;;</p>
</blockquote>

<p>通过监控Slow_queries的增长率，可以反映出当前MySQL服务器的性能状态：</p>

<blockquote>
<p>rate(mysql_global_status_slow_queries[2m])</p>
</blockquote>

<p><br></p>

<h4 id="toc_20">5.4 redis监控</h4>

<p><strong>(1) 启动redis-exporter</strong></p>

<p>使用docker启动，docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'

services:
    redis-exporter:
        container_name: redis-exporter
        image: bitnami/redis-exporter:1.27.1
        restart: always
        ports:
            - 9121:9121
        command:
            - &quot;-redis.addr=redis://192.168.83.133:6379&quot;
            - &quot;-redis.password=123456&quot;
</code></pre>

<p><br></p>

<p><strong>(2) 配置prometheus</strong></p>

<p>在Prometheus配置文件添加job，内容如下：</p>

<pre><code class="language-yaml">  - job_name: 'redis-exporter'
    scrape_interval: 15s
    static_configs:
      - targets: ['192.168.111.128:9121']
</code></pre>

<p>重载prometheus配置，使配置生效</p>

<blockquote>
<p>curl -X POST <a href="http://192.168.111.128:9090/-/reload" rel="nofollow">http://192.168.111.128:9090/-/reload</a></p>
</blockquote>

<p>注：启动prometheus时必须添加参数&ndash;web.enable-lifecycle，表示开启prometheus重载配置功能。</p>

<p><br></p>

<p><strong>(3) 导入grafana模板</strong></p>

<p>打开grafana，点击import，输入编号763，其中数字编号是官网 <a href="https://grafana.com/grafana/dashboards" rel="nofollow">https://grafana.com/grafana/dashboards</a> 的一个模板编号，确定之后，grafana会自动从grafana官网下载模板json文件。</p>

<p><br></p>

<h4 id="toc_21">5.5 blackbox exporter网络探测</h4>

<p>白盒监控包括主机的资源用量、容器的运行状态、数据库中间件的运行数据，通过白盒能够了解其内部的实际运行状态，通过对监控指标的观察能够预判可能出现的问题，从而对潜在的不确定因素进行优化。</p>

<p>黑盒监控相较于白盒监控最大的不同在于黑盒监控是以故障为导向当故障发生时，黑盒监控能快速发现故障，而白盒监控则侧重于主动发现或者预测潜在的问题。一个完善的监控目标是要能够从白盒的角度发现潜在问题，能够在黑盒的角度快速发现已经发生的问题。</p>

<p><strong>(1) 启动blackbox-exporter容器</strong></p>

<p>使用docker启动，docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'

services:
  blackbox-exporter:
    container_name: blackbox-exporter
    restart: always
    image: prom/blackbox-exporter:v0.19.0
    command:
      - &quot;--config.file=/config/blackbox.yml&quot;
    volumes:
      - $PWD/config/blackbox.yml:/config/blackbox.yml
    ports:
      - 9115:9115
</code></pre>

<p>配置文件blackbox.yml内容如下：</p>

<pre><code class="language-yaml">modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      method: GET
  tcp_connect:
    prober: tcp
    timeout: 5s
</code></pre>

<p><br></p>

<p><strong>(2) 配置prometheus</strong></p>

<p>在prometheus的配置文件scrape_configs下添加配置：</p>

<pre><code class="language-yaml">  # http export
  - job_name: 'blackbox'
    scrape_interval: 30s #每次获取数据的时间间隔
    metrics_path: /probe
    params:
      module: [http_2xx]  # Look for a HTTP 200 response.
    static_configs:
      - targets:
        # 监控目标
        - https://www.baidu.com
        - https://zhuyasen.com
        - https://test.demo.com
        - http://example:8080
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115  # The blackbox exporter's real hostname:port.
</code></pre>

<p>重载prometheus配置，使配置生效</p>

<blockquote>
<p>curl -X POST <a href="http://192.168.111.128:9090/-/reload" rel="nofollow">http://192.168.111.128:9090/-/reload</a></p>
</blockquote>

<p>注：启动prometheus时必须添加参数&ndash;web.enable-lifecycle，表示开启prometheus重载配置功能。</p>

<p><br></p>

<p><strong>(3) 导入grafana模板</strong></p>

<p>打开grafana，点击import，输入编号13659或9965，其中数字编号是官网 <a href="https://grafana.com/grafana/dashboards" rel="nofollow">https://grafana.com/grafana/dashboards</a> 的一个模板编号，确定之后，grafana会自动从grafana官网下载模板json文件。</p>

<p><br></p>

<h4 id="toc_22">5.6 linux进程监控</h4>

<p><strong>(1) 启动process-exporter</strong></p>

<p>使用docker启动，docker-compose.yml内容如下：</p>

<pre><code class="language-yaml">version: '3.1'

services:
    process-exporter:
        image: ncabatoff/process-exporter:0.7.5
        volumes:
            - /proc:/host/proc
            - $PWD/config/process.yml:/config/process.yml
        command:
            - '--procfs=/host/proc'
            - '--config.path=/config/process.yml'
        ports:
            - 9256:9256
        restart: always
</code></pre>

<p>GitHub地址：<a href="https://github.com/ncabatoff/process-exporter" rel="nofollow">https://github.com/ncabatoff/process-exporter</a></p>

<p><br></p>

<p>配置文件process.yml如下：</p>

<pre><code class="language-yaml">process_names:
  - comm:
    - chromium-browse
    - bash
    - prometheus
    - gvim
  - exe:
    - /sbin/upstart
    cmdline:
    - --user
    name: upstart:-user
</code></pre>

<p><br></p>

<p><strong>(2) 配置prometheus</strong></p>

<p>在Prometheus配置文件添加job，内容如下：</p>

<pre><code class="language-yaml">  - job_name: 'process-exporter'
    scrape_interval: 15s
    static_configs:
      - targets: ['192.168.83.133:9256']
</code></pre>

<p><br></p>

<p><strong>(3) 导入grafana模板</strong></p>

<p>打开grafana，点击import，输入编号249，其中数字编号是官网 <a href="https://grafana.com/grafana/dashboards" rel="nofollow">https://grafana.com/grafana/dashboards</a> 的一个模板编号，确定之后，grafana会自动从grafana官网下载模板json文件。</p>

<p><br><br></p>

<h3 id="toc_23">6 kubernetes监控</h3>

<h4 id="toc_24">6.1 使用Prometheus监控Kubernetes集群</h4>

<p>监控Kubernetes集群监控的各个维度以及策略：</p>

<table>
<thead>
<tr>
<th>目标</th>
<th>服务发现模式</th>
<th>监控方法</th>
<th>数据源</th>
</tr>
</thead>

<tbody>
<tr>
<td>从集群各节点kubelet组件中获取节点kubelet的基本运行状态的监控指标</td>
<td>node</td>
<td>白盒监控</td>
<td>kubelet</td>
</tr>

<tr>
<td>从集群各节点kubelet内置的cAdvisor中获取，节点中运行的容器的监控指标</td>
<td>node</td>
<td>白盒监控</td>
<td>kubelet</td>
</tr>

<tr>
<td>从部署到各个节点的Node Exporter中采集主机资源相关的运行资源</td>
<td>node</td>
<td>白盒监控</td>
<td>node exporter</td>
</tr>

<tr>
<td>对于内置了Promthues支持的应用，需要从Pod实例中采集其自定义监控指标</td>
<td>pod</td>
<td>白盒监控</td>
<td>custom pod</td>
</tr>

<tr>
<td>获取API Server组件的访问地址，并从中获取Kubernetes集群相关的运行监控指标</td>
<td>endpoints</td>
<td>白盒监控</td>
<td>api server</td>
</tr>

<tr>
<td>获取集群中Service的访问地址，并通过Blackbox Exporter获取网络探测指标</td>
<td>service</td>
<td>黑盒监控</td>
<td>blackbox exporter</td>
</tr>

<tr>
<td>获取集群中Ingress的访问信息，并通过Blackbox Exporter获取网络探测指标</td>
<td>ingress</td>
<td>黑盒监控</td>
<td>blackbox exporter</td>
</tr>
</tbody>
</table>

<p><strong>(1) 从Kubelet获取节点运行状态</strong></p>

<p>修改prometheus.yml配置文件，并添加以下采集任务配置：</p>

<pre><code class="language-yaml">    - job_name: 'kubernetes-kubelet'
      kubernetes_sd_configs:
      - role: node
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_node_label_(.+)
      - target_label: __address__
        replacement: kubernetes.default.svc:443
      - source_labels: [__meta_kubernetes_node_name]
        regex: (.+)
        target_label: __metrics_path__
        replacement: /api/v1/nodes/${1}/proxy/metrics
</code></pre>

<p>通过指标kubelet_pod_start_latency_microseconds可以获得当前节点中Pod启动时间相关的统计数据：</p>

<blockquote>
<p>kubelet_pod_start_latency_microseconds{quantile=&ldquo;0.99&rdquo;}/1000000</p>
</blockquote>

<p>监控指标kubelet<em>docker</em>*还可以体现出kubelet与当前节点的docker服务的调用情况，从而可以反映出docker本身是否会影响kubelet的性能表现等问题：</p>

<blockquote>
<p>kubelet_pod_start_latency_microseconds_sum / kubelet_pod_start_latency_microseconds_count</p>
</blockquote>

<p><br></p>

<p><strong>(2) 使用NodeExporter监控集群资源使用情况</strong></p>

<p>修改prometheus.yml配置文件，并添加以下采集任务配置：</p>

<pre><code class="language-yaml">    - job_name: 'kubernetes-nodes'
      kubernetes_sd_configs:
      - role: node
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_node_label_(.+)
      - target_label: __address__
        replacement: kubernetes.default.svc:443
      - source_labels: [__meta_kubernetes_node_name]
        regex: (.+)
        target_label: __metrics_path__
        replacement: /api/v1/nodes/${1}/proxy/metrics

    - job_name: 'kubernetes-pods'
      kubernetes_sd_configs:
      - role: pod
      relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)
      - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
        action: replace
        regex: ([^:]+)(?::\d+)?;(\d+)
        replacement: $1:$2
        target_label: __address__
      - action: labelmap
        regex: __meta_kubernetes_pod_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_pod_name]
        action: replace
        target_label: kubernetes_pod_name
</code></pre>

<p><br></p>

<p><strong>(3) 从kube-apiserver获取集群运行监控指标</strong></p>

<p>修改prometheus.yml配置文件，并添加以下采集任务配置：</p>

<pre><code class="language-yaml">    - job_name: 'kubernetes-apiservers'
      kubernetes_sd_configs:
      - role: endpoints
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      relabel_configs:
      - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
        action: keep
        regex: default;kubernetes;https
      - target_label: __address__
        replacement: kubernetes.default.svc:443
</code></pre>

<p><br></p>

<p><strong>(4) 对Ingress和Service进行网络探测</strong></p>

<p>修改prometheus.yml配置文件，并添加以下采集任务配置：</p>

<pre><code class="language-yaml">    - job_name: 'kubernetes-services'
      scrape_interval: 20s #每次获取数据的时间间隔
      kubernetes_sd_configs:
      - role: service
      metrics_path: /probe
      params:
        module: [http_2xx]
      relabel_configs:
      # 过滤探测目标service
      # 如果注释掉下面source_labels三行，会探测所有service
      # 如果不注释，只有service的metadata里添加注解(annotations) prometheus.io/probe: &quot;true&quot;才会被探测
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
        action: keep
        regex: true
      - source_labels: [__address__]
        target_label: __param_target
      - target_label: __address__
        replacement: blackbox-exporter:9115
      - source_labels: [__param_target]
        target_label: instance
      - action: labelmap
        regex: __meta_kubernetes_service_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_service_name]
        target_label: kubernetes_name

    - job_name: 'kubernetes-ingresses'
      scrape_interval: 20s #每次获取数据的时间间隔
      kubernetes_sd_configs:
      - role: ingress
      metrics_path: /probe
      params:
        module: [http_2xx]
      relabel_configs:
      # 过滤探测目标ingresses
      # 如果注释掉下面source_labels三行，会探测所有ingresses
      # 如果不注释，只有ingresses的metadata里添加注解(annotations) prometheus.io/probe: &quot;true&quot;才会被探测
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_ingress_scheme,__address__,__meta_kubernetes_ingress_path]
        regex: (.+);(.+);(.+)
        replacement: ${1}://${2}${3}
        target_label: __param_target
      - target_label: __address__
        replacement: blackbox-exporter:9115
      - source_labels: [__param_target]
        target_label: instance
      - action: labelmap
        regex: __meta_kubernetes_ingress_label_(.+)
      - source_labels: [__meta_kubernetes_namespace]
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_ingress_name]
        target_label: kubernetes_name
</code></pre>

<p><br></p>

<p>启动blackbox服务，启动脚本如下：</p>

<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: blackbox-exporter
  name: blackbox-exporter
  namespace: monitoring
data:
  blackbox.yml: |-
    modules:
      http_2xx:
        prober: http
        timeout: 30s
        http:
          valid_http_versions: [&quot;HTTP/1.1&quot;, &quot;HTTP/2&quot;]
          valid_status_codes: [200,302,301,401,404]
          method: GET
          preferred_ip_protocol: &quot;ip4&quot;

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: blackbox-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: blackbox-exporter
  replicas: 1
  template:
    metadata:
      labels:
        app: blackbox-exporter
    spec:
      restartPolicy: Always
      containers:
      - name: blackbox-exporter
        image: prom/blackbox-exporter:v0.14.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: blackbox-port
          containerPort: 9115
        readinessProbe:
          tcpSocket:
            port: 9115
          initialDelaySeconds: 20
          timeoutSeconds: 5
        resources:
          requests:
            memory: 10Mi
            cpu: 10m
          limits:
            memory: 60Mi
            cpu: 200m
        volumeMounts:
        - name: config
          mountPath: /etc/blackbox_exporter
        args:
        - --config.file=/etc/blackbox_exporter/blackbox.yml
        - --log.level=debug
        - --web.listen-address=:9115
      volumes:
      - name: config
        configMap:
          name: blackbox-exporter

---

apiVersion: v1
kind: Service
metadata:
  labels:
    app: blackbox-exporter
  name: blackbox-exporter
  namespace: monitoring
  annotations:
    prometheus.io/scrape: 'true'
spec:
  selector:
    app: blackbox-exporter
  ports:
  - name: blackbox
    port: 9115
    targetPort: 9115
    protocol: TCP
</code></pre>

<p><br></p>

<p>遇到问题：</p>

<p>在kubernetes-apiservers的target出现错误提示信息：</p>

<blockquote>
<p>Get <a href="https://192.168.99.100:10250/metrics:" rel="nofollow">https://192.168.99.100:10250/metrics:</a> x509: cannot validate certificate for 192.168.99.100 because it doesn&rsquo;t contain any IP SANs</p>
</blockquote>

<p>这是由于当前使用的ca证书中，并不包含192.168.99.100的地址信息。</p>

<p>解决方法：</p>

<p>第一种方法是直接跳过ca证书校验过程，通过在tls_config中设置 insecure_skip_verify为true即可。 这样Promthues在采集样本数据时，将会自动跳过ca证书的校验过程。</p>

<p>第二种方式，不直接通过kubelet的metrics服务采集监控数据，而通过Kubernetes的api-server提供的代理API访问各个节点中kubelet的metrics服务：</p>

<pre><code class="language-yaml">      - target_label: __address__
        replacement: kubernetes.default.svc:443
      - source_labels: [__meta_kubernetes_node_name]
        regex: (.+)
        target_label: __metrics_path__
        replacement: /api/v1/nodes/${1}/proxy/metrics
</code></pre>

<p><br></p>

<h4 id="toc_25">6.2 认识Prometheus Operator</h4>

<p>为了在Kubernetes能够方便的管理和部署Prometheus，使用ConfigMap来管理Prometheus配置文件。每次对Prometheus配置文件进行升级时，我们需要手动移除已经运行的Pod实例，从而让Kubernetes可以使用最新的配置文件创建Prometheus，这种通过手动的方式部署和升级Prometheus过程繁琐并且效率低下。</p>

<p><strong>(1) Prometheus Operator的工作原理</strong></p>

<p>从概念上来讲Operator就是针对管理特定应用程序的，在Kubernetes基本的Resource和Controller的概念上，以扩展Kubernetes api的形式。帮助用户创建，配置和管理复杂的有状态应用程序。从而实现特定应用程序的常见操作以及运维自动化。</p>

<p>Prometheus的本职就是一组用户自定义的CRD资源以及Controller的实现，Prometheus Operator负责监听这些自定义资源的变化，并且根据这些资源的定义自动化的完成如Prometheus Server自身以及配置的自动化管理工作。</p>

<p><img src="https://go-sponge.com/assets/images/blog/279024-9--prometheus-architecture.jpg" alt="Prometheus Operator框架图" /></p>

<p><br></p>

<p><strong>(2) Prometheus Operator能做什么</strong></p>

<p>Prometheus Operator能够帮助用户自动化的创建以及管理Prometheus Server以及其相应的配置，目前提供的️4类资源：</p>

<ul>
<li>Prometheus：声明式创建和管理Prometheus Server实例；</li>
<li>ServiceMonitor：负责声明式的管理监控配置；</li>
<li>PrometheusRule：负责声明式的管理告警配置；</li>
<li>Alertmanager：声明式的创建和管理Alertmanager实例。</li>
</ul>

<p><br></p>

<p><strong>(3) 安装Prometheus Operator</strong></p>

<p>部署prometheus-operator.yml脚本文件内容如下：</p>

<pre><code class="language-yaml">apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/version: v0.31.1
  name: prometheus-operator
  namespace: monitoring

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/version: v0.31.1
  name: prometheus-operator
rules:
- apiGroups:
  - apiextensions.k8s.io
  resources:
  - customresourcedefinitions
  verbs:
  - '*'
- apiGroups:
  - monitoring.coreos.com
  resources:
  - alertmanagers
  - prometheuses
  - prometheuses/finalizers
  - alertmanagers/finalizers
  - servicemonitors
  - podmonitors
  - prometheusrules
  verbs:
  - '*'
- apiGroups:
  - apps
  resources:
  - statefulsets
  verbs:
  - '*'
- apiGroups:
  - &quot;&quot;
  resources:
  - configmaps
  - secrets
  verbs:
  - '*'
- apiGroups:
  - &quot;&quot;
  resources:
  - pods
  verbs:
  - list
  - delete
- apiGroups:
  - &quot;&quot;
  resources:
  - services
  - services/finalizers
  - endpoints
  verbs:
  - get
  - create
  - update
  - delete
- apiGroups:
  - &quot;&quot;
  resources:
  - nodes
  verbs:
  - list
  - watch
- apiGroups:
  - &quot;&quot;
  resources:
  - namespaces
  verbs:
  - get
  - list
  - watch

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/version: v0.31.1
  name: prometheus-operator
  namespace: monitoring
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus-operator
subjects:
- kind: ServiceAccount
  name: prometheus-operator
  namespace: monitoring

---

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/version: v0.31.1
  name: prometheus-operator
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/component: controller
      app.kubernetes.io/name: prometheus-operator
  template:
    metadata:
      labels:
        app.kubernetes.io/component: controller
        app.kubernetes.io/name: prometheus-operator
        app.kubernetes.io/version: v0.31.1
    spec:
      containers:
      - args:
        - --kubelet-service=kube-system/kubelet
        - --logtostderr=true
        - --config-reloader-image=quay.io/coreos/configmap-reload:v0.0.1
        - --prometheus-config-reloader=quay.io/coreos/prometheus-config-reloader:v0.31.1
        #image: quay.io/coreos/prometheus-operator:v0.31.1
        image: 281073576117.dkr.ecr.cn-north-1.amazonaws.com.cn/prometheus-operator:v0.31.1
        name: prometheus-operator
        ports:
        - containerPort: 8080
          name: http
        resources:
          limits:
            cpu: 200m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 100Mi
        securityContext:
          allowPrivilegeEscalation: false
      nodeSelector:
        beta.kubernetes.io/os: linux
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
      serviceAccountName: prometheus-operator

---

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
    app.kubernetes.io/version: v0.31.1
  name: prometheus-operator
  namespace: monitoring
spec:
  clusterIP: None
  ports:
  - name: http
    port: 8080
    targetPort: http
  selector:
    app.kubernetes.io/component: controller
    app.kubernetes.io/name: prometheus-operator
</code></pre>

<p>启动prometheus operator：</p>

<blockquote>
<p>kubectl apply -f prometheus-operator.yml</p>
</blockquote>

<p>查看启动情况：</p>

<blockquote>
<p>kubectl get all -n monitoring</p>
</blockquote>

<p>查看新添加的自定义资源：</p>

<blockquote>
<p>kubectl get crd</p>
</blockquote>

<pre><code>NAME                                    CREATED AT
alertmanagers.monitoring.coreos.com     2019-08-14T05:56:46Z
podmonitors.monitoring.coreos.com       2019-08-14T05:56:50Z
prometheuses.monitoring.coreos.com      2019-08-14T05:56:46Z
prometheusrules.monitoring.coreos.com   2019-08-14T05:56:50Z
servicemonitors.monitoring.coreos.com   2019-08-14T05:56:50Z
</code></pre>

<p><br></p>

<h4 id="toc_26">6.3 使用Operator管理Prometheus</h4>

<p><strong>(1) 创建Prometheus实例</strong></p>

<p>当集群中已经安装Prometheus Operator之后，对于部署Prometheus Server实例就变成了声明一个Prometheus资源，安装Prometheus的部署脚本prometheus-inst.yml文件内容如下：</p>

<pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: inst
  namespace: monitoring
spec:
  resources:
    requests:
      memory: 400Mi
</code></pre>

<p>启动prometheus：</p>

<blockquote>
<p>kubectl apply -f prometheus-inst.yml</p>
</blockquote>

<p>查看sts和pod实例启动情况：</p>

<blockquote>
<p>kubectl get sts,pod -n monitoring</p>
</blockquote>

<p>通过port-forward访问Prometheus实例:</p>

<blockquote>
<p>kubectl port-forward &ndash;address=0.0.0.0 statefulsets/prometheus-inst 9090:9090 -n monitoring</p>
</blockquote>

<p>如果能在浏览器访问 http://<host-ip>:9090 访问，说明prometheus启动正常，此时prometheus的配置只有默认的配置，可以点击菜单Status&ndash;&gt;Configuration查看。</p>

<p><br></p>

<p><strong>(2) 使用ServiceMonitor管理监控配置</strong></p>

<p>修改监控配置项是Prometheus下常用的运维操作之一，为了能够自动化的管理Prometheus的配置，Prometheus Operator使用了自定义资源类型ServiceMonitor来描述监控对象的信息。</p>

<p>部署一个示例应用，部署脚本example-app.yaml内容如下：</p>

<pre><code class="language-yaml">kind: Service
apiVersion: v1
metadata:
  name: example-app
  labels:
    app: example-app
spec:
  selector:
    app: example-app
  ports:
  - name: web
    port: 8080
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: example-app
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: example-app
    spec:
      containers:
      - name: example-app
        image: fabxc/instrumented_app
        ports:
        - name: web
          containerPort: 8080
</code></pre>

<p><br></p>

<p>查看实例启动情况：</p>

<blockquote>
<p>kubectl get svc,pod | grep example-app</p>
</blockquote>

<p>通过port-forward访问任意Pod实例：</p>

<blockquote>
<p>kubectl port-forward &ndash;address=0.0.0.0 deployments/example-app 8080:8080</p>
</blockquote>

<p>在浏览器查看 http://<host-ip>:8080/metrics，能够看到很多符合prometheus采集的信息，此时prometheus还不能采集example-app监控数据。</p>

<p>为了能够让Prometheus能够采集应用的监控数据，在原生的Prometheus配置方式中，我们在Prometheus配置文件中定义单独的Job，同时使用kubernetes_sd定义整个服务发现过程。而在Prometheus Operator中，则可以直接声明一个ServiceMonitor对象，把监听的服务和端口添加进来，这是ServiceMonitor关联服务对象过程。</p>

<p>脚本prometheus-serviceMonitor.yml内容如下所示：</p>

<pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: example-app
  namespace: monitoring
  labels:
    team: frontend
spec:
  namespaceSelector:
    matchNames:
    - default
  selector:
    matchLabels:
      app: example-app
  endpoints:
  - port: web
</code></pre>

<p>通过定义selector中的标签定义选择监控目标的Pod对象，同时在endpoints中指定port名称为web的端口。默认情况下ServiceMonitor和监控对象必须是在相同Namespace下的。在本示例中由于Prometheus是部署在Monitoring命名空间下，因此为了能够关联default命名空间下的example对象，需要使用namespaceSelector定义让其可以跨命名空间关联ServiceMonitor资源。如果希望ServiceMonitor可以关联任意命名空间下的标签，则通过以下方式定义：</p>

<pre><code class="language-yaml">spec:
  namespaceSelector:
    any: true
</code></pre>

<p><br></p>

<p><strong>(3)关联ServiceMonitor与Promethues</strong></p>

<p>为了能够让Prometheus关联到ServiceMonitor，需要在Pormtheus定义中使用serviceMonitorSelector，我们可以通过标签选择当前Prometheus需要监控的ServiceMonitor对象，修改prometheus-inst.yaml中Prometheus的定义如下所示：</p>

<pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: inst
  namespace: monitoring
spec:
  serviceMonitorSelector:
    matchLabels:
      team: frontend
  resources:
    requests:
      memory: 400Mi
</code></pre>

<p>更新prometheus服务：</p>

<blockquote>
<p>kubectl apply -f prometheus-inst.yml</p>
</blockquote>

<p>等待一会时间，在浏览器打开http://<host-ip>:9090/config ，会发现prometheus自动添加monitoring/example-app/0的Job配置。</p>

<p>虽然自动添加Job配置，但是Prometheus的Target中并没包含任何的监控对象。查看Prometheus的Pod实例日志，可以看到如下信息：</p>

<pre><code>level=error ts=2019-08-14T10:01:27.707953281Z caller=klog.go:94 component=k8s_client_runtime func=ErrorDepth msg=&quot;/app/discovery/kubernetes/kubernetes.go:302: Failed to list *v1.Pod: pods is forbidden: User \&quot;system:serviceaccount:monitoring:default\&quot; cannot list pods at the cluster scope&quot;
</code></pre>

<p>说明在monitoring名称空间下的prometheus没有权限获取defaul名称空间的example-app信息。</p>

<p><br></p>

<p><strong>(4) 自定义ServiceAccount</strong></p>

<p>为了提升权限获取defaul名称空间的服务信息，需要在Monitoring命名空间下为创建一个名为Prometheus的ServiceAccount，并且为该账号赋予相应的集群访问权限。</p>

<p>资源文件prometheus-rbac.yaml内容如下：</p>

<pre><code class="language-yaml">apiVersion: v1
kind: ServiceAccount
metadata:
  name: prometheus
  namespace: monitoring
  
---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: prometheus
rules:
- apiGroups: [&quot;&quot;]
  resources:
  - nodes
  - services
  - endpoints
  - pods
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
- apiGroups: [&quot;&quot;]
  resources:
  - configmaps
  verbs: [&quot;get&quot;]
- nonResourceURLs: [&quot;/metrics&quot;]
  verbs: [&quot;get&quot;]
  
---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: prometheus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus
subjects:
- kind: ServiceAccount
  name: prometheus
  namespace: monitoring
</code></pre>

<p>在完成ServiceAccount创建后，修改prometheus-inst.yaml，并添加ServiceAccount如下所示：</p>

<pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: inst
  namespace: monitoring
spec:
  serviceAccountName: prometheus
  serviceMonitorSelector:
    matchLabels:
      team: frontend
  resources:
    requests:
      memory: 400Mi
</code></pre>

<p>等待Prometheus Operator完成相关配置变更后，此时查看Prometheus，我们就能看到当前Prometheus已经能够正常的采集实例应用的相关监控数据了。</p>

<p><br><br></p>

<h3 id="toc_27">常用的grafana面板</h3>

<table>
<thead>
<tr>
<th align="left">监控类型</th>
<th align="left">grafana id</th>
</tr>
</thead>

<tbody>
<tr>
<td align="left">node-exproter(linux)</td>
<td align="left">8919</td>
</tr>

<tr>
<td align="left">windows-exproter(windows)</td>
<td align="left">10467</td>
</tr>

<tr>
<td align="left">mysql</td>
<td align="left">7362</td>
</tr>

<tr>
<td align="left">redis</td>
<td align="left">9338</td>
</tr>

<tr>
<td align="left">cadvisor(容器)</td>
<td align="left">893</td>
</tr>
</tbody>
</table>

<p>grafana免密码登录设置，打开conf/defaults.ini，把auth.anonymous值改为true</p>

<pre><code>[auth.anonymous]
enabled = true
</code></pre>

<p><br><br></p>

<p>参考：</p>

<ul>
<li><a href="https://github.com/yunlzheng/prometheus-book" rel="nofollow">https://github.com/yunlzheng/prometheus-book</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/prometheus.html">https://zhuyasen.com/post/prometheus.html</a>，<a href="https://zhuyasen.com/post/prometheus.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
        <item>
            <title>rabbitmq基础和使用</title>
            <link>https://zhuyasen.com/post/rabbitmq.html</link>
            <comments>https://zhuyasen.com/post/rabbitmq.html#comments</comments>
            <guid>https://zhuyasen.com/post/rabbitmq.html</guid>
            <description>
                <![CDATA[<blockquote>

<h3 id="toc_0">rabbitMQ简介</h3>

<p>rabbitMQ是一个广泛使用的开源消息队列系统，它实现了高级消息队列协议(AMQP)标准，为分布式应用程序提供了强大的消息传递功能。rabbitMQ是 Erlang 语言编写的，具有高度的可扩展性和可靠性，因此被广泛用于构建分布式、异步的消息通信系统。</p>

<h4 id="toc_1">消息队列的概念</h4>

<p>消息队列是一种通信模式，用于在不同组件、服务或应用程序之间传递消息。它允许发送者将消息放入队列，而接收者可以从队列中获取消息，实现了解耦、异步通信和数据传递的目标。消息队列通常用于处理以下情况：</p>

<ul>
<li>异步通信：发送方和接收方之间不需要立即响应，提高了系统的可伸缩性和性能。</li>
<li>任务排队：将需要处理的任务放入队列，由工作进程异步执行。</li>
<li>解耦组件：允许不同的应用程序或服务之间进行松耦合的通信。</li>
</ul>

<p><br></p>

<h4 id="toc_2">rabbitMQ 工作模式</h4>

<ul>
<li>Direct Exchange(直连交换机)：对于每个队列与direct交换机绑定的key进行完全匹配。</li>
<li>Topic Exchange(主题交换机) ：对于每个队列与Topic交换机绑定的key进行模糊匹配。</li>
<li>Fanout Exchange(扇出型交换机)： Fanout类型的交换机会将消息分发给所有绑定了此交换机的队列。</li>
<li>Headers Exchange(头交换机)：Headers类型的交换机是通过headers信息来匹配的，工作原理与direct类型类似。</li>
<li>Delayed Message Exchange(延时交换机)：指定一个消息不是立即投递到队列，而是在指定的一段时间后才投递。</li>
</ul>

<p><br></p>

<h4 id="toc_3">rabbitMQ 的核心概念</h4>

<ul>
<li>Producer(生产者)：负责向消息队列发送消息的应用程序或服务。</li>
<li>Consumer(消费者)：负责从消息队列接收和处理消息的应用程序或服务。</li>
<li>Queue(队列)：用于存储消息的缓冲区，消费者从队列中获取消息进行处理。</li>
<li>Exchange(交换机)：接收生产者发送的消息并将其路由到一个或多个队列。</li>
<li>Binding(绑定)：定义了队列和交换机之间的关系，指定了如何将消息从交换机路由到队列。</li>
<li>Virtual Host(虚拟主机)：rabbitMQ允许将多个逻辑消息队列隔离到不同的虚拟主机中，以实现资源隔离和多租户支持。</li>
</ul>

<p><br></p>

<h4 id="toc_4">工作流程</h4>

<p>rabbitMQ的工作流程如下：</p>

<ul>
<li>生产者将消息发布到一个或多个交换机。</li>
<li>交换机根据绑定规则将消息路由到一个或多个队列。</li>
<li>消费者订阅队列并接收消息。</li>
<li>消费者处理消息，并可以确认消息已被成功处理。</li>
<li>消息可以持久化到磁盘，以确保在 rabbitMQ重启后不会丢失。</li>
</ul>

<p><br></p>

<h4 id="toc_5">消息确认和持久化</h4>

<p>rabbitMQ具有高度的可靠性，它支持消息确认机制，确保消息在成功处理后才从队列中删除。如果消费者在处理消息时发生错误，消息将被重新排队，而不会丢失。此外，rabbitMQ还支持将消息持久化到磁盘，以防止消息在系统故障时丢失。</p>

<p><br></p>

<h4 id="toc_6">可用性和扩展性</h4>

<p>rabbitMQ具有高可用性和可伸缩性的特性。它支持镜像队列(Queue Mirroring)来确保队列数据的冗余备份，以提高可用性。此外，rabbitMQ集群可以水平扩展，允许将多个节点添加到集群中以增加处理能力。</p>

<p><br></p>

<h4 id="toc_7">协议支持</h4>

<p>rabbitMQ支持多种协议，包括 AMQP(高级消息队列协议)、STOMP、MQTT 等。这使得不同类型的应用程序可以与 rabbitMQ进行通信，而无需修改现有代码。</p>

<p><br></p>

<h4 id="toc_8">应用场景</h4>

<ul>
<li>分布式系统通信：用于不同组件或服务之间的消息传递。</li>
<li>异步任务处理：将需要执行的任务放入队列，由工作者进行处理。</li>
<li>日志和监控数据的收集：将日志和监控数据发送到 rabbitMQ，以进行集中处理和分析。</li>
</ul>

<p><br></p>

<h3 id="toc_9">安装rabbitMQ</h3>

<h4 id="toc_10">在docker安装单机版rabbitMQ</h4>

<p>docker-compose.yaml配置文件内容如下：</p>

<pre><code class="language-yaml">version: '3'
 
services:
  rabbitmq:
    image: rabbitmq:3.12-management
    container_name: rabbitmq
    hostname: rabbitmq-service
    restart: always
    ports:
      - 5672:5672
      - 15672:15672
    volumes:
      - $PWD/data:/var/lib/rabbitmq
      - $PWD/plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins
      - $PWD/plugins/rabbitmq_delayed_message_exchange-3.12.0.ez:/plugins/rabbitmq_delayed_message_exchange-3.12.0.ez
    environment:
      TZ: Asia/Shanghai
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
      RABBITMQ_DEFAULT_VHOST: /
</code></pre>

<ul>
<li>enabled_plugins 是设置默认开启的插件，内容为 <code>[rabbitmq_delayed_message_exchange,rabbitmq_management,rabbitmq_prometheus]</code></li>
<li><a href="https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.12.0/rabbitmq_delayed_message_exchange-3.12.0.ez" rel="nofollow">rabbitmq_delayed_message_exchange-3.12.0.ez</a> 是延时队列插件。</li>
</ul>

<p>启动rabbitmq：</p>

<blockquote>
<p>docker-compose up -d</p>
</blockquote>

<p>可以在浏览器访问管理后台 <a href="http://localhost:15672，户名和密码都是guest。" rel="nofollow">http://localhost:15672，户名和密码都是guest。</a></p>

<p><br></p>

<h4 id="toc_11">在docker安装高可用的rabbitMQ集群</h4>

<p>安装根据实际需要使用普通模式和镜像模式，一共有三个rabbitmq节点和一个高可用代理服务haproxy，haproxy务作为代理连接入口，文件列表如下：</p>

<pre><code class="language-bash">.
├── cluster-entrypoint.sh
├── docker-compose.yml
├── .env
└── haproxy.cfg
</code></pre>

<p><br></p>

<p>(1) 加入rabbitMQ集群的脚本文件cluster-entrypoint.sh内容如下：</p>

<pre><code class="language-bash">#!/bin/bash

set -e

# Start RMQ from entry point.
# This will ensure that environment variables passed
# will be honored
/usr/local/bin/docker-entrypoint.sh rabbitmq-server -detached

# Do the cluster dance
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@rabbitmq1

# Stop the entire RMQ server. This is done so that we
# can attach to it again, but without the -detached flag
# making it run in the forground
rabbitmqctl stop

# Wait a while for the app to really stop
sleep 2s

# Start it
rabbitmq-server
</code></pre>

<p><br></p>

<p>(2) docker-compose配置文件的环境变量.env内容如下，.env文件包含可用于更改默认用户名，密码等。</p>

<pre><code class="language-bash">RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
RABBITMQ_DEFAULT_VHOST=/
RABBITMQ_ERLANG_COOKIE=rec123456
</code></pre>

<p><br></p>

<p>(3) 高可用服务的配置文件haproxy.cfg内容如下：</p>

<pre><code>    global
            log 127.0.0.1   local1
            maxconn 4096
     
    defaults
            log     global
            mode    tcp
            option  tcplog
            retries 3
            option redispatch
            maxconn 2000
            timeout connect 5000
            timeout client 50000
            timeout server 50000
     
    listen  stats
            bind *:1936
            mode http
            stats enable
            stats hide-version
            stats realm Haproxy\ Statistics
            stats uri /
     
    listen rabbitmq
            bind *:5672
            mode            tcp
            balance         roundrobin
            timeout client  3h
            timeout server  3h
            option          clitcpka
            server          rabbitmq1 rabbitmq1:5672  check inter 5s rise 2 fall 3
            server          rabbitmq2 rabbitmq2:5672  check inter 5s rise 2 fall 3
            server          rabbitmq3 rabbitmq3:5672  check inter 5s rise 2 fall 3

    listen mgmt
            bind *:15672
            mode            tcp
            balance         roundrobin
            timeout client  3h
            timeout server  3h
            option          clitcpka
            server          rabbitmq1 rabbitmq1:15672  check inter 5s rise 2 fall 3
            server          rabbitmq2 rabbitmq2:15672  check inter 5s rise 2 fall 3
            server          rabbitmq3 rabbitmq3:15672  check inter 5s rise 2 fall 3
</code></pre>

<p><br></p>

<p>(4) docker-compose.yml配置文件内容如下：</p>

<pre><code class="language-yaml">version: '3'

services:

  rabbitmq1:
    hostname: rabbitmq1
    image: rabbitmq:3.12-management
    restart: always
    environment:
      - RABBITMQ_ERLANG_COOKIE=${RABBITMQ_ERLANG_COOKIE}
      - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
      - RABBITMQ_DEFAULT_VHOST=${RABBITMQ_DEFAULT_VHOST}
    volumes:
      - ./data/rabbitmq1:/var/lib/rabbitmq/mnesia

  rabbitmq2:
    hostname: rabbitmq2
    image: rabbitmq:3.12-management
    restart: always
    depends_on:
      - rabbitmq1
    environment:
      - RABBITMQ_ERLANG_COOKIE=${RABBITMQ_ERLANG_COOKIE}
    volumes:
      - ./cluster-entrypoint.sh:/usr/local/bin/cluster-entrypoint.sh
      - ./data/rabbitmq2:/var/lib/rabbitmq/mnesia
    entrypoint: sh /usr/local/bin/cluster-entrypoint.sh

  rabbitmq3:
    hostname: rabbitmq3
    image: rabbitmq:3.12-management
    restart: always
    depends_on:
      - rabbitmq1
    environment:
      - RABBITMQ_ERLANG_COOKIE=${RABBITMQ_ERLANG_COOKIE}
    volumes:
      - ./cluster-entrypoint.sh:/usr/local/bin/cluster-entrypoint.sh
      - ./data/rabbitmq3:/var/lib/rabbitmq/mnesia
    entrypoint: sh /usr/local/bin/cluster-entrypoint.sh
    
  haproxy:
    image: haproxy:1.9
    restart: always
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
      - rabbitmq1
      - rabbitmq2
      - rabbitmq3
    ports:
      - 15672:15672
      - 5672:5672
</code></pre>

<p><br></p>

<p>启动rabbitMQ集群：</p>

<pre><code class="language-bash">docker-compose up -d

# 默认用户名guest，默认密码guest
# 代理连接地址：localhost:5672
# 管理界面地址：localhost:15672
</code></pre>

<p>可以在浏览器访问管理后台 <a href="http://localhost:15672，户名和密码都是guest。" rel="nofollow">http://localhost:15672，户名和密码都是guest。</a></p>

<p><br></p>

<h4 id="toc_12">在k8s安装rabbitMQ集群</h4>

<p>根据实际需要，安装rabbitmq集群可以选择普通模式和镜像模式，如果需要设置为镜像模式，在普通集群的中任意节点启用策略，策略会自动同步到集群节点：</p>

<blockquote>
<p>rabbitmqctl set_policy ha-all &ldquo;^ha.&rdquo; &lsquo;{&ldquo;ha-mode&rdquo;:&ldquo;all&rdquo;}&rsquo;</p>
</blockquote>

<p>注意：&rdquo;^ha&rdquo; 这个规则要根据自己修改，这个是指同步&rdquo;ha&rdquo;开头的队列名称，配置时使用的应用于所有队列，所以表达式为&rdquo;^&ldquo;。</p>

<p><br></p>

<p>创建rabbitmq集群的Erlang cookie，配置文件rabbitmq-secret.yml内容如下：</p>

<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: rabbitmq-config
  namespace: default
data:
  erlang-cookie: |-
    MTIzNDU2Nzg5MAo=
</code></pre>

<p><br></p>

<p>使用statefulset启动rabbitmq，配置文件rabbitmq-sts.yml内容如下：</p>

<pre><code class="language-yaml">apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: rabbitmq
spec:
  serviceName: rabbitmq
  replicas: 3
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      containers:
      - name: rabbitmq
        image: rabbitmq:3.12-management
        lifecycle:
          postStart:
            exec:
              command:
              - /bin/sh
              - -c
              - &gt;
                if [ -z &quot;$(grep rabbitmq /etc/resolv.conf)&quot; ]; then
                  sed &quot;s/^search \([^ ]\+\)/search rabbitmq.\1 \1/&quot; /etc/resolv.conf &gt; /etc/resolv.conf.new;
                  cat /etc/resolv.conf.new &gt; /etc/resolv.conf;
                  rm /etc/resolv.conf.new;
                fi;
                until rabbitmqctl node_health_check; do sleep 1; done;
                if [[ &quot;$HOSTNAME&quot; != &quot;rabbitmq-0&quot; &amp;&amp; -z &quot;$(rabbitmqctl cluster_status | grep rabbitmq-0)&quot; ]]; then
                  rabbitmqctl stop_app;
                  rabbitmqctl join_cluster rabbit@rabbitmq-0;
                  rabbitmqctl start_app;
                fi;
                rabbitmqctl set_policy ha-all &quot;.&quot; '{&quot;ha-mode&quot;:&quot;exactly&quot;,&quot;ha-params&quot;:3,&quot;ha-sync-mode&quot;:&quot;automatic&quot;}'
        env:
        - name: RABBITMQ_ERLANG_COOKIE
          valueFrom:
            secretKeyRef:
              name: rabbitmq-config
              key: erlang-cookie
        ports:
        - containerPort: 5672
          name: amqp
        volumeMounts:
        - name: rabbitmq
          mountPath: /var/lib/rabbitmq
  volumeClaimTemplates:
  - metadata:
      name: rabbitmq
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      resources:
        requests:
          storage: 2Gi
</code></pre>

<p>注：必须准备好存储卷后，pod才能启动。</p>

<p><br></p>

<p>rabbitmq服务配置文件rabbitmq-svc.yml内容如下，对外开放http端口，其它端口在集群内部使用。</p>

<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  # Expose the management HTTP port on each node
  name: rabbitmq-management
  labels:
    app: rabbitmq
spec:
  selector:
    app: rabbitmq
  type: NodePort
  ports:
  - port: 15672
    name: http
    nodePort: 30072

---

apiVersion: v1
kind: Service
metadata:
  # The required headless service for StatefulSets
  name: rabbitmq
  labels:
    app: rabbitmq
spec:
  selector:
    app: rabbitmq
  clusterIP: None
  ports:
  - port: 5672
    name: amqp
  - port: 4369
    name: epmd
  - port: 25672
    name: rabbitmq-dist
</code></pre>

<p><br></p>

<p>启动rabbitmq集群</p>

<pre><code class="language-bash">kubectl apply -f rabbitmq-secret.yml
kubectl apply -f rabbitmq-sts.yml
kubectl apply -f rabbitmq-svc.yml

# 默认用户名guest，默认密码guest
# 代理连接地址：rabbitmq.default:5672
# 管理界面地址：node-ip:30072
</code></pre>

<p><br></p>

<h3 id="toc_13">Direct类型消息队列的golang示例</h3>

<p><img src="https://go-sponge.com/assets/images/blog/rabbitmq/direct.png" alt="direct" /></p>

<h4 id="toc_14">生产端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;direct-exchange-demo&quot;
    queueName    = &quot;direct-queue-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    routingKey1 := &quot;info&quot;
    exchange1 := NewDirectExchange(exchangeName, routingKey1)
    p1, err := NewProducer(queueName, conn, exchange1)
    checkErr(err)
    defer p1.Close()

    routingKey2 := &quot;warning&quot;
    exchange2 := NewDirectExchange(exchangeName, routingKey2)
    p2, err := NewProducer(queueName, conn, exchange2)
    checkErr(err)
    defer p2.Close()

    var body string
    count := 0
    for i := 1; i &lt;= 5; i++ {
        body = fmt.Sprintf(&quot;%s message %d&quot;, routingKey1, i)
        err = p1.Publish(ctx, []byte(body)) // p1 发送消息
        checkErr(err)

        body = fmt.Sprintf(&quot;%s message %d&quot;, routingKey2, i)
        err = p2.Publish(ctx, []byte(body)) // p2 发送消息
        checkErr(err)
        count += 2
    }
    fmt.Println(&quot;publish total&quot;, count)
}

// Exchange 交换机
type Exchange struct {
    Name       string // exchange名称
    Type       string // exchange类型
    RoutingKey string // 路由key
}

// NewDirectExchange 实例化一个direct类型交换机
func NewDirectExchange(exchangeName string, routingKey string) *Exchange {
    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;direct&quot;,
        RoutingKey: routingKey,
    }
}

// Producer 生产者对象
type Producer struct {
    queueName string
    exchange  *Exchange
    conn      *amqp.Connection
    ch        *amqp.Channel
}

// NewProducer 实例化一个生产者
func NewProducer(queueName string, conn *amqp.Connection, exchange *Exchange) (*Producer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,              // 队列名称
        exchange.RoutingKey, // 路由key
        exchange.Name,       // 交换机名称
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Producer{
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        exchange:  exchange,
    }, nil
}

// Publish 发送消息
func (p *Producer) Publish(ctx context.Context, body []byte) error {
    err := p.ch.PublishWithContext(
        ctx,
        p.exchange.Name,       // exchange name
        p.exchange.RoutingKey, // key
        false,                 // mandatory 如果为true，根据自身exchange类型和routingKey规则无法找到符合条件的队列会把消息返还给发送者
        false,                 // immediate 如果为true，当exchange发送消息到队列后发现队列上没有消费者，则会把消息返还给发送者
        amqp.Publishing{
            //DeliveryMode: amqp.Persistent, // 如果队列的声明是持久化的，那么消息也设置为持久化
            ContentType: &quot;text/plain&quot;,
            Body:        body,
        },
    )
    if err != nil {
        return err
    }
    fmt.Println(&quot;[send]: &quot; + string(body))
    return nil
}

// Close 关闭生产者
func (p *Producer) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h4 id="toc_15">消费端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;sync/atomic&quot;
    &quot;syscall&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;direct-exchange-demo&quot;
    queueName    = &quot;direct-queue-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    routingKey1 := &quot;info&quot;
    exchange1 := NewDirectExchange(exchangeName, routingKey1)
    c1, err := NewConsumer(ctx, queueName, exchange1, conn)
    checkErr(err)
    c1.Consume() // 消费消息
    defer c1.Close()

    routingKey2 := &quot;warning&quot;
    exchange2 := NewDirectExchange(exchangeName, routingKey2)
    c2, err := NewConsumer(ctx, queueName, exchange2, conn)
    checkErr(err)
    c2.Consume() // 消费消息
    defer c2.Close()

    fmt.Println(&quot;exit press CTRL+C&quot;)
    exit := make(chan os.Signal, 1)
    signal.Notify(exit, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    &lt;-exit
    fmt.Println(&quot;exit consume messages,  received total&quot;, counter)
}

// Exchange 交换机
type Exchange struct {
    Name       string // exchange名称
    Type       string // exchange类型，支持&quot;direct&quot;、&quot;topic&quot;、&quot;fanout&quot;、&quot;headers&quot;
    RoutingKey string // 路由key
}

// NewDirectExchange 实例化一个direct类型交换机
func NewDirectExchange(exchangeName string, routingKey string) *Exchange {
    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;direct&quot;,
        RoutingKey: routingKey,
    }
}

// Consumer 消费者
type Consumer struct {
    ctx       context.Context
    queueName string
    conn      *amqp.Connection
    ch        *amqp.Channel
    delivery  &lt;-chan amqp.Delivery
    exchange  *Exchange
}

// NewConsumer 实例化一个消费者
func NewConsumer(ctx context.Context, queueName string, exchange *Exchange, conn *amqp.Connection) (*Consumer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    delivery, err := ch.ConsumeWithContext(
        ctx,
        queueName, // queue 名称
        &quot;&quot;,        // consumer 用来区分多个消费者
        true,      // auto-ack 是否自动应答
        false,     // exclusive 是否独有
        false,     // no-local 如果设置为true，表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
        false,     // no-wait 是否阻塞
        nil,       // args
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Consumer{
        ctx:       ctx,
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        delivery:  delivery,
        exchange:  exchange,
    }, nil
}

var counter int32 = 0

// Consume 接收消息
func (c *Consumer) Consume() {
    go func() {
        fmt.Printf(&quot;waiting for messages, type=%s, queue=%s, key=%s\n&quot;, c.exchange.Type, c.queueName, c.exchange.RoutingKey)
        for d := range c.delivery {
            // 处理消息
            if d.RoutingKey == &quot;info&quot; {
                fmt.Printf(&quot;[info received]: %s\n&quot;, d.Body)
            } else if d.RoutingKey == &quot;warning&quot; {
                fmt.Printf(&quot;[warning received]: %s\n&quot;, d.Body)
            }
            atomic.AddInt32(&amp;counter, 1)
            // _ = d.Ack(false) // 如果auto-ack为false时，需要手动ack
        }
    }()
}

// Close 关闭
func (c *Consumer) Close() {
    if c.ch != nil {
        _ = c.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_16">topic类型消息队列的golang示例</h3>

<p><img src="https://go-sponge.com/assets/images/blog/rabbitmq/topic.png" alt="topic" /></p>

<p>topic中，将routingkey通过&rdquo;.&ldquo;来分为多个部分，匹配规则</p>

<ul>
<li>&ldquo;*&ldquo;：代表一个部分，例如routingkey为key1.*或*.key2， topic=key1.key2都可以匹配</li>
<li>&rdquo;#&ldquo;：代表0个或多个部分，例如routingkey为key1.#或#.key3， topic=key1.key2.key3都可以匹配，注意：如果绑定的路由键为 &ldquo;#&rdquo; 时，则接受所有消息，因为路由键所有都匹配。</li>
</ul>

<h4 id="toc_17">生产端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;topic-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    queueName := &quot;topic-queue-1&quot;
    exchange := NewTopicExchange(exchangeName, &quot;*.orange.*&quot;)
    p, err := NewProducer(queueName, conn, exchange)
    checkErr(err)
    key := &quot;key1.orange.key3&quot;
    err = p.Publish(ctx, key, []byte(key+&quot; say hello&quot;))
    checkErr(err)
    defer p.Close()

    queueName = &quot;topic-queue-2&quot;
    exchange = NewTopicExchange(exchangeName, &quot;*.*.rabbit&quot;)
    p, err = NewProducer(queueName, conn, exchange)
    checkErr(err)
    defer p.Close()
    key = &quot;key1.key2.rabbit&quot;
    err = p.Publish(ctx, key, []byte(key+&quot; say hello&quot;))
    checkErr(err)

    exchange = NewTopicExchange(exchangeName, &quot;lazy.#&quot;)
    p, err = NewProducer(queueName, conn, exchange)
    checkErr(err)
    defer p.Close()
    key = &quot;lazy.key2.key3&quot;
    err = p.Publish(ctx, key, []byte(key+&quot; say hello&quot;))
    checkErr(err)
}

// Exchange 交换机
type Exchange struct {
    Name       string // exchange名称
    Type       string // exchange类型，支持&quot;direct&quot;、&quot;topic&quot;、&quot;fanout&quot;、&quot;headers&quot;
    RoutingKey string // 路由key
}

// NewTopicExchange 实例化一个topic类型交换机
func NewTopicExchange(exchangeName string, routingKey string) *Exchange {
    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;topic&quot;,
        RoutingKey: routingKey,
    }
}

// Producer 生产者对象
type Producer struct {
    queueName string
    exchange  *Exchange
    conn      *amqp.Connection
    ch        *amqp.Channel
}

// NewProducer 实例化一个生产者
func NewProducer(queueName string, conn *amqp.Connection, exchange *Exchange) (*Producer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型，支持direct、topic、fanout、headers
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Producer{
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        exchange:  exchange,
    }, nil
}

// Publish 发送消息
func (p *Producer) Publish(ctx context.Context, routingKey string, body []byte) error {
    err := p.ch.PublishWithContext(
        ctx,
        p.exchange.Name, // exchange name
        routingKey,      // key
        false,           // mandatory 如果为true，根据自身exchange类型和routingKey规则无法找到符合条件的队列会把消息返还给发送者
        false,           // immediate 如果为true，当exchange发送消息到队列后发现队列上没有消费者，则会把消息返还给发送者
        amqp.Publishing{
            ContentType: &quot;text/plain&quot;,
            Body:        body,
        },
    )
    if err != nil {
        return err
    }
    fmt.Printf(&quot;[send]: %s\n&quot;, body)
    return nil
}

// Close 关闭
func (p *Producer) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h4 id="toc_18">消费端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;topic-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()
    var queueName string // 对应的发送端的队列名称，如果发送的key模糊匹配命中，可以接收到消息

    queueName = &quot;topic-queue-1&quot;
    exchange := NewTopicExchange(exchangeName, &quot;*.orange.*&quot;)
    queue, err := NewConsumer(ctx, queueName, exchange, conn)
    checkErr(err)
    queue.Consume()
    defer queue.Close()

    queueName = &quot;topic-queue-2&quot;
    exchange = NewTopicExchange(exchangeName, &quot;*.*.rabbit&quot;)
    queue, err = NewConsumer(ctx, queueName, exchange, conn)
    checkErr(err)
    defer queue.Close()
    queue.Consume()
    exchange = NewTopicExchange(exchangeName, &quot;lazy.#&quot;)
    queue, err = NewConsumer(ctx, queueName, exchange, conn)
    checkErr(err)
    defer queue.Close()
    queue.Consume()

    fmt.Println(&quot;exit press CTRL+C&quot;)
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    &lt;-interrupt
    fmt.Println(&quot;exit consume messages&quot;)
}

// Exchange 交换机
type Exchange struct {
    Name       string // exchange名称
    Type       string // exchange类型，支持&quot;direct&quot;、&quot;topic&quot;、&quot;fanout&quot;、&quot;headers&quot;
    RoutingKey string // 路由key
}

// NewTopicExchange 实例化一个topic类型交换机
func NewTopicExchange(exchangeName string, routingKey string) *Exchange {
    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;topic&quot;,
        RoutingKey: routingKey,
    }
}

// Consumer 消费者
type Consumer struct {
    ctx       context.Context
    queueName string
    conn      *amqp.Connection
    ch        *amqp.Channel
    delivery  &lt;-chan amqp.Delivery
    exchange  *Exchange
}

// NewConsumer 实例化一个消费者
func NewConsumer(ctx context.Context, queueName string, exchange *Exchange, conn *amqp.Connection) (*Consumer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 为消息队列注册消费者
    delivery, err := ch.ConsumeWithContext(
        ctx,
        queueName, // queue 名称
        &quot;&quot;,        // consumer 用来区分多个消费者
        true,      // auto-ack 是否自动应答
        false,     // exclusive 是否独有
        false,     // no-local 如果设置为true，表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
        false,     // no-wait 是否阻塞
        nil,       // args
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Consumer{
        ctx:       ctx,
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        delivery:  delivery,
        exchange:  exchange,
    }, nil
}

// Consume 接收消息
func (c *Consumer) Consume() {
    go func() {
        fmt.Printf(&quot;waiting for messages, type=%s, queue=%s, key=%s\n&quot;, c.exchange.Type, c.queueName, c.exchange.RoutingKey)
        for d := range c.delivery {
            // 处理消息
            fmt.Printf(&quot;[%s received]: %s\n&quot;, c.queueName, d.Body)
            // _ = d.Ack(false) // 如果auto-ack为false时，需要手动ack
        }
    }()
}

// Close 关闭
func (c *Consumer) Close() {
    if c.ch != nil {
        _ = c.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_19">fanout类型消息队列的golang示例</h3>

<p><img src="https://go-sponge.com/assets/images/blog/rabbitmq/fanout.png" alt="fanout" /></p>

<p>fanout类型的交换机会将消息分发给所有绑定了此交换机的队列，此时routingkey参数相当于无效。可以使用fanout来实现发布订阅。</p>

<h4 id="toc_20">生产端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;fanout-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    queueName := &quot;fanout-queue-1&quot;
    exchange := NewFanOutExchange(exchangeName)
    p, err := NewProducer(queueName, conn, exchange)
    checkErr(err)
    defer p.Close()
    err = p.Publish(ctx, []byte(queueName+&quot; say hello&quot;))
    checkErr(err)

    queueName = &quot;fanout-queue-2&quot;
    exchange = NewFanOutExchange(exchangeName)
    p, err = NewProducer(queueName, conn, exchange)
    checkErr(err)
    defer p.Close()
    err = p.Publish(ctx, []byte(queueName+&quot; say hello&quot;))
    checkErr(err)

    queueName = &quot;fanout-queue-3&quot;
    exchange = NewFanOutExchange(exchangeName)
    p, err = NewProducer(queueName, conn, exchange)
    checkErr(err)
    defer p.Close()
    err = p.Publish(ctx, []byte(queueName+&quot; say hello&quot;))
    checkErr(err)
}

// Exchange 交换机
type Exchange struct {
    Name       string // exchange名称
    Type       string // exchange类型，支持direct、topic、fanout、headers
    RoutingKey string // 路由key
}

// NewFanOutExchange 实例化一个fanout类型交换机
func NewFanOutExchange(exchangeName string) *Exchange {
    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;fanout&quot;,
        RoutingKey: &quot;&quot;,
    }
}

// Producer 生产者对象
type Producer struct {
    queueName string
    exchange  *Exchange
    conn      *amqp.Connection
    ch        *amqp.Channel
}

// NewProducer 实例化一个生产者
func NewProducer(queueName string, conn *amqp.Connection, exchange *Exchange) (*Producer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey, // 如果交换机类型为fanout，此参数无效
        exchange.Name,
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Producer{
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        exchange:  exchange,
    }, nil
}

// Publish 发送消息
func (p *Producer) Publish(ctx context.Context, body []byte) error {
    err := p.ch.PublishWithContext(
        ctx,
        p.exchange.Name, // exchange name
        &quot;&quot;,              // key  如果类型为fanout，此参数无效
        false,           // mandatory 如果为true，根据自身exchange类型和routingKey规则无法找到符合条件的队列会把消息返还给发送者
        false,           // immediate 如果为true，当exchange发送消息到队列后发现队列上没有消费者，则会把消息返还给发送者
        amqp.Publishing{
            ContentType: &quot;text/plain&quot;,
            Body:        body,
        },
    )
    if err != nil {
        return err
    }
    fmt.Printf(&quot;[send]: %s\n&quot;, body)
    return nil
}

// Close 关闭生产者
func (p *Producer) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h4 id="toc_21">消费端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;fanout-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()
    var queueName string // 对应的发送端的队列名称，如果发送的key模糊匹配命中，可以接收到消息

    queueName = &quot;fanout-queue&quot;
    exchange := NewFanOutExchange(exchangeName)
    queue, err := NewConsumer(ctx, queueName, exchange, conn)
    checkErr(err)
    queue.Consume() // 消费消息
    defer queue.Close()

    fmt.Println(&quot;exit press CTRL+C&quot;)
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    &lt;-interrupt
    fmt.Println(&quot;exit consume messages&quot;)
}

// Exchange 交换机
type Exchange struct {
    Name       string // exchange名称
    Type       string // exchange类型，支持&quot;direct&quot;、&quot;topic&quot;、&quot;fanout&quot;、&quot;headers&quot;
    RoutingKey string // 路由key
}

// NewFanOutExchange 实例化一个fanout类型交换机
func NewFanOutExchange(exchangeName string) *Exchange {
    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;fanout&quot;,
        RoutingKey: &quot;&quot;,
    }
}

// Consumer 消费者
type Consumer struct {
    ctx       context.Context
    queueName string
    conn      *amqp.Connection
    ch        *amqp.Channel
    delivery  &lt;-chan amqp.Delivery
    exchange  *Exchange
}

// NewConsumer 实例化一个消费者
func NewConsumer(ctx context.Context, queueName string, exchange *Exchange, conn *amqp.Connection) (*Consumer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,              // 队列名称
        exchange.RoutingKey, // 如果是fanout类型，无效
        exchange.Name,       // 交换机名称
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 为消息队列注册消费者
    delivery, err := ch.ConsumeWithContext(
        ctx,
        queueName, // queue 名称
        &quot;&quot;,        // consumer 用来区分多个消费者
        true,      // auto-ack 是否自动应答
        false,     // exclusive 是否独有
        false,     // no-local 如果设置为true，表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
        false,     // no-wait 是否阻塞
        nil,       // args
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Consumer{
        ctx:       ctx,
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        delivery:  delivery,
        exchange:  exchange,
    }, nil
}

// Consume 接收消息
func (c *Consumer) Consume() {
    go func() {
        fmt.Printf(&quot;waiting for messages, queue = %s\n&quot;, c.queueName)
        for d := range c.delivery {
            // 处理消息
            fmt.Printf(&quot;[%s received]: %s\n&quot;, c.queueName, d.Body)
            // _ = d.Ack(false) // 如果auto-ack为false时，需要手动ack
        }
    }()
}

// Close 关闭
func (c *Consumer) Close() {
    if c.ch != nil {
        _ = c.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

</code></pre>

<p><br></p>

<h3 id="toc_22">header类型消息队列的golang示例</h3>

<p>headers匹配AMQP消息的header而不是路由键，此时routingkey参数相当于无效，headers交换机和direct交换机类似。</p>

<p>消费方指定的headers中必须指定一个&rdquo;x-match&rdquo;的键，键&rdquo;x-match&rdquo;的值只有2个</p>

<ul>
<li>x-match=all：表示所有的键值对都匹配才能接收到消息</li>
<li>x-match=any：表示只要键值对匹配就能接收消息</li>
</ul>

<h4 id="toc_23">生产端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;headers-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    // 完全匹配headers，消费端才能收到消息
    queueName1 := &quot;headers-queue-1&quot;
    kv1 := map[string]interface{}{&quot;hello1&quot;: &quot;world1&quot;, &quot;foo1&quot;: &quot;bar1&quot;}
    exchange1 := NewHeaderExchange(exchangeName, &quot;all&quot;, kv1)
    p1, err := NewProducer(queueName1, conn, exchange1)
    checkErr(err)
    defer p1.Close()
    headersKey1 := kv1 // 完全匹配，消费端队列可以收到消息
    err = p1.Publish(ctx, headersKey1, []byte(p1.queueName+&quot; say hello 1&quot;))
    checkErr(err)
    headersKey1 = map[string]interface{}{&quot;k&quot;: &quot;v&quot;} // 完全不匹配，消费端队列不能收到消息
    err = p1.Publish(ctx, headersKey1, []byte(p1.queueName+&quot; say hello 2&quot;))
    checkErr(err)
    headersKey1 = map[string]interface{}{&quot;foo1&quot;: &quot;bar1&quot;} // 部分匹配，消费端队列不能收到消息
    err = p1.Publish(ctx, headersKey1, []byte(p1.queueName+&quot; say hello 3&quot;))
    checkErr(err)

    // 部分匹配headers，消费端能收到消息
    queueName2 := &quot;headers-queue-2&quot;
    kv2 := map[string]interface{}{&quot;hello2&quot;: &quot;world2&quot;, &quot;foo2&quot;: &quot;bar2&quot;}
    exchange2 := NewHeaderExchange(exchangeName, &quot;any&quot;, kv2)
    p2, err := NewProducer(queueName2, conn, exchange2)
    checkErr(err)
    defer p2.Close()
    headersKey2 := kv2 // 完全匹配，消费端队列可以收到消息
    err = p2.Publish(ctx, headersKey2, []byte(p2.queueName+&quot; say hello 4&quot;))
    checkErr(err)
    headersKey2 = map[string]interface{}{&quot;k&quot;: &quot;v&quot;} // 完全不匹配，消费端队列不能收到消息
    err = p2.Publish(ctx, headersKey2, []byte(p2.queueName+&quot; say hello 5&quot;))
    checkErr(err)
    headersKey2 = map[string]interface{}{&quot;foo2&quot;: &quot;bar2&quot;} // 部分匹配，消费端队列可以收到消息
    err = p2.Publish(ctx, headersKey2, []byte(p2.queueName+&quot; say hello 6&quot;))
    checkErr(err)
}

// Exchange 交换机
type Exchange struct {
    Name       string                 // exchange名称
    Type       string                 // exchange类型，支持&quot;direct&quot;、&quot;topic&quot;、&quot;fanout&quot;、&quot;headers&quot;
    RoutingKey string                 // 路由key，如果类型为fanout和headers，此字段无效，不需要设置
    Headers    map[string]interface{} // 如果类型为headers，此字段必填
}

// NewHeaderExchange 实例化一个header类型的交换机，headerType支持all和any
func NewHeaderExchange(exchangeName string, headerType string, kv map[string]interface{}) *Exchange {
    if kv == nil {
        kv = make(map[string]interface{})
    }

    switch headerType {
    case &quot;all&quot;, &quot;any&quot;:
        kv[&quot;x-match&quot;] = headerType
    default:
        kv[&quot;x-match&quot;] = &quot;all&quot;
    }

    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;headers&quot;,
        RoutingKey: &quot;&quot;,
        Headers:    kv,
    }
}

// Producer 生产者对象
type Producer struct {
    queueName string
    exchange  *Exchange
    conn      *amqp.Connection
    ch        *amqp.Channel
}

// NewProducer 实例化一个生产者
func NewProducer(queueName string, conn *amqp.Connection, exchange *Exchange) (*Producer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        exchange.Headers,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Producer{
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        exchange:  exchange,
    }, nil
}

// Publish 发送消息
func (p *Producer) Publish(ctx context.Context, headers map[string]interface{}, body []byte) error {
    err := p.ch.PublishWithContext(
        ctx,
        p.exchange.Name,       // exchange name
        p.exchange.RoutingKey, // key
        false,                 // mandatory 如果为true，根据自身exchange类型和routingKey规则无法找到符合条件的队列会把消息返还给发送者
        false,                 // immediate 如果为true，当exchange发送消息到队列后发现队列上没有消费者，则会把消息返还给发送者
        amqp.Publishing{
            Headers:     headers,
            ContentType: &quot;text/plain&quot;,
            Body:        body,
        },
    )
    if err != nil {
        return err
    }
    fmt.Printf(&quot;[send]: %s\n&quot;, body)
    return nil
}

// Close 关闭生产者
func (p *Producer) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h4 id="toc_24">消费端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;headers-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    queueName1 := &quot;headers-queue-1&quot;
    kv1 := map[string]interface{}{&quot;hello1&quot;: &quot;world1&quot;, &quot;foo1&quot;: &quot;bar1&quot;}
    exchange1 := NewHeadersExchange(exchangeName, &quot;all&quot;, kv1)
    c1, err := NewConsumer(ctx, queueName1, exchange1, conn)
    checkErr(err)
    c1.Consume()
    defer c1.Close()

    queueName2 := &quot;headers-queue-2&quot;
    kv2 := map[string]interface{}{&quot;hello2&quot;: &quot;world2&quot;, &quot;foo2&quot;: &quot;bar2&quot;}
    exchange2 := NewHeadersExchange(exchangeName, &quot;any&quot;, kv2)
    c2, err := NewConsumer(ctx, queueName2, exchange2, conn)
    checkErr(err)
    c2.Consume()
    defer c2.Close()

    fmt.Println(&quot;exit press CTRL+C&quot;)
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    &lt;-interrupt
    fmt.Println(&quot;exit consume messages&quot;)
}

// Exchange 交换机
type Exchange struct {
    Name       string                 // exchange名称
    Type       string                 // exchange类型，支持&quot;direct&quot;、&quot;topic&quot;、&quot;fanout&quot;、&quot;headers&quot;
    RoutingKey string                 // 路由key，如果类型为fanout和headers，此字段无效，不需要设置
    Headers    map[string]interface{} // 如果类型为headers，此字段必填
}

// NewHeadersExchange 创建一个header类型的交换机，headerType支持all和any
func NewHeadersExchange(exchangeName string, headerType string, kv map[string]interface{}) *Exchange {
    if kv == nil {
        kv = make(map[string]interface{})
    }

    switch headerType {
    case &quot;all&quot;, &quot;any&quot;:
        kv[&quot;x-match&quot;] = headerType
    default:
        kv[&quot;x-match&quot;] = &quot;all&quot;
    }

    return &amp;Exchange{
        Name:       exchangeName,
        Type:       &quot;headers&quot;,
        RoutingKey: &quot;&quot;,
        Headers:    kv,
    }
}

// Consumer 消费者
type Consumer struct {
    ctx       context.Context
    queueName string
    conn      *amqp.Connection
    ch        *amqp.Channel
    delivery  &lt;-chan amqp.Delivery
    exchange  *Exchange
}

// NewConsumer 实例化一个消费者
func NewConsumer(ctx context.Context, queueName string, exchange *Exchange, conn *amqp.Connection) (*Consumer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        exchange.Headers,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 为消息队列注册消费者
    delivery, err := ch.ConsumeWithContext(
        ctx,
        queueName, // queue 名称
        &quot;&quot;,        // consumer 用来区分多个消费者
        true,      // auto-ack 是否自动应答
        false,     // exclusive 是否独有
        false,     // no-local 如果设置为true，表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
        false,     // no-wait 是否阻塞
        nil,       // args
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Consumer{
        ctx:       ctx,
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        delivery:  delivery,
        exchange:  exchange,
    }, nil
}

// Consume 接收消息
func (c *Consumer) Consume() {
    go func() {
        fmt.Printf(&quot;waiting for messages, queue = %s\n&quot;, c.queueName)
        for d := range c.delivery {
            // 处理消息
            fmt.Printf(&quot;[%s received]: %s\n&quot;, c.queueName, d.Body)
            // _ = d.Ack(false) // 如果auto-ack为false时，需要手动ack
        }
    }()
}

// Close 关闭
func (c *Consumer) Close() {
    if c.ch != nil {
        _ = c.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_25">延时类型消息队列的golang示例代码</h3>

<p>rabbitMQ默认不支持延时消息队列类型，需要另外安装插件来实现，使用延时队列需要指定具体一种消息类型(direct、topic、fanout、headers)，下面以direct类型的延时消息队列为例。</p>

<h4 id="toc_26">生产端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;strconv&quot;
    &quot;time&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;delayed-message-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    queueName := &quot;delayed-message-queue&quot;
    routingKey := &quot;delayed-key&quot;
    delayedMessageType := &quot;direct&quot;
    exchange := NewDelayedMessageExchange(exchangeName, delayedMessageType, routingKey)
    q, err := NewProducer(queueName, conn, exchange)
    checkErr(err)
    defer q.Close()
    for i := 1; i &lt;= 5; i++ {
        body := time.Now().Format(&quot;2006-01-02 15:04:05.000&quot;) + &quot; hello world &quot; + strconv.Itoa(i)
        err = q.Publish(ctx, time.Second*5, []byte(body)) // 发送消息
        checkErr(err)
        time.Sleep(time.Second)
    }
}

// Exchange 交换机
type Exchange struct {
    Name                string // exchange名称
    Type                string // exchange类型，支持direct、topic、fanout、headers、x-delayed-message
    RoutingKey          string // 路由key
    XDelayedMessageType string // 延时消息类型，支持direct、topic、fanout、headers
}

// NewDelayedMessageExchange 实例化一个delayed-message类型交换机，参数delayedMessageType 消息类型direct、topic、fanout、headers
func NewDelayedMessageExchange(exchangeName string, delayedMessageType string, routingKey string) *Exchange {
    return &amp;Exchange{
        Name:                exchangeName,
        Type:                &quot;x-delayed-message&quot;,
        RoutingKey:          routingKey,
        XDelayedMessageType: delayedMessageType,
    }
}

// Producer 生产者对象
type Producer struct {
    queueName string
    exchange  *Exchange
    conn      *amqp.Connection
    ch        *amqp.Channel
}

// NewProducer 实例化一个生产者
func NewProducer(queueName string, conn *amqp.Connection, exchange *Exchange) (*Producer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, //  x-delayed-message
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        amqp.Table{
            &quot;x-delayed-type&quot;: exchange.XDelayedMessageType, // 延时消息的类型direct、topic、fanout、headers
        },
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Producer{
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        exchange:  exchange,
    }, nil
}

// Publish 发送消息
func (p *Producer) Publish(ctx context.Context, delayTime time.Duration, body []byte) error {
    err := p.ch.PublishWithContext(
        ctx,
        p.exchange.Name,       // exchange name
        p.exchange.RoutingKey, // key
        false,                 // mandatory 如果为true，根据自身exchange类型和routingKey规则无法找到符合条件的队列会把消息返还给发送者
        false,                 // immediate 如果为true，当exchange发送消息到队列后发现队列上没有消费者，则会把消息返还给发送者
        amqp.Publishing{
            DeliveryMode: amqp.Persistent, // 如果队列的声明是持久化的，那么消息也设置为持久化
            ContentType:  &quot;text/plain&quot;,
            Body:         body,
            Headers: amqp.Table{
                &quot;x-delay&quot;: int(delayTime / time.Millisecond), // 延迟时间: 毫秒
            },
        },
    )
    if err != nil {
        return err
    }
    fmt.Printf(&quot;[send]: %s\n&quot;, body)
    return nil
}

// Close 关闭生产者
func (p *Producer) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h4 id="toc_27">消费端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;
    &quot;time&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;delayed-message-exchange-demo&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    queueName := &quot;delayed-message-queue&quot;
    routingKey := &quot;delayed-key&quot;
    delayedMessageType := &quot;direct&quot;
    exchange := NewDelayedMessageExchange(exchangeName, delayedMessageType, routingKey)
    c, err := NewConsumer(ctx, queueName, exchange, conn)
    checkErr(err)
    c.Consume() // 消费消息
    defer c.Close()

    fmt.Println(&quot;exit press CTRL+C&quot;)
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    &lt;-interrupt
    fmt.Println(&quot;exit consume messages&quot;)
}

// Exchange 交换机
type Exchange struct {
    Name                string // exchange名称
    Type                string // exchange类型，支持direct、topic、fanout、headers、x-delayed-message
    RoutingKey          string // 路由key
    XDelayedMessageType string // 延时消息类型，支持direct、topic、fanout、headers
}

// NewDelayedMessageExchange 实例化一个delayed-message类型交换机，参数delayedMessageType 消息类型direct、topic、fanout、headers
func NewDelayedMessageExchange(exchangeName string, delayedMessageType string, routingKey string) *Exchange {
    return &amp;Exchange{
        Name:                exchangeName,
        Type:                &quot;x-delayed-message&quot;,
        RoutingKey:          routingKey,
        XDelayedMessageType: delayedMessageType,
    }
}

// Consumer 消费者
type Consumer struct {
    ctx       context.Context
    queueName string
    conn      *amqp.Connection
    ch        *amqp.Channel
    delivery  &lt;-chan amqp.Delivery
    exchange  *Exchange
}

// NewConsumer 实例化一个消费者
func NewConsumer(ctx context.Context, queueName string, exchange *Exchange, conn *amqp.Connection) (*Consumer, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchange.Name, // 交换机名称
        exchange.Type, // 交换机的类型，支持direct、topic、fanout、headers
        true,          // 是否持久化
        false,         // 是否自动删除
        false,         // 是否公开，false即公开
        false,         // 是否等待
        amqp.Table{
            &quot;x-delayed-type&quot;: exchange.XDelayedMessageType, // 延时消息的类型direct、topic、fanout、headers
        },
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,
        exchange.RoutingKey,
        exchange.Name,
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 为消息队列注册消费者
    delivery, err := ch.ConsumeWithContext(
        ctx,
        queueName, // queue 名称
        &quot;&quot;,        // consumer 用来区分多个消费者
        true,      // auto-ack 是否自动应答
        false,     // exclusive 是否独有
        false,     // no-local 如果设置为true，表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
        false,     // no-wait 是否阻塞
        nil,       // args
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Consumer{
        queueName: queueName,
        conn:      conn,
        ch:        ch,
        delivery:  delivery,
        exchange:  exchange,
    }, nil
}

// Consume 接收消息
func (c *Consumer) Consume() {
    go func() {
        fmt.Printf(&quot;waiting for messages, type=%s, queue=%s, key=%s\n&quot;, c.exchange.Type, c.queueName, c.exchange.RoutingKey)
        for d := range c.delivery {
            // 处理消息
            fmt.Printf(&quot;%s %s [received]: %s\n&quot;, time.Now().Format(&quot;2006-01-02 15:04:05.000&quot;), c.exchange.RoutingKey, d.Body)
            // _ = d.Ack(false) // 如果auto-ack为false时，需要手动ack
        }
    }()
}

// Close 关闭
func (c *Consumer) Close() {
    if c.ch != nil {
        _ = c.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_28">发布订阅的golang示例代码</h3>

<p>发布订阅是在fanout消息类型基础上实现的。</p>

<h4 id="toc_29">发布端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;strconv&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;pub-sub&quot;
)

func main() {
    // 连接rabbitmq
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    // 实例化一个发布者
    p, err := NewPublisher(exchangeName, conn)
    checkErr(err)
    defer p.Close()

    ctx := context.Background()

    // 发送消息
    for i := 1; i &lt;= 10; i++ {
        err = p.Publish(ctx, []byte(&quot;hello world &quot;+strconv.Itoa(i)))
        checkErr(err)
    }
}

// Publisher 发布者
type Publisher struct {
    exchangeName string
    conn         *amqp.Connection
    ch           *amqp.Channel
}

// NewPublisher 实例化一个发布者
func NewPublisher(exchangeName string, conn *amqp.Connection) (*Publisher, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchangeName, // 交换机名称
        &quot;fanout&quot;,     // 交换机的类型
        true,         // 是否持久化
        false,        // 是否自动删除
        false,        // 是否公开，false即公开
        false,        // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Publisher{
        exchangeName: exchangeName,
        conn:         conn,
        ch:           ch,
    }, nil
}

func (p *Publisher) Publish(ctx context.Context, body []byte) error {
    err := p.ch.PublishWithContext(
        ctx,
        p.exchangeName, // exchange name
        &quot;&quot;,             // 消息类型为fanout，此参数无效
        false,          // mandatory 如果为true，根据自身exchange类型和routingKey规则无法找到符合条件的队列会把消息返还给发送者
        false,          // immediate 如果为true，当exchange发送消息到队列后发现队列上没有消费者，则会把消息返还给发送者
        amqp.Publishing{
            ContentType: &quot;text/plain&quot;,
            Body:        body,
        },
    )
    if err != nil {
        return err
    }
    fmt.Printf(&quot;[send]: %s\n&quot;, body)
    return nil
}

// Close 关闭生产者
func (p *Publisher) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h4 id="toc_30">订阅端示例代码</h4>

<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;

    amqp &quot;github.com/rabbitmq/amqp091-go&quot;
)

var (
    url          = &quot;amqp://guest:guest@192.168.3.37:5672/&quot;
    exchangeName = &quot;pub-sub&quot;
)

func main() {
    conn, err := amqp.Dial(url)
    checkErr(err)
    defer conn.Close()

    ctx := context.Background()

    queueName1 := &quot;pub-sub-queue-1&quot;
    s1, err := NewSubscriber(ctx, exchangeName, queueName1, conn)
    checkErr(err)
    s1.Subscribe() // 消费信息
    defer s1.Close()

    queueName2 := &quot;pub-sub-queue-2&quot;
    s2, err := NewSubscriber(ctx, exchangeName, queueName2, conn)
    checkErr(err)
    s2.Subscribe() // 消费信息
    defer s2.Close()

    fmt.Println(&quot;exit press CTRL+C&quot;)
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    &lt;-interrupt
    fmt.Println(&quot;finished receiving messages&quot;)
}

// Subscriber 订阅者
type Subscriber struct {
    ctx          context.Context
    exchangeName string
    queueName    string
    conn         *amqp.Connection
    ch           *amqp.Channel
    delivery     &lt;-chan amqp.Delivery
}

// NewSubscriber 实例化一个订阅者
func NewSubscriber(ctx context.Context, exchangeName string, queueName string, conn *amqp.Connection) (*Subscriber, error) {
    // 创建管道
    ch, err := conn.Channel()
    if err != nil {
        return nil, err
    }

    // 声明交换机类型
    err = ch.ExchangeDeclare(
        exchangeName, // 交换机名称
        &quot;fanout&quot;,     // 交换机的类型
        true,         // 是否持久化
        false,        // 是否自动删除
        false,        // 是否公开，false即公开
        false,        // 是否等待
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 声明队列，如果队列不存在则自动创建，存在则跳过创建
    q, err := ch.QueueDeclare(
        queueName, // 消息队列名称
        true,      // 是否持久化
        false,     // 是否自动删除
        false,     // 是否具有排他性(仅创建它的程序才可用)
        false,     // 是否阻塞处理
        nil,       // 额外的属性
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 绑定队列和交换机
    err = ch.QueueBind(
        q.Name,       // 队列名称
        &quot;&quot;,           // 消息类型为fanout时无效
        exchangeName, // 交换机名称
        false,
        nil,
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    // 为消息队列注册消费者
    delivery, err := ch.ConsumeWithContext(
        ctx,
        queueName, // queue 名称
        &quot;&quot;,        // consumer 用来区分多个消费者
        true,      // auto-ack 是否自动应答
        false,     // exclusive 是否独有
        false,     // no-local 如果设置为true，表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
        false,     // no-wait 是否阻塞
        nil,       // args
    )
    if err != nil {
        _ = ch.Close()
        return nil, err
    }

    return &amp;Subscriber{
        ctx:          ctx,
        exchangeName: exchangeName,
        queueName:    queueName,
        conn:         conn,
        ch:           ch,
        delivery:     delivery,
    }, nil
}

// Subscribe 订阅
func (c *Subscriber) Subscribe() {
    go func() {
        fmt.Printf(&quot;waiting for messages, queue = %s\n&quot;, c.queueName)
        for d := range c.delivery {
            // 处理消息
            fmt.Printf(&quot;[%s received]: %s\n&quot;, c.queueName, d.Body)
            // _ = d.Ack(false) // 如果auto-ack为false时，需要手动ack
        }
    }()
}

// Close 关闭
func (p *Subscriber) Close() {
    if p.ch != nil {
        _ = p.ch.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}
</code></pre>

<p><br></p>

<h3 id="toc_31">死信队列队列</h3>

<h4 id="toc_32">什么是死信队列？</h4>

<p>&ldquo;死信&rdquo;(Dead Letter)是指消息在正常的消息队列中无法被成功消费或处理而被重新路由到另一个专门的队列中，这个专门的队列称为&rdquo;死信队列&rdquo;(Dead Letter Queue，DLQ)。死信队列用于存储那些未能被正常处理的消息，以便后续分析和处理。</p>

<p>以下是一些常见的导致消息变成死信的原因：</p>

<ol>
<li><strong>消息被拒绝(Rejection)</strong>：消费者处理消息时显式地拒绝了该消息(<code>Reject</code>或<code>Nack</code>)，并且没有要求将其重新入队。</li>
<li><strong>消息过期(TTL Expiration)</strong>：消息在队列中存储的时间超过了设置的TTL(声明队列时arg设置参数<code>x-message-ttl</code>，单位为毫秒)，即消息的存活时间。</li>
<li><strong>队列长度限制(Queue Length Limit)</strong>：队列的消息数量达到了预设的最大值(声明队列时arg设置参数<code>x-max-length</code>)，新的消息无法被加入到队列中。</li>
<li>消息返回到仲裁队列的次数超过了投递限制的次数。</li>
</ol>

<p><img src="https://go-sponge.com/assets/images/blog/dead-letter.png" alt="dead-letter" /></p>

<h4 id="toc_33">如何设置死信队列</h4>

<p>在RabbitMQ中，可以通过设置队列的参数(args)来指定死信队列。主要涉及以下几个参数：</p>

<ul>
<li><code>x-dead-letter-exchange</code>：指定死信消息要发送到的交换机。</li>
<li><code>x-dead-letter-routing-key</code>：指定死信消息的路由键（可选）。</li>
</ul>

<p>在go语言设置死信队列 <a href="https://github.com/zhufuyi/sponge/blob/main/pkg/rabbitmq/README.md#example-of-dead-letter" rel="nofollow">示例</a>。</p>

<h4 id="toc_34">使用场景</h4>

<ul>
<li><strong>错误处理</strong>：将无法处理的消息转移到死信队列，可以进行后续分析和重试。</li>
<li><strong>消息监控</strong>：监控死信队列中的消息，及时发现和处理异常情况。</li>
<li><strong>延迟队列</strong>：通过TTL和死信队列的组合，可以实现消息的延迟投递。</li>
</ul>

<p>通过合理配置和使用死信队列，可以提高系统的可靠性和可维护性。</p>

<p><br></p>

<h3 id="toc_35">注意事项</h3>

<ol>
<li><p>设置生产者的的queue的args与消费者queue的args必须一致，否则会报错 <code>Exception (406) Reason: \&quot;PRECONDITION_FAILED - inequivalent arg ......</code></p></li>

<li><p>对于已经设置的queue，如果后面添加args，会报错 <code>报错Exception (504) Reason: &quot;channel/connection is not open&quot;</code></p></li>
</ol>

<p><br></p>

<h3 id="toc_36">总结</h3>

<p>上面介绍了rabbitMQ各个类型消息队列的简单使用，在实际使用中，连接rabbitMQ应该有网络断开重连功能。如果消费端处理消息比较慢，在消费端设置channel.Qos来限制每次消费消息的数量，平衡消息吞吐量和公平性，防止消费者受到消息突发流量冲击。例如设置prefetch_count=1，则表示每个消费者每次只会处理1条消息，处理完成后才会获取下一条消息。这可以防止少数消费者处理能力弱导致大量消息堆积。 适当地设置这些参数,可以优化rabbitMQ在大量消息场景下的性能表现。</p>

<p>这是在<code>github.com/rabbitmq/amqp091-go</code>基础上封装的 <a href="https://github.com/zhufuyi/sponge/tree/main/pkg/rabbitmq" rel="nofollow">rabbitmq</a> 库。</p>

<p><br></p>

<p>参考：</p>

<ul>
<li><a href="https://www.rabbitmq.com" rel="nofollow">https://www.rabbitmq.com</a></li>
</ul>
<p>本文链接：<a href="https://zhuyasen.com/post/rabbitmq.html">https://zhuyasen.com/post/rabbitmq.html</a>，<a href="https://zhuyasen.com/post/rabbitmq.html#comments">参与评论 »</a></p>]]>
            </description>
        </item>
        
    </channel>
</rss>
