<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>SSpiritsの秘密基地</title><link>https://blog.lv5.moe/</link><description>Recent content on SSpiritsの秘密基地</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 01 May 2025 11:02:00 +0800</lastBuildDate><atom:link href="https://blog.lv5.moe/index.xml" rel="self" type="application/rss+xml"/><item><title>网络抓包分析案例：SNI 阻断</title><link>https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis</link><pubDate>Thu, 01 May 2025 11:02:00 +0800</pubDate><guid>https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis</guid><description>&lt;img src="https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis/cover.jpg" alt="Featured image of post 网络抓包分析案例：SNI 阻断" />&lt;h2 id="背景">背景&lt;/h2>
&lt;p>某客户反馈其部分终端设备无法访问我方服务，故障现象呈现设备选择性断连的特征，需要我们协助排查。好在该问题可以稳定复现，所以我们请客户在测试环境中复现问题并进行抓包，于是便开始了本次排查之旅&lt;/p>
&lt;h2 id="mtls-认证排查">mTLS 认证排查&lt;/h2>
&lt;p>因为客户使用 mTLS 进行认证和加密连接，并且只有部分设备出现问题。所以优先怀疑设备客户端证书非法或者过期。我们的服务端实现了对客户端证书链的校验逻辑，在证书校验不通过时会记录审计日志。所以我们请客户在测试环境中复现问题并进行抓包，希望能够拿到用户设备使用的证书以便于我们进行排查：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 196;
flex-basis: 472px"
>
&lt;a href="https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis/client-packet-capture.jpg" data-size="3128x1590">
&lt;img src="https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis/client-packet-capture.jpg"
width="3128"
height="1590"
loading="lazy"
alt="设备侧网络抓包">
&lt;/a>
&lt;figcaption>设备侧网络抓包&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>看到这个抓包文件就发现事情并不这么简单：报文中并没有证书交换的过程，只有一个 &lt;code>Client Hello&lt;/code> 报文，然后收到了 RST 报文导致连接重置。这说明服务端在没有接收到客户端证书的情况下断开了连接，我们在服务端的审计日志中没有在用户抓包时段看到任何证书校验失败的记录也佐证了这点&lt;/p>
&lt;p>由于应用层无有效信息，需要进一步在服务端执行网络抓包以定位问题&lt;/p>
&lt;h2 id="网络链路分析">网络链路分析&lt;/h2>
&lt;p>在开始服务端抓包之前首先需要梳理一下网络链路：客户的设备通过公网访问我们的服务，用户侧没有网关设备。但是在服务侧有接入网关和负载均衡器，用户的设备通过自定义的域名 CNAME 解析到我们的接入网关，接入网关会将请求转发到应用服务器。整个链路大致如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">用户终端设备
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | 公网
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">公网接入网关
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | 内网
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">L4 负载均衡器
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> | 内网
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">应用服务器
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>直觉上怀疑用户网络环境上的 DNS 配置有问题，导致流量没有打到我们服务的接入网关上，而是被劫持到其他地方。但是通过对比用户侧正常的设备和异常的设备的抓包，发现 DNS 解析的结果是一样的，说明 DNS 解析没有问题&lt;/p>
&lt;p>接下来我们在应用服务器上进行抓包，验证客户设备的请求是否正常抵达，以排除网络路由的问题&lt;/p>
&lt;h2 id="抓包分析">抓包分析&lt;/h2>
&lt;p>通过和用户约定时间同时抓包发现，确实有请求到达了我们的应用服务器：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 318;
flex-basis: 764px"
>
&lt;a href="https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis/server-packet-capture.jpg" data-size="3448x1082">
&lt;img src="https://blog.lv5.moe/p/decoding-sni-blocking-through-packet-capture-analysis/server-packet-capture.jpg"
width="3448"
height="1082"
loading="lazy"
alt="服务端网络抓包">
&lt;/a>
&lt;figcaption>服务端网络抓包&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>但是对比客户端抓包，并没有出现 &lt;code>Client Hello&lt;/code> 报文，并且更奇怪的是服务端抓包显示 RST 报文来自对端！也就是说客户端和服务端均认为对方发了 RST 报文，那么大概率就是“中间人”同时向两侧发的 RST 报文&lt;/p>
&lt;p>那么怎么快速判断所谓的“中间人”是哪一侧的网络设备呢？这里有个小技巧，可以通过 TTL 来判断：&lt;/p>
&lt;p>服务端的流量记录：收到了三个来自客户端的报文&lt;/p>
&lt;ul>
&lt;li>SYN：TTL=52&lt;/li>
&lt;li>ACK：TTL=52&lt;/li>
&lt;li>RST：TTL=52&lt;/li>
&lt;/ul>
&lt;p>客户端的流量记录：这里客户端收到了三个来自服务端的报文&lt;/p>
&lt;ul>
&lt;li>SYN, ACK：TTL=50&lt;/li>
&lt;li>RST：TTL=64&lt;/li>
&lt;/ul>
&lt;p>可以看到客户端收到的报文的 TTL 值改变了，这说明客户端收到的 RST 报文不是服务端发的，而且相比于服务端的 TTL 值更大。可以推测出这个 RST 报文可能是来自于靠近用户侧的网络设备发出&lt;/p>
&lt;h2 id="结论">结论&lt;/h2>
&lt;p>考虑到用户使用公网自定义域名访问，并且在发出 &lt;code>Client Hello&lt;/code> 报文后就被重置连接，怀疑是遭遇了 SNI 阻断。向用户反馈了这个情况后，用户和他们的运营商进行了联系发现是运营商一侧的网络配置问题导致，本次事件也算是圆满解决了&lt;/p></description></item><item><title>MCP with GraphQL —— LLM 大模型高效访问异构数据源</title><link>https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources</link><pubDate>Sat, 29 Mar 2025 23:25:00 +0800</pubDate><guid>https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources</guid><description>&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/cover.jpg" alt="Featured image of post MCP with GraphQL —— LLM 大模型高效访问异构数据源" />&lt;p>在 RocketMQ 的日常运维中，我们经常需要访问不同的数据源来获取诊断信息。传统的方式往往需要编写复杂的查询语句，或者在多个系统之间切换，效率低下&lt;/p>
&lt;p>为了解决这个问题，我们使用 LLM 结合 MCP 来实现高效的数据访问。通过这种方式，可以用&lt;strong>自然语言一站式查询&lt;/strong>所需的信息，提高运维效率&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/operation-system-vs-llm.jpg" width="651" height="533"/>&lt;figcaption>
&lt;h4>传统方法对比大模型&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>在 RocketMQ 的日常运维中，我们经常需要访问不同的数据源来获取诊断信息。下图是我们诊断问题时需要访问的常见数据源和他们提供的查询接口：&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">flowchart TD
subgraph Observability
direction LR
Metrics[&amp;#34;Metrics &amp;lt;br&amp;gt;(WebUI/PromQL)&amp;#34;]
Logs[&amp;#34;Logs &amp;lt;br&amp;gt;(WebUI/SQL)&amp;#34;]
Trace[&amp;#34;Trace &amp;lt;br&amp;gt;(WebUI/gRPC)&amp;#34;]
end
subgraph External Storage
direction LR
ObjectStorage[&amp;#34;Object Storage &amp;lt;br&amp;gt;(WebUI/S3 API)&amp;#34;]
DB[&amp;#34;Database &amp;lt;br&amp;gt;(WebUI/SQL)&amp;#34;]
ConfigCenter[&amp;#34;Config Center &amp;lt;br&amp;gt;(WebUI/REST API)&amp;#34;]
end
subgraph Runtime Data
direction LR
NameServer[&amp;#34;RocketMQ NameServer &amp;lt;br&amp;gt;(CLI/Binary Protocol)&amp;#34;]
Broker[&amp;#34;RocketMQ Broker &amp;lt;br&amp;gt;(CLI/Binary Protocol)&amp;#34;]
end
subgraph IaaS Data
direction LR
Kubnetes[&amp;#34;Kubernetes &amp;lt;br&amp;gt;(WebUI/K8S API)&amp;#34;]
end
&lt;/div>
&lt;p>一般来说运维人员如果想要获取某个数据源的信息，通常需要先登录到对应的系统，然后编写查询语句，最后再解析返回的结果。这不但需要运维人员熟悉每个数据源的查询语法，还需要在不同的系统之间切换，效率低下&lt;/p>
&lt;p>聪明的团队会开发一个“运营系统”，提供统一的查询界面。但是仍然没有解决反复切换工具，查询碎片化的问题；更进一步可以将常见的排查过程中用到的工具串联到一起，形成一个“查询流水线”。但是这样的系统往往需要排查问题的专家经验沉淀、大量的开发和维护工作，且难以适应快速变化的需求&lt;/p>
&lt;p>为了解决这个问题，我们可以使用 LLM 结合 MCP 来实现高效的数据访问。通过这种方式，用&lt;strong>自然语言一站式查询&lt;/strong>所需的信息，提高问题诊断的效率&lt;/p>
&lt;h2 id="talk-is-cheap-show-me-the-demo">Talk is cheap, show me the demo!&lt;/h2>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/llm-workflow.jpg" width="770" height="305"/>&lt;figcaption>
&lt;h4>LLM Workflow&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>借助 LLM + Chatbox + MCP + GraphQL 的组合，用自然语言查询 RocketMQ 集群的状态、Topic 的信息、消息的内容等&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 44;
flex-basis: 107px"
>
&lt;a href="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/rocketmq-mcp-cluster.jpg" data-size="1923x4302">
&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/rocketmq-mcp-cluster.jpg"
width="1923"
height="4302"
loading="lazy"
alt="查询集群和节点">
&lt;/a>
&lt;figcaption>查询集群和节点&lt;/figcaption>
&lt;/figure>&lt;figure
class="gallery-image"
style="
flex-grow: 79;
flex-basis: 191px"
>
&lt;a href="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/rocketmq-mcp-topic.jpg" data-size="1632x2048">
&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/rocketmq-mcp-topic.jpg"
width="1632"
height="2048"
loading="lazy"
alt="查询 Topic">
&lt;/a>
&lt;figcaption>查询 Topic&lt;/figcaption>
&lt;/figure>&lt;figure
class="gallery-image"
style="
flex-grow: 74;
flex-basis: 178px"
>
&lt;a href="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/rocketmq-mcp-message.jpg" data-size="1524x2048">
&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/rocketmq-mcp-message.jpg"
width="1524"
height="2048"
loading="lazy"
alt="查询消息">
&lt;/a>
&lt;figcaption>查询消息&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>Awesome！理论上只要是在我们系统内的信息，都可以通过&lt;strong>一次自然语言交互&lt;/strong>查出来，并且可以连续追问直到找到问题根因。再也不需要访问一大堆服务，编辑命令行参数/SQL 或者在前端界面之间跳来跳去了&lt;/p>
&lt;h2 id="实现原理">实现原理&lt;/h2>
&lt;h3 id="llm-大语言模型">LLM 大语言模型&lt;/h3>
&lt;p>在这个组合（LLM + Chatbox + MCP + GraphQL）中，LLM 充当了自然语言处理的核心组件。用户通过 Chatbox 输入自然语言查询，LLM 将其转换为 GraphQL 查询语句，并通过 MCP （大模型调用工具的协议，这里可以认为是 GraphQL 客户端）提交到后端服务。后端服务返回的 JSON 格式查询结果又会被 LLM 转换为人类更易懂的格式，从而实现了自然语言与数据源之间的高效交互&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 176;
flex-basis: 423px"
>
&lt;a href="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/llm-example.jpg" data-size="1610x912">
&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/llm-example.jpg"
width="1610"
height="912"
loading="lazy"
alt="Chatbox 中的对话">
&lt;/a>
&lt;figcaption>Chatbox 中的对话&lt;/figcaption>
&lt;/figure>&lt;figure
class="gallery-image"
style="
flex-grow: 178;
flex-basis: 427px"
>
&lt;a href="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/graphql-example.jpg" data-size="1798x1010">
&lt;img src="https://blog.lv5.moe/p/mcp-with-graphql-llm-access-heterogeneous-data-sources/graphql-example.jpg"
width="1798"
height="1010"
loading="lazy"
alt="对应的 GraphQL 查询和结果">
&lt;/a>
&lt;figcaption>对应的 GraphQL 查询和结果&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock ">
&lt;p>图中的 introspect-schema 用来获取 GraphQL 的 schema 信息，帮助 LLM 理解数据结构和字段&lt;/p>
&lt;p>实际上只需要在开始对话的时候调用一次即可，这里每次交互都调用是因为博主使用的 Chatbox 没有正确使用 MCP 协议，换成支持 &lt;a class="link" href="https://modelcontextprotocol.io/docs/concepts/resources" target="_blank" rel="noopener"
>MCP Resources&lt;/a> 的工具例如 Claude Desktop 就可以避免多次请求 schema&lt;/p>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>通过 LLM 来组织查询相比于传统的运营系统，可以节约开发大量页面的成本，能够按需获取想要查询的字段，并且自适应数据结构的演进。为了最大化利用 LLM 的查询灵活性，我们引入了 GraphQL 作为数据查询的中间层&lt;/p>
&lt;h3 id="道理我都懂为什么是-graphql">道理我都懂，为什么是 GraphQL？&lt;/h3>
&lt;p>GraphQL 是一种用于提供 API 的查询语言，它允许&lt;strong>客户端指定&lt;/strong>所需的数据结构和字段。我们使用 GraphQL 作为连接 LLM 和所有数据源的桥梁： LLM 使用 GraphQL 描述要查询的数据，在一次查询中访问多种数据源&lt;/p>
&lt;p>回忆一下本文开头列出的多种数据源，我们取其中 Runtime Data 中的 RocketMQ Broker 数据源为例，它的数据模型大致如下所示：&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">flowchart TD
subgraph Cluster
direction LR
Broker_One --&amp;gt;|包含| Topic_A
Broker_One --&amp;gt;|包含| Topic_B
Broker_One --&amp;gt;|包含| Group_X
subgraph Topic
direction LR
Topic_A --&amp;gt;|包含| Queue_A1
Topic_A --&amp;gt;|包含| Queue_A2
Topic_B --&amp;gt;|包含| Queue_B1
Queue_A1 --&amp;gt;|包含| Message_A1
Queue_A2 --&amp;gt;|包含| Message_A2
Queue_B1 --&amp;gt;|包含| Message_B1
end
subgraph Group
direction LR
Group_X --&amp;gt;|包含| Consumer_A
Group_X --&amp;gt;|包含| Consumer_B
Consumer_A --&amp;gt;|消费| Message_A1
Consumer_B --&amp;gt;|消费| Message_B1
Consumer_B --&amp;gt;|消费| Message_A2
end
end
&lt;/div>
&lt;p>我们将这个数据模型组织成一个树状的结构，GraphQL 的查询语法非常适合这种树状结构的查询：可以通过 GraphQL 的嵌套查询来获取 Broker、Topic、Queue 和 Message 之间的关系&lt;/p>
&lt;p>例如，我们可以通过以下的 GraphQL 查询语句来获取 Broker_One 中 Topic_A 的队列 0 的相关信息：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-graphql" data-lang="graphql">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">query&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询 Broker节点 Broker_One&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nc">brokers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Broker_One&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询该 Broker 节点的接入点&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nc">addr&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询 Broker 节点的配置项 messageIndexEnable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">config&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;messageIndexEnable&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nc">name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">value&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询该 Broker 节点的 Topic 信息：Topic_A&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">topics&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;Topic_A&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询该 Topic 的队列 0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nc">queues&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nc">0&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询该队列的消息数（maxOffset - minOffset）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">minOffset&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">maxOffset&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 查询该队列的第 100 条消息&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">messages&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="py">offset&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nc">100&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="py">payload&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个查询会被翻译成对 RocketMQ Broker 数据源以下接口的调用：&lt;/p>
&lt;ol>
&lt;li>Broker 信息（接入点）&lt;/li>
&lt;li>Broker 配置项（messageIndexEnable）&lt;/li>
&lt;li>Topic 信息（队列数）&lt;/li>
&lt;li>Topic 队列的信息（minOffset、maxOffset）&lt;/li>
&lt;li>Topic 的消息（该队列第 100 条消息）&lt;/li>
&lt;/ol>
&lt;p>也就是说，我们通过 GraphQL 的嵌套查询语法将 5 个查询组合在一起。上面只是一个简单的例子，实际上可以组合面向不同数据源的查询。不管他们提供的是 REST API 还是 Binary Protocol 都可以合并到一个 GraphQL 查询中。对于 LLM 来说这样做尤其有意义：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>简化开发&lt;/strong>：不需要为每种数据源编写单独的 MCP Server，统一使用 GraphQL 作为数据查询的中间层，用 GraphQL 的 schema 来帮助 LLM 理解数据结构&lt;/li>
&lt;li>&lt;strong>简化查询&lt;/strong>：LLM 只需要理解 GraphQL 的查询语法，而不需要了解每个数据源的具体实现细节。对于复杂的查询可以有效降低 LLM 的理解难度，减少出错的概率&lt;/li>
&lt;li>&lt;strong>减少查询次数&lt;/strong>：通过一次 GraphQL 查询，可以获取多个数据源的信息，减少了多次 LLM 调用的 Token 开销&lt;/li>
&lt;li>&lt;strong>降低上下文开销&lt;/strong>：不需要组织多次查询，也不需要输入多种 MCP tools 的参数和用法。可以将 LLM 有限的上下文用于描述问题的本质&lt;/li>
&lt;li>&lt;strong>灵活变更数据结构&lt;/strong>：GraphQL 自带 schema，对于 LLM 来说可以通过 schema 来获取数据的字段和不同数据结构之间的关系。简而言之我们提供的查询接口是自描述的，可以随时增加新的数据结构，LLM 也能自动适应&lt;/li>
&lt;/ul>
&lt;h2 id="总结与后续展望">总结与后续展望&lt;/h2>
&lt;p>通过 LLM + Chatbox + MCP + GraphQL 的组合，我们实现了对多个异构数据源的高效查询。用户不需要具备大量的排查经验和对系统的深入理解，只需要用自然语言描述所需的信息，系统就能自动生成查询语句并返回结果。这种方式不仅提高了排查问题效率，还降低了对运维人员的技术要求&lt;/p>
&lt;p>目前我们实现了 LLM 高效访问数据，并且通过 GraphQL 的 schema 能够理解多种数据结构之间的关联。下一步我们将继续迭代这个系统，将我们问题排查的专家经验以知识库的形式输入到 LLM 中，帮助 LLM 不仅能理解数据，更能理解问题的本质。最终实现一个一站式问题排查系统，让 LLM 能够自动化地完成问题排查的工作&lt;/p></description></item><item><title>Java 应用内存占用异常排查思路</title><link>https://blog.lv5.moe/p/java-application-memory-allocation-troubleshooting</link><pubDate>Thu, 23 Nov 2023 16:00:00 +0800</pubDate><guid>https://blog.lv5.moe/p/java-application-memory-allocation-troubleshooting</guid><description>&lt;img src="https://blog.lv5.moe/img/67003464_p0.jpg" alt="Featured image of post Java 应用内存占用异常排查思路" />&lt;p>本文用一个线上问题的排查过程来介绍 Java 应用的内存管理，以及 Linux 内存分析工具的使用，供读者排查 Java 应用的内存泄露和 OOM 问题时参考&lt;/p>
&lt;h2 id="前情提要">前情提要&lt;/h2>
&lt;p>博主自信满满压测一晚，结果隔天早上查看监控发现 Java 进程占用内存远超预期，于是便开始了本文艰辛的排查之旅&lt;/p>
&lt;p>这个场景的技术栈是 Java 17 + ZGC，但是本文介绍的方法也适用于排查其他版本 Java 应用的内存泄露和 OOM 问题&lt;/p>
&lt;p>我们知道 Java 应用的内存可以分为三种：&lt;/p>
&lt;ol>
&lt;li>堆内存：Java 对象分配的空间&lt;/li>
&lt;li>堆外内存：方法区、线程栈、Direct Buffer 等&lt;/li>
&lt;li>非 JVM 内存：native library 分配的内存&lt;/li>
&lt;/ol>
&lt;p>博主的应用使用了 NIO ByteBuffer + Netty + RocksDB 可谓是五毒俱全，所以我们需要先确认内存泄露发生在哪个内存区域&lt;/p>
&lt;h2 id="分析堆内存溢出">分析堆内存溢出&lt;/h2>
&lt;p>堆内存泄露问题最容易分析：设置 VM 参数 &lt;code>-XX:+HeapDumpOnOutOfMemoryError&lt;/code> 在应用崩溃时 dump 堆内存，或者使用 &lt;code>jmap -dump:format=b,file=heap.bin &amp;lt;pid&amp;gt;&lt;/code> 手动 dump 堆内存。然后通过 MAT/JProfiler 等工具对大对象进行引用分析即可得知内存泄露的原因&lt;/p>
&lt;p>不幸的是博主的应用没有生成堆转储文件，通过 JVM 监控也可以得知在运行期间，堆内存使用率几乎没有增长，那么泄漏必然是在堆外发生&lt;/p>
&lt;h2 id="分析堆外内存溢出">分析堆外内存溢出&lt;/h2>
&lt;p>方法区、线程栈导致的堆外内存溢出会导致 JVM 崩溃并在运行目录下产生 &lt;code>hs_err_pid&amp;lt;pid&amp;gt;.log&lt;/code> 文件，查看报错的线程栈可以分析出 VM 参数设置不合理或开启线程过多等原因&lt;/p>
&lt;p>另外一种堆外内存是 DirectByteBuffer / FileChannel.map 等分配的内存，这部分内存可以使用 &lt;code>-XX:MaxDirectMemorySize=size&lt;/code> 限制，但是这个选项只会影响到 java.nio 包下的内存分配，详见 JDK 的文档：&lt;/p>
&lt;blockquote>
&lt;p>-XX:MaxDirectMemorySize=size&lt;/p>
&lt;p>Sets the maximum total size (in bytes) of the java.nio package, direct-buffer allocations. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, or g or G to indicate gigabytes. By default, the size is set to 0, meaning that the JVM chooses the size for NIO direct-buffer allocations automatically.&lt;/p>
&lt;/blockquote>
&lt;p>博主应用的缓存池使用了 Netty 提供的 ByteBuf，底层正是使用 java.nio.DirectByteBuffer&lt;/p>
&lt;p>堆外内存也受 JVM 管控，可以使用 JVM 提供的 Native Memory Tracking 来分析。添加 VM 参数 &lt;code>-XX:NativeMemoryTracking=[off | summary | detail]&lt;/code> 来开启 NMT，重启 JVM 后即可使用 &lt;code>jcmd&lt;/code> 工具来查看 NMT 数据：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">jcmd &amp;lt;pid&amp;gt; VM.native_memory &lt;span class="o">[&lt;/span>summary &lt;span class="p">|&lt;/span> detail &lt;span class="p">|&lt;/span> baseline &lt;span class="p">|&lt;/span> summary.diff &lt;span class="p">|&lt;/span> detail.diff &lt;span class="p">|&lt;/span> shutdown&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>&lt;span class="nv">scale&lt;/span>&lt;span class="o">=&lt;/span> KB &lt;span class="p">|&lt;/span> MB &lt;span class="p">|&lt;/span> GB&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>NMT 会输出类似下面的报告：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">jcmd &lt;span class="m">1713702&lt;/span> VM.native_memory summary &lt;span class="nv">scale&lt;/span>&lt;span class="o">=&lt;/span>MB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1713702:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Native Memory Tracking:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">(&lt;/span>Omitting categories weighting less than 1MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Total: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>207768MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>6213MB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> malloc: 1823MB &lt;span class="c1">#670978&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>205945MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>4390MB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Java Heap &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>196608MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>4096MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>196608MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>4096MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Class &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>260MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>14MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>classes &lt;span class="c1">#17028)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> instance classes &lt;span class="c1">#16123, array classes #905)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>4MB &lt;span class="c1">#74947) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>256MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>11MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> Metadata: &lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>128MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>82MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> &lt;span class="nv">used&lt;/span>&lt;span class="o">=&lt;/span>82MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> &lt;span class="nv">waste&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">0MB&lt;/span> &lt;span class="o">=&lt;/span>0.43%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> Class space:&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>256MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>11MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> &lt;span class="nv">used&lt;/span>&lt;span class="o">=&lt;/span>10MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span> &lt;span class="nv">waste&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">0MB&lt;/span> &lt;span class="o">=&lt;/span>4.21%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Thread &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>472MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>50MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>thread &lt;span class="c1">#472)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>stack: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>471MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>49MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>1MB &lt;span class="c1">#2839) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">arena&lt;/span>&lt;span class="o">=&lt;/span>1MB &lt;span class="c1">#941)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Code &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>248MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>83MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>6MB &lt;span class="c1">#23199) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>242MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>77MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- GC &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>8336MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>176MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>112MB &lt;span class="c1">#48322) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>8224MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>64MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Compiler &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>4MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>4MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>4MB &lt;span class="c1">#2150) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Internal &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>6MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>6MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>6MB &lt;span class="c1">#57737) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Other &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>1658MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>1658MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>1658MB &lt;span class="c1">#613) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Symbol &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>18MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>18MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>16MB &lt;span class="c1">#432773) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">arena&lt;/span>&lt;span class="o">=&lt;/span>2MB &lt;span class="c1">#1)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Native Memory Tracking &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>11MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>11MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>1MB &lt;span class="c1">#7935) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>tracking &lt;span class="nv">overhead&lt;/span>&lt;span class="o">=&lt;/span>10MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Shared class space &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>16MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>12MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>16MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>12MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Serviceability &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>1MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>1MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>&lt;span class="nv">malloc&lt;/span>&lt;span class="o">=&lt;/span>1MB &lt;span class="c1">#14544) &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Metaspace &lt;span class="o">(&lt;/span>&lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>128MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>83MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">(&lt;/span>mmap: &lt;span class="nv">reserved&lt;/span>&lt;span class="o">=&lt;/span>128MB, &lt;span class="nv">committed&lt;/span>&lt;span class="o">=&lt;/span>82MB&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Java 11 之后 DirectByteBuffer 使用的内存被归入 Other 中（之前是 Internal），我们需要关注的是其中的 committed 部分，代表了真实使用的物理内存。这里可以发现 DirectByteBuffer 只占用了 1658MB 内存，并不是发生内存泄露的原因&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
根据 JDK 和操作系统版本的不同，committed 的数值可能会大于等于操作系统计算的 RSS。具体原因和修复方案可以参考 &lt;a class="link" href="https://bugs.openjdk.org/browse/JDK-8191369" target="_blank" rel="noopener"
>JDK-8191369&lt;/a> 和 &lt;a class="link" href="https://bugs.openjdk.org/browse/JDK-8249666" target="_blank" rel="noopener"
>JDK-8249666&lt;/a>
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="分析非-jvm-内存溢出">分析非 JVM 内存溢出&lt;/h2>
&lt;p>通过上述排查流程，我们已经确认了泄露的内存并不受 JVM 的管控，这就需要深入到对操作系统内存分配情况的分析&lt;/p>
&lt;h3 id="换用-jemalloc">换用 jemalloc&lt;/h3>
&lt;p>Java 默认使用 glibc 的 malloc，有时会出现碎片问题，可以使用 jemalloc 替代并开启分析功能：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">apt install libjemalloc-dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">LD_PRELOAD&lt;/span>&lt;span class="o">=&lt;/span>/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 每分配 128K 内存记录堆栈信息，每分配 1GB 内存输出到文件&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">MALLOC_CONF&lt;/span>&lt;span class="o">=&lt;/span>prof:true,lg_prof_interval:30,lg_prof_sample:17
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>重启应用后在运行目录下会生成类似 &lt;code>jeprof.&amp;lt;pid&amp;gt;.0.i0.heap&lt;/code> 的文件，我们可以使用 &lt;code>jeprof&lt;/code> 来输出内存分配情况&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">jeprof --svg &lt;span class="sb">`&lt;/span>which java&lt;span class="sb">`&lt;/span> jeprof*.heap &amp;gt; jeprof.svg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;figure>&lt;img src="https://blog.lv5.moe/p/java-application-memory-allocation-troubleshooting/jemalloc-prof.webp" width="669px" height="645px"/>&lt;figcaption>
&lt;h4>Jemalloc Prof&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>这里看到分配内存占比 89% 的函数是 &lt;code>Unsafe_AllocateMemory0&lt;/code>。但是 jemalloc 不能进一步分析 java 虚拟机的堆栈，我们需要进一步配合 &lt;a class="link" href="https://github.com/async-profiler/async-profiler" target="_blank" rel="noopener"
>async-profler&lt;/a> 生成火焰图：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 可以将 Unsafe_AllocateMemory0 换成其他任何想观测的函数名&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./profiler.sh -d &amp;lt;duration&amp;gt; -e Unsafe_AllocateMemory0 -f unsafe_allocate.html &amp;lt;pid&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 173;
flex-basis: 417px"
>
&lt;a href="https://blog.lv5.moe/p/java-application-memory-allocation-troubleshooting/async-profiler.webp" data-size="720x414">
&lt;img src="https://blog.lv5.moe/p/java-application-memory-allocation-troubleshooting/async-profiler.webp"
width="720"
height="414"
loading="lazy"
alt="Flame Graph">
&lt;/a>
&lt;figcaption>Flame Graph&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>生成的火焰图中可以看到 Unsafe_AllocateMemory0 分配的内存实际上是 DirectByteBuffer 使用的，这和 NMT 的报告中显示的占用量基本一致，并不是导致内存占用异常的元凶&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Other (reserved=1658MB, committed=1658MB)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (malloc=1658MB #613)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="分析内存分布">分析内存分布&lt;/h3>
&lt;p>既然应用运行中没有明显的内存泄露，那么就需要看下消耗的内存用在了哪里：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">cat /proc/2031108/status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name: java
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RssAnon: &lt;span class="m">805844&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RssFile: &lt;span class="m">38036&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RssShmem: &lt;span class="m">12582912&lt;/span> kB
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>占用内存最多的 RssShmem 有 12G，这是个很不同寻常的情况，一般来说 page cache 以及 mmap 进行文件映射都会算到 RssFile 上。接下来需要进一步找到这 12G 的内存用在了哪里，我们可以使用 &lt;code>pmap&lt;/code> 来查看内存分布：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">pmap -x &lt;span class="m">2031108&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address Kbytes RSS Dirty Mode Mapping
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000040000000000&lt;/span> &lt;span class="m">4194304&lt;/span> &lt;span class="m">4194304&lt;/span> &lt;span class="m">4194304&lt;/span> rw-s- memfd:java_heap &lt;span class="o">(&lt;/span>deleted&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000040100000000&lt;/span> &lt;span class="m">62914560&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="m">0&lt;/span> ----- &lt;span class="o">[&lt;/span> anon &lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000080000000000&lt;/span> &lt;span class="m">4194304&lt;/span> &lt;span class="m">4194304&lt;/span> &lt;span class="m">4194304&lt;/span> rw-s- memfd:java_heap &lt;span class="o">(&lt;/span>deleted&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000080100000000&lt;/span> &lt;span class="m">62914560&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="m">0&lt;/span> ----- &lt;span class="o">[&lt;/span> anon &lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000100000000000&lt;/span> &lt;span class="m">4194304&lt;/span> &lt;span class="m">4194304&lt;/span> &lt;span class="m">4194304&lt;/span> rw-s- memfd:java_heap &lt;span class="o">(&lt;/span>deleted&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000100100000000&lt;/span> &lt;span class="m">62914560&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="m">0&lt;/span> ----- &lt;span class="o">[&lt;/span> anon &lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>很容易发现疑似的内存区域，java 堆被映射到了三个虚拟内存地址上：40000000000、80000000000、100000000000。博主应用的堆大小设置为 4G，理论上映射了三次就产生了 12G 的 RssShmem 占用。为了验证这个猜想接下来 dump 这三段内存比较其中的内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">dd &lt;span class="k">if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/proc/2031108/mem&amp;#34;&lt;/span> &lt;span class="nv">of&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/dev/stdout&amp;#34;&lt;/span> &lt;span class="nv">bs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="nv">skip&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span>&lt;span class="m">0&lt;/span>x40000000000&lt;span class="k">))&lt;/span> &lt;span class="nv">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">128&lt;/span> &lt;span class="p">|&lt;/span> hexdump
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records in
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records out
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">128&lt;/span> bytes copied, 0.000292939 s, &lt;span class="m">437&lt;/span> kB/s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000000&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0003&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000010&lt;/span> e938 &lt;span class="m">0005&lt;/span> &lt;span class="m">0400&lt;/span> &lt;span class="m">0000&lt;/span> e970 &lt;span class="m">0005&lt;/span> &lt;span class="m">0400&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000020&lt;/span> e9c0 &lt;span class="m">0005&lt;/span> &lt;span class="m">0400&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000030&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0007&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000040&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000070&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0020&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000080&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dd &lt;span class="k">if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/proc/2031108/mem&amp;#34;&lt;/span> &lt;span class="nv">of&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/dev/stdout&amp;#34;&lt;/span> &lt;span class="nv">bs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="nv">skip&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span>&lt;span class="m">0&lt;/span>x80000000000&lt;span class="k">))&lt;/span> &lt;span class="nv">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">128&lt;/span> &lt;span class="p">|&lt;/span> hexdump
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records in
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records out
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">128&lt;/span> bytes copied, 0.000298448 s, &lt;span class="m">429&lt;/span> kB/s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000000&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0003&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000010&lt;/span> e938 &lt;span class="m">0005&lt;/span> &lt;span class="m">0400&lt;/span> &lt;span class="m">0000&lt;/span> e970 &lt;span class="m">0005&lt;/span> &lt;span class="m">0400&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000020&lt;/span> e9c0 &lt;span class="m">0005&lt;/span> &lt;span class="m">0400&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000030&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0007&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000040&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000070&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0020&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000080&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dd &lt;span class="k">if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/proc/2031108/mem&amp;#34;&lt;/span> &lt;span class="nv">of&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/dev/stdout&amp;#34;&lt;/span> &lt;span class="nv">bs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="nv">skip&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span>&lt;span class="m">0&lt;/span>x100000000000&lt;span class="k">))&lt;/span> &lt;span class="nv">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">128&lt;/span> &lt;span class="p">|&lt;/span> hexdump
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records in
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records out
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">128&lt;/span> bytes copied, 0.000330489 s, &lt;span class="m">387&lt;/span> kB/s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000000&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0003&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000010&lt;/span> e938 &lt;span class="m">0005&lt;/span> &lt;span class="m">0800&lt;/span> &lt;span class="m">0000&lt;/span> e970 &lt;span class="m">0005&lt;/span> &lt;span class="m">0800&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000020&lt;/span> e9c0 &lt;span class="m">0005&lt;/span> &lt;span class="m">0800&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000030&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0007&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000040&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000070&lt;/span> &lt;span class="m">0001&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">1550&lt;/span> &lt;span class="m">0000&lt;/span> &lt;span class="m">0020&lt;/span> &lt;span class="m">0000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0000080&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以发现这些内容是完全一致的，那么就找到了罪魁祸首：正是这三个堆内存映射造成了内存占用飙高的问题&lt;/p>
&lt;h2 id="知其所以然">知其所以然&lt;/h2>
&lt;p>我们发现了元凶首恶，但是还需要进一步分析为什么映射的内存会被算为 RssShmem 以及为什么会出现三次映内存映射&lt;/p>
&lt;h3 id="匿名文件映射">匿名文件映射&lt;/h3>
&lt;p>首先来看一下映射的文件 &lt;code>memfd:java_heap (deleted)&lt;/code>，其中 &lt;code>memfd&lt;/code> 是 Linux 的一个特性，可以创建一个匿名文件驻留在内存中。这个文件不会出现在文件系统中，只能通过 &lt;code>/proc/&amp;lt;pid&amp;gt;/fd&lt;/code> 查看，映射的内容会在进程退出时被释放&lt;/p>
&lt;p>这个文件的 fd 和普通的 fd 并无二致，自然也能使用 &lt;code>mmap&lt;/code> 来创建文件映射，多次映射同一个文件会共享同一块物理内存，可以通过类似下面的代码来实现：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 1G
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">static&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">SIZE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1024&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">1024&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">int&lt;/span> &lt;span class="n">fd&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">syscall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SYS_memfd_create&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;shma&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ftruncate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">ptr0&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mmap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">NULL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PROT_READ&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">PROT_WRITE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">MAP_SHARED&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">memset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ptr0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sc">&amp;#39;A&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">ptr1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mmap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">NULL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PROT_READ&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">PROT_WRITE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">MAP_SHARED&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">memset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ptr1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sc">&amp;#39;B&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">ptr2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mmap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">NULL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PROT_READ&lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="n">PROT_WRITE&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">MAP_SHARED&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">fd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">memset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ptr2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sc">&amp;#39;C&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SIZE&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这段代码会创建一个匿名文件，然后映射三次并分别写入 A、B、C 三种字符：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">cat /proc/2306924/status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Name: memfd_create
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RssAnon: &lt;span class="m">96&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RssFile: &lt;span class="m">1320&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">RssShmem: &lt;span class="m">3145540&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pmap -x &lt;span class="m">2306924&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2306924: ./memfd_create
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address Kbytes RSS Dirty Mode Mapping
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">00007f1b16ee7000 &lt;span class="m">1048576&lt;/span> &lt;span class="m">1048576&lt;/span> &lt;span class="m">1048576&lt;/span> rw-s- memfd:shma &lt;span class="o">(&lt;/span>deleted&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">00007f1b56ee7000 &lt;span class="m">1048576&lt;/span> &lt;span class="m">1048576&lt;/span> &lt;span class="m">1048576&lt;/span> rw-s- memfd:shma &lt;span class="o">(&lt;/span>deleted&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">00007f1b96ee7000 &lt;span class="m">1048576&lt;/span> &lt;span class="m">1048576&lt;/span> &lt;span class="m">1048576&lt;/span> rw-s- memfd:shma &lt;span class="o">(&lt;/span>deleted&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dd &lt;span class="k">if&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/proc/2306924/mem&amp;#34;&lt;/span> &lt;span class="nv">of&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/dev/stdout&amp;#34;&lt;/span> &lt;span class="nv">bs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="nv">skip&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$((&lt;/span>&lt;span class="m">0&lt;/span>x00007f1b16ee7000&lt;span class="k">))&lt;/span> &lt;span class="nv">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">128&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC128+0 records in
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">128+0 records out
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">128&lt;/span> bytes copied, 0.000432721 s, &lt;span class="m">296&lt;/span> kB/s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以看到创建的匿名文件被映射了三次，并且对应进程的 RssShmem 也是 3G&lt;/p>
&lt;h3 id="zgc-中堆内存映射">ZGC 中堆内存映射&lt;/h3>
&lt;p>需要回答的下一个问题是：为什么 Java 将堆内存映射三次？这源于 ZGC 引入的指针染色技术，即用对象指针中的某几位来作为 GC 标记，这样就不需要额外的对象头空间来记录 GC 标记了。比如我们用 16 进制下的对象指针最高位来作为 GC 标记，那么指向 0x13210 的对象 GC 标记是 0x1，地址是 0x3210&lt;/p>
&lt;p>对于计算机来说，可以用掩码实现从对象指针中提取 GC 标记和对象地址：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Pointer value: 0x13210 : 0001 0011 0010 0001 0000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Metadata mask: &amp;amp; 0xf0000 : 1111 0000 0000 0000 0000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Metadata bits: 0x1 : 0001
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Pointer value: 0x13210 : 0001 0011 0010 0001 0000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address mask: &amp;amp; 0x0ffff : 0000 1111 1111 1111 1111
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Address bits: 0x3210 : 0011 0010 0001 0000
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每次访问对象时，都需要将 GC 标记位清零以得到真实的地址，这是不可忽略的开销。ZGC 使用多重映射技术巧妙的避免了这个问题。考虑到 0x13210、0x23210 都指向同一个对象，那么我们可以将这两个地址映射到同一块物理内存上，这样就可以避免每次访问对象时的掩码操作：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">+-----------+ 0x10000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| X | 0x13210 -----+ +----------------------+
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| | \ | |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">+-----------+ 0x20000 +---&amp;gt; | X @ offset 0x3210 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| | / | |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| X | 0x23210 -----+ +----------------------+
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">+-----------+ 0x30000
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们将堆映射到 0x10000~0x20000 和 0x20000~0x30000 这两个虚拟内存空间上。只要相对于 0x10000 和 0x20000 的偏移量相同，就能访问到同一块物理内存&lt;/p>
&lt;p>这是一种空间换时间的做法。实际上浪费的空间是虚拟内存的空间，用页表的开销换取了每次对象访问时的掩码操作，是一个非常值得的 trade off&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
更详细的设计可以参考 &lt;a class="link" href="https://wiki.openjdk.org/display/zgc/Pointer&amp;#43;Metadata&amp;#43;using&amp;#43;Multi-Mapped&amp;#43;memory" target="_blank" rel="noopener"
>OpenJDK Wiki&lt;/a>
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="观测真实内存占用">观测真实内存占用&lt;/h3>
&lt;p>Linux 中一个进程占用的内存有多种统计方式，可以分为 VSS、RSS、PSS、USS：&lt;/p>
&lt;ul>
&lt;li>VSS：Virtual Set Size，进程申请的虚拟内存大小&lt;/li>
&lt;li>RSS：Resident Set Size，进程的常驻内存大小，包括代码段、堆、栈、共享库、映射文件等&lt;/li>
&lt;li>PSS：Proportional Set Size，进程的比例内存大小，RSS 中的共享内存按照比例分摊到各个进程&lt;/li>
&lt;li>USS：Unique Set Size，进程独占的内存大小，RSS 中的共享内存不计入 USS&lt;/li>
&lt;/ul>
&lt;p>我们可以通过 PSS 来观测真实的内存占用情况，这里使用了 &lt;code>/proc/[pid]/smaps_rollup&lt;/code> 来查看 PSS：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">cat /proc/2306924/smaps_rollup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">5626d2e58000-7ffca670f000 ---p &lt;span class="m">00000000&lt;/span> 00:00 &lt;span class="m">0&lt;/span> &lt;span class="o">[&lt;/span>rollup&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Rss: &lt;span class="m">3147164&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Pss: &lt;span class="m">1048716&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Pss_Anon: &lt;span class="m">100&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Pss_File: &lt;span class="m">40&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Pss_Shmem: &lt;span class="m">1048575&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Shared_Clean: &lt;span class="m">1324&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Shared_Dirty: &lt;span class="m">3145728&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Private_Clean: &lt;span class="m">12&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Private_Dirty: &lt;span class="m">100&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Referenced: &lt;span class="m">3147164&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Anonymous: &lt;span class="m">100&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">LazyFree: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">AnonHugePages: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ShmemPmdMapped: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">FilePmdMapped: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Shared_Hugetlb: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Private_Hugetlb: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Swap: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SwapPss: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Locked: &lt;span class="m">0&lt;/span> kB
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以发现 Pss_Shmem 是 RssShmem 的三分之一，更能反应真实内存占用&lt;/p></description></item><item><title>RocketMQ 多级存储设计与实现</title><link>https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq</link><pubDate>Sun, 26 Feb 2023 10:45:00 +0800</pubDate><guid>https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq</guid><description>&lt;img src="https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq/tiered_storage.png" alt="Featured image of post RocketMQ 多级存储设计与实现" />&lt;p>随着 RocketMQ 5.1.0 的正式发布，多级存储作为 RocketMQ 一个新的独立模块到达了 Technical Preview 里程碑：允许用户将消息从本地磁盘卸载到其他更便宜的存储介质，可以用较低的成本延长消息保留时间。本文详细介绍 RocketMQ 多级存储设计与实现&lt;/p>
&lt;h2 id="设计总览">设计总览&lt;/h2>
&lt;p>RocketMQ 多级存储旨在&lt;strong>不影响热数据读写&lt;/strong>的前提下将数据卸载到其他存储介质中，适用于两种场景：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>冷热数据分离：RocketMQ 新近产生的消息会缓存在 page cache 中，我们称之为&lt;strong>热数据&lt;/strong>；当缓存超过了内存的容量就会有热数据被换出成为&lt;strong>冷数据&lt;/strong>。如果有少许消费者尝试消费冷数据就会从硬盘中重新加载冷数据到 page cache，这会导致读写 IO 竞争并挤压 page cache 的空间。而将冷数据的读取链路切换为多级存储就可以避免这个问题&lt;/p>
&lt;/li>
&lt;li>
&lt;p>延长消息保留时间：将消息卸载到更大更便宜的存储介质中，可以用较低的成本实现更长的消息保存时间。同时多级存储支持为 topic 指定不同的消息保留时间，可以根据业务需要灵活配置消息 TTL&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>RocketMQ 多级存储对比 Kafka 和 Pulsar 的实现最大的不同是我们使用准实时的方式上传消息，而不是等一个 CommitLog 写满后再上传，主要基于以下几点考虑：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>均摊成本：RocketMQ 多级存储需要将全局 CommitLog 转换为 topic 维度并重新构建消息索引，一次性处理整个 CommitLog 文件会带来性能毛刺&lt;/p>
&lt;/li>
&lt;li>
&lt;p>对小规格实例更友好：小规格实例往往配置较小的内存，这意味着热数据会更快换出成为冷数据，等待 CommitLog 写满再上传本身就有冷读风险。采取准实时上传的方式既能规避消息上传时的冷读风险，又能尽快使得冷数据可以从多级存储读取&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="quick-start">Quick Start&lt;/h2>
&lt;p>多级存储在设计上希望降低用户心智负担：用户无需变更客户端就能实现无感切换冷热数据读写链路，通过简单的修改服务端配置即可具备多级存储的能力，只需以下两步：&lt;/p>
&lt;ol>
&lt;li>修改 Broker 配置，指定使用 &lt;code>org.apache.rocketmq.tieredstore.TieredMessageStore&lt;/code> 作为 &lt;code>messageStorePlugIn&lt;/code>&lt;/li>
&lt;li>配置你想使用的储存介质，以卸载消息到其他硬盘为例：配置 &lt;code>tieredBackendServiceProvider&lt;/code> 为 &lt;code>org.apache.rocketmq.tieredstore.provider.posix.PosixFileSegment&lt;/code>，同时指定新储存的文件路径：&lt;code>tieredStoreFilepath&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
可选项：支持修改 &lt;code>tieredMetadataServiceProvider&lt;/code> 切换元数据存储的实现，默认是基于 json 的文件存储
&lt;/div>
&lt;p>&lt;/p>
&lt;p>更多使用说明和配置项可以在 GitHub 上查看多级存储的 &lt;a class="link" href="https://github.com/apache/rocketmq/blob/develop/tieredstore/README.md" target="_blank" rel="noopener"
>README&lt;/a>&lt;/p>
&lt;h2 id="技术架构">技术架构&lt;/h2>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq/tiered_storage_arch.jpg" width="708" height="800"/>&lt;figcaption>
&lt;h4>architecture&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;ul>
&lt;li>
&lt;p>接入层：TieredMessageStore/TieredDispatcher/TieredMessageFetcher&lt;br />
接入层实现 MessageStore 中的部分读写接口，并为他们增加了异步语意。TieredDispatcher 和 TieredMessageFetcher 分别实现了多级存储的上传/下载逻辑，相比于底层接口这里做了较多的性能优化：包括使用独立的线程池，避免慢 IO 阻塞访问热数据；使用预读缓存优化性能等&lt;/p>
&lt;/li>
&lt;li>
&lt;p>容器层：TieredCommitLog/TieredConsumeQueue/TieredIndexFile/TieredFileQueue&lt;br />
容器层实现了和 DefaultMessageStore 类似的逻辑文件抽象，同样将文件划分为 CommitLog、ConsumeQueue、IndexFile，并且每种逻辑文件类型都通过 FileQueue 持有底层物理文件的引用。有所不同的是多级存储的 CommitLog 改为 queue 维度&lt;/p>
&lt;/li>
&lt;li>
&lt;p>驱动层：TieredFileSegment&lt;br />
驱动层负责维护逻辑文件到物理文件的映射，通过实现 TieredStoreProvider 对接底层文件系统读写接口(Posix、S3、OSS、MinIO 等)。目前提供了 PosixFileSegment 的实现，可以将数据转移到其他硬盘或通过 fuse 挂载的对象存储上&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="消息上传">消息上传&lt;/h3>
&lt;p>RocketMQ 多级存储的消息上传是由 dispatch 机制触发的：初始化多级存储时会将 TieredDispatcher 注册为 CommitLog 的 dispacher。这样每当有消息发送到 Broker 会调用 TieredDispatcher 进行消息分发，TieredDispatcher 将该消息写入到 upload buffer 后立即返回成功。整个 dispatch 流程中不会有任何阻塞逻辑，确保不会影响本地 ConsumeQueue 的构建&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq/dispatch.jpg" width="420" height="295"/>&lt;figcaption>
&lt;h4>TieredDispatcher&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>TieredDispatcher 写入 upload buffer 的内容仅为消息的引用，不会将消息的 body 读入内存。因为多级储存以 queue 维度构建 CommitLog，此时需要重新生成 commitLog offset 字段&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq/upload_buffer.jpg" width="318" height="145"/>&lt;figcaption>
&lt;h4>upload buffer&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>触发 upload buffer 上传时读取到每条消息的 commitLog offset 字段时采用拼接的方式将新的 offset 嵌入到原消息中&lt;/p>
&lt;h4 id="上传进度控制">上传进度控制&lt;/h4>
&lt;p>每个队列都会有两个关键位点控制上传进度：&lt;/p>
&lt;ol>
&lt;li>dispatch offset：已经写入缓存但是未上传的消息位点&lt;/li>
&lt;li>commit offset：已上传的消息位点&lt;/li>
&lt;/ol>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/introduce-tiered-storage-for-rocketmq/upload_progress.jpg" width="471" height="206"/>&lt;figcaption>
&lt;h4>upload progress&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>类比消费者，dispatch offset 相当于拉取消息的位点，commit offset 相当于确认消费的位点。commit offset 到 dispatch offset 之间的部分相当于已拉取未消费的消息&lt;/p>
&lt;h3 id="消息读取">消息读取&lt;/h3>
&lt;p>TieredMessageStore 实现了 MessageStore 中的消息读取相关接口，通过请求中的逻辑位点（queue offset）判断是否从多级存储中读取消息，根据配置（tieredStorageLevel）有四种策略：&lt;/p>
&lt;ul>
&lt;li>DISABLE：禁止从多级存储中读取消息&lt;/li>
&lt;li>NOT_IN_DISK：不在 DefaultMessageStore 中的消息从多级存储中读取&lt;/li>
&lt;li>NOT_IN_MEM：不在 page cache 中的消息即冷数据从多级存储读取&lt;/li>
&lt;li>FORCE：强制所有消息从多级存储中读取，目前仅供测试使用&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * Asynchronous get message
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @see #getMessage(String, String, int, long, int, MessageFilter) getMessage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param group Consumer group that launches this query.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param topic Topic to query.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param queueId Queue ID to query.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param offset Logical offset to start from.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param maxMsgNums Maximum count of messages to query.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param messageFilter Message filter used to screen desired messages.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @return Matched messages.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CompletableFuture&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">GetMessageResult&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="nf">getMessageAsync&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kd">final&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="n">group&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">queueId&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="kt">long&lt;/span> &lt;span class="n">offset&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">maxMsgNums&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">MessageFilter&lt;/span> &lt;span class="n">messageFilter&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要从多级存储中读取的消息会交由 TieredMessageFetcher 处理：首先校验参数是否合法，然后按照逻辑位点（queue offset）发起拉取请求。TieredConsumeQueue/TieredCommitLog 将逻辑位点换算为对应文件的物理位点从 TieredFileSegment 读取消息&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// TieredMessageFetcher#getMessageAsync similar with TieredMessageStore#getMessageAsync
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">public&lt;/span> &lt;span class="n">CompletableFuture&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">GetMessageResult&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="nf">getMessageAsync&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">String&lt;/span> &lt;span class="n">group&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">queueId&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">long&lt;/span> &lt;span class="n">queueOffset&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">maxMsgNums&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">MessageFilter&lt;/span> &lt;span class="n">messageFilter&lt;/span>&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>TieredFileSegment 维护每个储存在文件系统中的物理文件位点，并通过为不同存储介质实现的接口从中读取所需的数据&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * Get data from backend file system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param position the index from where the file will be read
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param length the data size will be read
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @return data to be read
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CompletableFuture&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">ByteBuffer&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="nf">read0&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kt">long&lt;/span> &lt;span class="n">position&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">length&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="预读缓存">预读缓存&lt;/h4>
&lt;p>TieredMessageFetcher 读取消息时会预读一部分消息供下次使用，这些消息暂存在预读缓存中&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">protected&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">Cache&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageCacheKey&lt;/span> &lt;span class="cm">/* topic, queue id and queue offset */&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">SelectMappedBufferResultWrapper&lt;/span> &lt;span class="cm">/* message data */&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">readAheadCache&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>预读缓存的设计参考了 TCP Tahoe 拥塞控制算法，每次预读的消息量类似拥塞窗口采用加法增、乘法减的机制控制：&lt;/p>
&lt;ul>
&lt;li>加法增：从最小窗口开始，每次增加等同于客户端 batchSize 的消息量&lt;/li>
&lt;li>乘法减：当缓存的消息超过了缓存过期时间仍未被全部拉取，在清理缓存的同时会将下次预读消息量减半&lt;/li>
&lt;/ul>
&lt;p>预读缓存支持在读取消息量较大时分片并发请求，以取得更大带宽和更小的延迟&lt;/p>
&lt;p>某个 topic 消息的预读缓存由消费这个 topic 的所有 group 共享，缓存失效策略为：&lt;/p>
&lt;ol>
&lt;li>所有订阅这个 topic 的 group 都访问了缓存&lt;/li>
&lt;li>到达缓存过期时间&lt;/li>
&lt;/ol>
&lt;h3 id="故障恢复">故障恢复&lt;/h3>
&lt;p>上文中我们介绍上传进度由 commit offset 和 dispatch offset 控制。多级存储会为每个 topic、queue、fileSegment 创建元数据并持久化这两种位点。当 Broker 重启后会从元数据中恢复，继续从 commit offset 开始上传消息，之前缓存的消息会重新上传并不会丢失&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">classDiagram
class TopicMetadata
TopicMetadata: &amp;#43;int topicId
TopicMetadata: &amp;#43;String topic
TopicMetadata: &amp;#43;long reserveTime
TopicMetadata: &amp;#43;int status
TopicMetadata: &amp;#43;long updateTimeStamp
class QueueMetadata
QueueMetadata: &amp;#43;MessageQueue queue
QueueMetadata: &amp;#43;long minOffset
QueueMetadata: &amp;#43;long maxOffset
QueueMetadata: &amp;#43;long updateTimeStamp
&lt;/div>
&lt;div class="mermaid" style="margin: auto; width: 80%;">classDiagram
class FileSegmentMetadata
FileSegmentMetadata: &amp;#43;MessageQueue queue
FileSegmentMetadata: &amp;#43;int status
FileSegmentMetadata: &amp;#43;int type
FileSegmentMetadata: &amp;#43;long baseOffset
FileSegmentMetadata: &amp;#43;String path
FileSegmentMetadata: &amp;#43;long size
FileSegmentMetadata: &amp;#43;long createTimestamp
FileSegmentMetadata: &amp;#43;long beginTimestamp
FileSegmentMetadata: &amp;#43;long endTimestamp
FileSegmentMetadata: &amp;#43;long sealTimestamp
&lt;/div>
&lt;h2 id="开发计划">开发计划&lt;/h2>
&lt;p>面向云原生的存储系统要最大化利用云上存储的价值，而对象存储正是云计算红利的体现。 RocketMQ 多级存储希望一方面利用对象存储低成本的优势延长消息存储时间、拓展数据的价值；另一方面利用其共享存储的特性在多副本架构中兼得成本和数据可靠性，以及未来向 Serverless 架构演进&lt;/p>
&lt;h3 id="tag-过滤">tag 过滤&lt;/h3>
&lt;p>多级存储拉取消息时没有计算消息的 tag 是否匹配，tag 过滤交给客户端处理。这样会带来额外的网络开销，计划后续在服务端增加 tag 过滤能力&lt;/p>
&lt;h3 id="广播消费以及多个消费进度不同的消费者">广播消费以及多个消费进度不同的消费者&lt;/h3>
&lt;p>预读缓存失效需要所有订阅这个 topic 的 group 都访问了缓存，这在多个 group 消费进度不一致的情况下很难触发，导致无用的消息在缓存中堆积&lt;/p>
&lt;p>需要计算出每个 group 的消费 qps 来估算某个 group 能否在缓存失效前用上缓存的消息。如果缓存的消息预期在失效前都不会被再次访问，那么它应该被立即过期。相应的对于广播消费，消息的过期策略应被优化为所有 Client 都读取这条消息后才失效&lt;/p>
&lt;h3 id="和高可用架构的融合">和高可用架构的融合&lt;/h3>
&lt;p>目前主要面临以下三个问题：&lt;/p>
&lt;ol>
&lt;li>元数据同步：如何可靠的在多个节点间同步元数据，slave 晋升时如何校准和补全缺失的元数据&lt;/li>
&lt;li>禁止上传超过 confirm offset 的消息：为了避免消息回退，上传的最大 offset 不能超过 confirm offset&lt;/li>
&lt;li>slave 晋升时快速启动多级存储：只有 master 节点具有写权限，在 slave 节点晋升后需要快速拉起多级存储断点续传&lt;/li>
&lt;/ol>
&lt;h3 id="高级消息类型">高级消息类型&lt;/h3>
&lt;h4 id="事务消息">事务消息&lt;/h4>
&lt;p>事务半消息回查用到了 commitLog offset，而多级存储对消息的 commitLog offset 做了重新映射会导致兼容问题。当前默认禁止上传事务半消息 topic &lt;code>RMQ_SYS_TRANS_HALF_TOPIC&lt;/code> 中的消息，确保其生命周期小于本地消息保留时间&lt;/p>
&lt;h4 id="定时消息">定时消息&lt;/h4>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
本节中的定时消息特指在 &lt;a class="link" href="https://shimo.im/docs/gXqme9PKKpIeD7qo/read" target="_blank" rel="noopener"
>RIP-43&lt;/a> 中引入的基于时间轮的定时消息实现
&lt;/div>
&lt;p>&lt;/p>
&lt;p>为了支持超过消息保留时间的定时时长，基于时间轮的定时消息会在按照 &lt;code>timerRollWindowSlots&lt;/code> 配置指定的时间滚动定时消息。比如 broker 配置的消息保留时间为 3 天，timerRollWindowSlots 配置的滚动间隔为 2 天，消息的定时时间为 7 天，那么每隔两天消息会重复写入.所以只要 &lt;code>timerRollWindowSlots&lt;/code> 配置的时间小于消息保留时间即可&lt;/p>
&lt;p>为了避免消息频繁滚动带来的写放大，下一步计划在多级存储中实现多级时间轮机制，即将大于消息保留时间的定时消息写入多级存储中，在消息定时到期时读取回本地处理&lt;/p></description></item><item><title>RocketMQ 可观测性之 Metrics</title><link>https://blog.lv5.moe/p/rocketmq-observability-metrics</link><pubDate>Tue, 15 Nov 2022 11:16:09 +0800</pubDate><guid>https://blog.lv5.moe/p/rocketmq-observability-metrics</guid><description>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/otel-collector.jpg" alt="Featured image of post RocketMQ 可观测性之 Metrics" />&lt;h2 id="从消息的生命周期看可观测能力">从消息的生命周期看可观测能力&lt;/h2>
&lt;p>在进入主题之前先来看一下 RocketMQ 生产者、消费者和服务端交互的流程：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 259;
flex-basis: 623px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/producer-consumer.jpg" data-size="2410x927">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/producer-consumer.jpg"
width="2410"
height="927"
loading="lazy"
alt="message produce and consume process">
&lt;/a>
&lt;figcaption>message produce and consume process&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>RocketMQ 的消息是按照队列的方式分区有序储存的，这种队列模型使得生产者、消费者和读写队列都是多对多的映射关系，彼此之间可以无限水平扩展。对比传统的消息队列如 RabbitMQ 是很大的优势，尤其是在流式处理场景下能够保证同一队列的消息被相同的消费者处理，对于批量处理、聚合处理更友好&lt;/p>
&lt;p>接下来我们来看一下消息的整个生命周期中需要关注的重要节点：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 329;
flex-basis: 790px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/msg-life-cycle.jpg" data-size="4026x1222">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/msg-life-cycle.jpg"
width="4026"
height="1222"
loading="lazy"
alt="message life cycle">
&lt;/a>
&lt;figcaption>message life cycle&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>首先是消息发送：发送耗时是指一条消息从生产者开始发送到服务端接收到并储存在硬盘上的时间。如果是定时消息，需要到达指定的定时时间才能被消费者可见&lt;/p>
&lt;p>服务端收到消息后需要根据消息类型进行处理，对于定时/事务消息只有到了定时时间/事务提交才对消费者可见。RocketMQ 提供了消息堆积的特性，即消息发送到服务端后并不一定立即被拉取，可以按照客户端的消费能力进行投递&lt;/p>
&lt;p>从消费者的角度上看，有三个需要关注的阶段：&lt;/p>
&lt;ul>
&lt;li>拉取消息：消息从开始拉取到抵达客户端的网络和服务端处理耗时&lt;/li>
&lt;li>消息排队：等待处理资源，即从消息抵达客户端到开始处理消息&lt;/li>
&lt;li>消息消费：从开始处理消息到最后提交位点/返回 ACK&lt;/li>
&lt;/ul>
&lt;p>消息在生命周期的任何一个阶段，都可以清晰地被定义并且被观测到，这就是 RocketMQ 可观测的核心理念。而本文要介绍的 Metrics 就践行了这种理念，提供覆盖消息生命周期各个阶段的监控埋点。借助 Metrics 提供的原子能力我们可以搭建适合业务需要的监控系统：&lt;/p>
&lt;ul>
&lt;li>日常巡检与监控预警&lt;/li>
&lt;li>宏观趋势/集群容量分析&lt;/li>
&lt;li>故障问题诊断&lt;/li>
&lt;/ul>
&lt;h2 id="rocketmq-4x-metrics-实现----exporter">RocketMQ 4.x Metrics 实现 &amp;ndash; Exporter&lt;/h2>
&lt;p>RocketMQ 团队贡献的 RocketMQ exporter 已被 Prometheus 官方的开源 Exporter 生态所收录，提供了 Broker、Producer、Consumer 各个阶段丰富的监控指标&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/exporter-metrics-spec.png" width="563" height="328"/>&lt;figcaption>
&lt;h4>exporter metrics spec&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;h3 id="exporter-原理解析">Exporter 原理解析&lt;/h3>
&lt;p>RocketMQ expoter 获取监控指标的流程如下图所示，Expoter 通过 MQAdminExt 向 RocketMQ 集群请求数据。获取的数据转换成 Prometheus 需要的格式，然后通过 /metics 接口暴露出来&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/rocketmq-exporter.jpg" width="629" height="378"/>&lt;figcaption>
&lt;h4>rocketmq exporter&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>随着 RocketMQ 的演进，exporter 模式逐渐暴露出一些缺陷：&lt;/p>
&lt;ul>
&lt;li>无法支持 RocketMQ 5.x 中新加入的 Proxy 等模块的可观测需求&lt;/li>
&lt;li>指标定义不符合开源规范，难以和其他开源可观测组件搭配使用&lt;/li>
&lt;li>大量 RPC 调用给 Broker 带来额外的压力&lt;/li>
&lt;li>拓展性差，增加/修改指标需要先修改 Broker 的 admin 接口&lt;/li>
&lt;/ul>
&lt;p>为解决以上问题，RocketMQ 社区决定拥抱社区标准，在 RocketMQ 5.x 中推出了基于 OpenTelemtry 的 Metrics 方案&lt;/p>
&lt;h2 id="rocketmq-5x-原生-metrics-实现">RocketMQ 5.x 原生 Metrics 实现&lt;/h2>
&lt;h3 id="基于-opentelemtry-的-metrics">基于 OpenTelemtry 的 Metrics&lt;/h3>
&lt;p>OpenTelemetry 是 CNCF 的一个可观测性项目，旨在提供可观测性领域的标准化方案，解决观测数据的数据模型、采集、处理、导出等的标准化问题，提供与三方 vendor 无关的服务&lt;/p>
&lt;p>在讨论新的 Metrics 方案时 RocketMQ 社区决定遵守 OpenTelemetry 规范，完全重新设计新 metrics 的指标定义：数据类型选用兼容 Promethues 的 Counter、Guage、Histogram，并且遵循 Promethues 推荐的指标命名规范，不兼容旧有的 rocketmq-exporter 指标。新指标覆盖 broker、proxy、producer、consumer 等各个 module，对消息生命周期的全阶段提供监控能力&lt;/p>
&lt;h3 id="指标上报方式">指标上报方式&lt;/h3>
&lt;p>我们提供了三种指标上报的方式：&lt;/p>
&lt;ul>
&lt;li>Pull 模式：适合自运维 K8s 和 Promethues 集群的用户&lt;/li>
&lt;li>Push 模式：适合希望对 metrics 数据做后处理或接入云厂商的可观测服务的用户&lt;/li>
&lt;li>Exporter 兼容模式：适合已经在使用 Exporter 和有跨数据中心（或其他网络隔离环境）传输 metrics 数据需求的用户&lt;/li>
&lt;/ul>
&lt;h4 id="pull">Pull&lt;/h4>
&lt;p>Pull 模式旨在与 Prometheus 兼容。在 K8s 部署环境中无需部署额外的组件，prometheus 可以通过社区提供的 K8s 服务发现机制（创建 PodMonitor、ServiceMonitor CDR）自动获取要拉取的 broker/proxy 列表，并从他们提供的 endpoint 中拉取 metrics 数据&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/pull-mode.jpg" width="551" height="472"/>&lt;figcaption>
&lt;h4>pull mode&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;h4 id="push">Push&lt;/h4>
&lt;p>OpenTelemetry 推荐使用 Push 模式，这意味着它需要部署一个 collector 来传输指标数据&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/push-mode.jpg" width="583" height="444"/>&lt;figcaption>
&lt;h4>push mode&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>OpenTelemetry 官方提供了 collector 的实现，支持对指标做自定义操作如过滤、富化，可以利用社区提供的插件实现自己的 collector。并且云厂商提供的可观测服务（如 AWS CloudWatch、阿里云 SLS）大多已经拥抱了 OpenTelemetry 社区，可以直接将数据推送到它们提供的 collector 中，无需额外的组件进行桥接&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/otel-collector.jpg" width="919" height="513"/>&lt;figcaption>
&lt;h4>OpenTelemetry collector&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;h4 id="兼容-rocketmq-exporter">兼容 RocketMQ Exporter&lt;/h4>
&lt;p>新的 Metrics 也提供对 RocketMQ Exporter 的兼容，现在使用 exporter 的用户无需变更部署架构即可接入新 Metrics。而且控制面应用（如 Promethues）和数据面应用（如 RocketMQ）有可能隔离部署。因此借助 Exporter 作为代理来获取新的 Metrics 数据也不失为一种好的选择&lt;/p>
&lt;p>RocketMQ 社区在 Exporter 中嵌入了一个 OpenTelemetry collector 实现，Broker 将 Metrics 数据导出到 Exporter，Exporter 提供了一个新的 endpoint（下图中的 metrics-v2）供 Prometheus 拉取&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/exporter-mode.jpg" width="767" height="447"/>&lt;figcaption>
&lt;h4>exporter mode&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;h2 id="构建监控体系最佳实践">构建监控体系最佳实践&lt;/h2>
&lt;p>丰富的指标覆盖与对社区标准的遵循使得可以轻而易举的借助 RocketMQ 的 Metrics 能力构建出适合业务需求的监控体系，这个章节主要以一个典型的流程介绍构建监控体系的最佳实践：&lt;/p>
&lt;p>集群监控/巡检 -&amp;gt; 触发告警 -&amp;gt; 排查分析&lt;/p>
&lt;h3 id="集群状态监控与巡检">集群状态监控与巡检&lt;/h3>
&lt;p>我们将指标采集到 Promethues 后就可以基于这些指标配置监控，这里给出一些示例：&lt;/p>
&lt;p>接口监控：&lt;br />
监控接口调用情况，可以据此快速抓出异常的请求对症下药&lt;br />
下图给出一些相关示例：所有 RPC 的耗时（avg、pt90、pt99 等）、成功率、失败原因、接口调用与返回值分布情况等&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 406;
flex-basis: 976px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/rpc-metrics.jpg" data-size="4962x1220">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/rpc-metrics.jpg"
width="4962"
height="1220"
loading="lazy"
alt="rpc metrics">
&lt;/a>
&lt;figcaption>rpc metrics&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>客户端监控：&lt;br />
监控客户端的使用情况，发现非预期的客户端使用如超大消息发送、客户端上下线、客户端版本治理等&lt;br />
下图给出一些相关示例：客户端连接数、客户端语言/版本分布、发送的消息大小/类型分布&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 326;
flex-basis: 784px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/client-metrics.jpg" data-size="4976x1522">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/client-metrics.jpg"
width="4976"
height="1522"
loading="lazy"
alt="client metrics">
&lt;/a>
&lt;figcaption>client metrics&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>Broker 监控：&lt;br />
监控 Broker 的水位和服务质量，及时发现集群容量瓶颈&lt;br />
下图给出一些相关示例：Dispatch 延迟、消息保留时间、线程池排队、消息堆积情况&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 404;
flex-basis: 971px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/broker-metrics.jpg" data-size="4956x1224">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/broker-metrics.jpg"
width="4956"
height="1224"
loading="lazy"
alt="broker metrics">
&lt;/a>
&lt;figcaption>broker metrics&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>以上的示例只是 Metrics 的冰山一角，需要根据业务需要灵活组合不同的指标配置监控与巡检&lt;/p>
&lt;h3 id="告警配置">告警配置&lt;/h3>
&lt;p>有了完善的监控就可以对需要关注的指标配置告警，比如可以配置 Broker 监控中 Dispatch 延迟这个指标的告警：&lt;/p>
&lt;figure>&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/broker-alert.jpg" width="440" height="726"/>&lt;figcaption>
&lt;h4>broker alert&lt;/h4>
&lt;/figcaption>
&lt;/figure>
&lt;p>收到告警后可以联动监控查看具体原因，关联发送接口的失败率可以发现有 1.7% 的消费发送失败，对应的报错是没有创建订阅组：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 402;
flex-basis: 966px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/problem-analysis.jpg" data-size="2416x600">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/problem-analysis.jpg"
width="2416"
height="600"
loading="lazy"
alt="promblem analysis">
&lt;/a>
&lt;figcaption>promblem analysis&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="问题排查分析">问题排查分析&lt;/h3>
&lt;p>最后以消息堆积这个场景为例来看一下如何基于 Metrics 分析线上问题&lt;/p>
&lt;h4 id="从消息生命周期看堆积问题">从消息生命周期看堆积问题&lt;/h4>
&lt;p>正如本文开篇所述，排查 RocketMQ 的问题需要结合消息的生命周期综合分析，如果片面的认定是服务端/客户端的故障未免会误入歧途&lt;/p>
&lt;p>对于堆积问题，我们主要关注消息生命周期中的两个阶段：&lt;/p>
&lt;ul>
&lt;li>就绪消息：就绪消息是可供消费但还未被拉取的消息，即在服务端堆积的消息&lt;/li>
&lt;li>处理中消息：处理中的消息是被客户端拉取但是还未被消费的消息&lt;/li>
&lt;/ul>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 256;
flex-basis: 614px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-observability-metrics/consume-lag.jpg" data-size="2008x784">
&lt;img src="https://blog.lv5.moe/p/rocketmq-observability-metrics/consume-lag.jpg"
width="2008"
height="784"
loading="lazy"
alt="consume lag">
&lt;/a>
&lt;figcaption>consume lag&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h4 id="多维度指标分析堆积问题">多维度指标分析堆积问题&lt;/h4>
&lt;p>对于堆积问题，RocketMQ 提供了消费延迟相关指标 &lt;code>rocketmq_consumer_lag_latency&lt;/code> 可以基于这个指标配置告警。告警的阈值需要根据当前业务对消费延迟的容忍程度灵活指定&lt;/p>
&lt;p>触发告警后就需要对消息堆积在还是就绪消息和处理中消息进行分析，RocketMQ 提供了 &lt;code>rocketmq_consumer_ready_messages&lt;/code> 和 &lt;code>rocketmq_consumer_inflight_messages&lt;/code> 这两个指标，结合其他消费相关指标与客户端配置综合分析即可判断出消息堆积的根因：&lt;/p>
&lt;ul>
&lt;li>case 1：就绪消息持续上涨，处理中消息达到客户端堆积上限&lt;/li>
&lt;/ul>
&lt;p>这是最常见的堆积场景，客户端处理中的消息量 &lt;code>rocketmq_consumer_inflight_messages&lt;/code> 达到了客户端配置的阈值，即消费者的消费能力低于消息发送量。如果业务要求尽可能实时的消费消息就需要增加消费者机器数量，如果业务对消息延迟不是很敏感可以等待业务高峰过去后再消化堆积的消息&lt;/p>
&lt;ul>
&lt;li>case 2：就绪消息几乎为 0，处理中消息持续上涨&lt;/li>
&lt;/ul>
&lt;p>这个 case 多出现在使用 RocketMQ 4.x 客户端的场景，此时消费位点是顺序提交的，如果某条消息的消费卡住会导致位点无法提交。看起来的现象是消息在客户端大量堆积，即处理中消息持续上涨。可以结合消费轨迹和 &lt;code>rocketmq_process_time&lt;/code> 这个指标抓出消费慢的消息分析上下游链路，找到根因优化消费逻辑&lt;/p>
&lt;ul>
&lt;li>case 3: 就绪消息持续上涨，处理中消息几乎为 0&lt;/li>
&lt;/ul>
&lt;p>此种场景说明客户端没有拉取到消息，一般有如下几种情况：&lt;/p>
&lt;ul>
&lt;li>鉴权问题：检查 ACL 配置，如果使用公有云产品请检查 AK、SK 配置&lt;/li>
&lt;li>消费者 hang 住：尝试打印线程堆栈或 gc 信息判断是否是进程卡死&lt;/li>
&lt;li>服务端响应慢：结合 RPC 相关指标查看拉取消息接口调用量与耗时、硬盘读写延迟。检查是否为服务端问题，如硬盘 IOPS 被打满了等等&lt;/li>
&lt;/ul></description></item><item><title>使用 K3s 搭建基于 Kubernetes 环境的 HomeLab</title><link>https://blog.lv5.moe/p/use-k3s-to-build-homelab-based-on-kubernetes</link><pubDate>Mon, 07 Nov 2022 15:42:00 +0800</pubDate><guid>https://blog.lv5.moe/p/use-k3s-to-build-homelab-based-on-kubernetes</guid><description>&lt;p>博主在和朋友搭建游戏私服的时候不幸将家里的虚拟化平台搞挂了（论备份的重要性）正好最近有用 Kubernetes 的需求，就把 HomeLab 做一次架构升级，水一篇文章记录下这次客串 SRE 的经验和踩过的坑&lt;/p>
&lt;p>技术选型： K3s（Kubernetes） + Rancher（Kubernetes Dashboard） + Traefik（Gateway）&lt;/p>
&lt;h2 id="安装-k3s">安装 K3s&lt;/h2>
&lt;p>K3s 是一个轻量级的 Kubernetes 发行版，只需要一行命令即可安装完整的 Kubernetes 环境&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sfL https://get.k3s.io &lt;span class="p">|&lt;/span> sh -s - server
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装好的 K3s 服务端只有一个可执行文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 通过 k3s server 或 k3s agent 启动&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">root@k3s-server:~# ls /usr/local/bin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">crictl ctr k3s k3s-killall.sh k3s-uninstall.sh kubectl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>K3s 占用的资源也很低，只需 1 核 CPU 和 512M 内存即可跑起来，详细分析见官方文档：&lt;a class="link" href="https://docs.k3s.io/installation/requirements" target="_blank" rel="noopener"
>最低需求&lt;/a> 和 &lt;a class="link" href="https://docs.k3s.io/reference/resource-profiling" target="_blank" rel="noopener"
>资源分析&lt;/a>&lt;/p>
&lt;h3 id="安装-k3s-server">安装 K3s Server&lt;/h3>
&lt;p>Kubernetes 由 Master 和 Worker 节点组成，对应 K3s 的 server 和 agent，即需要在一台机器上安装 K3s Server 作为 Master Node，其他机器上安装 K3s Agent 作为 Worker Node&lt;/p>
&lt;p>后续会安装 Rancher 来管理 K3s，所以这里需要使用受支持的 K3s 版本。可以从&lt;a class="link" href="https://rancher.com/support-maintenance-terms/" target="_blank" rel="noopener"
>Rancher 文档&lt;/a>中找到 Rancher 支持的 Kubernetes 版本列表&lt;/p>
&lt;p>然后使用环境变量 INSTALL_K3S_VERSION 来安装指定版本的 K3s server&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sfL https://get.k3s.io &lt;span class="p">|&lt;/span> &lt;span class="nv">INSTALL_K3S_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;***&amp;#34;&lt;/span> sh -s - server
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装完成后就可以使用 kubectl 来查看 K3s 集群：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">root@k3s-server:~# kubectl get node
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME STATUS ROLES AGE VERSION
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">k3s-server Ready control-plane,master 4d4h v1.24.7+k3s1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
安装 K3s 会附带 csictl 和 kubectl，前者可以管理 Node 上的镜像和容器；后者应该无需多介绍了，默认使用的 kubeconfig 为 &lt;code>/etc/rancher/k3s/k3s.yaml&lt;/code>
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="安装-k3s-agent">安装 K3s Agent&lt;/h3>
&lt;p>K3s agent 的安装也十分简单，去掉上述安装命令中的 server 参数即可&lt;/p>
&lt;p>此外，还需要两个额外的环境变量来指定 K3s agent 如何连接到 server：&lt;/p>
&lt;ul>
&lt;li>K3S_URL：K3s server 提供的 API server 地址，默认为 https://server-ip:6443&lt;/li>
&lt;li>K3S_TOKEN：K3s server 安装时指定或生成的 token：默认在 /var/lib/rancher/k3s/server/node-token&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sfL https://get.k3s.io &lt;span class="p">|&lt;/span> &lt;span class="nv">INSTALL_K3S_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;***&amp;#34;&lt;/span> &lt;span class="nv">K3S_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;api-server-url&amp;#34;&lt;/span> &lt;span class="nv">K3S_TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;server-token&amp;#34;&lt;/span> sh -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装完成后再来看一下 Node 列表，可以发现 worker 节点已经添加到集群中了&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">root@k3s-server:~# kubectl get node
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME STATUS ROLES AGE VERSION
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">k3s-server Ready control-plane,master 1m v1.24.7+k3s1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">k3s-worker-0 Ready &amp;lt;none&amp;gt; 1m v1.24.7+k3s1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="安装-rancher">安装 Rancher&lt;/h2>
&lt;p>Rancher 是一个帮你管理 K3s 集群中的各种资源的 dashboard，功能十分丰富但是占用的资源也比较多。如果你只想要一个轻量的 Kubernetes dashboard 来浏览资源可以略过本节并参考这篇文档 &lt;a class="link" href="https://docs.k3s.io/installation/kube-dashboard" target="_blank" rel="noopener"
>Configure the Kubernetes Dashboard on K3s&lt;/a>&lt;/p>
&lt;p>Rancher 可以在 Docker 或者 Kubernetes 中安装，我们这里介绍使用 Helm 将 Rancher 安装到上面创建好的 K3s 集群中&lt;/p>
&lt;h3 id="安装-cert-manager">安装 cert-manager&lt;/h3>
&lt;p>Rancher 使用 cert-manager 来生成和管理证书，如果要自行上传证书也可以跳过这一节&lt;/p>
&lt;p>首先来安装 Helm，不同的 Linux 发行版的安装方式略有区别，非 Debian/Ubuntu 可以参考&lt;a class="link" href="https://helm.sh/docs/intro/install/" target="_blank" rel="noopener"
>官方文档&lt;/a>进行安装&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">apt install helm
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用 Helm 来安装 cert-manager，注意这里的版本 v1.10.0 可以根据需要进行替换&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.0/cert-manager.crds.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo add jetstack https://charts.jetstack.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install cert-manager jetstack/cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v1.10.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="签发-letsencrypt-证书">签发 letsencrypt 证书&lt;/h4>
&lt;p>Rancher 默认会使用 cert-manager 创建自签名证书。如果你希望使用能通过浏览器校验的合法证书，也可以用 cert-manager 获取 letsencrypt 签发的证书&lt;/p>
&lt;p>使用 cert-manager 签发证书的流程分为以下几步：&lt;/p>
&lt;ol>
&lt;li>创建 Issuer&lt;/li>
&lt;/ol>
&lt;p>cert-manager 使用 ACME 来签发证书，支持 &lt;a class="link" href="https://cert-manager.io/docs/configuration/acme/http01/" target="_blank" rel="noopener"
>HTTP01&lt;/a> 和 &lt;a class="link" href="https://cert-manager.io/docs/configuration/acme/dns01/" target="_blank" rel="noopener"
>DNS01&lt;/a> 两种方式进行校验&lt;/p>
&lt;p>博主这里使用的是 letsencrypt + DNS01 校验方式，DNS 提供商为 Cloudflare。如果你也希望使用同样的校验方式，修改下面配置中的 your-api-key 和 &lt;a class="link" href="mailto:admin@example.com" >admin@example.com&lt;/a> 即可&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cloudflare-api-key-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api-key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">&amp;lt;your-api-key&amp;gt;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">acme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">admin@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://acme-v02.api.letsencrypt.org/directory&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKeySecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">solvers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">dns01&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cloudflare&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">admin@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeySecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cloudflare-api-key-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
&lt;p>如果想将这个 Issuer 设置为默认使用，可以用如下命令升级 cert-manager&lt;/p>
&lt;p>&lt;code>helm upgrade cert-manager jetstack/cert-manager --set 'extraArgs={--default-issuer-name=letsencrypt-prod,--default-issuer-kind=ClusterIssuer}' -n cert-manager&lt;/code>&lt;/p>
&lt;/div>
&lt;p>&lt;/p>
&lt;ol start="2">
&lt;li>创建证书&lt;/li>
&lt;/ol>
&lt;p>首先需要创建一个 namespace &lt;code>cattle-system&lt;/code>，这也会用在后续的 Rancher 安装中&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create namespace cattle-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后就可以使用上面创建的 Issuer &lt;code>letsencrypt-prod&lt;/code> 签发证书了，将 rancher.example.com 替换成你的域名即可&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Certificate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tls-rancher-ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cattle-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tls-rancher-ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commonName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rancher.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">rancher.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuerRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
证书的 secretName 需要指定为 tls-rancher-ingress，这样 Rancher 才能找到这个证书
&lt;/div>
&lt;p>&lt;/p>
&lt;ol start="3">
&lt;li>Debug&lt;/li>
&lt;/ol>
&lt;p>如果你创建证书的过程一番风顺，那么恭喜你可以直接安装 Rancher 了，但是如果 Certificate 一直是 Pending 状态那么就得看看是哪出了问题&lt;/p>
&lt;p>创建证书的流程分为 Certificate -&amp;gt; CertificateRequest -&amp;gt; Order -&amp;gt; Challenge，分别对应 定义资源 -&amp;gt; 生成 CSR -&amp;gt; 提交签发证书请求 -&amp;gt; 校验域名所有权。上述的每个步骤都是一种 CRD，debug 的流程就是 describe 每种 CRD 的资源看看有什么报错&lt;/p>
&lt;p>比如博主遇到的问题是校验域名时卡在&lt;/p>
&lt;p>&lt;code>Waiting for dns-01 challenge propagation: DNS record for &amp;quot;rancher.example.com&amp;quot; not yet propagated&lt;/code>&lt;/p>
&lt;p>这是因为 K3s 使用的 DNS 有问题，不能正确获取 DNS Challenge 中设置的 TXT 记录，使用如下命令指定 cert-manager 使用的 DNS 即可正常签发证书&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># https://cert-manager.io/docs/configuration/acme/dns01/#setting-nameservers-for-dns01-self-check&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade cert-manager jetstack/cert-manager --set &lt;span class="s1">&amp;#39;extraArgs={--dns01-recursive-nameservers-only,--dns01-recursive-nameservers=8.8.8.8:53\,1.1.1.1:53&amp;#39;&lt;/span> -n cert-manager
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="安装-rancher-1">安装 Rancher&lt;/h3>
&lt;p>使用 Helm 安装 Rancher，需要将 rancher.example.com 改为上面证书的域名并设置好域名解析&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm repo add rancher-latest https://releases.rancher.com/server-charts/latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create namespace cattle-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install rancher rancher-latest/rancher &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace cattle-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">hostname&lt;/span>&lt;span class="o">=&lt;/span>rancher.example.com &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">replicas&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">bootstrapPassword&lt;/span>&lt;span class="o">=&lt;/span>&amp;lt;admin-password&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set ingress.tls.source&lt;span class="o">=&lt;/span>secret
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安装完成后即可访问 &lt;code>rancher.example.com&lt;/code> 来使用 Rancher&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
如果你跳过了 &lt;a class="link" href="#%e7%ad%be%e5%8f%91-letsencrypt-%e8%af%81%e4%b9%a6" >签发 letsencrypt 证书&lt;/a> 这个步骤并且也没有自行上传证书需要去掉 &lt;code>--set ingress.tls.source=secret&lt;/code>，这样 Rancher 会使用 cert-manager 来创建自签名证书
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="安装你的应用">安装你的应用&lt;/h2>
&lt;p>安装应用的部分就靠读者各显神通了，博主是使用 Kompose 将之前的 docker-compose 文件转换为转换为 Kubernetes 资源描述，这里给出遇到的两个问题的解法&lt;/p>
&lt;h3 id="挂载储存">挂载储存&lt;/h3>
&lt;p>K3s 默认只提供 host-path 这一个 CSI。使用 host-path 会使你的数据和 Node 强绑定，这显然很不云原生。博主这里推荐两个 CSI：&lt;a class="link" href="https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner" target="_blank" rel="noopener"
>nfs-subdir-external-provisioner&lt;/a> 和 &lt;a class="link" href="https://github.com/longhorn/longhorn" target="_blank" rel="noopener"
>longhorn&lt;/a>&lt;/p>
&lt;ul>
&lt;li>nfs-subdir-external-provisioner 可以帮你最简单的实现储存计算分离，只要你的储存池支持 NFS 就可以让 Pod 可以在不同的 Node 间飘移&lt;/li>
&lt;li>longhorn 是真正的分布式文件系统，提供多副本和备份/还原等能力&lt;/li>
&lt;/ul>
&lt;h3 id="分配应用到指定的-node">分配应用到指定的 Node&lt;/h3>
&lt;p>如果你只想使用 host-path 或者有应用和 Node 绑定的需求可以尝试使用 nodeAffinity，有 required 和 preferred 两种规则，这里直接给出&lt;a class="link" href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/" target="_blank" rel="noopener"
>官方文档&lt;/a>中的示例&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Pod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">with-node-affinity&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">affinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeAffinity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requiredDuringSchedulingIgnoredDuringExecution&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nodeSelectorTerms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matchExpressions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">topology.kubernetes.io/zone&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">In&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">antarctica-east1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">antarctica-west1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preferredDuringSchedulingIgnoredDuringExecution&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">preference&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchExpressions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">another-node-label-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">In&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">values&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">another-node-label-value&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">with-node-affinity&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.k8s.io/pause:2.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="对外暴露你的应用">对外暴露你的应用&lt;/h2>
&lt;h3 id="kubernetes-overlay-网络">Kubernetes Overlay 网络&lt;/h3>
&lt;p>首先来看一下运行应用 Pod 的信息：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@k3s-server:~/service/k8s# kubectl get pods -o wide
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">media-index-754bc9dfdd-6cbkl 4/4 Running 12 (25h ago) 27h 10.42.1.27 k3s-worker-0 &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以发现 Pod 获取到的 ip &lt;code>10.42.1.26&lt;/code>，和它所在的 Node ip &lt;code>192.168.1.2&lt;/code> 完全不在一个网段中，也就是说无法从 Kubernetes 外部直接访问我们的应用&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@k3s-server:~/service/k8s# kubectl get node -o wide
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">k3s-server Ready control-plane,master 4d12h v1.24.7+k3s1 192.168.1.1 &amp;lt;none&amp;gt; Debian GNU/Linux 11 (bullseye) 5.10.0-19-amd64 containerd://1.6.8-k3s1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">k3s-worker-0 Ready &amp;lt;none&amp;gt; 32h v1.24.7+k3s1 192.168.1.2 &amp;lt;none&amp;gt; Debian GNU/Linux 11 (bullseye) 5.10.0-19-amd64 containerd://1.6.8-k3s1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们将 Node 所在的网络称为 Underlay 网络，是真实的底层物理网络；而将 Pod 所在的网络称为 Overlay 网络，是由软件定义的逻辑网络。这样做的好处是打通了不同 Node 间的网络，即使 Node 在不同的子网下（甚至是不同的云服务商）也可为 Pod 提供一致的网络环境。K3s 默认使用 Flannel 通过 VXLAN 技术构建 Overlay 网络，相关文章可以阅读&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://zhuanlan.zhihu.com/p/36165475" target="_blank" rel="noopener"
>VXLAN vs VLAN&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.cnblogs.com/hukey/p/14296710.html" target="_blank" rel="noopener"
>Flannel 介绍及使用场景&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>所以应用部署完成后想要分配一个域名并对外提供服务就需要使用 Kubernetes 的 Ingress 网关，我们先来看一下官方文档中对 Ingress 的说明：&lt;/p>
&lt;blockquote>
&lt;p>Ingress exposes HTTP and HTTPS routes from outside the cluster to services within the cluster. Traffic routing is controlled by rules defined on the Ingress resource.&lt;br />
&lt;figure
>
&lt;a href="https://blog.lv5.moe/p/use-k3s-to-build-homelab-based-on-kubernetes/ingress.svg" >
&lt;img src="https://blog.lv5.moe/p/use-k3s-to-build-homelab-based-on-kubernetes/ingress.svg"
loading="lazy"
alt="ingress">
&lt;/a>
&lt;figcaption>ingress&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;/blockquote>
&lt;p>即我们访问应用实际是先访问到 Kubernetes 的 Ingress 网关，然后 Ingress 根据我们配置的规则把流量路由到对应的 Service 上，最后由 Service 关联的 Pod 提供服务&lt;/p>
&lt;h3 id="使用-traefik-ingress">使用 Traefik Ingress&lt;/h3>
&lt;p>K3s 默认使用 Traefik 作为 Ingress Controller，Traefik 会自动将 Ingress 和 IngressRoute（Traefik 提供的 CRD）转换为路由规则，我们下面以 Traefik dashboard 为例看下如何对外暴露服务&lt;/p>
&lt;p>签发证书 tls-traefik-ingress 并且将 traefik.example.com 替换为你的域名即可在 Kubernetes 外访问 Traefik dashboard&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik.containo.us/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IngressRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik-dashboard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entryPoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">websecure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Host(`traefik.example.com`) &amp;amp;&amp;amp; (PathPrefix(`/`) || PathPrefix(`/dashboard`) || PathPrefix(`/api`))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Rule&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api@internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TraefikService&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tls-traefik-ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个 yaml 的 spec 主要分为 entryPoints、routes、services 三个部分&lt;/p>
&lt;ul>
&lt;li>entryPoints：指定服务的接入点，有两个预置的 entryPoint：web 和 websecure，前者对应 80 端口的 http 服务，后者对应 443 端口的 https 服务。如果使用 websecure 需要配置 tls，否者会使用 Traefik 默认的自签名证书&lt;/li>
&lt;li>routes：指定路由规则，其中 match 字段用于匹配对何种流量应用本规则&lt;/li>
&lt;li>services：指定后端服务，Kind 字段可以为 Service 或 TraefikService 两种。我这里使用的 api@internal 是 Traefik 预置的 TraefikService&lt;/li>
&lt;/ul>
&lt;p>更详细的说明可以参考官方文档：&lt;a class="link" href="https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/" target="_blank" rel="noopener"
>Traefik &amp;amp; Kubernetes&lt;/a>&lt;/p>
&lt;h4 id="代理非-kubernetes-应用">代理非 Kubernetes 应用&lt;/h4>
&lt;p>前面提到 Traefik 根据我们配置的规则把流量路由到对应的 Service 上，实际上是获取 Service 对应的 Endpoints 的 ip：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">root@k3s-server:~# kubectl get svc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">index ClusterIP 10.43.218.102 &amp;lt;none&amp;gt; 8989/TCP,7878/TCP,9117/TCP 41h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">root@k3s-server:~# kubectl get endpoints
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME ENDPOINTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">index 10.42.1.27:7878,10.42.1.27:8989,10.42.1.27:9117 41h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">root@k3s-server:~/service/k8s# kubectl get pods -o wide
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">media-index-754bc9dfdd-6cbkl 4/4 Running 12 (25h ago) 27h 10.42.1.27 k3s-worker-0 &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以发现 Endpoints 中的 ip 就是应用 Pod 的 ip，Kubernetes 会根据 Service 中配置的 selector 来找到关联的 Pod 并创建 Endpoints&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">index&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">media&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sonarr&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8989&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">radarr&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">7878&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jackett&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">9117&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">index&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>所以让 Traefik 代理非 Kubernetes 应用就需要手动创建 Endpoints 并且不在 Service 中指定 selector：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Endpoints&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">media&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">subsets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">addresses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">192.168.1.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostname&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">media&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik.containo.us/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IngressRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">media&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entryPoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">websecure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Host(`downloader.example.com`)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Rule&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">downloader&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">media&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="配置跳转-https">配置跳转 https&lt;/h4>
&lt;p>Traefik 的路由规则支持正则匹配，我们可以借此配置一些默认规则：http 自动跳转 https、静态资源缓存策略、CORS 规则等等&lt;/p>
&lt;p>这里给出一个使用 Middleware 将所有网站的 http 请求跳转到 https 的例子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik.containo.us/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Middleware&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">redirect-to-https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redirectScheme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scheme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">permanent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik.containo.us/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">IngressRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-redirect-http-to-https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entryPoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">web&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HostRegexp(`{alldomain:.*}`)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Rule&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ping@internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TraefikService&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">middlewares&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">redirect-to-https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="配置默认证书">配置默认证书&lt;/h4>
&lt;p>Traefik 不支持使用 cert-manager 自动申请证书，我们可以手动申请泛域名证书并且配置为 Traefik 的默认证书，这样就不必为每个网站手动配置证书&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Certificate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tls-example-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tls-example-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commonName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;*.example.com&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;*.example.com&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuerRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik.containo.us/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TLSStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultCertificate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tls-example-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
创建名为 &lt;code>default&lt;/code> 的 TLSStore 会覆盖 Traefik 的默认 tls 配置
&lt;/div>
&lt;p>&lt;/p>
&lt;h4 id="配置-strict-sni">配置 Strict SNI&lt;/h4>
&lt;p>有了泛域名证书我们就可以对外提供多个网站的 https 服务，但是这样有存在一个风险：如果你的服务器暴露在公网中，并且不幸被脚本小子扫到，那么他访问 443 端口就可以拿到你的网站信息：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">root@k3s-server:~# curl https://192.168.1.1 -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">* Server certificate:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">* subject: &lt;span class="nv">CN&lt;/span>&lt;span class="o">=&lt;/span>*.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">* start date: Nov &lt;span class="m">4&lt;/span> 14:01:21 &lt;span class="m">2022&lt;/span> GMT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">* expire date: Feb &lt;span class="m">2&lt;/span> 14:01:20 &lt;span class="m">2023&lt;/span> GMT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这就是由 SNI 导致的域名泄露，我们可以通过设置 Strict SNI 来拒绝掉非法域名的访问：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">traefik.containo.us/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TLSOption&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kube-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sniStrict&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
配置了 Strict SNI 会使得默认证书失效，需要为每个网站显式指定证书&lt;br />
关于 SNI 的详细介绍可以阅读 &lt;a class="link" href="https://www.cloudflare.com/zh-cn/learning/ssl/what-is-sni/" target="_blank" rel="noopener"
>Cloudflare 的文章&lt;/a>
&lt;/div>
&lt;p>&lt;/p></description></item><item><title>使用对象存储代替硬盘文件系统最佳实践</title><link>https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system</link><pubDate>Sun, 28 Aug 2022 02:00:00 +0800</pubDate><guid>https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system</guid><description>&lt;img src="https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system/cover.png" alt="Featured image of post 使用对象存储代替硬盘文件系统最佳实践" />&lt;p>博主最近使用对象存储作为硬盘等块存储文件系统的冷存储替代方案，节约数据储存成本。本文介绍这个方案实现过程中的踩坑记录，以及阿里云 OSS 的几种使用方式的最佳实践与性能分析&lt;/p>
&lt;h2 id="项目背景">项目背景&lt;/h2>
&lt;p>随着我们系统的数据量越来越大，昂贵的 SSD 硬盘在机器成本中占了大头。但是绝大多数数据随着保存时间的增长会变成“冷数据”，即很少有读取需求。如果我们能将这些冷数据转移到更便宜的储存介质中就可以大幅降低成本&lt;/p>
&lt;p>下面是阿里云几种文件存储服务的对比：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>类型&lt;/th>
&lt;th>规格&lt;/th>
&lt;th>延迟&lt;/th>
&lt;th>带宽（MB/s）&lt;/th>
&lt;th>价格（元/GB/月）&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>块存储&lt;/td>
&lt;td>普通云盘&lt;/td>
&lt;td>微秒级&lt;/td>
&lt;td>30~40&lt;/td>
&lt;td>0.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>块存储&lt;/td>
&lt;td>高效云盘&lt;/td>
&lt;td>微秒级&lt;/td>
&lt;td>140&lt;/td>
&lt;td>0.35&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>块存储&lt;/td>
&lt;td>SSD云盘&lt;/td>
&lt;td>微秒级&lt;/td>
&lt;td>300&lt;/td>
&lt;td>1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NAS&lt;/td>
&lt;td>通用型&lt;/td>
&lt;td>10ms&lt;/td>
&lt;td>150&lt;/td>
&lt;td>0.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NAS&lt;/td>
&lt;td>极速型&lt;/td>
&lt;td>2ms&lt;/td>
&lt;td>600&lt;/td>
&lt;td>1.62&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OSS&lt;/td>
&lt;td>标准型&lt;/td>
&lt;td>见下文性能分析&lt;/td>
&lt;td>见下文性能分析&lt;/td>
&lt;td>0.12&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>其中 OSS 拥有极为明显的价格优势，是我们目前使用的 SSD 价格的 12%，是通用型 NAS 的 40%。如果将 1T 的 SSD 数据盘换成 OSS 节省的成本相当于一台 8C32G 的 ECS&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
因为技术选型原因，博主使用阿里云 OSS 提供的对象存储服务，下文中“对象存储”与“阿里云 OSS”可以等价互换
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="oss-读写方式">OSS 读写方式&lt;/h2>
&lt;p>块储存和 NAS 原生支持挂载为文件系统，对现有系统无侵入。OSS 的读写方式则比较特殊，主要有以下几种：&lt;/p>
&lt;h3 id="api">API&lt;/h3>
&lt;h4 id="oss-api">OSS API&lt;/h4>
&lt;p>使用 OSS 的客户端或者自行封装 OSS 提供的 Open API 即可使用 OSS 提供的对象存储服务，文档链接：&lt;a class="link" href="https://help.aliyun.com/document_detail/31948.html" target="_blank" rel="noopener"
>OSS API 参考&lt;/a>&lt;/p>
&lt;p>这里不得不吐槽下 OSS 官方 SDK，都 2202 年了还不提供异步接口，人家 AWS S3 早几年就支持了&amp;hellip;&amp;hellip;&lt;/p>
&lt;h4 id="s3-api">S3 API&lt;/h4>
&lt;p>OSS 兼容部分 S3 API，基本覆盖了上传、下载和查询元数据接口，相关文档：&lt;a class="link" href="https://help.aliyun.com/document_detail/389025.html" target="_blank" rel="noopener"
>AWS S3兼容性&lt;/a>&lt;/p>
&lt;p>理论上支持 S3 的工具也可在 OSS 上使用&lt;/p>
&lt;h3 id="挂载文件系统">挂载文件系统&lt;/h3>
&lt;h4 id="linux-fuse">Linux fuse&lt;/h4>
&lt;p>得益于 OSS 支持 S3 API，可以使用 &lt;a class="link" href="https://github.com/s3fs-fuse/s3fs-fuse" target="_blank" rel="noopener"
>S3FS&lt;/a> 来将 OSS 挂载为本地目录&lt;/p>
&lt;p>S3FS 使用 Linux fuse 技术在用户空间构建文件系统，部分兼容 POSIX。换句话说就是可以当成一块硬盘挂载使用，相关使用限制可以阅读项目文档 &lt;a class="link" href="https://github.com/s3fs-fuse/s3fs-fuse#limitations" target="_blank" rel="noopener"
>limitations 部分&lt;/a>&lt;/p>
&lt;p>S3FS 是一个纯粹的开源软件，主要贡献者是 Google 和 Yahoo 的两个老哥。缓存等功能还不成熟，稳定性既没有商业公司背书也没有找到大规模生产使用的案例&lt;/p>
&lt;h4 id="虚拟文件系统">虚拟文件系统&lt;/h4>
&lt;p>基于对象储存构建的分布式文件系统，比如 JuiceFS。它主要解决了三个问题：&lt;/p>
&lt;ol>
&lt;li>完全兼容 POSIX、HDFS&lt;/li>
&lt;li>支持通过 fuse、csi 挂载到服务器或 k8s pod 中，也可使用 S3 client、WebDAV client、Hadoop client 访问&lt;/li>
&lt;li>托管文件元数据，解决元数据访问的性能问题&lt;/li>
&lt;/ol>
&lt;p>JuiceFS 官方文档中有与 S3FS 的对比：&lt;a class="link" href="https://juicefs.com/docs/community/comparison/juicefs_vs_s3fs/" target="_blank" rel="noopener"
>JuiceFS vs. S3FS&lt;/a>&lt;/p>
&lt;p>JuiceFS 上储存的文件会被拆分成为一个个 4MB 的 Block 储存在对象储存中，这意味着不再能直接从 OSS 读取文件，必须依赖 JuiceFS server 的转换&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 121;
flex-basis: 292px"
>
&lt;a href="https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system/juicefs-storage-format.jpg" data-size="1640x1346">
&lt;img src="https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system/juicefs-storage-format.jpg"
width="1640"
height="1346"
loading="lazy"
alt="JuiceFS 文件储存格式">
&lt;/a>
&lt;figcaption>JuiceFS 文件储存格式&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="oss-性能分析">OSS 性能分析&lt;/h2>
&lt;p>将 OSS 挂载为文件系统的方案各有缺点，我们只能赤膊上阵用 OSS API 来改造我们的现有系统。首先要做的工作就是性能测试，看下 OSS 的性能是否能满足我们的需要。做性能分析之前需要先了解对象存储的特性，对其能达到的性能有一个预期：&lt;/p>
&lt;ul>
&lt;li>无限的储存空间&lt;/li>
&lt;li>文件一旦写入无法修改（Appendable 的文件可追加写入）&lt;/li>
&lt;li>仅支持有限的 API：元数据操作如遍历文件性能差、无法重命名（相当于重新上传）、无法监听文件变更等等&lt;/li>
&lt;li>IOPS/QPS 很低且&lt;a class="link" href="https://help.aliyun.com/document_detail/54464.html" target="_blank" rel="noopener"
>受限&lt;/a>，需尽量 batch 上传/下载，但访问延迟与每次请求的数据量正相关&lt;/li>
&lt;li>单线程带宽有限，如需更高带宽要并发请求&lt;/li>
&lt;li>&lt;a class="link" href="https://www.aliyun.com/price/product#/oss/detail/ossbag" target="_blank" rel="noopener"
>计费方式&lt;/a>为按储存容量和 API 调用次数计费&lt;/li>
&lt;/ul>
&lt;p>基于对象存储的以上特性，在访问的数据量一定时延迟、带宽、成本构成三元悖论，需要针对不同场景做出权衡：&lt;/p>
&lt;ul>
&lt;li>每次请求上传/下载更多的数据可以提高带宽但是带来更大的延迟，反之可以降低延迟但是减少带宽&lt;/li>
&lt;li>多个连接并发请求可以提升带宽并降低延迟，但是会提高请求的 QPS 进而提高成本&lt;/li>
&lt;/ul>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 153;
flex-basis: 368px"
>
&lt;a href="https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system/impossible-trinity.jpg" data-size="779x508">
&lt;img src="https://blog.lv5.moe/p/best-practices-for-alibaba-cloud-oss-to-replace-disk-file-system/impossible-trinity.jpg"
width="779"
height="508"
loading="lazy"
alt="三元悖论">
&lt;/a>
&lt;figcaption>三元悖论&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="上传">上传&lt;/h3>
&lt;p>OSS 提供三种上传 API：&lt;/p>
&lt;ul>
&lt;li>AppendObject&lt;/li>
&lt;li>MultipartUpload&lt;/li>
&lt;li>PutObject&lt;/li>
&lt;/ul>
&lt;p>其中 MultipartUpload 在所有分片全部上传完之前整个文件是不可见的，这种方式不适用于我们的场景首先 pass 掉&lt;/p>
&lt;p>剩下 AppendObject 和 PutObject 对比：AppendObject 更加灵活，只不过是一些版本控制、加密等功能无法使用，并且 AppendObject 的写入性能相比 PutObject 更高：&lt;/p>
&lt;ul>
&lt;li>AppendObject：60MB/s&lt;/li>
&lt;li>PutObject：50MB/s&lt;/li>
&lt;/ul>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
以上数据的测试方法均为阿里云 ECS 上使用 OSS 的内网接入点上传 1G 随机内容的文件。下文对下载性能的测试使用类似方法不再赘述
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="下载">下载&lt;/h3>
&lt;p>三种上传 API 分别对应三种文件类型：&lt;/p>
&lt;ul>
&lt;li>Appendable&lt;/li>
&lt;li>Multipart&lt;/li>
&lt;li>Normal&lt;/li>
&lt;/ul>
&lt;p>这三种类型的文件均可使用 GetObject API 来下载某个区间的数据，Appendable 和 Normal 类型的文件下载性能测试如下：&lt;/p>
&lt;ul>
&lt;li>Appendable：
&lt;ul>
&lt;li>单线程 6.6MB/s&lt;/li>
&lt;li>5 线程 27.9MB/s&lt;/li>
&lt;li>20 线程 108.0MB/s&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Normal：
&lt;ul>
&lt;li>单线程 35.9MB/s&lt;/li>
&lt;li>5 线程 85.0MB/s&lt;/li>
&lt;li>20 线程 152.9MB/s&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>Normal 文件和 Appendable 文件下载性能相比于上传差距要大得多。特别是在并发线程数少的场景，Normal 文件相比于 Appendable 有巨大的性能优势。可见 Appendable 文件适合读写比小（读取与写入量的比值。这个值越低说明对读取的需求越小，即对读取性能要求更低）的文件&lt;/p>
&lt;h3 id="元数据操作">元数据操作&lt;/h3>
&lt;p>考虑到 OSS 不能重新写入文件，所以一但上传失败/中断就需要重新上传整个文件。为了避免失败重传整个大文件，我们需要将本地文件分割成一个个较小的文件切片再上传&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
分割后的文件也不能过于小，否则会影响 OSS 内部的预读策略
&lt;/div>
&lt;p>&lt;/p>
&lt;p>但是这种方案会导致 OSS 上的文件数量急剧增多，而且 ListObjects API 获取文件元数据性能差且每次最多只能返回 1000 个文件。我们最终采用本地管理元数据的方案：即本地维护一份 OSS 上文件路径、大小等信息。读取请求先遍历本地元数据，找到相应的文件路径后再从 OSS 获取数据&lt;/p>
&lt;h3 id="总结">总结&lt;/h3>
&lt;p>最后总结下我们的系统如何平衡成本和性能：&lt;/p>
&lt;ul>
&lt;li>读写比小的文件使用 AppendObject 追加上传，读取时加大并发换取更大的带宽&lt;/li>
&lt;li>读写比大的文件本地缓存起来，攒够一定大小使用 PutObject 上传，保证性能的同时节约成本&lt;/li>
&lt;li>元数据本地管理进一步降低 API 调用费&lt;/li>
&lt;/ul></description></item><item><title>从 Kubernetes Pod 内存占用谈 Linux 内存管理</title><link>https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management</link><pubDate>Fri, 10 Jun 2022 09:54:00 +0800</pubDate><guid>https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management</guid><description>&lt;img src="https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management/ram.jpg" alt="Featured image of post 从 Kubernetes Pod 内存占用谈 Linux 内存管理" />&lt;p>本文对一个线上 k8s 内存水位误报警深入分析 Linux 内存管理中各种内存指标计算的原理&lt;/p>
&lt;p>TLDR：如果你的应用会涉及较多的文件读写，可以将 k8s 内存水位告警指标由 container_memory_working_set_bytes 改为 container_memory_rss。这样可以防止 page cache 占用空闲内存带来的误报警&lt;/p>
&lt;h2 id="问题描述">问题描述&lt;/h2>
&lt;p>前些天我们的 k8s 线上集群触发内存水位报警，报警显示某个 Pod 的内存使用率高达 85%。然而登陆到 pod 上发现应用实际占用的内存占用只有 50%，但是用 free 命令看到的内存占用又是符合报警水位（total 31G，free 2.6G）&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 892;
flex-basis: 2143px"
>
&lt;a href="https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management/free.png" data-size="759x85">
&lt;img src="https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management/free.png"
width="759"
height="85"
loading="lazy"
alt="free 命令输出">
&lt;/a>
&lt;figcaption>free 命令输出&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>再细看一下 free 打印出的信息发现 buff/cache 占用有 12G，这部分内存用到哪里了？10G 的 available 是不是指这些 cache 中有 10G 是可以释放的？那不能释放的 2G 是用来做什么的？应用真实内存占用应该看哪个指标？接下来我们依次回答这些问题&lt;/p>
&lt;h2 id="linux-内存分类">Linux 内存分类&lt;/h2>
&lt;p>我们知道 Linux 中一个非常重要的概念是万物皆文件，Linux 的内存实际上是外存上各种文件的缓存。也就是说我们申请的内存实际上对应一个真实的文件（各种设备/Socket），这种内存被称为 page cache&lt;/p>
&lt;p>我们来看看 Linux 权威文档中是如何介绍这种内存的：&lt;/p>
&lt;blockquote>
&lt;p>The physical memory is volatile and the common case for getting data into the memory is to read it from files. &lt;mark>Whenever a file is read, the data is put into the page cache to avoid expensive disk access on the subsequent reads.&lt;/mark> Similarly, when one writes to a file, the data is placed in the page cache and eventually gets into the backing storage device. The written pages are marked as dirty and when Linux decides to reuse them for other purposes, it makes sure to synchronize the file contents on the device with the updated data.&lt;br />
——&lt;a class="link" href="https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html#page-cache" target="_blank" rel="noopener"
>The Linux kernel user’s and administrator’s guide&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>其中最关键的一句话：每当读取一个文件，数据会被缓存在 page cache 中以避免后续访问时重复读取磁盘带来的昂贵开销。也就是说我们平时访问文件（read 或 mmap 系统调用）都会创建对应的 page cache，这部分内存由操作系统管理，并不记录在用户程序的内存开销中&lt;/p>
&lt;p>这个文档中同时也介绍了另外一种内存类型：匿名内存（anonymous memory）&lt;/p>
&lt;blockquote>
&lt;p>&lt;mark>The anonymous memory or anonymous mappings represent memory that is not backed by a filesystem.&lt;/mark> Such mappings are implicitly created for program’s stack and heap or by explicit calls to mmap(2) system call. Usually, the anonymous mappings only define virtual memory areas that the program is allowed to access. The read accesses will result in creation of a page table entry that references a special physical page filled with zeroes. When the program performs a write, a regular physical page will be allocated to hold the written data. The page will be marked dirty and if the kernel decides to repurpose it, the dirty page will be swapped out.&lt;br />
——&lt;a class="link" href="https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html#anonymous-memory" target="_blank" rel="noopener"
>The Linux kernel user’s and administrator’s guide&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>这里的匿名指的是不需要手动指定对应的文件，系统会为其指定一个填充为 0 的特殊文件（/dev/zero）。程序的堆栈就属于这种内存，此外还有一种匿名映射（anonymous mappings）也属于匿名内存&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
&lt;p>mmap 设置 flag 为 MAP_PRIVATE 时创建的内存才属于匿名内存，注意与 MAP_ANONYMOUS 区分&lt;/p>
&lt;ul>
&lt;li>MAP_ANONYMOUS 作用是不显式指定映射的文件（默认为 /dev/zero）&lt;/li>
&lt;li>MAP_PRIVATE 作用是使内存的修改对其他进程不可见（即 copy on write 模式）并且不会将脏页写回文件&lt;/li>
&lt;/ul>
&lt;p>MAP_ANONYMOUS 一般和 MAP_SHARED 或 MAP_PRIVATE 同时使用：&lt;br />
mmap 的 flag 设置为 MAP_ANONYMOUS|MAP_SHARED 时创建的内存属于 page cache，常用于在相关进程间共享内存&lt;br />
mmap 的 flag 设置为 MAP_ANONYMOUS|MAP_PRIVATE 时创建的内存属于匿名内存，常用于内存分配（glibc 分配大块内存）&lt;/p>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>现在可以回答第一个问题，buff/cache 占用的 12G 内存都用在哪里：我们的系统使用 mmap 映射了很多文件，buff/cache 占用的 12G 内存就是这些文件 page cache 占用的空间&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
这里的 buffer 指的是 buffer cache 块设备缓存，而 page cache 是页缓存。从名字就可以发现它们的数据是一样的，现代 Linux 中这两者也是融合的：buffer cache 的数据直接储存在 page cache 中
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="linux-内存回收策略">Linux 内存回收策略&lt;/h2>
&lt;p>按照我们朴素的观点只有匿名内存是不可回收的， cache 都是可回收的。但是为什么 free 打印出的信息中 available(10G) 小于 buffer/cache(12G) 的值？&lt;/p>
&lt;p>我们可以按照 Linux 文档的说明尝试清除 page cache（&lt;code>echo 1 &amp;gt; /proc/sys/vm/drop_caches&lt;/code>）来验证这个假设&lt;/p>
&lt;blockquote>
&lt;p>Writing to this will cause the kernel to drop clean caches, as well as reclaimable slab objects like dentries and inodes. Once dropped, their memory becomes free.&lt;/p>
&lt;p>To free pagecache:&lt;br />
    echo 1 &amp;gt; /proc/sys/vm/drop_caches&lt;br />
To free reclaimable slab objects (includes dentries and inodes):&lt;br />
    echo 2 &amp;gt; /proc/sys/vm/drop_caches&lt;br />
To free slab objects and pagecache:&lt;br />
    echo 3 &amp;gt; /proc/sys/vm/drop_caches&lt;/p>
&lt;p>&lt;mark>This is a non-destructive operation and will not free any dirty objects.&lt;/mark> To increase the number of objects freed by this operation, the user may run `sync&amp;rsquo; prior to writing to /proc/sys/vm/drop_caches. This will minimize the number of dirty objects on the system and create more candidates to be dropped.&lt;br />
——&lt;a class="link" href="https://www.kernel.org/doc/Documentation/sysctl/vm.txt" target="_blank" rel="noopener"
>Documentation for /proc/sys/vm/*&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>我们发现 free 命令看到的 buff/cache 并没有全部被回收。文档中也给出了解释：脏页并不会回收（will not free any dirty objects），这里说的脏页分为以下几种：&lt;/p>
&lt;ol>
&lt;li>被修改但未写回磁盘的 page cache：用 sync 强制脏页落盘后再尝试 drop cache 即可回收&lt;/li>
&lt;li>tmpfs/shmem：tmpfs 和共享内存（SysV shared memory 和 POSIX shared memory）&lt;/li>
&lt;li>shared anonymous mmap：使用 flag 为 MAP_ANONYMOUS|MAP_SHARED 的 mmap 获得的内存&lt;/li>
&lt;/ol>
&lt;p>后两者在 free 命令的输出中既属于 buff/cache，也属于 shared。这些内存在主动释放前不能被回收&lt;/p>
&lt;p>除了脏页外还有一些特殊的不可回收的页面：&lt;/p>
&lt;ol>
&lt;li>内核使用的页面：DMA buffer 等&lt;/li>
&lt;li>标记为 unreclaimable 的页面：如使用 mlock 锁定的内存&lt;/li>
&lt;/ol>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
内核使用的页面也有可回收的，比如文件系统元数据（dentries/inodes）这种可以从存储设备中重新读取的页面。这些页面被称为 SReclaimable，可以使用 &lt;code>echo 2 &amp;gt; /proc/sys/vm/drop_caches&lt;/code> 进行回收
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="linux-内存水位控制与可用内存计算">Linux 内存水位控制与可用内存计算&lt;/h2>
&lt;p>既然但是为什么 available 的值不等于 reclaimable + free？我们继续从 Linux 文档中寻找答案&lt;/p>
&lt;blockquote>
&lt;p>When the system is not loaded, most of the memory is free and allocation requests will be satisfied immediately from the free pages supply. As the load increases, the amount of the free pages goes down and when it reaches a certain threshold (&lt;mark>low watermark&lt;/mark>), an allocation request will awaken the kswapd daemon. It will asynchronously scan memory pages and either just free them if the data they contain is available elsewhere, or evict to the backing storage device (remember those dirty pages?). As memory usage increases even more and reaches another threshold - &lt;mark>min watermark&lt;/mark> - an allocation will trigger direct reclaim. In this case allocation is stalled until enough memory pages are reclaimed to satisfy the request.&lt;br />
——&lt;a class="link" href="https://www.kernel.org/doc/html/latest/admin-guide/mm/concepts.html#reclaim" target="_blank" rel="noopener"
>The Linux kernel user’s and administrator’s guide&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>这里提到了两个 watermark：&lt;/p>
&lt;ol>
&lt;li>low watermark：当 free 内存低于 low watermark 时触发异步内存回收&lt;/li>
&lt;li>min watermark：当内存低于 min watermark 时暂停内存分配，立即进行内存回收&lt;/li>
&lt;/ol>
&lt;p>也就是说系统中剩余的内存不能低于 min watermark，这是一个操作系统的保护机制：预留一部分内存给内存回收等关键程序使用。这些程序使用 PF_MEMALLOC 标识忽略 watermark 的限制，在需要的时候不必等待内存回收就可以立刻获得内存&lt;/p>
&lt;p>可以通过 &lt;code>cat /proc/zoneinfo&lt;/code> 看到 min watermark 的取值，单位是页。下面这个示例中系统预留了 10478 页，也就是大约 40M 的内存&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">cat /proc/zoneinfo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Node 0, zone Normal
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pages free &lt;span class="m">780839&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> min &lt;span class="m">10478&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> low &lt;span class="m">13097&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>所以 free 的内存并不都是可分配的，需要减去系统保留内存，即 available = reclaimable + free - reversed&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 146;
flex-basis: 352px"
>
&lt;a href="https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management/available.png" data-size="374x255">
&lt;img src="https://blog.lv5.moe/p/from-k8s-pod-memory-usage-to-linux-memory-management/available.png"
width="374"
height="255"
loading="lazy"
alt="available 内存计算">
&lt;/a>
&lt;figcaption>available 内存计算&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
除了 watermark 以外还有 min_free_kbytes 和 watermark_scale_factor 等参数影响系统保留内存的计算，这部分内存本文中统称为 reversed 不再详述
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="k8s-内存监控">k8s 内存监控&lt;/h2>
&lt;p>上文中说的 buff/cache、free、available 都是从 free 命令看到的结果，而 k8s 的内存相关监控指标略有不同&lt;/p>
&lt;ul>
&lt;li>free 命令是从 &lt;code>/proc/meminfo&lt;/code> 中取值计算，相关原理和各字段解释可以阅读这篇文章： &lt;a class="link" href="http://linuxperf.com/?p=142" target="_blank" rel="noopener"
>/PROC/MEMINFO之谜&lt;/a>&lt;/li>
&lt;li>k8s cadvisor metrics 指标是从 &lt;code>/sys/fs/cgroups/memory/memory.stat&lt;/code> 中取值计算，可以阅读这篇文章来验证：&lt;a class="link" href="https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/pod-overhead/#%E9%AA%8C%E8%AF%81-pod-cgroup-%E9%99%90%E5%88%B6" target="_blank" rel="noopener"
>验证 Pod cgroup 限制&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>这里给出部分 k8s cadvisor metrics 指标的解释：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>cadvisor 指标&lt;/th>
&lt;th>取值&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>container_memory_usage_bytes&lt;/td>
&lt;td>分配的总内存&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>container_memory_working_set_bytes&lt;/td>
&lt;td>分配的总内存 - 不活跃的 page cache&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>container_memory_rss&lt;/td>
&lt;td>使用的匿名内存&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>container_memory_cache&lt;/td>
&lt;td>page cache 占用内存&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>container_memory_working_set_bytes 是最常用的内存监控指标，也是判断 limit 的依据。&lt;/p>
&lt;p>回到文章开头的问题，我们线上使用的报警指标就是 container_memory_working_set_bytes，这个指标是包含了一部分 page cache 的所以要偏高一些。用这个指标来配置告警如果应用使用 page cache 较多（比如打开大量文件）就会产生误报&lt;/p>
&lt;p>监控内存水位是为了及时发现应用存在 OOM 的风险，实际上我们应该关心的是 available 指标。但是 k8s 又没有提供和 available 等价的 metrics，只能迂回使用 container_memory_rss 来配置告警，这个指标是应用真实占用的内存即 unreclaimable 部分&lt;/p></description></item><item><title>巧用 DNS 实现国内外域名 ip 分流上网</title><link>https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges</link><pubDate>Fri, 03 Jun 2022 21:03:00 +0800</pubDate><guid>https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges</guid><description>&lt;img src="https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges/dns.png" alt="Featured image of post 巧用 DNS 实现国内外域名 ip 分流上网" />&lt;p>这篇文章记录了对几种分流上网方案（iptables、OSPF、DNS 等）的尝试与优劣比较，文中会详细介绍博主目前使用的基于 DNS 的分流方案的原理与配置教程&lt;/p>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>本文适合使用软路由做透明代理的同学阅读，并且需要有一定的网络基础知识。如果你是一个小白那么直接跳到&lt;code>极简配置&lt;/code>部分照着操作即可&lt;/p>
&lt;p>博主尝试过如下几种思路实现策略路由（PBR, &lt;a class="link" href="https://en.wikipedia.org/wiki/Policy-based_routing" target="_blank" rel="noopener"
>Policy Based Routing&lt;/a>）&lt;/p>
&lt;ol>
&lt;li>iptables 给链接打标&lt;/li>
&lt;li>OSPF 等路由协议&lt;/li>
&lt;li>Clash、V2ray 等代理工具&lt;/li>
&lt;li>DNS 分流&lt;/li>
&lt;/ol>
&lt;p>前两者的思路类似，都是使用中国 ip 白名单来将国外 ip 转发到旁路由等透明代理设备上。但是无论是 iptables 还是 OSPF 配置都非常复杂，特别是 OSPF 等路由协议科班出身的人都不一定玩得转。我们配置家庭网络追求的是简单好用，使用这样复杂的方案前期调试以及后期更新 ip 列表都很不方便&lt;/p>
&lt;p>使用代理工具的分流功能做策略路由简单实用，Clash、V2ray 等客户端都会有开箱即用的配置提供。但是缺点也很明显：所有的流量都会经过代理程序，无法做到分设备代理，对于有有 BT、PCDN 等需求只想部分设备走代理的同学不太适用&lt;/p>
&lt;p>使用 DNS 分流方案拥有 iptables 和 OSPF 做策略路由的丰富功能，并且兼具使用代理工具分流配置简单的优点&lt;/p>
&lt;ol>
&lt;li>丰富的分流策略：可以按国内外 ip 分流也可以按域名分流&lt;/li>
&lt;li>配置简单：最简单的使用方法只需修改 Clash 的配置即可&lt;/li>
&lt;li>去广告：可以阻断广告域名的 DNS 解析来禁止广告加载&lt;/li>
&lt;li>分设备代理：对需要代理的设备使用分流 DNS 转发到代理服务器，其他设备使用上游公共 DNS 直连出公网&lt;/li>
&lt;/ol>
&lt;p>但是这个方案也有缺点，对于直接使用 ip 的软件就无能为力了。当然这种情况非常罕见（目前博主只发现 Telegram），而且只需额外添加一条静态路由即可解决&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
什么？你问我 DNS 是啥玩意？那我建议你&lt;del>关掉本文&lt;/del>，或者阅读这篇文章：&lt;a class="link" href="https://draveness.me/dns-coredns/" target="_blank" rel="noopener"
>详解 DNS 与 CoreDNS 的实现原理&lt;/a>
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="网络架构">网络架构&lt;/h2>
&lt;p>需要两个额外组件（极简配置下这两个组件合二为一）：&lt;/p>
&lt;ol>
&lt;li>做透明代理的软路由，可以是主路由也可以是旁路由&lt;/li>
&lt;li>分流 DNS&lt;/li>
&lt;/ol>
&lt;p>原理其实很简单：分流 DNS 劫持 DNS 请求，需要走代理的域名返回透明代理 ip，无需走代理的域名直接返回真实 ip&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">flowchart BT
DNS[(分流 DNS)] --&amp;gt;|国内直连| UpDNS
DNS --&amp;gt;|国外走代理| Proxy --&amp;gt; 代理服务器
UpDNS[上游公共 DNS] --&amp;gt;|直接出公网| Internet
Internet[互联网]
Proxy[(透明代理)]
Desktop(电脑) --&amp;gt;|分流| DNS
Phone(手机) --&amp;gt;|分流| DNS
BT(BT 下载机) --&amp;gt;|全局直连| UpDNS
IOT(智能家居设备) --&amp;gt;|全局直连| UpDNS
&lt;/div>
&lt;h2 id="dns-分流配置">DNS 分流配置&lt;/h2>
&lt;h3 id="极简配置">极简配置&lt;/h3>
&lt;p>使用 fake-ip 模式的 Clash 既作为透明代理又作为 DNS Server，这种方式配置非常简单，只需要一个能运行 Clash 的设备即可&lt;/p>
&lt;p>首先你需要部署好 Clash（请自行寻找教程，作为主路由、旁路由都可以，使用虚拟机、docker、lxc 没限制） 然后修改如下配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">dns&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// 开启 fake-ip 模式&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enhanced-mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fake-ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// 选择保留 ip 段，一般保持默认即可&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fake-ip-range&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">198.18.0.1&lt;/span>&lt;span class="l">/16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// 指定 Clash DNS 监听地址&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listen&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// 指定上游公共 DNS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nameserver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="m">223.5.5.5&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// 添加需要直连的国内域名&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fake-ip-filter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">baidu.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">tun&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stack&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">// 自动为 fake-ip-range 中的 ip 段添加路由&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auto-route&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个配置做了三件事&lt;/p>
&lt;ol>
&lt;li>开启 dns 服务并指定为 fake-ip 模式，监听 53 端口&lt;/li>
&lt;li>对于 fake-ip-filter 中的国内域名直接返回真实 ip，这些域名的流量不再经过代理&lt;/li>
&lt;li>开启 tun 模式并自动添加路由，将发往 fake-ip 的请求交给 clash 处理&lt;/li>
&lt;/ol>
&lt;p>最关键的是需要将常见国内域名加入到 fake-ip-filter 中，这里推荐一个每日更新国内域名的项目：&lt;a class="link" href="https://github.com/felixonmars/dnsmasq-china-list" target="_blank" rel="noopener"
>felixonmars/dnsmasq-china-list&lt;/a>&lt;/p>
&lt;p>你可以自行下载 accelerated-domains.china.conf 文件并为其中的域名加上 &lt;code>+.&lt;/code> 前缀然后添加到 fake-ip-filter 中。如果你恰好使用 openclash 可以用博主的脚本，可以自动下载国内域名列表并写入 openclash 的配置文件中&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">rm -f accelerated-domains.china.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wget https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf -O /root/accelerated-domains.china.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s1">&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">#LAN
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.lan
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.localdomain
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.example
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.invalid
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.localhost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.local
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.home.arpa
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">#放行NTP服务
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">time.*.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">time.*.gov
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">time.*.edu.cn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">time.*.apple.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">ntp.*.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.time.edu.cn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.ntp.org.cn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">+.pool.ntp.org
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">time1.cloud.tencent.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">*.cn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;&lt;/span> &amp;gt; /etc/openclash/custom/openclash_custom_fake_filter.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk -F / &lt;span class="s1">&amp;#39;{print &amp;#34;+.&amp;#34;$2}&amp;#39;&lt;/span> /root/accelerated-domains.china.conf &amp;gt;&amp;gt; /etc/openclash/custom/openclash_custom_fake_filter.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> fake-ip-filter: &amp;gt; /tmp/openclash_fake_filter.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">awk &lt;span class="s1">&amp;#39;{print &amp;#34; - &amp;#39;&lt;/span>&lt;span class="se">\&amp;#39;&lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;$1&amp;#34;&amp;#39;&lt;/span>&lt;span class="se">\&amp;#39;&lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;}&amp;#39;&lt;/span> /etc/openclash/custom/openclash_custom_fake_filter.list &amp;gt;&amp;gt; /tmp/openclash_fake_filter.list
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>看到这里恭喜你全部配置已经完成，是不是很简单？接下来将需要走代理的设备 DNS 改为 Clash DNS 的地址即可享受分流上网&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
如果你的 clash 运行在旁路由上，还需要在主路由的路由表中添加一项，将 fake-ip 指向旁路由
&lt;/div>
&lt;p>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 198.18.0.1/16 是 fake-ip-range 配置的 ip 段&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 192.168.2.254 是旁路由 ip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 198.18.0.1/16 gw 192.168.2.254
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
除非你熟悉 linux 网络配置，否则尽量使用原生 clash 而不是 openclash 之类的周边项目，它们往往会添加 iptables 规则将经过本机的连接都转发到 clash
&lt;/div>
&lt;p>&lt;/p>
&lt;p>在运行 clash 的服务器上使用 mtr 或 traceroute 来验证分流策略是否生效：如果 mtr 的结果显示无需代理的域名不止一跳并且没有经过 fake-ip-range 的 ip 段则分流配置正确；也可以观察 Clash 控制台，看看是否有无需走代理域名连上来&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 配置有误&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">root@OpenWrt:~# mtr -r --tcp www.baidu.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Start: 2022-06-09T21:37:36+0800
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">HOST: OpenWrt Loss% Snt Last Avg Best Wrst StDev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1.&lt;span class="p">|&lt;/span>-- 198.18.0.19&lt;span class="o">(&lt;/span>fake-ip&lt;span class="o">)&lt;/span> 0.0% &lt;span class="m">10&lt;/span> 0.2 0.1 0.1 0.2 0.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 配置正确&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">root@OpenWrt:~# mtr -r --tcp ntp.aliyun.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Start: 2022-06-09T21:40:51+0800
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">HOST: OpenWrt Loss% Snt Last Avg Best Wrst StDev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1.&lt;span class="p">|&lt;/span>-- &amp;lt;主路由 ip&amp;gt; 0.0% &lt;span class="m">10&lt;/span> 0.1 0.1 0.1 0.2 0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 2.&lt;span class="p">|&lt;/span>-- &amp;lt;公网出口 ip&amp;gt; 10.0% &lt;span class="m">10&lt;/span> 6.0 5.2 3.1 9.5 1.9
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 3.&lt;span class="p">|&lt;/span>-- 117.49.47.202 0.0% &lt;span class="m">10&lt;/span> 4.1 5.0 4.0 6.5 1.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 4.&lt;span class="p">|&lt;/span>-- 116.251.94.198 0.0% &lt;span class="m">10&lt;/span> 10.0 11.0 10.0 13.0 1.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 5.&lt;span class="p">|&lt;/span>-- ??? 100.0 &lt;span class="m">10&lt;/span> 0.0 0.0 0.0 0.0 0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 6.&lt;span class="p">|&lt;/span>-- ??? 100.0 &lt;span class="m">10&lt;/span> 0.0 0.0 0.0 0.0 0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 7.&lt;span class="p">|&lt;/span>-- ??? 100.0 &lt;span class="m">10&lt;/span> 0.0 0.0 0.0 0.0 0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 8.&lt;span class="p">|&lt;/span>-- 203.107.6.88 0.0% &lt;span class="m">10&lt;/span> 24.7 24.7 24.4 26.2 0.5
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="精准分流配置">精准分流配置&lt;/h3>
&lt;p>使用 Clash DNS 的方案虽然配置非常简单，但是毕竟不可能将所有国内域名都统计出来，还需要根据 ip 进行更精准的分流&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">flowchart BT
DNS[分流 DNS] --&amp;gt;|国内 ip| UpDNS
DNS --&amp;gt;|国内域名| UpDNS
DNS --&amp;gt;|国外 ip| ClashDNS
DNS --&amp;gt;|GFW 黑名单域名| ClashDNS
ClashDNS[Clash DNS] --&amp;gt; UpDNS
UpDNS[上游公共 DNS]
&lt;/div>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
多数系统不支持指定 DNS 端口，所以分流 DNS 和 Clash DNS 最好分配不同的 ip（可以在单网卡上分配多个内网 ip）
&lt;/div>
&lt;p>&lt;/p>
&lt;p>博主选用 &lt;a class="link" href="https://github.com/IrineSistiana/mosdns-cn" target="_blank" rel="noopener"
>IrineSistiana/mosdns-cn&lt;/a> 作为分流 DNS，这里引用 mosdns-cn 官方文档上介绍的分流规则&lt;/p>
&lt;blockquote>
&lt;p>分流模式&lt;br />
mosdns-cn 会根据用户提供的数据采用以下分流模式。&lt;/p>
&lt;p>配置了 &amp;ndash;local-ip 本地 IP&lt;/p>
&lt;ol>
&lt;li>如果请求的域名匹配到 &amp;ndash;local-domain 本地域名。则直接使用 &amp;ndash;local-upstream 本地上游。结束。&lt;/li>
&lt;li>如果请求的域名匹配到 &amp;ndash;remote-domain 远程域名。则直接使用&amp;ndash;remote-upstream 远程上游。结束。&lt;/li>
&lt;li>非 A/AAAA 类型的请求将直接使用 &amp;ndash;local-upstream 本地上游。结束。&lt;/li>
&lt;li>同时转发至本地和远程上游获取应答。&lt;/li>
&lt;li>如果本地上游的应答包含 &amp;ndash;local-ip 本地 IP。则直接采用本地上游的结果。结束。&lt;/li>
&lt;li>否则采用远程上游的结果。结束。&lt;/li>
&lt;/ol>
&lt;p>只配置了 &amp;ndash;local-domain 本地域名&lt;/p>
&lt;ol>
&lt;li>如果请求的域名匹配到 &amp;ndash;local-domain 本地域名。则直接使用 &amp;ndash;local-upstream 本地上游。结束。&lt;/li>
&lt;li>其他所有请求会使用 &amp;ndash;remote-upstream 远程上游。结束。&lt;/li>
&lt;/ol>
&lt;p>只配置了 &amp;ndash;remote-domain 远程域名&lt;/p>
&lt;ol>
&lt;li>如果请求的域名匹配到 &amp;ndash;remote-domain 远程域名。则直接使用&amp;ndash;remote-upstream 远程上游。结束。&lt;/li>
&lt;li>其他所有请求会使用 &amp;ndash;local-upstream 本地上游。结束。&lt;/li>
&lt;/ol>
&lt;/blockquote>
&lt;p>Github 上有详细使用教程，这里给出博主使用的启动脚本，每次启动时拉取最新的国内外域名/ip 数据&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">RELEASE_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>curl -s https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest &lt;span class="p">|&lt;/span> grep browser_download_url&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">GEOSITE_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$RELEASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> grep geosite.dat&lt;span class="se">\&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> cut -d&lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span> -f4&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">GEOIP_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$RELEASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> grep geoip.dat&lt;span class="se">\&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> cut -d&lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span> -f4&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -z &lt;span class="nv">$GEOSITE_URL&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;GEOSITE_URL required!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -z &lt;span class="nv">$GEOIP_URL&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;GEOIP_URL required!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$GEOSITE_URL&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="nv">$GEOIP_URL&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wget &lt;span class="nv">$GEOSITE_URL&lt;/span> -O geosite.dat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wget &lt;span class="nv">$GEOIP_URL&lt;/span> -O geoip.dat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mosdns-cn -s :53 --blacklist-domain &lt;span class="s2">&amp;#34;geosite.dat:category-ads-all&amp;#34;&lt;/span> --local-upstream 223.5.5.5:53 --local-domain &lt;span class="s2">&amp;#34;geosite.dat:cn&amp;#34;&lt;/span> --local-ip &lt;span class="s2">&amp;#34;geoip.dat:cn&amp;#34;&lt;/span> --remote-upstream https://8.8.8.8/dns-query --remote-domain &lt;span class="s2">&amp;#34;geosite.dat:geolocation-!cn&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="去广告dns-解析统计配置">去广告/DNS 解析统计配置&lt;/h3>
&lt;p>mos-dns 支持配置黑名单域名直接给出空响应，可以使用这一机制阻断广告域名的访问：&lt;code>mosdns-cn --blacklist-domain &amp;quot;geosite.dat:category-ads-all&amp;quot;&lt;/code>&lt;/p>
&lt;p>但是目前少有大佬分享适配 mosdns-cn 的去广告规则，这种专业的事情还是交给 Adguard Home 来做：&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">flowchart BT
DNS[分流 DNS] --&amp;gt;|国内 ip| AdguardHome
DNS --&amp;gt;|国内域名| AdguardHome
DNS --&amp;gt;|国外 ip| ClashDNS
DNS --&amp;gt;|GFW 黑名单域名| ClashDNS
ClashDNS[Clash DNS] --&amp;gt; AdguardHome
AdguardHome[Adguard Home] --&amp;gt; UpDNS
UpDNS[上游公共 DNS]
&lt;/div>
&lt;p>将 Adguard Home 放在 mosdns-cn 和 Clash DNS 的上游还有其他好处，可以借助它的仪表盘和日志分析出整个网络访问的网站&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 112;
flex-basis: 270px"
>
&lt;a href="https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges/adguard-home.jpg" data-size="1217x1078">
&lt;img src="https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges/adguard-home.jpg"
width="1217"
height="1078"
loading="lazy"
alt="Adguard Home 仪表盘">
&lt;/a>
&lt;figcaption>Adguard Home 仪表盘&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>Adguard Home 的部署教程网上有很多，能看到这里的人肯定也不用我赘述了。这里推荐一个博主自用的过滤规则，有了它就不需要其他规则了：&lt;a class="link" href="https://github.com/BlueSkyXN/AdGuardHomeRules" target="_blank" rel="noopener"
>BlueSkyXN/AdGuardHomeRules&lt;/a>&lt;/p>
&lt;h2 id="其他配置">其他配置&lt;/h2>
&lt;h3 id="自动设置设备的-dns-服务器">自动设置设备的 DNS 服务器&lt;/h3>
&lt;p>在&lt;code>极简配置&lt;/code>中博主提到&lt;code>将需要走代理的设备 DNS 改为分流 DNS&lt;/code>，这一步可以手动改也可以借助 DHCP 自动配置：DHCP Option 6 的作用就是指定客户端使用的 DNS，在 Openwrt LAN 接口的高级设置中即可找到该设置&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 254;
flex-basis: 610px"
>
&lt;a href="https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges/openwrt-dhcp-option.jpg" data-size="1134x446">
&lt;img src="https://blog.lv5.moe/p/use-dns-to-create-split-routing-for-different-domain-or-ip-ranges/openwrt-dhcp-option.jpg"
width="1134"
height="446"
loading="lazy"
alt="Openwrt DHCP Option 配置">
&lt;/a>
&lt;figcaption>Openwrt DHCP Option 配置&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="为直接使用-ip-的-app-设置路由">为直接使用 IP 的 APP 设置路由&lt;/h3>
&lt;p>直接使用 ip 的软件没有经过 DNS 自然就无法获取到 Clash 的 fake-ip，对于这种情况需要在主路由上添加静态路由将这些软件使用的 ip 的下一跳路由改为 Clash 的 fake-ip&lt;/p>
&lt;p>目前我只发现 Telegram 存在这种情况，需要添加的路由如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 192.18.1.254 是 fake-ip，可以从 fake-ip-range 中随便选择一个&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 91.108.4.0/22 gw 192.18.1.254
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 91.108.8.0/22 gw 192.18.1.254
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 91.108.12.0/22 gw 192.18.1.254
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 91.108.16.0/22 gw 192.18.1.254
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 91.108.56.0/22 gw 192.18.1.254
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">route add -net 149.154.160.0/20 gw 192.18.1.254
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>PVE 虚拟化黑苹果显卡直通及远程访问教程</title><link>https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial</link><pubDate>Wed, 13 Apr 2022 17:14:00 +0800</pubDate><guid>https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial</guid><description>&lt;img src="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/hackintosh-logo.png" alt="Featured image of post PVE 虚拟化黑苹果显卡直通及远程访问教程" />&lt;p>PVE 虚拟化黑苹果显卡直通教程（核显&amp;amp;独显通用），低延迟远程访问方案：VNC、ARD、ToDesk、ParSec、Jump Desktop 等远程桌面协议/软件测试横评&lt;/p>
&lt;h2 id="适用场景">适用场景&lt;/h2>
&lt;p>博主曾经用笔记本装黑苹果做过一段时间的主力机，但是因为有使用 Windows 的强需求，双系统切换不方便所以最终换回的 Windows。当时就在想如果有一台服务器跑着 MacOS，随时可以从主力机 Windows 远程访问岂不是两全其美。但是近年矿潮显卡价格虚高，实在是没有入手的欲望，最近趁矿难显卡价格下跌入手一块 AMD RX460 实践一下这个想法&lt;/p>
&lt;p>阅读本文需要的前置步骤：&lt;/p>
&lt;ol>
&lt;li>一台运行 PVE 的主机&lt;/li>
&lt;li>在 PVE 上成功安装黑苹果（使用 ）&lt;/li>
&lt;li>至少进入黑苹果一次开启 &lt;code>设置-&amp;gt;共享-&amp;gt;屏幕共享&lt;/code>（显卡直通可能会导致 PVE 自带的 VNC 卡在白苹果界面）&lt;/li>
&lt;/ol>
&lt;p>黑苹果能否成功运行和硬件以及驱动有很大的关系，而使用 PVE 使用的 KVM 虚拟化技术可以最大程度上屏蔽硬件的差异提高成功率，借助 &lt;a class="link" href="https://github.com/thenickdude/KVM-Opencore" target="_blank" rel="noopener"
>KVM-Opencore&lt;/a> 项目提供的驱动可以做到开箱即用，不用折腾&lt;/p>
&lt;p>PVE 和黑苹果安装教程这里推荐两篇博客，写的非常详细&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://www.nicksherlock.com/2021/10/installing-macos-12-monterey-on-proxmox-7" target="_blank" rel="noopener"
>Installing macOS 12 “Monterey” on Proxmox 7&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.sqlsec.com/2022/04/pve.html" target="_blank" rel="noopener"
>国光的 PVE 生产环境配置优化记录&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
对于国光这篇文章有一处勘误，文章中说到 PVE 7.1 不能显卡直通，实际上我测试是可以的，读者可以在下文中找到我使用的软件版本
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="主要配置">主要配置&lt;/h2>
&lt;h3 id="硬件配置">硬件配置&lt;/h3>
&lt;p>CPU: i5 10400&lt;br />
GPU: UHD630、AMD RX460&lt;/p>
&lt;p>其他硬件均虚拟化&lt;/p>
&lt;h3 id="软件版本">软件版本&lt;/h3>
&lt;p>虚拟化平台：PVE&lt;/p>
&lt;p>主要软件包版本：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">~ pveversion -v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">proxmox-ve: 7.1-1 &lt;span class="o">(&lt;/span>running kernel: 5.13.19-6-pve&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pve-manager: 7.1-12 &lt;span class="o">(&lt;/span>running version: 7.1-12/b3c09de3&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pve-kernel-helper: 7.1-14
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pve-kernel-5.13: 7.1-9
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>MacOS 版本：macOS Monterey 12.3.1(21E258)（使用 &lt;a class="link" href="https://github.com/thenickdude/OSX-KVM" target="_blank" rel="noopener"
>OSX-KVM&lt;/a> 项目制作镜像）&lt;br />
OpenCore&amp;amp;EFI 版本：&lt;a class="link" href="https://github.com/thenickdude/KVM-Opencore/releases/tag/v16" target="_blank" rel="noopener"
>KVM-Opencore v16&lt;/a>&lt;/p>
&lt;h2 id="显卡直通">显卡直通&lt;/h2>
&lt;p>因为我是通过远程访问使用黑苹果，所以并没有把要直通的 GPU 设置为主 GPU，这是本文和其他直通教程的主要区别。这样做的好处是可以用 PVE 的后台直接查看黑苹果启动情况、进入 Recovery 模式等&lt;/p>
&lt;p>参考 &lt;a class="link" href="https://wiki.archlinux.org/title/PCI_passthrough_via_OVMF_%28%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87%29" target="_blank" rel="noopener"
>Arch Linux wiki&lt;/a>&lt;/p>
&lt;h3 id="启用-iommu">启用 IOMMU&lt;/h3>
&lt;p>这里引用 Arch Linux wiki 中对 IOMMU 的介绍：&lt;/p>
&lt;blockquote>
&lt;p>IOMMU 是 Intel VT-d 和 AMD-Vi 的通用名称。&lt;br />
VT-d 指的是直接输入/输出虚拟化(Intel Virtualization Technology for Directed I/O)，不应与 VT-x(x86 平台下的 Intel 虚拟化技术，Intel Virtualization Technology)混淆。VT-x 可以让一个硬件平台作为多个“虚拟”平台，而 VT-d 提高了虚拟化的安全性、可靠性和 I/O 性能。&lt;/p>
&lt;/blockquote>
&lt;p>首先在 BIOS 中开启 VT-d&lt;/p>
&lt;p>然后修改内核参数开启 IOMMU：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- GRUB_CMDLINE_LINUX_DEFAULT=&amp;#34;quiet&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ GRUB_CMDLINE_LINUX_DEFAULT=&amp;#34;quiet intel_iommu=on iommu=pt pcie_acs_override=downstream video=efifb:off&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里解释下这几个参数的作用：&lt;/p>
&lt;ul>
&lt;li>intel_iommu=on：开启 IOMMU，对于 AMD CPU 需要使用 amd_iommu=on&lt;/li>
&lt;li>iommu=pt：pt 是 passthrough 的缩写，可以提高性能&lt;/li>
&lt;li>pcie_acs_override=downstream: 可以将同一 Group 中的设备分开直通&lt;/li>
&lt;li>video=efifb:off：禁用 efifb 驱动，防止出现报错 BAR 3: cannot reserve [mem]&lt;/li>
&lt;/ul>
&lt;p>更新内核参数&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">update-grub
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>重启后可以用以下脚本测试&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">bash -c &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://gist.githubusercontent.com/ShadowySpirits/018ea8675100baf768afff0d835e7862/raw/8e1c12f5766f0d308628ad1373b2f8603c523480/check_iommu.sh&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果你遇到网络问题可以直接复制并执行以下脚本内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bas" data-lang="bas">&lt;span class="line">&lt;span class="cl">&lt;span class="err">#&lt;/span>&lt;span class="o">!/&lt;/span>&lt;span class="vg">bin&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="vg">bash&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="vg">shopt&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="vg">s&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">nullglob&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="vg">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">g&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vg">find&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="vg">sys&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="vg">kernel&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="vg">iommu_groups&lt;/span>&lt;span class="o">/*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="vg">maxdepth&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="il">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="vg">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">d&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">sort&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="vg">V&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="vg">echo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;IOMMU Group ${g##*/}:&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="vg">for&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">d&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">in&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="vg">g&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="vg">devices&lt;/span>&lt;span class="o">/*&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="vg">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="vg">echo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="vg">e&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;\t$(lspci -nns ${d##*/})&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="vg">done&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="vg">done&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>看到类似以下信息说明 IOMMU 开启成功&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">IOMMU Group 0:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 00:00.0 Host bridge [0600]: Intel Corporation Comet Lake-S 6c Host Bridge/DRAM Controller [8086:9b53] (rev 05)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IOMMU Group 1:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 00:01.0 PCI bridge [0604]: Intel Corporation 6th-10th Gen Core Processor PCIe Controller (x16) [8086:1901] (rev 05)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IOMMU Group 6:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 00:1c.0 PCI bridge [0604]: Intel Corporation Device [8086:a394] (rev f0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Baffin [Radeon RX 460/560D / Pro 450/455/460/555/555X/560/560X] [1002:67ef] (rev cf)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 03:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Baffin HDMI/DP Audio [Radeon RX 550 640SP / RX 560/560X] [1002:aae0]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
如果上面没有开启 &lt;code>pcie_acs_override=downstream&lt;/code> 就只能将整个 Group 下的设备都直通给某个虚拟机
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="隔离-gpu">隔离 GPU&lt;/h3>
&lt;p>我们需要使用占位驱动程序（vfio）接管显卡，这样才能后续将显卡分配给虚拟机&lt;/p>
&lt;p>在 PVE 宿主机的 /etc/modules 中添加 vfio 模块&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">vfio
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vfio_iommu_type1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vfio_pci
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">vfio_virqfd
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>修改 /etc/modprobe.d/vfio.conf 将显卡的供应商-设备 ID 传递给 vfio 驱动，供应商-设备 ID 可以在上面脚本的输出的 &lt;code>[]&lt;/code> 中找到，多个设备用 &lt;code>,&lt;/code> 分隔&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">options vfio-pci ids=device_id1,device_id2 disable_vga=1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>以我的 RX460 为例，它的供应商-设备 ID 是 1002:67ef 和 1002:aae0&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">IOMMU Group 6:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 00:1c.0 PCI bridge [0604]: Intel Corporation Device [8086:a394] (rev f0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Baffin [Radeon RX 460/560D / Pro 450/455/460/555/555X/560/560X] [1002:67ef] (rev cf)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 03:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Baffin HDMI/DP Audio [Radeon RX 550 640SP / RX 560/560X] [1002:aae0]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>所以需要添加的内容是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">options vfio-pci ids=1002:67ef,1002:aae0 disable_vga=1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后在 PVE 宿主机的 /etc/modprobe.d/blacklist.conf 中禁用其他显卡驱动，防止这些驱动在 vfio 前加载&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"># NVIDIA
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist nvidiafb
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist nouveau
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist nvidia
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist snd_hda_intel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># Intel
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist snd_hda_codec_hdmi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist i915
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># AMD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">blacklist radeon
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后应用更改并重启&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">update-initramfs -u
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="分配显卡">分配显卡&lt;/h3>
&lt;p>重启后就可以将显卡分配给虚拟机了：&lt;/p>
&lt;p>在设备中选择添加 PCI 设备，然后选择你要添加的显卡即可（独显核显都可以）&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 238;
flex-basis: 572px"
>
&lt;a href="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/pve-passthrough.jpeg" data-size="599x251">
&lt;img src="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/pve-passthrough.jpeg"
width="599"
height="251"
loading="lazy"
alt="pve 直通选项">
&lt;/a>
&lt;figcaption>pve 直通选项&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
不同的显卡这里选择的选项不太一样，根据我的试验：&lt;br />
直通 UHD630 只需要勾选 &lt;code>全功能（All Functions）&lt;/code>&lt;br />
直通 AMD RX460 除了 &lt;code>主 GPU（Primary GPU）&lt;/code> 外的选项都需要勾选
&lt;/div>
&lt;p>&lt;/p>
&lt;p>直通之后 PVE 自带的 VNC 可能会卡在白苹果界面，其实系统已经正常启动，可以使用 MacOS 自带的 VNC 进行连接&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 141;
flex-basis: 339px"
>
&lt;a href="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/stuck-apple.png" data-size="670x474">
&lt;img src="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/stuck-apple.png"
width="670"
height="474"
loading="lazy"
alt="白苹果">
&lt;/a>
&lt;figcaption>白苹果&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="远程访问">远程访问&lt;/h2>
&lt;h3 id="分辨率调整">分辨率调整&lt;/h3>
&lt;p>当你通过 PVE 自带的 VNC 连接黑苹果的时候会发现有一个分辨率为 1080p 的内置显示器，并且没有其他分辨率的选项，所以需要一些奇技淫巧来强制修改分辨率：&lt;/p>
&lt;p>这里用到两个软件：BetterDummy 和 SwitchResX&lt;/p>
&lt;p>首先使用 BetterDummy 创建一个和你物理显示器比例一致的虚拟显示器并设为主显示器&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 114;
flex-basis: 275px"
>
&lt;a href="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/create-new-dummy.jpeg" data-size="525x458">
&lt;img src="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/create-new-dummy.jpeg"
width="525"
height="458"
loading="lazy"
alt="创建虚拟显示器">
&lt;/a>
&lt;figcaption>创建虚拟显示器&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>然后使用 SwitchResX 修改虚拟显示器分辨率为你物理显示器的原生分辨率&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 209;
flex-basis: 502px"
>
&lt;a href="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/switch-res.jpeg" data-size="538x257">
&lt;img src="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/switch-res.jpeg"
width="538"
height="257"
loading="lazy"
alt="修改分辨率">
&lt;/a>
&lt;figcaption>修改分辨率&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
不推荐使用带有 HiDPI 的分辨率，因为 HiDPI 是一种超采样技术（HiDPI 原理可以参考&lt;a class="link" href="https://blog.skk.moe/post/hidpi-what-why-how/" target="_blank" rel="noopener"
>这篇文章&lt;/a>）靠渲染更多的像素来使图像“看起来”更清晰。但是大部分远程桌面软件都会将原生分辨率压缩为当前物理屏幕分辨率进行传输，所以开启 HiDPI 除了会浪费计算资源、增加延迟外没有任何意义
&lt;/div>
&lt;p>&lt;/p>
&lt;p>（可选）使用 SwitchResX 关闭默认显示器&lt;br />
这个步骤不是必须的，如果你的远程桌面软件无法选择用于串流的显示器（比如 VNC Viewer）可以关闭默认显示器来强制软件使用虚拟显示器&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 174;
flex-basis: 418px"
>
&lt;a href="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/disable-default-display.jpeg" data-size="469x269">
&lt;img src="https://blog.lv5.moe/p/pve-virtualized-hackintosh-gpu-passthrough-and-remote-access-tutorial/disable-default-display.jpeg"
width="469"
height="269"
loading="lazy"
alt="关闭默认显示器">
&lt;/a>
&lt;figcaption>关闭默认显示器&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="原生方案">原生方案&lt;/h3>
&lt;p>MacOS 原生提供 VNC 和 ARD 两种协议进行远程访问，可以在 &lt;code>设置-&amp;gt;分享&lt;/code> 中开启&lt;/p>
&lt;h4 id="vnc">VNC&lt;/h4>
&lt;p>自带的 VNC 是阉割版的，体验上做的很差：不支持调整画质、分辨率，不支持选择显示器（多显示器会横向拼接显示内容）从 Windows 访问键位映射有问题并且无法修改，卡顿严重，拖动窗口的时候尤其明显。唯一的优点是画质非常好，是本文介绍的所有方案中唯一一个使用原生分辨率传输（开启 HiDPI 画质明显提升）&lt;/p>
&lt;h4 id="ard">ARD&lt;/h4>
&lt;p>ARD 属于是 VNC 套壳，可以使用 Apple 官方的 &lt;a class="link" href="https://apps.apple.com/us/app/apple-remote-desktop/id409907375?mt=12" target="_blank" rel="noopener"
>Apple Remote Desktop&lt;/a> 软件（售价高达 518，不会真有冤大头会买吧。。。）连接黑苹果主机。相比于 VNC，ARD 支持选择显示器，提供 4 挡可调的图像质量，在保证画质的前提下提供不错的延迟表现。并且除了远程桌面以外还提供命令执行、系统报告、文件传输等系统管理功能，缺点是只支持 MacOS（Windows 下可以使用支持 VNC 协议的软件连接，但是会退化成和原生 VNC 一样的垃圾体验）&lt;/p>
&lt;h4 id="自建-vnc-server">自建 VNC Server&lt;/h4>
&lt;p>既然自带的 VNC 如此不堪使用，ARD 又不提供 Windows 客户端，我们只能求助于第三方软件来提供满血版 VNC 协议支持。这里推荐 &lt;a class="link" href="https://www.realvnc.com/en/connect/download/vnc/macos/" target="_blank" rel="noopener"
>Real VNC&lt;/a>，可以兼具 VNC 高画质和 ARD 的低延迟，除了不支持 HiDPI 外基本上能提供和连接显示器一致的体验&lt;/p>
&lt;h3 id="第三方软件私有协议">第三方软件/私有协议&lt;/h3>
&lt;h4 id="todesk">ToDesk&lt;/h4>
&lt;p>ToDesk 在我的体验中卡顿非常严重。除了提供免费的内网穿透以外，相比其他方案基本上毫无优势可言，如果你有公网 IP 的话不要选择它。所以名气大的（尤其是国产软件）不一定真的好用。。。&lt;/p>
&lt;h4 id="parsec">ParSec&lt;/h4>
&lt;p>ParSec 的原本用途是游戏串流，提供精细的配置项可供选择，细节上体验很舒适。相比于其他方案它的延迟和画面质量很稳定，不会在画面快速变化时卡顿或者糊掉，并且支持播放被控主机声音。可能是现在 Mac 版还处于 Beta 阶段的原因，ParSec 对性能要求很高，RX460 在 2560×1440 分辨率下延迟 20ms 左右，3440×1440（2k 带鱼屏）分辨率下延迟 40ms 左右&lt;/p>
&lt;h4 id="jump-desktop">Jump Desktop&lt;/h4>
&lt;p>Jump Desktop 是老牌 mac 远程桌面应用，使用私有的 Fluid 协议。延迟低、带宽占用低，但是画面也是最糊的，特别是窗口拖动等画面快速变化的场景涂抹感非常严重。Jump Desktop 支持自定义任何按键或是组合键的映射，和 ParSec 一样也支持播放被控主机声音&lt;/p>
&lt;h2 id="总结与性能测试">总结与性能测试&lt;/h2>
&lt;p>测试环境：&lt;/p>
&lt;p>GPU： 直通 RX460 GeekBench 5 Metal 分 21000 左右，性能大致相当于 M1 核显&lt;br />
网络：内网（网络延迟 &amp;lt;1ms）&lt;br />
分辨率：3440x1440（非 HiDPI）&lt;br />
软件版本：各软件均使用当前最新免费/试用版，画质选择最高一档&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>软件/协议&lt;/th>
&lt;th>延迟&lt;/th>
&lt;th>画质&lt;/th>
&lt;th>按键映射&lt;/th>
&lt;th>多显示器&lt;/th>
&lt;th>声音&lt;/th>
&lt;th>文件传输&lt;/th>
&lt;th>连接方式&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>原生 VNC&lt;/td>
&lt;td>高&lt;/td>
&lt;td>最好（支持 HiDPI）&lt;/td>
&lt;td>不支持修改&lt;/td>
&lt;td>拼接所有显示器&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>直连&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RealVNC&lt;/td>
&lt;td>低（2ms）&lt;/td>
&lt;td>最好&lt;/td>
&lt;td>自定义任何按键映射&lt;/td>
&lt;td>服务端配置&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>支持&lt;/td>
&lt;td>直连&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ARD&lt;/td>
&lt;td>中等&lt;/td>
&lt;td>一般（滚动时画面会糊）&lt;/td>
&lt;td>不支持修改&lt;/td>
&lt;td>客户端选择&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>支持&lt;/td>
&lt;td>直连&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ToDesk&lt;/td>
&lt;td>高&lt;/td>
&lt;td>一般（有明显涂抹感）&lt;/td>
&lt;td>自定义功能键映射&lt;/td>
&lt;td>客户端选择&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>支持&lt;/td>
&lt;td>免费内网穿透&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ParSec&lt;/td>
&lt;td>中等（40ms）&lt;/td>
&lt;td>较好（有轻微涂抹感）&lt;/td>
&lt;td>不支持修改&lt;/td>
&lt;td>客户端选择&lt;/td>
&lt;td>支持&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>直连（自动 UPNP）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Jump Desktop&lt;/td>
&lt;td>低（10ms）&lt;/td>
&lt;td>差（画面最糊）&lt;/td>
&lt;td>自定义任何按键映射&lt;/td>
&lt;td>客户端选择&lt;/td>
&lt;td>支持&lt;/td>
&lt;td>不支持&lt;/td>
&lt;td>直连（自动 UPNP）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
除 ToDesk 外的方案若想要通过公网访问均需要被控端具备公网 IP 并配置端口映射或自行搭建内网穿透&lt;br />
ParSec 和 Jump Desktop 会通过 UPNP 帮你自动映射端口，并且登陆它们的账号可以直接看到你的被控端主机，不用通过 IP 手动添加主机，尤其适用于你的公网 IP 是动态 IP 的情况
&lt;/div>
&lt;p>&lt;/p></description></item><item><title>RocketMQ 负载均衡时机和影响</title><link>https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence</link><pubDate>Mon, 28 Mar 2022 17:15:00 +0800</pubDate><guid>https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence</guid><description>&lt;img src="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/rocketmq_logo.png" alt="Featured image of post RocketMQ 负载均衡时机和影响" />&lt;p>本文综合 RocketMQ client 与 broker 的源码介绍负载均衡机制发生的时间、客户端发生负载对消费的影响（消息堆积/消费毛刺等）并且给出一些最佳实践的推荐&lt;/p>
&lt;h2 id="写在前面">写在前面&lt;/h2>
&lt;p>网上大多数讲 RocketMQ 负载均衡的文章只介绍几种分配 MessageQueue 的策略或是长篇大论分析客户端 RebalanceService 的代码。但是其实负载均衡是客户端与服务端互相配合的过程，本文综合服务端和客户端代码回答如下三个问题：&lt;/p>
&lt;ol>
&lt;li>何时会发生负载均衡&lt;/li>
&lt;li>负载均衡对消费有何影响&lt;/li>
&lt;li>如何减少负载均衡对消费的影响&lt;/li>
&lt;/ol>
&lt;p>如果不想看详细分析，这里直接给出结论：&lt;/p>
&lt;p>负载均衡时机：&lt;/p>
&lt;ul>
&lt;li>主动负载均衡
&lt;ol>
&lt;li>启动时立即进行负载均衡&lt;/li>
&lt;li>定时（默认 20s）负载均衡一次&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>被动负载均衡（收到 broker 通知）
&lt;ol>
&lt;li>客户端上下线&lt;/li>
&lt;/ol>
&lt;ul>
&lt;li>上线
&lt;ol>
&lt;li>新客户端发送心跳到 broker&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>下线
&lt;ol>
&lt;li>客户端发送下线请求到 broker&lt;/li>
&lt;li>底层连接异常：响应 netty channel 的 IDLE/CLOSE/EXCEPTION 事件&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;ol start="2">
&lt;li>订阅关系变化：订阅新 topic 或有旧的 topic 不再订阅&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;p>负载均衡对消费的影响：&lt;/p>
&lt;ol>
&lt;li>对于新分配的队列可能会重复消费，这也是官方要求消费要做好幂等的原因&lt;/li>
&lt;li>对于不再负责的队列会短时间消费停止，如果原本的消费 TPS 很高或者正好出现生产高峰就会造成消费毛刺&lt;/li>
&lt;/ol>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 180;
flex-basis: 433px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/consume_glitch.png" data-size="725x401">
&lt;img src="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/consume_glitch.png"
width="725"
height="401"
loading="lazy"
alt="消费毛刺（绿色是正常消费曲线，黄色为出现毛刺的消费曲线）">
&lt;/a>
&lt;figcaption>消费毛刺（绿色是正常消费曲线，黄色为出现毛刺的消费曲线）&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="源码分析">源码分析&lt;/h2>
&lt;p>首先明确下上下文：源码分析基于 &lt;a class="link" href="https://github.com/apache/rocketmq/tree/release-4.9.3" target="_blank" rel="noopener"
>RocketMQ release-4.9.3&lt;/a> 分支的代码以及集群消费模式的 push 消费者。广播模式不在本文的讨论范围内&lt;/p>
&lt;h3 id="client-主动触发">Client 主动触发&lt;/h3>
&lt;p>在同一个 JVM 中不管创建多少 consumer，它们总是共享同一个 MQClientInstance，这个 MQClientInstance 接管和所有 consumer 和 broker 的交互以及协调负载均衡&lt;/p>
&lt;div class="mermaid" style="margin: auto; width: 80%;">classDiagram
class MQProducer
&amp;lt;&amp;lt;interface&amp;gt;&amp;gt; MQProducer
MQProducer &amp;lt;|-- DefaultMQProducer
ClientConfig &amp;lt;|-- DefaultMQProducer
DefaultMQProducer: &amp;#43;DefaultMQProducerImpl defaultMQProducerImpl
DefaultMQProducer: &amp;#43;start()
DefaultMQProducer ..&amp;gt; DefaultMQProducerImpl: use
DefaultMQProducerImpl: &amp;#43;MQClientInstance mQClientFactory
DefaultMQProducerImpl: &amp;#43;start()
class MQConsumer
&amp;lt;&amp;lt;interface&amp;gt;&amp;gt; MQConsumer
MQConsumer &amp;lt;|-- DefaultMQPushConsumer
ClientConfig &amp;lt;|-- DefaultMQPushConsumer
DefaultMQPushConsumer: &amp;#43;DefaultMQPushConsumerImpl defaultMQPushConsumerImpl
DefaultMQPushConsumer: &amp;#43;start()
DefaultMQPushConsumer ..&amp;gt; DefaultMQPushConsumerImpl: use
DefaultMQPushConsumerImpl: &amp;#43;MQClientInstance mQClientFactory
DefaultMQPushConsumerImpl: &amp;#43;start()
DefaultMQProducerImpl ..&amp;gt; MQClientInstance: use
DefaultMQPushConsumerImpl ..&amp;gt; MQClientInstance: use
MQClientInstance: &amp;#43;start()
&lt;/div>
&lt;p>MQClientInstance 有两个负载均衡相关的方法：&lt;code>rebalanceImmediately&lt;/code> 和 &lt;code>doRebalance&lt;/code>&lt;br />
前者在消费者启动和收到 Broker 通知时唤醒 RebalanceService 进行负载均衡，而 RebalanceService 调用后者执行负载均衡逻辑&lt;br />
跟踪 &lt;code>doRebalance&lt;/code> 方法，我们发现实际的负载均衡逻辑在 &lt;code>RebalanceImpl#rebalanceByTopic&lt;/code> 中实现：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">rebalanceByTopic&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kd">final&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="n">isOrder&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 获取所有 MessageQueue
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Set&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageQueue&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">mqSet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">topicSubscribeInfoTable&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">topic&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 获取当前 group 所有在线的消费者
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">cidAll&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">mQClientFactory&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">findConsumerIdList&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">consumerGroup&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 排序
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Collections&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">sort&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mqAll&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Collections&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">sort&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">cidAll&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 获取当前客户端配置的负载均衡策略
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">AllocateMessageQueueStrategy&lt;/span> &lt;span class="n">strategy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">allocateMessageQueueStrategy&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageQueue&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">allocateResult&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 根据指定的负载均衡策略计算自己要负责的队列
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">allocateResult&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">strategy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">allocate&lt;/span>&lt;span class="o">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">consumerGroup&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">mQClientFactory&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getClientId&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mqAll&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 根据计算结果创建 ProcessQueue （用于拉取、消费消息的数据结构）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="n">changed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">updateProcessQueueTableInRebalance&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">allocateResultSet&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">isOrder&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">changed&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 如果 ProcessQueue 有更新则进行一些处理工作
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">messageQueueChanged&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">mqSet&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">allocateResultSet&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>通过分析以上代码我们可以看出负载均衡的实际工作是计算出当前客户端分配的 MessageQueue，并且保持 ProcessQueue 与分配到的 MessageQueue 一一对应，创建 ProcessQueue 的具体流程在 &lt;code>RebalanceImpl#updateProcessQueueTableInRebalance&lt;/code> 方法中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="nf">updateProcessQueueTableInRebalance&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kd">final&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="n">topic&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">Set&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageQueue&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">mqSet&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="n">isOrder&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 删除不再负责的 MessageQueue 对应的 ProcessQueue
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Iterator&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Entry&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageQueue&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">ProcessQueue&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">it&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">processQueueTable&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">entrySet&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">iterator&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">hasNext&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Entry&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageQueue&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">ProcessQueue&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">next&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MessageQueue&lt;/span> &lt;span class="n">mq&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">next&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getKey&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ProcessQueue&lt;/span> &lt;span class="n">pq&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">next&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getValue&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">mq&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getTopic&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">equals&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">topic&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(!&lt;/span>&lt;span class="n">mqSet&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">contains&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mq&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 停止 ProcessQueue 的消费
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">pq&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">setDropped&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 向 Broker 更新 offset，如果是顺序消费会释放申请的锁
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">removeUnnecessaryMessageQueue&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mq&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pq&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">it&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">remove&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">changed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">log&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">info&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s">&amp;#34;doRebalance, {}, remove unnecessary mq, {}&amp;#34;&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">consumerGroup&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">mq&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 为新分配的 MessageQueue 创建 ProcessQueue
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">PullRequest&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">pullRequestList&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">ArrayList&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">PullRequest&lt;/span>&lt;span class="o">&amp;gt;();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">MessageQueue&lt;/span> &lt;span class="n">mq&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="n">mqSet&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">processQueueTable&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">containsKey&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mq&lt;/span>&lt;span class="o">))&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ProcessQueue&lt;/span> &lt;span class="n">pq&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">ProcessQueue&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">nextOffset&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">0&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ProcessQueue&lt;/span> &lt;span class="n">pre&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">processQueueTable&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">putIfAbsent&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">mq&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">pq&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="broker-通知触发">Broker 通知触发&lt;/h3>
&lt;p>通过上文我们知道除了 RebalanceService 定时进行负载均衡以外，还可以调用 &lt;code>MQClientInstance#rebalanceImmediately&lt;/code> 方法立刻触发负载均衡。这个方法会在消费者启动和收到 Broker 通知时调用（&lt;code>ClientRemotingProcessor#notifyConsumerIdsChanged&lt;/code>）&lt;br />
而 Broker 会在如下几个方法中发送通知：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 474;
flex-basis: 1139px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/notify_timing.png" data-size="660x139">
&lt;img src="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/notify_timing.png"
width="660"
height="139"
loading="lazy"
alt="Broker 通知">
&lt;/a>
&lt;figcaption>Broker 通知&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>我们依次分析这三个方法：&lt;/p>
&lt;p>&lt;code>ConsumerManager#registerConsumer&lt;/code> 方法处理来自客户端的心跳请求&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="nf">registerConsumer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kd">final&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="n">group&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">ClientChannelInfo&lt;/span> &lt;span class="n">clientChannelInfo&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ConsumeType&lt;/span> &lt;span class="n">consumeType&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">MessageModel&lt;/span> &lt;span class="n">messageModel&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">ConsumeFromWhere&lt;/span> &lt;span class="n">consumeFromWhere&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">Set&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SubscriptionData&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">subList&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="n">isNotifyConsumerIdsChangedEnable&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 是否是新客户端
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="n">r1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">consumerGroupInfo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">updateChannel&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">clientChannelInfo&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">consumeType&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">messageModel&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">consumeFromWhere&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 更新订阅数据，当订阅新 topic 或有旧的 topic 不再订阅时返回 true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">boolean&lt;/span> &lt;span class="n">r2&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">consumerGroupInfo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">updateSubscription&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">subList&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">r1&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="n">r2&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 可以在 BrokerConfig 中配置，默认为 true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">isNotifyConsumerIdsChangedEnable&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 通知客户端进行负载均衡
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">consumerIdsChangeListener&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">handle&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">ConsumerGroupEvent&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">CHANGE&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">group&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">consumerGroupInfo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getAllChannel&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ConsumerManager#unregisterConsumer&lt;/code> 方法处理来自客户端的下线请求，和 &lt;code>ConsumerManager#registerConsumer&lt;/code> 类似，不再赘述&lt;/p>
&lt;p>&lt;code>ConsumerManager#doChannelCloseEvent&lt;/code> 方法在如下三个方法中调用&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 802;
flex-basis: 1926px"
>
&lt;a href="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/channel_close.png" data-size="570x71">
&lt;img src="https://blog.lv5.moe/p/rocketmq-rebalancing-timing-and-influence/channel_close.png"
width="570"
height="71"
loading="lazy"
alt="Channel Close">
&lt;/a>
&lt;figcaption>Channel Close&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>这三个方法在 &lt;code>NettyRemotingAbstract#run&lt;/code> 中调用，处理 netty channel 的事件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">run&lt;/span>&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">ChannelEventListener&lt;/span> &lt;span class="n">listener&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">NettyRemotingAbstract&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getChannelEventListener&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="o">(!&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">isStopped&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">NettyEvent&lt;/span> &lt;span class="n">event&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">eventQueue&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">poll&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">3000&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">TimeUnit&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">MILLISECONDS&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">event&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">listener&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">switch&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getType&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="n">IDLE&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listener&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">onChannelIdle&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getRemoteAddr&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getChannel&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="n">CLOSE&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listener&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">onChannelClose&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getRemoteAddr&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getChannel&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="n">CONNECT&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listener&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">onChannelConnect&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getRemoteAddr&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getChannel&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="n">EXCEPTION&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">listener&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">onChannelException&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getRemoteAddr&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getChannel&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">Exception&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">log&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">warn&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getServiceName&lt;/span>&lt;span class="o">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s">&amp;#34; service has exception. &amp;#34;&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>也就是说 channel 事件 IDLE/CLOSE/EXCEPTION 都会触发重新负载均衡，除了底层连接的变化外 Broker 也会在长时间没收到客户端心跳时主动断开连接触发负载均衡，详见 &lt;code>ConsumerManager#scanNotActiveChannel&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kt">void&lt;/span> &lt;span class="nf">scanNotActiveChannel&lt;/span>&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Iterator&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Entry&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Channel&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">ClientChannelInfo&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">itChannel&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">channelInfoTable&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">entrySet&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">iterator&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">itChannel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">hasNext&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Entry&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Channel&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">ClientChannelInfo&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">nextChannel&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">itChannel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">next&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ClientChannelInfo&lt;/span> &lt;span class="n">clientChannelInfo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">nextChannel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getValue&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">long&lt;/span> &lt;span class="n">diff&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">currentTimeMillis&lt;/span>&lt;span class="o">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">clientChannelInfo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getLastUpdateTimestamp&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 默认两分钟
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">diff&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">CHANNEL_EXPIRED_TIMEOUT&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 关闭连接
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">RemotingUtil&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">closeChannel&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">clientChannelInfo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getChannel&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">itChannel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">remove&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后总结一下在如下场景 Broker 会主动通知客户端触发负载均衡：&lt;/p>
&lt;ol>
&lt;li>客户端上下线
&lt;ul>
&lt;li>上线
&lt;ol>
&lt;li>新客户端发送心跳到 broker&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>下线
&lt;ol>
&lt;li>新客户端发送下线请求到 broker&lt;/li>
&lt;li>底层连接异常：响应 netty channel 的 IDLE/CLOSE/EXCEPTION 事件&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>订阅关系变化：订阅新 topic 或有旧的 topic 不再订阅&lt;/li>
&lt;/ol>
&lt;h2 id="最佳实践">最佳实践&lt;/h2>
&lt;h3 id="避免频繁上下线">避免频繁上下线&lt;/h3>
&lt;p>从上述源码分析可以看出，负载均衡是一个非常频繁的操作，并不是每一次负载均衡都会对消费产生影响。负载均衡对客户端的影响反映在对 ProcessQueue 的变更上。因为没有一个协调机制确保新旧 ProcessQueue 的创建顺序，所以可能会发生两种情况：&lt;/p>
&lt;ol>
&lt;li>旧 ProcessQueue 销毁早于新 ProcessQueue 创建：此时消费短暂停止，会造成消费延迟&lt;/li>
&lt;li>新 ProcessQueue 创建早于旧 ProcessQueue 销毁：此时会有短暂的时间两个消费者同时消费同一个队列，会消费位点回退或重复消费，这也是官方要求消费要做好幂等的原因&lt;/li>
&lt;/ol>
&lt;p>所以为了避免负载均衡的影响应该尽量减少客户端的上下线，同时做好消费幂等&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
在有应用重启或下线前要调用 &lt;code>shutdown&lt;/code> 方法，这样 Broker 收到客户端的下线请求后会立刻触发负载均衡
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="选择合适的负载均衡策略">选择合适的负载均衡策略&lt;/h3>
&lt;p>前文已经介绍了负载均衡对客户端的影响反映在对 ProcessQueue 的变更上，每次需要变更的 ProcessQueue 的数量和受到影响的客户端数量是由负载均衡策略决定的&lt;/p>
&lt;p>如果我们有 4 个客户端，24 个队列，当第二个客户端下线时：&lt;/p>
&lt;p>以默认的负载均衡策略（AllocateMessageQueueAveragely）为例，重建 ProcessQueue 的队列数量为 8&lt;br />
默认的负载均衡策略能将队列尽量均衡的分配到每个客户端，但是每次负载均衡重建 ProcessQueue 数量较多，尤其是在客户端数量很多的场景&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>客户端&lt;/th>
&lt;th>队列分配变化&lt;/th>
&lt;th>队列数变化&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Client1&lt;/td>
&lt;td>1~6 -&amp;gt; 1~8&lt;/td>
&lt;td>6 -&amp;gt; 8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client2&lt;/td>
&lt;td>7~12 -&amp;gt; -&lt;/td>
&lt;td>6 -&amp;gt; 0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client3&lt;/td>
&lt;td>13~18 -&amp;gt; 9~16&lt;/td>
&lt;td>6 -&amp;gt; 8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client4&lt;/td>
&lt;td>19~24 -&amp;gt; 17~24&lt;/td>
&lt;td>6 -&amp;gt; 8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>以一致性哈希算法（AllocateMessageQueueConsistentHash）为例，重建 ProcessQueue 的队列数量为 6（不考虑虚拟节点）&lt;br />
基于一致性哈希算法的负载均衡策略每次负载均衡会重建尽可能少的 ProcessQueue 数量，但是可能会出现负载不均的情况&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>客户端&lt;/th>
&lt;th>队列分配变化&lt;/th>
&lt;th>队列数变化&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Client1&lt;/td>
&lt;td>1~6 -&amp;gt; 1~9&lt;/td>
&lt;td>6 -&amp;gt; 9&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client2&lt;/td>
&lt;td>7~12 -&amp;gt; -&lt;/td>
&lt;td>6 -&amp;gt; 0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client3&lt;/td>
&lt;td>13~18 -&amp;gt; 10~18&lt;/td>
&lt;td>6 -&amp;gt; 9&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client4&lt;/td>
&lt;td>19~24 -&amp;gt; 19~24&lt;/td>
&lt;td>6 -&amp;gt; 8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
以上两个示例是对简单情况的模拟，在生产中需要根据需要选择合适的负载均衡策略
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="客户端参数保持一致">客户端参数保持一致&lt;/h3>
&lt;p>RocketMQ 的负载均衡是每个客户端独立进行计算，所以务必要保证每个客户端的负载均衡算法和订阅语句一致&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock error">
负载均衡策略不一致会导致多个客户端分配到相同队列或有客户端分不到队列&lt;br />
订阅语句不一致会导致有消息未能消费
&lt;/div>
&lt;p>&lt;/p></description></item><item><title>Spring + Kotlin ORM 框架 Exposed 教程</title><link>https://blog.lv5.moe/p/guide-to-spring-and-kotlin-orm-framework-exposed</link><pubDate>Sat, 19 Mar 2022 16:34:00 +0800</pubDate><guid>https://blog.lv5.moe/p/guide-to-spring-and-kotlin-orm-framework-exposed</guid><description>&lt;img src="https://blog.lv5.moe/p/guide-to-spring-and-kotlin-orm-framework-exposed/exposed_logo.jpg" alt="Featured image of post Spring + Kotlin ORM 框架 Exposed 教程" />&lt;p>本教程包括 Kotlin ORM 框架 Exposed 的使用方法和一些进阶技巧，并介绍 Exposed 与 Spring 集成的方法以及博主踩过的一些坑&lt;/p>
&lt;h2 id="exposed-介绍">Exposed 介绍&lt;/h2>
&lt;p>&lt;a class="link" href="https://github.com/JetBrains/Exposed" target="_blank" rel="noopener"
>Exposed&lt;/a> 是 JetBrains 官方出品的 Kotlin ORM 框架，有如下优点：&lt;/p>
&lt;ol>
&lt;li>支持多种数据库：H2、MySQL、PostgreSQL、SQL Server、SQLite 等&lt;/li>
&lt;li>提供两套 API：SQL DSL 和 DAO API（不知道什么是 DSL 可以阅读我的之前的文章：&lt;a class="link" href="https://blog.lv5.moe/p/introduction-to-kotlin-dsl" >Kotlin DSL 简介&lt;/a>）&lt;/li>
&lt;li>JetBrains 官方出品，文档完善，易于使用&lt;/li>
&lt;/ol>
&lt;h3 id="基本概念">基本概念&lt;/h3>
&lt;h4 id="连接数据库">连接数据库&lt;/h4>
&lt;p>首先需要引入依赖：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.jetbrains.exposed&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>exposed-core&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>0.37.3&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.jetbrains.exposed&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>exposed-dao&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>0.37.3&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.jetbrains.exposed&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>exposed-jdbc&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>0.37.3&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后建立数据库连接：&lt;br />
关于数据库和数据源的更多说明查看&lt;a class="link" href="https://github.com/JetBrains/Exposed/wiki/DataBase-and-DataSource" target="_blank" rel="noopener"
>官方 WIKI&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Database&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">connect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jdbc:h2:mem:test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">driver&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;org.h2.Driver&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后开启事务操作数据库：&lt;br />
不论是 SQL DSL 还是 DAO API 都需要在 &lt;code>transaction&lt;/code> 块中执行&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">addLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">StdOutSqlLogger&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Do something
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Do something
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">rollback&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>事务支持返回结果：&lt;br />
Blob、text 以及一对多/多对一的字段不能在事务外使用，更多说明查看&lt;a class="link" href="https://github.com/JetBrains/Exposed/wiki/Transactions" target="_blank" rel="noopener"
>官方 WIKI&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="py">result&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="tabledao">Table/DAO&lt;/h4>
&lt;p>建表：&lt;br />
如下的 Queries 类创建一个名为 query 的表，并添加了 4 个字段&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">object&lt;/span> &lt;span class="nc">Queries&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntIdTable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">varchar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">1024&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">userId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">varchar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">256&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">varchar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">256&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">createTime&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">timestamp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;createTime&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Exposed 不会自动生成 migration，但是可以使用 &lt;code>SchemaUtils#create&lt;/code> 来在数据库中运行建表语句&lt;br />
官方提供一个 gradle 插件来根据数据库结构生成 Table 代码：&lt;a class="link" href="https://github.com/JetBrains/exposed-intellij-plugin" target="_blank" rel="noopener"
>exposed-intellij-plugin&lt;/a>&lt;/p>
&lt;p>创建实体类：&lt;br />
如果要使用 Exposed 的 DAO API，需要创建表对应的实体（Queries -&amp;gt; QueryEntity）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">QueryEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Int&lt;/span>&lt;span class="p">&amp;gt;)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">companion&lt;/span> &lt;span class="k">object&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntityClass&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">Queries&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">title&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">title&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">userId&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">userId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">type&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">type&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">createTime&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">createTime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="crud">CRUD&lt;/h4>
&lt;p>这里给出两种 API 的简单示例：&lt;/p>
&lt;p>SQL DSL：&lt;br />
&lt;a class="link" href="https://github.com/JetBrains/Exposed/wiki/DSL" target="_blank" rel="noopener"
>文档&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">insert&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">userId&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;123&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">createTime&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Instant&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">select&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span> &lt;span class="n">eq&lt;/span> &lt;span class="m">1&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">update&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;titleUpdate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">userId&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;456&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;typeUpdate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">createTime&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Instant&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">deleteWhere&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span> &lt;span class="n">eq&lt;/span> &lt;span class="m">1&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DAO API：&lt;br />
&lt;a class="link" href="https://github.com/JetBrains/Exposed/wiki/DAO" target="_blank" rel="noopener"
>文档&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">new&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">userId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;123&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">createTime&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Instant&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">result&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">?.&lt;/span>&lt;span class="n">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;titleUpdate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">?.&lt;/span>&lt;span class="n">userId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;456&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">?.&lt;/span>&lt;span class="n">type&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;titleUpdate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">?.&lt;/span>&lt;span class="n">createTime&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Instant&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">?.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="进阶使用">进阶使用&lt;/h3>
&lt;h4 id="索引">索引&lt;/h4>
&lt;p>Exposed 支持创建单列/多列索引：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">object&lt;/span> &lt;span class="nc">Queries&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntIdTable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">userId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">varchar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">256&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">userId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">varchar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">256&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">uniqueIndex&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">userId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">varchar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">256&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;index_userId_unique&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">true&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">index&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">index&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;index_title_userId_unique&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">title&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">userId&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="一对多多对一">一对多/多对一&lt;/h4>
&lt;p>首先创建外键：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">object&lt;/span> &lt;span class="nc">Histories&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntIdTable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;history&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">queryId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">reference&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;query_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">onDelete&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">ReferenceOption&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">CASCADE&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后在实体类上添加相应的字段：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">QueryEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Int&lt;/span>&lt;span class="p">&amp;gt;)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 一对多
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">val&lt;/span> &lt;span class="py">histories&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">HistoryEntity&lt;/span> &lt;span class="n">referrersOn&lt;/span> &lt;span class="n">Histories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">queryId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">HistoryEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Int&lt;/span>&lt;span class="p">&amp;gt;)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 多对一
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">var&lt;/span> &lt;span class="py">query&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">QueryEntity&lt;/span> &lt;span class="n">referencedOn&lt;/span> &lt;span class="n">Histories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">queryId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在查询中即可直接访问对应字段：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 使用 load 提前加载 histories 字段，避免 N+1 问题
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">QueryEntity&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">histories&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?.&lt;/span>&lt;span class="n">histories&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?.&lt;/span>&lt;span class="n">forEach&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// do something
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="upsert">upsert&lt;/h4>
&lt;p>Exposed 并没有提供开箱即用的 upsert 功能（类似 MySQL 的 ON DUPLICATE KEY UPDATE），需要自己拓展（详见这个 &lt;a class="link" href="https://github.com/JetBrains/Exposed/issues/167" target="_blank" rel="noopener"
>Issue&lt;/a>）&lt;/p>
&lt;p>这里推荐一个库帮我们实现了 upsert：&lt;a class="link" href="https://github.com/reposilite-playground/exposed-upsert" target="_blank" rel="noopener"
>exposed-upsert&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 也可以使用 conflictIndex 指定自行创建的唯一索引
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">upsert&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">conflictColumn&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">userId&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">insertBody&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">queryData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">title&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">userId&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">queryData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">userId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">queryData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">type&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">createTime&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Instant&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span> &lt;span class="n">updateBody&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">queryData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">title&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">it&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">type&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">queryData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">type&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="exposed-与-spring-集成">Exposed 与 Spring 集成&lt;/h2>
&lt;p>官方提供 Exposed Spring Boot Starter，用 Exposed 替换 Hibernate&lt;/p>
&lt;h3 id="配置">配置&lt;/h3>
&lt;p>引入依赖：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependencies&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.jetbrains.exposed&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>exposed-spring-boot-starter&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>0.37.3&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependencies&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配置数据库：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spring&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">datasource&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jdbc:h2:mem:testdb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driverClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">org.h2.Driver&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exposed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 自动在数据库中建表&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">generate-ddl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="transaction">transaction&lt;/h3>
&lt;p>Exposed 的两种 API 都需要在 &lt;code>transaction&lt;/code> 块中执行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">transaction&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">all&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 spring 中可以用 Transactional 注解代替 transaction 块：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Transactional&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">all&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">all&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">toList&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果需要调用 rollback、commit 等方法需要使用 &lt;code>TransactionManager#current&lt;/code> 获取当前 Transaction 实例&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Transactional&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">rollback&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// Do something
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">TransactionManager&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">current&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">rollback&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="json-转换">json 转换&lt;/h3>
&lt;p>这里以 spring 默认使用的 json 库 jackson 为例，gson 或 fastjson 原理上是一样的&lt;/p>
&lt;p>首先回顾下我们之前创建的实体类 QueryEntity：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">QueryEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Int&lt;/span>&lt;span class="p">&amp;gt;)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">companion&lt;/span> &lt;span class="k">object&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntityClass&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">QueryEntity&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">Queries&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">title&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">title&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">userId&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">userId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">type&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">type&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">var&lt;/span> &lt;span class="py">createTime&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">Queries&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">createTime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">histories&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">HistoryEntity&lt;/span> &lt;span class="n">referrersOn&lt;/span> &lt;span class="n">Histories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">queryId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以发现实体类 QueryEntity 继承自 IntEntity，序列化/反序列化时我们只需要非 IntEntity 类的字段，而 IntEntity 又继承自 Entity，我的做法是让 jackson 忽略所有属于 Entity 或 IntEntity 类的字段：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">JacksonConfig&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">Jackson2ObjectMapperBuilderCustomizer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">customize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">jacksonObjectMapperBuilder&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Jackson2ObjectMapperBuilder&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">jacksonObjectMapperBuilder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">modulesToInstall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">object&lt;/span> &lt;span class="err">:&lt;/span>&lt;span class="nc">Module&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">version&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="n">Version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">getModuleName&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">setupModule&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SetupContext&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">insertAnnotationIntrospector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">object&lt;/span> &lt;span class="err">: &lt;/span>&lt;span class="nc">JacksonAnnotationIntrospector&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">hasIgnoreMarker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">AnnotatedMember&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="n">Boolean&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">declaringClass&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="k">class&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">java&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">||&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">declaringClass&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">Entity&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="k">class&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">java&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">||&lt;/span> &lt;span class="k">super&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">hasIgnoreMarker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
不能直接使用 &lt;code>Jackson2ObjectMapperBuilder#annotationIntrospector&lt;/code> 注入 annotationIntrospector，否则会使 KotlinModule 的 annotationIntrospector 失效
&lt;/div>
&lt;p>&lt;/p>
&lt;p>如果需要序列化 &lt;code>id&lt;/code> 字段，需要允许 id 和 getId 字段参与序列化。因为 id 字段不是 Int 或 String 等基础类型，所以我们还需要自定义序列化器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">JacksonConfig&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">Jackson2ObjectMapperBuilderCustomizer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">customize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">jacksonObjectMapperBuilder&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Jackson2ObjectMapperBuilder&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">jacksonObjectMapperBuilder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">serializerByType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">EntityID&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="k">class&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">java&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">object&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">JsonSerializer&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;*&amp;gt;&amp;gt;()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">serialize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;*&amp;gt;?,&lt;/span> &lt;span class="n">gen&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">JsonGenerator&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">serializers&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SerializerProvider&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="k">null&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">writeNull&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="n">Int&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">writeNumber&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">Int&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="n">Long&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">writeNumber&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">Long&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">gen&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">writeString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">toString&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">jacksonObjectMapperBuilder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">modulesToInstall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">object&lt;/span> &lt;span class="err">:&lt;/span>&lt;span class="nc">Module&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">version&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="n">Version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">getModuleName&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="n">String&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">setupModule&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SetupContext&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">insertAnnotationIntrospector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">object&lt;/span> &lt;span class="err">: &lt;/span>&lt;span class="nc">JacksonAnnotationIntrospector&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">override&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">hasIgnoreMarker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">AnnotatedMember&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="n">Boolean&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;getId&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">declaringClass&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="k">class&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">java&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">||&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">name&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;getId&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">declaringClass&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">Entity&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="k">class&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">java&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">||&lt;/span> &lt;span class="k">super&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">hasIgnoreMarker&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>因为委托属性不可以直接使用 JsonIgnore、JsonInclude 等注解，需要使用 &lt;code>@get:JsonIgnore&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Kotlin" data-lang="Kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">QueryEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EntityID&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Int&lt;/span>&lt;span class="p">&amp;gt;)&lt;/span> &lt;span class="p">:&lt;/span> &lt;span class="n">IntEntity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">..&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@get&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">JsonIgnore&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">histories&lt;/span> &lt;span class="k">by&lt;/span> &lt;span class="n">HistoryEntity&lt;/span> &lt;span class="n">referrersOn&lt;/span> &lt;span class="n">Histories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">queryId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>RocketMQ 消息堆积算法详解与优化</title><link>https://blog.lv5.moe/p/explanation-and-optimization-of-apache-rocketmq-lag</link><pubDate>Thu, 16 Sep 2021 15:39:06 +0800</pubDate><guid>https://blog.lv5.moe/p/explanation-and-optimization-of-apache-rocketmq-lag</guid><description>&lt;img src="https://blog.lv5.moe/img/keqing.jpg" alt="Featured image of post RocketMQ 消息堆积算法详解与优化" />&lt;h2 id="背景">背景&lt;/h2>
&lt;p>消息堆积是消息中间件的一大特色，也是消息中间件的核心能力。但是消息堆积也是消费滞后的一种表现形式，过量的堆积难免会影响上下游的业务。所以对消费堆积量和延迟时间的监控意义重大，本文详解 RocketMQ 消费堆积量和消息消费延时这两个重要指标的含义和算法实现，说明其在对消息延时要求较高和启用消息过滤等场景中的局限并给出优化方法&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
本文适用于集群消费模式下使用 DefaultMQPushConsumer 进行消费的情况
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="消费位点的管理consumeroffsetpulloffset-和-maxoffset">消费位点的管理：ConsumerOffset、PullOffset 和 MaxOffset&lt;/h2>
&lt;p>RocketMQ 中每个 Topic 都会创建若干个 ConsumerQueue。消费者订阅某个 Topic 实际上会分配到一个或几个 ConsumerQueue，然后消费 ConsumerQueue 上的消息，所以 RocketMQ 对消费位点的管理也是基于 ConsumerQueue 的&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 244;
flex-basis: 587px"
>
&lt;a href="https://blog.lv5.moe/p/explanation-and-optimization-of-apache-rocketmq-lag/consumerQueue.png" data-size="746x305">
&lt;img src="https://blog.lv5.moe/p/explanation-and-optimization-of-apache-rocketmq-lag/consumerQueue.png"
width="746"
height="305"
loading="lazy"
alt="ConsumerQueue">
&lt;/a>
&lt;figcaption>ConsumerQueue&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>上图中展示了三个和消费进度有关的位点：&lt;/p>
&lt;ul>
&lt;li>ConsumerOffset：消费者确认消费成功的位点，也称为 CommitOffset&lt;/li>
&lt;li>PullOffset：消费者拉取消息的位点&lt;/li>
&lt;li>MaxOffset：消费者可以消费到的最大位点&lt;/li>
&lt;/ul>
&lt;p>下面详细解释这三个位点的计算方法&lt;/p>
&lt;h3 id="consumeroffset">ConsumerOffset&lt;/h3>
&lt;p>消费者在每次拉取消息的请求中都会设置一个 commitOffset 字段（PullMessageRequestHeader#commitOffset）Broker 根据这个字段调用 ConsumerOffsetManager#commitOffset 来更新 offsetTable。offsetTable 的储存结构如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;offsetTable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// topic@consumerGroup
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nt">&amp;#34;test-topic@test-group&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// queueId: consumerOffset
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nt">&amp;#34;0&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">88526&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">88528&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下面我们来分析一下消费者拉取请求中的 PullMessageRequestHeader#commitOffset 字段是如何计算出来的：&lt;/p>
&lt;p>PullMessageRequestHeader 是在 PullAPIWrapper#pullKernelImpl 方法中构造的，而这个方法被 DefaultMQPushConsumerImpl#pullMessage 调用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">long&lt;/span> &lt;span class="n">commitOffsetValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">0L&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">MessageModel&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">CLUSTERING&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">defaultMQPushConsumer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMessageModel&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">commitOffsetValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">offsetStore&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">readOffset&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">pullRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMessageQueue&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">ReadOffsetType&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">READ_FROM_MEMORY&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">commitOffsetValue&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">0&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">commitOffsetEnable&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">pullAPIWrapper&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">pullKernelImpl&lt;/span>&lt;span class="o">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pullRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMessageQueue&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subExpression&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subscriptionData&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getExpressionType&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subscriptionData&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getSubVersion&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pullRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getNextOffset&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">defaultMQPushConsumer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getPullBatchSize&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sysFlag&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">commitOffsetValue&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BROKER_SUSPEND_MAX_TIME_MILLIS&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CommunicationMode&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">ASYNC&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pullCallback&lt;/span>&lt;span class="o">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">subProperties&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>消费者在内存中维护了一个 offsetStore，每次拉取消息时从 offsetStore 中读取 commitOffset 发送给 Broker&lt;/p>
&lt;p>那么问题就变成了 offsetStore 是如何维护位点信息的：集群模式下 offsetStore 的实现是 RemoteBrokerOffsetStore，消费者处理消费结果时 ConsumeMessageConcurrentlyService#processConsumeResult 会调用 RemoteBrokerOffsetStore#updateOffset 方法更新位点：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">long&lt;/span> &lt;span class="n">offset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">consumeRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getProcessQueue&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">removeMessage&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">consumeRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMsgs&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">offset&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">0&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">consumeRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getProcessQueue&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">isDropped&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">defaultMQPushConsumerImpl&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getOffsetStore&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">updateOffset&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">consumeRequest&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMessageQueue&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">offset&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分析上面代码可以看出更新的位点通过调用 ProcessQueue#removeMessage 获取。ProcessQueue 持有一个 TreeMap 作为消息本地缓存，消费者每次拉取的消息都会先缓存在 ProcessQueue 中，当消费完成时再从 ProcessQueue 中移除对应的消息：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm">* message list, contains message in waiting &amp;amp; consuming list
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm">*/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">TreeMap&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">MessageExt&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">msgTreeMap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">TreeMap&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Long&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="n">MessageExt&lt;/span>&lt;span class="o">&amp;gt;();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kt">long&lt;/span> &lt;span class="nf">removeMessage&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kd">final&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">MessageExt&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">msgs&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 省略消息删除过程
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="n">msgTreeMap&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">isEmpty&lt;/span>&lt;span class="o">()&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">queueOffsetMax&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">1&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="n">msgTreeMap&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">firstKey&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ProcessQueue#removeMessage 的返回值是当前缓存的消息中最小的位点，也就是说消费者每次更新的 commitOffset 是&lt;strong>当前还未消费的第一个消息的位点&lt;/strong>：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 389;
flex-basis: 935px"
>
&lt;a href="https://blog.lv5.moe/p/explanation-and-optimization-of-apache-rocketmq-lag/msgTreeMap.png" data-size="748x192">
&lt;img src="https://blog.lv5.moe/p/explanation-and-optimization-of-apache-rocketmq-lag/msgTreeMap.png"
width="748"
height="192"
loading="lazy"
alt="ProcessQueue#msgTreeMap">
&lt;/a>
&lt;figcaption>ProcessQueue#msgTreeMap&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>如上图所示的情况下更新的位点应该为 1 而不是 6&lt;/p>
&lt;h3 id="pulloffset">PullOffset&lt;/h3>
&lt;p>Broker 在回复消费者拉取消息的响应中有 nextBeginOffset 字段（PullResult#nextBeginOffset）这个字段也就是 PullOffset&lt;/p>
&lt;p>分析 PullMessageProcessor#processRequest 可以发现 nextBeginOffset 字段是从 DefaultMessageStore#getMessage 方法的返回值中提取的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="n">getMessageResult&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">brokerController&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMessageStore&lt;/span>&lt;span class="o">().&lt;/span>&lt;span class="na">getMessage&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">requestHeader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getConsumerGroup&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requestHeader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getTopic&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">requestHeader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getQueueId&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">requestHeader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getQueueOffset&lt;/span>&lt;span class="o">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requestHeader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getMaxMsgNums&lt;/span>&lt;span class="o">(),&lt;/span> &lt;span class="n">messageFilter&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">responseHeader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">setNextBeginOffset&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">getMessageResult&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getNextBeginOffset&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分析 DefaultMessageStore#getMessage 方法发现 nextBeginOffset 是当前拉取到的消息的下一条消息的位点，这段源码太复杂就不贴出来了&lt;/p>
&lt;h3 id="maxoffset">MaxOffset&lt;/h3>
&lt;p>RocketMQ 的消息是先储存在 CommitLog 中然后由 ReputMessageService 异步构建 ConsumerQueue 和 IndexFile，最终调用 ConsumeQueue#putMessagePositionInfo 写入磁盘&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">byteBufferIndex&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">flip&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">byteBufferIndex&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">limit&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">CQ_STORE_UNIT_SIZE&lt;/span>&lt;span class="o">);&lt;/span> &lt;span class="c1">// CQ_STORE_UNIT_SIZE = 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">byteBufferIndex&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">putLong&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">offset&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">byteBufferIndex&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">putInt&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">size&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">byteBufferIndex&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">putLong&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">tagsCode&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">MappedFile&lt;/span> &lt;span class="n">mappedFile&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">mappedFileQueue&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getLastMappedFile&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">expectLogicOffset&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">mappedFile&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">mappedFile&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">appendMessage&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">byteBufferIndex&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">array&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里可以发现 ConsumeQueue 的每个 item 都是固定长度 20 字节。所以 MaxOffset 的计算就很简单了，用 ConsumeQueue 文件大小除以 20 即可&lt;/p>
&lt;h2 id="消费堆积算法">消费堆积算法&lt;/h2>
&lt;p>有了上述位点就可以很容易的算出三种消息堆积量：&lt;/p>
&lt;div>
$$
\begin{aligned}
未确认的消息量：ConsumerLag &amp;= &amp;MaxOffset\ -\ &amp;ConsumerOffset \\
正在消费的消息量：InflightMessageCount &amp;= &amp;PullOffset\ -\ &amp;ConsumerOffset \\
等待拉取的消息量：AvailableMessageCount &amp;= &amp;MaxOffset\ -\ &amp;PullOffset
\end{aligned}
$$
&lt;/div>
&lt;p>RocketMQ 中默认的消费堆积是上式中的 ConsumerLag&lt;/p>
&lt;p>消息延时的算法类似，将上式中的 offset 换成对应位点消息的时间即可&lt;/p>
&lt;h2 id="堆积计算优化">堆积计算优化&lt;/h2>
&lt;h3 id="消息延时要求较高的场景">消息延时要求较高的场景&lt;/h3>
&lt;p>通过对 ConsumerOffset 计算方法的分析可以看出 ConsumerOffset 的更新是存在延迟的，这个延迟分为两个方面：&lt;/p>
&lt;ol>
&lt;li>拉取延时：ConsumerOffset 在每次拉取新消息时更新，而 RocketMQ 是使用长轮询方式更新消息，每次长轮询的默认超时时间是 30s。也就是说如果没有足够数量的消息产生，ConsumerOffset 要 30s 才能更新一次&lt;/li>
&lt;li>消费延时：消费者每次提交的 commitOffset 字段是当前还未消费的第一个消息的位点而不是最后一个消费成功的消息的位点。之前的消息未能结束消费的情况下后面已经消费完的消息位点就迟迟得不到更新&lt;/li>
&lt;/ol>
&lt;p>所以在消息数量很少但是消费速度很慢的场景下由 ConsumerOffset 计算出来的消息的堆积量和延迟时间的指标就会虚高，但是实际上这些消息已经被消费者拉取到并进行处理了。这时就应该使用 PullOffset 来计算堆积量&lt;/p>
&lt;h3 id="开启消息过滤时的堆积计算">开启消息过滤时的堆积计算&lt;/h3>
&lt;p>因为堆积量是通过位点直接计算得来的，在消费者开启消息过滤时这种算法不能正确处理将被过滤掉的消息，所以会导致堆积量偏高。这种情况下可以计算一下当前 topic 中命中过滤规则的消息占总消息的比例，然后用这个比例乘以堆积量来估计一下真实的堆积量&lt;/p></description></item><item><title>Cloudflare or Vercel —— 网站托管与函数计算服务选择</title><link>https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection</link><pubDate>Sun, 22 Aug 2021 15:08:44 +0800</pubDate><guid>https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection</guid><description>&lt;img src="https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection/cover.svg" alt="Featured image of post Cloudflare or Vercel —— 网站托管与函数计算服务选择" />&lt;p>在上一篇文章 &lt;a class="link" href="https://blog.lv5.moe/p/selection-and-migration-from-typecho-to-hugo" >从 Typecho 到 Hugo 的选择与迁移&lt;/a> 中提到了 Hugo 很适合使用 Serverless 的方式部署并拓展功能。本文就通过博主 Cloudflare 和 Vercel 的使用经验对他们的网站托管（Cloudflare pages vs Vercel）和函数计算（Cloudflare workers vs Vercel function）服务进行简单的介绍和对比&lt;/p>
&lt;h2 id="网站托管">网站托管&lt;/h2>
&lt;p>Cloudflare pages 和 Vercel 都是十分强大的网站托管服务，提供以下基本特性：&lt;/p>
&lt;ul>
&lt;li>支持从 Github、GitLab 等导入代码&lt;/li>
&lt;li>支持多种预设框架自动构建，没有预置的框架可以手动提供构建命令和输出路径&lt;/li>
&lt;li>支持绑定自定义域名&lt;/li>
&lt;li>支持 CDN&lt;/li>
&lt;li>支持生成预览版本&lt;/li>
&lt;/ul>
&lt;h3 id="cloudflare-pages">Cloudflare pages&lt;/h3>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock success">
&lt;ul>
&lt;li>集成 Cloudflare Access，可以自定义访问策略&lt;/li>
&lt;li>集成 Cloudflare Web Analytics，提供开箱即用的访问统计&lt;/li>
&lt;li>其他 Cloudflare 提供的功能，详情可以参考我之前写过的一篇文章：&lt;a class="link" href="https://blog.lv5.moe/p/individual-website-cdn-usage-reference" >个人网站 CDN 选用指北&lt;/a>&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>Cloudflare pages 提供的功能简单但是一般情况下已经足够了，它的主要优点是可以和 Cloudflare 的其他服务结合使用，如 DNS、CDN、WAF、Apps 等&lt;/p>
&lt;h3 id="vercel">Vercel&lt;/h3>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock success">
&lt;ul>
&lt;li>丰富的可配置项：可以通过配置文件修改缓存规则、响应头、url 格式等，甚至配置路由规则（类似 Nginx 的 redirect 和 rewrite）&lt;/li>
&lt;li>提供多种插件：Slack、MongoDB、Logtail 等&lt;/li>
&lt;li>集成 Analytics，但是仅支持 Next.js、Nuxt.js、Gatsby&lt;/li>
&lt;li>预览构建结果：提供文件管理器查看构建出的文件&lt;/li>
&lt;li>提供易读易记的域名：格式类似这样：projectname-git-branch-username.vercel.app，在调试的时候很方便&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>Vercel 提供的功能要比 Cloudflare 丰富一些，但是它最吸引我的地方是它在国内的访问速度，下面放一下我的博客在 Cloudflare pages 和 Vercel 的访问速度对比图：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 320;
flex-basis: 768px"
>
&lt;a href="https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection/cf-pages-speed.png" data-size="1272x397">
&lt;img src="https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection/cf-pages-speed.png"
width="1272"
height="397"
loading="lazy"
alt="Cloudflare pages 速度测试">
&lt;/a>
&lt;figcaption>Cloudflare pages 速度测试&lt;/figcaption>
&lt;/figure>&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 308;
flex-basis: 739px"
>
&lt;a href="https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection/vercel-speed.png" data-size="1233x400">
&lt;img src="https://blog.lv5.moe/p/website-hosting-and-function-computing-service-selection/vercel-speed.png"
width="1233"
height="400"
loading="lazy"
alt="Vercel 速度测试">
&lt;/a>
&lt;figcaption>Vercel 速度测试&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>可以看出 Vercel 要远比 Cloudflare pages 快，甚至 Vercel 最慢的响应时间比 Cloudflare pages 平均响应时间还短。。。&lt;/p>
&lt;p>另外 Vercel 支持配置无限量的路由规则，相比之下 Cloudflare pages 免费版只支持 5 条&lt;/p>
&lt;h2 id="函数计算">函数计算&lt;/h2>
&lt;h3 id="cloudflare-workers">Cloudflare workers&lt;/h3>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock success">
&lt;ul>
&lt;li>提供在线编辑器，可以方便的调试代码&lt;/li>
&lt;li>添加新的 worker 无需重新部署网站&lt;/li>
&lt;li>提供详细的用量、性能统计&lt;/li>
&lt;li>支持 cron 触发器&lt;/li>
&lt;li>提供开箱即用的 KV 存储&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock error">
&lt;ul>
&lt;li>只支持 js&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>如果你只使用 js 的话 Cloudflare workers 比 Vercel function 要方便和好用很多&lt;/p>
&lt;h3 id="vercel-function">Vercel function&lt;/h3>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock success">
&lt;ul>
&lt;li>支持多种语言：nodejs、php、python、ruby、go&lt;/li>
&lt;li>支持包管理工具引入第三方库，如 go mod&lt;/li>
&lt;li>提供实时 log 查看&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock error">
&lt;ul>
&lt;li>必须重新部署整个网站才能发布对 function 的新增或修改&lt;/li>
&lt;li>不提供在线编辑器，只能部署好后通过 log 调试&lt;/li>
&lt;li>存储功能需要第三方服务支持&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>Vercel function 在对语言的支持上要强大很多，借助于对第三方数据库的集成你甚至可以部署 Typecho 或 WordPress。缺点是 Vercel function 与网站托管功能强耦合，必须重新部署整个网站才能发布对 function 的新增或修改，并且 function 不能使用独立的域名&lt;/p></description></item><item><title>从 Typecho 到 Hugo 的选择与迁移</title><link>https://blog.lv5.moe/p/selection-and-migration-from-typecho-to-hugo</link><pubDate>Fri, 20 Aug 2021 17:03:40 +0800</pubDate><guid>https://blog.lv5.moe/p/selection-and-migration-from-typecho-to-hugo</guid><description>&lt;p>本站最近从 Typecho 迁移到 Hugo，写这篇文章分析一下 Typecho 和 Hugo 各自的优缺点，给读者在这两者之间选择提供参考。最后记录一下我的迁移过程供后来者参考：Typecho 在服务器已经挂掉的情况下如何恢复所有的文章，然后保存为 Hugo 的文件组织方式&lt;/p>
&lt;h2 id="hugo-与-typecho-比较">Hugo 与 Typecho 比较&lt;/h2>
&lt;h3 id="hugo">Hugo&lt;/h3>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock success">
&lt;p>优点：&lt;/p>
&lt;ul>
&lt;li>静态博客生成器：生成静态 HTML，性能好，部署方便，很多 serverless 服务商支持一键部署 Hugo&lt;/li>
&lt;li>使用模板语法和 shortcodes 实现灵活的内容组装&lt;/li>
&lt;li>使用 Hugo pipeline 自动化处理资源文件：SCSS 生成 CSS、minify js、压缩或生成不同尺寸的图片&lt;/li>
&lt;li>提供一系列现代化功能： 输出 JSON 或 AMP 页面、拓展的 Markdown 语法、接入 Google Analytics、i18n 等&lt;/li>
&lt;li>文档完善，社区活跃&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock error">
&lt;p>缺点：&lt;/p>
&lt;ul>
&lt;li>DIY 门槛比较高，学习曲线比较陡峭&lt;/li>
&lt;li>主题数量比较少，大多都是英文主题，并且主题提供的功能或可以自定义的选项比较少&lt;/li>
&lt;li>很难实现一些拓展功能，比如博主开发的几款 Typecho 插件就很难在 Hugo 上实现类似的功能&lt;/li>
&lt;li>没有评论系统。虽然支持集成 disqus，但是由于众所周知的原因 disqus 在中国不能访问&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="typecho">Typecho&lt;/h3>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock success">
&lt;p>优点：&lt;/p>
&lt;ul>
&lt;li>主题丰富：有很多漂亮的中文主题，并且提供很多 DIY 选项&lt;/li>
&lt;li>插件丰富：有第三方插件市场， 提供丰富的拓展功能&lt;/li>
&lt;li>新手友好：中文社区中教程很多，出问题也有很多大佬可以请教&lt;/li>
&lt;li>提供管理后台，支持注册用户、发布文章等&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock error">
&lt;p>缺点：&lt;/p>
&lt;ul>
&lt;li>早已停止功能更新，只进行维护&lt;/li>
&lt;li>Typecho 是基于 php 的动态博客系统，性能不如静态博客&lt;/li>
&lt;li>部署比较麻烦，需要部署 web server、php、database，很难使用 serverless 的方式部署&lt;/li>
&lt;/ul>
&lt;/div>
&lt;p>&lt;/p>
&lt;h3 id="typecho-与-hugo-选择我之见">Typecho 与 Hugo 选择我之见&lt;/h3>
&lt;p>博主从 Typecho 迁移到 Hugo 的主要动力是之前白嫖的服务器到期了。。。相比于 Typecho 用的 php 我对 Hugo 的 go 语言更熟悉一些，而且 Hugo 完善的文档更是极大程度上方便于我的 DIY 大业。&lt;/p>
&lt;p>Hugo 作为一个静态博客系统最主要的缺点( &lt;del>优点&lt;/del> )是没有( &lt;del>不需要&lt;/del> )后端，但是这可以通过各家云厂商的函数计算来弥补。函数计算+云上托管的数据库就可以实现一切你想实现的插件功能，本文评论系统的 api 就使用了 Vercel 提供的 function 服务。当然，以上构想需要你具备一定的编程知识，这恐怕不是简单的照猫画虎可以快速掌握的。&lt;/p>
&lt;p>博主这里给出一些 Hugo 和 Typecho 的选择建议供读者参考：&lt;/p>
&lt;ul>
&lt;li>如果你对静态博客生成器有强需求，那我估计你也不会读到这 qwq&lt;br />
&lt;/br>&lt;/li>
&lt;li>如果你有编程背景，熟悉云服务，崇尚 GitOps，那么选择 Hugo&lt;/li>
&lt;li>如果你想 DIY 并且原意折腾，反复调试各种错误并乐此不疲，那么选择 Hugo&lt;/li>
&lt;li>如果你追求极值的访问速度/没有也不想有自己的服务器/有多语言等依赖于 Hugo 特性的需求，那么选择 Hugo&lt;br />
&lt;/br>&lt;/li>
&lt;li>如果你是小白或有一定的 DIY 需求但是并不想折腾，那么选择 Typecho&lt;/li>
&lt;li>如果你想有一个在线写作/方便的更新文章的平台，那么选择 Typecho&lt;/li>
&lt;li>如果你想一劳永逸的部署/有一个用户&amp;amp;评论系统/有收费等特殊的插件需求，那么选择 Typecho&lt;/li>
&lt;/ul>
&lt;h2 id="从-typecho-到-hugo">从 Typecho 到 Hugo&lt;/h2>
&lt;p>网上有一些 Typecho 迁移插件可以将 Typecho 的文章和附件导出为 Hugo 的文件组织方式，但是我的服务器已经挂掉了没法用这些插件，只能进行手动迁移&lt;/p>
&lt;h3 id="迁移文章">迁移文章&lt;/h3>
&lt;p>直接从数据库的 &lt;code>typecho_contents&lt;/code> 表中提取文章标题和内容等信息，这里给出一个示例 sql 语句：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">blog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">typecho_contents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">where&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;post&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以写一个脚本来将所有文章输出到以各自文章名命名的文件中&lt;/p>
&lt;h3 id="迁移图片等附件">迁移图片等附件&lt;/h3>
&lt;p>我的博客使用了又拍云的对象储存来保存图片等附件，所以只需要把他们下载下来就好。比较麻烦的是修改这些图片在文章中的链接，如果要手动一个个改太耗时间。推荐在 Hugo 的 static 文件夹中新建一个和之前的图片目录结构一样的文件夹，这样就不用修改链接了&lt;/p>
&lt;h3 id="hugo-目录结构">Hugo 目录结构&lt;/h3>
&lt;p>我的 Hugo 文件夹结构如图所示：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">tree content/post
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── solutions-to-certificate-invalidation-after-android-7
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   ├── cal-hash.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   ├── index.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   ├── install-ca-certificate.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   └── ssl-handshake-failed.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── web-font-optimization
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── blog_title.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── font_type.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── index.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── sign.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── woff2_support.jpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每个文章新建一个以文章的 slug 命名的文件夹，将文章用到的图片包括文章头图都放在这个文件夹下。在主题中可以用 &lt;code>.Context.Resources.GetMatch&lt;/code> 得到图片的 resource 对象，这样既便于管理，也能将图像交给 Hugo 进行处理。比如 ssl-handshake-failed.jpg 这张图片就会生成以下几个版本：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ls resources/_gen/images/post/solutions-to-certificate-invalidation-after-android-7
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssl-handshake-failed.2a74b1a908dba312f29b1f3f15095116_hu9d2358b8f64836a3db846b7dbcdad0ac_92801_250x150_fill_q75_box_smart1.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssl-handshake-failed_hu9d2358b8f64836a3db846b7dbcdad0ac_92801_1024x0_resize_q75_box.jpg
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssl-handshake-failed_hu9d2358b8f64836a3db846b7dbcdad0ac_92801_480x0_resize_q75_box.jpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>反码和补码的数学原理</title><link>https://blog.lv5.moe/p/the-mathematical-principle-of-ones-complement-and-twos-complement</link><pubDate>Mon, 16 Aug 2021 19:18:16 +0800</pubDate><guid>https://blog.lv5.moe/p/the-mathematical-principle-of-ones-complement-and-twos-complement</guid><description>&lt;img src="https://blog.lv5.moe/img/58255799_p0.jpg" alt="Featured image of post 反码和补码的数学原理" />&lt;p>本文介绍了使用反码和补码的加法代替减法，并分析了这样做背后的数学原理&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
本文公式较多，建议使用电脑或平板阅读
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="反码与补码的表示">反码与补码的表示&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>原码的表示方法：&lt;br />
符号位加上它的绝对值，即用第一位表示符号，其余位表示值。如果是 $8$ 位二进制：&lt;/p>
&lt;div>
$$
\begin{array}{l}
[+1] = [00000001]_原 \\
[-1] = [10000001]_原
\end{array}
$$
&lt;/div>
&lt;/li>
&lt;li>
&lt;p>反码的表示方法：&lt;br />
正数的反码是其本身&lt;br />
负数的反码是在其原码的基础上，符号位不变，其余各个位取反.&lt;/p>
&lt;div>
$$
\begin{array}{l}
[+1] = [00000001]_原 = [00000001]_反 \\
[-1] = [10000001]_原 = [11111110]_反
\end{array}
$$
&lt;/div>
&lt;/li>
&lt;li>
&lt;p>补码的表示方法：&lt;br />
正数的补码就是其本身&lt;br />
负数的补码是在其原码的基础上，符号位不变，其余各位取反，最后 $+1$. (即在反码的基础上 $+1$)&lt;/p>
&lt;div>
$$
\begin{array}{l}
[+1] = [00000001]_原 = [00000001]_反 = [00000001]_补 \\
[-1] = [10000001]_原 = [11111110]_反 = [11111111]_补
\end{array}
$$
&lt;/div>
&lt;/li>
&lt;/ol>
&lt;h2 id="反码和补码的意义">反码和补码的意义&lt;/h2>
&lt;p>我在上一篇文章&lt;a class="link" href="https://blog.lv5.moe/p/talk-about-negative-number-conversion-and-absolute-value-operation-through-math-abs" >由 Math.abs 谈负数转换与绝对值运算&lt;/a>中有提到补码的作用：&lt;/p>
&lt;blockquote>
&lt;p>对于计算机来说加减是最基础的运算要设计的尽量简单，而减法相当于加上一个负数，所以完全可以使用加法来代替减法。但是这就出现了如何处理符号位的问题，short、int、long 的位数不同符号位自然也不同，为了避免运算前还需要判断符号位的位置，前人提出了将符号位也引入计算的机制，也就是补码。&lt;/p>
&lt;/blockquote>
&lt;p>确切的说，用反码就可以实现加法来代替减法， 其中唯一特殊的是 $0$：&lt;/p>
&lt;div>
$$
\begin{split}
&amp;1 - 1 \\
&amp;= 1 + (-1) \\
&amp;= [0000 0001]_原 + [1000 0001]_原 \\
&amp;= [0000 0001]_反 + [1111 1110]_反 \\
&amp;= [1111 1111]_反 \\
&amp;= [1000 0000]_原 \\
&amp;-0
\end{split}
$$
&lt;/div>
&lt;p>这样算出来的结果是 $-0$，然而对于 $0$ 来说正负号是无意义的，并且 $-0$ 的表示方法使得 $0$ 出现了两个编码&lt;br />
此外，用反码加法代替减法需要循环进位：&lt;/p>
&lt;div>
$$
\begin{split}
&amp;4 - 2 \\
&amp;= 4 + (-2) \\
&amp;= [0000 0100]_原 + [1000 0010]_原 \\
&amp;= [0000 0100]_反 + [1111 1101]_反 \\
\end{split}
$$
&lt;/div>
&lt;p>这里如果按正常的二进制加法计算 $0000 0100 + 1111 1101$ 会得出 $0000 0001$ 即 $1$ 的错误结果。正确的做法是将溢出的进位再加到最低位上，得到正确结果 $0000 0010$ 即 $2$&lt;/p>
&lt;p>为了解决 $0$ 有两个编码并且需要循环进位的问题，出现了补码:&lt;br />
补码用 $[00000000]_补$ 表示 0，而 $[10000000]_补$ 表示 $-128$，这样就消除了歧义&lt;/p>
&lt;div>
$$
\begin{split}
&amp;1 - 1 \\
&amp;= 1 + (-1) \\
&amp;= [0000 0001]_原 + [1000 0001]_原 \\
&amp;= [0000 0001]_补 + [1111 1111]_补 \\
&amp;= [0000 0000]_补 \\
&amp;= [0000 0000]_原 \\
&amp;= 0
\end{split}
$$
&lt;/div>
&lt;p>综上所述反码和补码的意义在于可以让符号位参与计算，从而可以用加法代替减法的数学原理。接下来我们接下来我们探讨一下可以这样做背后的数学原理&lt;/p>
&lt;h2 id="用反码和补码的加法代替减法的数学原理">用反码和补码的加法代替减法的数学原理&lt;/h2>
&lt;p>首先我们来介绍两个数学概念：&lt;/p>
&lt;h3 id="取模运算">取模运算&lt;/h3>
&lt;p>取模运算公式：&lt;/p>
&lt;p>$$ x \bmod y = x - y \lfloor x / y \rfloor $$&lt;/p>
&lt;p>以 $ -3 \bmod 2 $ 为例:&lt;/p>
&lt;div>
$$
\begin{split}
&amp;-3 \bmod 2 \\
&amp;= -3 - 2 \times \lfloor -3/2 \rfloor \\
&amp;= -3 - 2 \times \lfloor -1.5 \rfloor \\
&amp;= -3 - 2 \times (-2) \\
&amp;= -3 + 4 \\
&amp;= 1
\end{split}
$$
&lt;/div>
&lt;p>这里给出一个取模的算法实现：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="nf">mod&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="同余">同余&lt;/h3>
&lt;p>两个整数 a，b，若它们除以整数 m 所得的余数相等，则称 a，b 对于模 m 同余&lt;/p>
&lt;p>记作 $ a \equiv b \pmod m $，读作 a 与 b 关于模 m 同余&lt;/p>
&lt;div>
$$
\begin{split}
\left. \begin{gathered}
4 \bmod 12 &amp;= 4 \\
16 \bmod 12 &amp;= 4
\end{gathered} \right\}
\implies
4 \equiv 16 \pmod{12}
\end{split}
$$
&lt;/div>
&lt;p>同余具有的性质：&lt;/p>
&lt;ol>
&lt;li>自反性，对称性，传递性&lt;/li>
&lt;/ol>
&lt;div>
$$
\begin{align}
&amp;a \equiv a \pmod m \\
&amp;a \equiv b \pmod m \iff b \equiv a \pmod m \\
&amp;a \equiv b \pmod m \text{ 且 } b \equiv c \pmod m \implies a \equiv c \pmod m \\
\end{align}
$$
&lt;/div>
&lt;ol start="2">
&lt;li>线性运算&lt;/li>
&lt;/ol>
&lt;div>
$$
\begin{split}
a \equiv b \pmod m \text{ 且 } c \equiv d \pmod m \implies
\left\{ \begin{gathered}
a \pm c &amp;\equiv b \pm d \pmod m \\
a \times c &amp;\equiv b \times d \pmod m
\end{gathered} \right.
\end{split}
$$
&lt;/div>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
同余的除法的性质与加减乘法不同：&lt;br />
$$ a \equiv b \pmod m \quad 且 \quad c \equiv d \pmod m \implies a \equiv b\left(\bmod \frac{m}{\operatorname{gcd}(c, m)}\right) $$
&lt;/div>
&lt;p>&lt;/p>
&lt;ol start="3">
&lt;li>幂运算&lt;br />
$$ a \equiv b \pmod m \implies a^{n} \equiv b^{n} \pmod m $$&lt;/li>
&lt;/ol>
&lt;h3 id="证明">证明&lt;/h3>
&lt;p>我们还是以 $ 4 - 2 $ 为例，根据同余数的性质我们很容易得出这样的结论：&lt;/p>
&lt;div>
$$
\begin{split}
\left. \begin{gathered}
4 &amp;\equiv 4 \pmod{7} \\
-2 &amp;\equiv 5 \pmod{7} \\
\end{gathered} \right\}
\implies 4 - 2 \equiv 4 + 5 \pmod{7}
\end{split}
$$
&lt;/div>
&lt;p>也就是说我们计算 $ 4 - 2 $ 就相当于计算 $ (4 + 5) \bmod 7 $&lt;br />
如果我们将 $-2$ 和 $5$ 换成二进制会发现 $-2$ 的反码的数值部分正好等于 $5$，也就是说补码的计算实际上是求其模数为当前数制的上限（这个例子中为 $7$）的同余数&lt;/p>
&lt;div>
$$
\begin{split}
-2 &amp;= [1010]_原 = [1101]_反 \\
5 &amp;= [0101]_原 = [0101]_反
\end{split}
$$
&lt;/div>
&lt;p>这样循环进位就相当于溢出的数对当前数制的上限（这个例子中为 $7$）取模后再与原结果相加&lt;/p>
&lt;p>$$ (4 + 5) \bmod 7 = 8 \bmod 7 + 1 = 2 $$&lt;/p>
&lt;p>这里的 $8$ 相当于进位&lt;/p>
&lt;p>而补码的计算相比于反码只不过是增加了模的值：这时我们计算 $ 4 - 2 $ 就相当于计算 $ (4 + 6) \bmod 8 $&lt;/p>
&lt;p>$$ (4 + 6) \bmod 8 = 8 \bmod 8 + 2 = 2 $$&lt;/p>
&lt;p>因为进位 $ 8 \bmod 8 = 0 $ 所以不需要循环进位&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock ">
另一种数学推导可以看这篇文章：&lt;a class="link" href="https://note.sbwcwso.com/pages/1dd33b" target="_blank" rel="noopener"
>反码与补码加法的理解&lt;/a>
&lt;/div>
&lt;p>&lt;/p></description></item><item><title>由 Math.abs 谈负数转换与绝对值运算</title><link>https://blog.lv5.moe/p/talk-about-negative-number-conversion-and-absolute-value-operation-through-math-abs</link><pubDate>Mon, 16 Aug 2021 15:53:15 +0800</pubDate><guid>https://blog.lv5.moe/p/talk-about-negative-number-conversion-and-absolute-value-operation-through-math-abs</guid><description>&lt;p>本文通过分析一个 Java 中 &lt;code>Math.abs()&lt;/code> 误用引发的 bug 介绍了计算机中数的储存、负数转换与绝对值运算&lt;/p>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>最近遇到了一个奇妙深刻的 bug：我们的系统中使用了一个 int 型的变量来计数，这个计数器变量的绝对值取模作为某个 list 的 index，但是今天出现了异常 IndexOutOfBoundsException&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 满足某些条件计数器自增
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">0&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">count&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="err">；&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">int&lt;/span> &lt;span class="n">index&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Math&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">abs&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">count&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">%&lt;/span> &lt;span class="n">list&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">size&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// java.lang.IndexOutOfBoundsException: Index -2147483648 out of bounds for length 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">list&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">index&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="mathabs-遇上-integermin_value">Math.abs 遇上 Integer.MIN_VALUE&lt;/h2>
&lt;p>我们知道 int 是用 32 位二进制储存的，其中最高位是符号位。上述代码不断自增 count 变量最终会使其超过上限从而反转成负数（&lt;code>Integer.MAX_VALUE + 1 == Integer.MIN_VALUE&lt;/code> 会返回 true） 我们预料到了这种情况发生所以使用 &lt;code>Math.abs(count)&lt;/code> 来确保取模的结果为正数，但是结果事与愿违&lt;/p>
&lt;p>我们查看报错信息发现 -2147483648 正好是 Integer.MIN_VALUE，所以我们先试一下 &lt;code>Math.abs(Integer.MIN_VALUE)&lt;/code> 结果居然是 -2147483648，这显然不符合我们的预期，让我们先来看一下 &lt;code>Math.abs()&lt;/code> 的实现：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl"> &lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * Returns the absolute value of an {@code int} value.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * If the argument is not negative, the argument is returned.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * If the argument is negative, the negation of the argument is returned.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * &amp;lt;p&amp;gt;Note that if the argument is equal to the value of
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * {@link Integer#MIN_VALUE}, the most negative representable
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * {@code int} value, the result is that same value, which is
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * negative.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @param a the argument whose absolute value is to be determined
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> * @return the absolute value of the argument.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nd">@HotSpotIntrinsicCandidate&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">public&lt;/span> &lt;span class="kd">static&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="nf">abs&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">0&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">a&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注释中说明了如果 a 是 Integer.MIN_VALUE 那么返回值也是负数。到这里就破案了，只要把 &lt;code>Math.abs(count) % list.size()&lt;/code> 改为 &lt;code>(count % list.size() + list.size()) % list.size()&lt;/code> 即可，下面分析一下为什么会出现这种情况&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
改为 &lt;code>(count % list.size() + list.size()) % list.size()&lt;/code> 而不是 &lt;code>Math.abs(count % list.size())&lt;/code> 是因为前者符合取余的数学定义&lt;br />
详情可以看我的另一篇文章 &lt;a class="link" href="https://blog.lv5.moe/p/the-mathematical-principle-of-ones-complement-and-twos-complement" >反码和补码的数学原理&lt;/a>
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="运算符---的原理">运算符 - 的原理&lt;/h2>
&lt;p>分析 &lt;code>Math.abs()&lt;/code> 的代码可以发现问题出在运算符 &lt;code>-&lt;/code> 上，也就是说 &lt;code>-Integer.MIN_VALUE == Integer.MIN_VALUE&lt;/code>，显然出现这种情况说明运算符 &lt;code>-&lt;/code> 的作用不可能是简单的将符号位取反。&lt;/p>
&lt;p>要继续分析就要先了解数在计算机中的储存原理：对于计算机来说加减是最基础的运算要设计的尽量简单，而减法相当于加上一个负数，所以完全可以使用加法来代替减法。但是这就出现了如何处理符号位的问题，short、int、long 的位数不同符号位自然也不同，为了避免运算前还需要判断符号位的位置，前人提出了将符号位也引入计算的机制，也就是补码。&lt;/p>
&lt;p>补码的表示方法是:&lt;/p>
&lt;ul>
&lt;li>正数的补码就是其本身&lt;/li>
&lt;li>负数的补码是在其原码的基础上符号位不变，其余各位取反，最后 +1&lt;/li>
&lt;/ul>
&lt;p>计算机中计算 1 - 1 的原理：&lt;/p>
&lt;div>
$$
\begin{split}
&amp;1 - 1 \\
&amp;= 1 + (-1) \\
&amp;= [0000 0001]_原 + [1000 0001]_原 \\
&amp;= [0000 0001]_补 + [1111 1111]_补 \\
&amp;= [0000 0000]_补 \\
&amp;= [0000 0000]_原 \\
&amp;= 0
\end{split}
$$
&lt;/div>
&lt;p>所以运算符 &lt;code>-&lt;/code> 的原理是进行求其补码的运算，即按位取反然后 +1，于是我们来分析 &lt;code>-Integer.MIN_VALUE&lt;/code> 的运算过程&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="o">-&lt;/span>&lt;span class="n">Integer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">MIN_VALUE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">Integer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">MIN_VALUE&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">~&lt;/span>&lt;span class="n">0x80000000&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">0x7fffffff&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">0x80000000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以发现在 &lt;code>0x7fffffff + 1&lt;/code> 的过程中发生了溢出，所以最后的结果还是 0x80000000 也就是 Integer.MIN_VALUE&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>通过以上分析我们可以知道，使用 &lt;code>Math.abs()&lt;/code> 对 Short.MIN_VALUE、Integer.MIN_VALUE、Long.MIN_VALUE 取绝对值都得不到正确的结果。那有没有方法可以得到正确的结果呢？答案是 Integer.MIN_VALUE 的绝对值在 Integer 的范围内无法表示，因为 0 的存在 Integer. MAX_VALUE 比 Integer.MIN_VALUE 的绝对值小 1。如果确实要得到 Integer.MIN_VALUE 的绝对值可以先转换为 Long：&lt;code>Math.abs(Long.valueOf(Integer.MIN_VALUE))&lt;/code>&lt;/p></description></item><item><title>对一个并发问题的思考</title><link>https://blog.lv5.moe/p/thinking-about-a-concurrency-problem</link><pubDate>Wed, 13 Nov 2019 22:29:00 +0800</pubDate><guid>https://blog.lv5.moe/p/thinking-about-a-concurrency-problem</guid><description>&lt;p>遇到了一个并发问题，将对锁的设计的思考记录一下：&lt;/p>
&lt;p>这个问题的逻辑是：需要根据 id 获取数据库中指定的一行数据，如果这行数据的某个字段为 null 则请求远程接口来获取数据（每次请求接口这个数据都会更新），然后将获取的值写入到数据库中&lt;/p>
&lt;p>关键的问题在于如何能在高并发的情况下保证最后数据库中存的是最新获取的数据，显然我们可以用 synchronized 锁来解决。但是这个方法中有三个耗时操作，如果这样粗暴的解决肯定是行不通的，所以要考虑如何减小锁的粒度。&lt;/p>
&lt;p>我的解决方案是使用一个 HashMap 来为每个 id 生成一把锁（采用 lazyload 策略，尝试获取锁时再生成）这样只有对多个 id 的连续请求才会阻塞&lt;/p>
&lt;p>但是这样随之而来的是另一个新问题：HashMap 会无限地新增锁，如果 id 的数量过多那岂不是得被挤爆内存，所以我这里新开一个线程去删除暂时不用的锁&lt;/p>
&lt;p>这个删除逻辑又有两个问题：&lt;/p>
&lt;ol>
&lt;li>如何保证前一个线程执行完下一个线程有机会获取锁&lt;/li>
&lt;li>如何保证删除的锁真的是没人用的&lt;/li>
&lt;/ol>
&lt;p>第一个问题通过把删除线程睡眠 3s 来解决，第二个问题我通过重新读取 lock 来解决，并且获取 map 的锁来保证没有其他线程能在检测过程中能访问 map 以及获得新 lock、检测是否占用、从 map 中删除这三个操作的原子性&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// private static final ReentrantLock mapLock = new ReentrantLock();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// private static final HashMap&amp;lt;Long, ReentrantLock&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">private&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="nf">checkAndUpdate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mapLock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">lock&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ReentrantLock&lt;/span> &lt;span class="n">lock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">map&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getOrDefault&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">ReentrantLock&lt;/span>&lt;span class="o">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mapLock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">unlock&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">lock&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// check and update
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">lock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">unlock&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="n">Thread&lt;/span>&lt;span class="o">(()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Thread&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">sleep&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">3000&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mapLock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">lock&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ReentrantLock&lt;/span> &lt;span class="n">newLock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">map&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">newLock&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="n">newLock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">isLocked&lt;/span>&lt;span class="o">())&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">map&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">remove&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mapLock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">unlock&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="n">InterruptedException&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}).&lt;/span>&lt;span class="na">start&lt;/span>&lt;span class="o">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以优化的点：&lt;/p>
&lt;ol>
&lt;li>采用线程池来执行删除逻辑&lt;/li>
&lt;li>获取到 lock 后先判断是否已经加锁，如果已经加了则直接放弃更新，否则尝试加锁&lt;br />
因为如果竞争锁失败那么说明已经有线程在尝试更新这个为 null 的值了，如果更新成功那这个值不再为 null 就不用再次更新了，如果更新失败那显然要提示用户失败，用户会再次尝试。所以还是直接放弃的好&lt;/li>
&lt;/ol></description></item><item><title>Nginx SSL/TLS 配置优化</title><link>https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization</link><pubDate>Sun, 06 Oct 2019 18:38:00 +0800</pubDate><guid>https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization</guid><description>&lt;img src="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/ssl-logo.jpg" alt="Featured image of post Nginx SSL/TLS 配置优化" />&lt;p>本文描述的优化技巧基于 Nginx 1.17、OpenSSL 1.1.1d、TLS1.2 和 TLS1.3&lt;/p>
&lt;h2 id="tls12-session-复用">TLS1.2 Session 复用&lt;/h2>
&lt;p>session 复用有两种方式，我选择的是 Session Identifier，下面讨论下这两种方式的机制：&lt;/p>
&lt;h3 id="session-identifier">Session Identifier&lt;/h3>
&lt;p>客户端保存 session ID，在发起 Client Hello 时将上次使用的 session ID 发送给服务端，服务端根据收到的 session ID 找到保存好的对称密钥。&lt;/p>
&lt;p>nginx 中的配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 指定缓存大小为 30m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ssl_session_cache&lt;/span> &lt;span class="s">shared:SSL:30m&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 指定缓存时间为 1 天
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ssl_session_timeout&lt;/span> &lt;span class="s">1d&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 这里需要关闭默认开启的 ssl_session_tickets
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ssl_session_tickets&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里有两个问题：&lt;/p>
&lt;ul>
&lt;li>服务器缓存 session 信息会对服务器性能造成影响&lt;/li>
&lt;li>不支持分布式部署，缓存只在单机上保存。这个问题可以使用 OpenResty 配合 Redis 解决：
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_session_fetch_by_lua_file&lt;/span> &lt;span class="s">lua/session_fetch.lua&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_session_store_by_lua_file&lt;/span> &lt;span class="s">lua/session_store.lua&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_session_timeout&lt;/span> &lt;span class="s">1d&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_session_tickets&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h3 id="session-ticket">Session Ticket&lt;/h3>
&lt;p>服务器将 session 信息加密后保存在 session ticket 中交由客户端保存，客户端会在 client hello 的拓展中加上 session ticket，服务器解密后就可以恢复会话信息&lt;/p>
&lt;p>这个方式安全性要差一些：&lt;/p>
&lt;ul>
&lt;li>Nginx 和 Apache 都只在重启后才会更改加密使用的密钥&lt;/li>
&lt;li>session ticket 不具有前向保密性，并且一旦 session ticket 被解密会使 TLS 的前向保密性机制完全失效。相关讨论可以参考&lt;a class="link" href="https://www.imperialviolet.org/2013/06/27/botchingpfs.html" target="_blank" rel="noopener"
>这篇文章&lt;/a>：&lt;/li>
&lt;/ul>
&lt;h2 id="tls13-early-data0-rtt">TLS1.3 Early data（0-RTT）&lt;/h2>
&lt;p>TLS 1.3 中抛弃了之前的两种 session 复用方式，转而采用 PSK 复用：&lt;/p>
&lt;p>&lt;strong>和 TLS1.2 的对比：&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>TLS1.2 首次连接&lt;/th>
&lt;th>TLS1.2 会话复用&lt;/th>
&lt;th>TLS1.3 首次连接&lt;/th>
&lt;th>TLS1.3 会话复用&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DNS 解析&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>0-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>0-RTT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TCP 握手&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TLS 握手&lt;/td>
&lt;td>2-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>0-RTT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HTTP Request&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;td>1-RTT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>总计&lt;/td>
&lt;td>5-RTT&lt;/td>
&lt;td>3-RTT&lt;/td>
&lt;td>4-RTT&lt;/td>
&lt;td>2-RTT&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="实现机制">实现机制&lt;/h3>
&lt;p>第一次连接建立后服务器向客户端发送 New Session Ticket 包，其中的 early_data 拓展指定了是否能使用 Early data（0-RTT 连接）及其参数&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 206;
flex-basis: 494px"
>
&lt;a href="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/new-session-ticket.png" data-size="616x299">
&lt;img src="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/new-session-ticket.png"
width="616"
height="299"
loading="lazy"
alt="New Session Ticket">
&lt;/a>
&lt;figcaption>New Session Ticket&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>第二次连接客户端发送 Client Hello 时同时发送 Early data&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 532;
flex-basis: 1277px"
>
&lt;a href="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/early-data.png" data-size="740x139">
&lt;img src="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/early-data.png"
width="740"
height="139"
loading="lazy"
alt="Early data">
&lt;/a>
&lt;figcaption>Early data&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>可以使用 openssl 测试：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">openssl s_client -connect :443 -tls1_3 -sess_in session.pem -early_data request.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a class="link" href="https://gist.github.com/5e639bfaf012ad5840f36a591f7d4904#file-test-early-data-sh" target="_blank" rel="noopener"
>完整测试脚本&lt;/a>&lt;/p>
&lt;p>更多细节参考 RFC 8446：&lt;a class="link" href="https://tools.ietf.org/html/rfc8446#section-2.3" target="_blank" rel="noopener"
>https://tools.ietf.org/html/rfc8446#section-2.3&lt;/a>&lt;/p>
&lt;p>这种方式虽然可以省下一个 RTT 但是牺牲了前向安全性和抵抗重放攻击的能力&lt;/p>
&lt;p>在 nginx 中开启 Early data 的方式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_early_data&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 可选项，表示此 HTTP 请求在 Early data 中发送
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">proxy_set_header&lt;/span> &lt;span class="s">Early-Data&lt;/span> &lt;span class="nv">$ssl_early_data&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ocsp-stapling">OCSP stapling&lt;/h2>
&lt;p>OCSP stapling 是一项 TLS 的拓展：&lt;/p>
&lt;p>服务器将 OCSP response 缓存在服务器中，然后把它们作为 TLS 握手的一部分发送给客户端。因此，客户端在和一个支持 OCSP Stapling 技术的服务器通信时，它可以同时接收到服务器证书和该证书的状态&lt;/p>
&lt;p>这项技术有效的保护了用户的隐私同时保证了 OCSP 的可用性&lt;/p>
&lt;p>Nginx 中开启 OCSP stapling：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_stapling&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_stapling_verify&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">resolver&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="s">.1.1.1&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="s">.0.0.1&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="s">.8.8.8&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="s">.8.4.4&lt;/span> &lt;span class="mi">208&lt;/span>&lt;span class="s">.67.222.222&lt;/span> &lt;span class="mi">208&lt;/span>&lt;span class="s">.67.220.220&lt;/span> &lt;span class="s">valid=60s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">resolver_timeout&lt;/span> &lt;span class="s">2s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要注意的是如果你的 CA 提供的 OCSP 需要验证的话，必须用 &lt;code>ssl_trusted_certificate&lt;/code> 指定 CA 的中级证书和根证书（PEM 格式，放在一个文件中）的位置，否则会报错 &lt;code>[error] 17105#17105: OCSP_basic_verify() failed&lt;/code>&lt;/p>
&lt;h2 id="strict-sni">Strict SNI&lt;/h2>
&lt;p>为 Nginx 开启严格的 SNI 匹配，防止使用 ip 或错误域名的访问暴露证书&lt;/p>
&lt;p>Nginx 本身不提供这个功能，1.15.10 以后的版本可以使用这个 patch：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># run in nginx directory
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">curl&lt;/span> &lt;span class="s">https://github.com/hakasenyang/openssl-patch/blob/master/nginx_strict-sni_1.15.10.patch&lt;/span> &lt;span class="s">|&lt;/span> &lt;span class="s">patch&lt;/span> &lt;span class="s">-p1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后在 http 模块中开启&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">strict_sni&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">strict_sni_header&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="使用-ecc-证书">使用 ECC 证书&lt;/h2>
&lt;p>ECC 证书即使用 ECDSA（Elliptic Curve Digital Signature Algorithm）的证书，拥有更小的密钥长度和与主流算法相比更好的加密强度（相当于 RSA 3072）&lt;/p>
&lt;p>下图即是一个 ECC 证书：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 162;
flex-basis: 390px"
>
&lt;a href="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/ecc-cert.png" data-size="446x274">
&lt;img src="https://blog.lv5.moe/p/nginx-ssl-tls-configuration-optimization/ecc-cert.png"
width="446"
height="274"
loading="lazy"
alt="ECC 证书">
&lt;/a>
&lt;figcaption>ECC 证书&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="密码套件选择">密码套件选择&lt;/h2>
&lt;p>我选用的服务器偏好密码套件：&lt;/p>
&lt;p>密钥交换算法：ECDH （具有前向安全性并且比 DH 资源消耗低）&lt;/p>
&lt;p>签名算法：ECC&lt;/p>
&lt;p>对称加密算法：AES-GCM （是一种 AEAD 算法并且加密解密均可并行计算）&lt;/p>
&lt;p>完整配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_ecdh_curve&lt;/span> &lt;span class="s">X25519:P-256:P-384:P-224:P-521&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_ciphers&lt;/span> &lt;span class="s">[TLS_AES_256_GCM_SHA384|TLS_AES_128_GCM_SHA256|TLS_CHACHA20_POLY1305_SHA256]:[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305|ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_prefer_server_ciphers&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="等价加密算法组">等价加密算法组&lt;/h3>
&lt;p>上文中 &lt;code>[XXX|XXX]&lt;/code> 是等价密码组的写法，在开启 &lt;code>ssl_prefer_server_ciphers on&lt;/code> 的情况下给予客户端在有限的几种密码套件之间选择最合适的一种&lt;/p>
&lt;p>本站使用这项技术为不支持 AES-NI 的设备自动协商 CHACHA20-POLY1305 算法&lt;br />
OpenSSL 中不支持此项技术，可以使用 boringssl 或打上&lt;a class="link" href="https://github.com/hakasenyang/openssl-patch/blob/master/openssl-equal-1.1.1d_ciphers.patch" target="_blank" rel="noopener"
>这个 patch&lt;/a>：&lt;/p>
&lt;p>PS. ARMv8 中就提供了 AES 指令集支持，都 9102 年了，新手机基本上没有不支持的&lt;/p>
&lt;h3 id="dh-参数">DH 参数&lt;/h3>
&lt;p>为上文中 DHE 和 ECDHE 指定一个更强的参数（这里是 4096 位）&lt;/p>
&lt;p>step 1. 使用 openssl 生成 &lt;code>dhparam.pem&lt;/code> 文件&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">openssl&lt;/span> &lt;span class="s">dhparam&lt;/span> &lt;span class="s">-out&lt;/span> &lt;span class="s">dhparam.pem&lt;/span> &lt;span class="mi">4096&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>step 2. 配置 nginx 参数&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">ssl_dhparam&lt;/span> &lt;span class="s">path-to-dhparam-file&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Nginx 编译安装脚本</title><link>https://blog.lv5.moe/p/nginx-compile-and-install-script</link><pubDate>Fri, 23 Aug 2019 11:20:00 +0800</pubDate><guid>https://blog.lv5.moe/p/nginx-compile-and-install-script</guid><description>&lt;img src="https://blog.lv5.moe/p/nginx-compile-and-install-script/nginx-logo.jpg" alt="Featured image of post Nginx 编译安装脚本" />&lt;p>此脚本适用于 Ubuntu，自动编译安装 nginx&lt;/p>
&lt;h2 id="为什么需要此脚本">为什么需要此脚本&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>安装 Nginx 最新 Mainline version&lt;/p>
&lt;/li>
&lt;li>
&lt;p>对 nginx 和 OpenSSL 打补丁&lt;/p>
&lt;ul>
&lt;li>为 nginx 添加 SPDY、FULL HPACK、Dynamic TLS Record 支持&lt;/li>
&lt;li>为 nginx 提供选择 TLS 1.3 密码套件的功能&lt;/li>
&lt;li>为 openssl 添加等价密码组、chacha 草案支持&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>使用 jemalloc 优化内存管理&lt;/p>
&lt;/li>
&lt;li>
&lt;p>使用 Cloudflare 优化的 zlib，提升性能&lt;/p>
&lt;/li>
&lt;li>
&lt;p>添加 brotli 压缩支持&lt;/p>
&lt;/li>
&lt;li>
&lt;p>添加 Strict-SNI 支持&lt;/p>
&lt;/li>
&lt;li>
&lt;p>添加 Lua 支持&lt;/p>
&lt;/li>
&lt;li>
&lt;p>可选项：nginx 流媒体支持（Rmtp、HLS、Dash）、简易的 web 应用防火墙（lua_waf）&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="使用方法">使用方法&lt;/h2>
&lt;p>GitHub 仓库：&lt;a class="link" href="https://github.com/ShadowySpirits/vps-setup" target="_blank" rel="noopener"
>ShadowySpirits/vps-setup&lt;/a>&lt;/p>
&lt;p>运行此行代码即可&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl https://raw.githubusercontent.com/ShadowySpirits/vps-setup/master/nginx_install.sh &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>运行脚本时添加以下参数开启可选功能：&lt;/p>
&lt;ul>
&lt;li>&amp;ndash;with-vod         : 添加 nginx-vod-module（nginx 流媒体支持）&lt;/li>
&lt;li>&amp;ndash;enable-mkv   : 开启 nginx-vod-module 的 mkv 格式支持 (实验性功能)&lt;/li>
&lt;li>&amp;ndash;with-waf         : 添加 lua_waf （简易的 web 应用防火墙）&lt;/li>
&lt;/ul>
&lt;p>使用以下命令开启、关闭或重启服务&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo service nginx start/stop/restart
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
此脚本依赖的代码仓库多位于国外服务器，如安装失败请检查是否是网络问题
&lt;/div>
&lt;p>&lt;/p></description></item><item><title>Deluge 2.0.3 修改 user-agent 和 peer-id 教程</title><link>https://blog.lv5.moe/p/deluge-modify-user-agent-and-peer-id-tutorials</link><pubDate>Fri, 23 Aug 2019 10:57:00 +0800</pubDate><guid>https://blog.lv5.moe/p/deluge-modify-user-agent-and-peer-id-tutorials</guid><description>&lt;img src="https://blog.lv5.moe/p/deluge-modify-user-agent-and-peer-id-tutorials/deluge-logo.jpg" alt="Featured image of post Deluge 2.0.3 修改 user-agent 和 peer-id 教程" />&lt;p>本文提供一种修改 Deluge 的 user-agent 和 peer-id 的方法，用于伪装其他 BT 下载工具，绕过某些限制&lt;/p>
&lt;p>PS：附部分 BT 下载工具 user-agent 和 peer-id 列表&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
如果你不知道如何安装 Deluge 请先阅读 &lt;a class="link" href="https://blog.lv5.moe/p/deluge-oneclick-installation-script" >Deluge 一键安装脚本&lt;/a>
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>可以使用 dzhuang 打包好的 docker iamge: &lt;a class="link" href="https://hub.docker.com/r/dzhuang/docker-deluge" target="_blank" rel="noopener"
>dzhuang/docker-deluge&lt;/a>&lt;/p>
&lt;p>在设置中直接修改 user-agent 和 peer-id&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 96;
flex-basis: 230px"
>
&lt;a href="https://blog.lv5.moe/p/deluge-modify-user-agent-and-peer-id-tutorials/deluge-config.png" data-size="485x504">
&lt;img src="https://blog.lv5.moe/p/deluge-modify-user-agent-and-peer-id-tutorials/deluge-config.png"
width="485"
height="504"
loading="lazy"
alt="config">
&lt;/a>
&lt;figcaption>config&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
dzhuang 提供了&lt;a class="link" href="https://github.com/dzhuang/deluge-alpine-build" target="_blank" rel="noopener"
>源码仓库&lt;/a>，请使用者自行评估
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="修改-user-agent">修改 user-agent&lt;/h2>
&lt;p>打开文件 &lt;code>/usr/lib/python3/dist-packages/deluge/core/core.py&lt;/code>&lt;/p>
&lt;p>修改 123 行左右：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl"># Start the libtorrent session.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- user_agent = &amp;#39;Deluge/{} libtorrent/{}&amp;#39;.format(DELUGE_VER, LT_VERSION)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ user_agent = &amp;#39;Transmission/2.11&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="修改-peer-id">修改 peer-id&lt;/h2>
&lt;p>修改 291 行左右：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">peer_id = substitute_chr(peer_id, 6, release_chr)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- return peer_id
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ return &amp;#39;-TR2110-&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
虽然这是 Deluge 2.0.3 的教程，但 Deluge 其他版本修改方式大同小异，搜索字符串 user_agent 和 peer_id 也能找到关键代码位置
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="部分-bt-下载工具-user-agent-和-peer-id-列表">部分 BT 下载工具 user-agent 和 peer-id 列表&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>name&lt;/th>
&lt;th>user-agent&lt;/th>
&lt;th>peer-id&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>utorrentMac 1.6.4&lt;/td>
&lt;td>uTorrentMac/1640(27255)&lt;/td>
&lt;td>-UM1640-&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>utorrent 2.2.1&lt;/td>
&lt;td>uTorrent/2210(25110)&lt;/td>
&lt;td>-UT2210-&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Transmission 2.11&lt;/td>
&lt;td>Transmission/2.11&lt;/td>
&lt;td>-TR2110-&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Deluge 1.3.5&lt;/td>
&lt;td>Deluge/1350&lt;/td>
&lt;td>-DE1350-&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table></description></item><item><title>Deluge 一键安装脚本</title><link>https://blog.lv5.moe/p/deluge-oneclick-installation-script</link><pubDate>Fri, 23 Aug 2019 10:26:00 +0800</pubDate><guid>https://blog.lv5.moe/p/deluge-oneclick-installation-script</guid><description>&lt;img src="https://blog.lv5.moe/p/deluge-oneclick-installation-script/deluge-logo.jpg" alt="Featured image of post Deluge 一键安装脚本" />&lt;p>此脚本适用于 Ubuntu，自动安装 deluged 和 deluge-webui&lt;/p>
&lt;h2 id="为什么需要此脚本">为什么需要此脚本&lt;/h2>
&lt;ol>
&lt;li>自动安装 deluge 最新稳定版&lt;/li>
&lt;li>创建 deluge 用户以运行 deluge 程序&lt;/li>
&lt;li>创建 deluged 和 deluge-web 服务，开机自启动&lt;/li>
&lt;/ol>
&lt;h2 id="使用方法">使用方法&lt;/h2>
&lt;p>GitHub 仓库：&lt;a class="link" href="https://github.com/ShadowySpirits/vps-setup" target="_blank" rel="noopener"
>ShadowySpirits/vps-setup&lt;/a>&lt;/p>
&lt;p>运行此行代码即可&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl https://raw.githubusercontent.com/ShadowySpirits/vps-setup/master/deluge_install.sh &lt;span class="p">|&lt;/span> sudo bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用以下命令开开启、关闭或重启服务&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo service deluged start/stop/restart
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo service deluge-web start/stop/restart
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后访问 &lt;code>http://your-ip:8112&lt;/code> 就可以进入 Deluge WebUI&lt;/p>
&lt;p>输入默认密码 deluge 后，点击 connect 就可以连接上服务器，开始使用了&lt;/p></description></item><item><title>使用 Kotlin DSL 代替 Builder 模式</title><link>https://blog.lv5.moe/p/from-builder-to-kotlin-dsl</link><pubDate>Mon, 29 Apr 2019 23:22:00 +0800</pubDate><guid>https://blog.lv5.moe/p/from-builder-to-kotlin-dsl</guid><description>&lt;img src="https://blog.lv5.moe/p/from-builder-to-kotlin-dsl/kotlin-logo.png" alt="Featured image of post 使用 Kotlin DSL 代替 Builder 模式" />&lt;p>本文旨在介绍如何用 Kotlin DSL 来代替 Builder 模式，如果你不知道什么是 DSL 或者不了解 Kotlin 中的 DSL 可以阅读我的上一篇文章：&lt;a class="link" href="https://blog.lv5.moe/p/introduction-to-kotlin-dsl" >Kotlin DSL 简介&lt;/a>&lt;/p>
&lt;h2 id="举个例子">举个例子&lt;/h2>
&lt;p>我们想编写一个 HTML 构建器输出如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>HTML encoding with Kotlin&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>HTML encoding with Kotlin&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>this format can be used as an alternative markup to HTML&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;http://jetbrains.com/kotlin&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> Kotlin &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>some text&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果使用 Builder 模式代码类似这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="py">html&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">HTMLBuilder&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">addHead&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">HeadBuilder&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">addTitle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TitleBuilder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;HTML encoding with Kotlin&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">).&lt;/span>&lt;span class="n">addBody&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BodyBuilder&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="n">addH1&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">H1Builder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;HTML encoding with Kotlin&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">addP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PBuilder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;this format can be used as an alternative markup to HTML&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">addA&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ABuilder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Kotlin&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">setHerf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;http://jetbrains.com/kotlin&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">).&lt;/span>&lt;span class="n">addP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PBuilder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;some text&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">).&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写这些 XXXBuilder 就很麻烦了，而且代码十分臃肿，可读性很差。再来看看 Kotlin DSL 的写法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">fun&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Array&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">&amp;gt;)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">result&lt;/span> &lt;span class="p">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">html&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">head&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">title&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;HTML encoding with Kotlin&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">body&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">h1&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;HTML encoding with Kotlin&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;this format can be used as an alternative markup to HTML&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// an element with attributes and text content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">href&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://jetbrains.com/kotlin&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;Kotlin&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">p&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;some text&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>显然更加清晰明了，并且代码量要少的多。&lt;/p>
&lt;h2 id="具体实现">具体实现&lt;/h2>
&lt;p>我们来分析一下这段 Kotlin DSL 实现：构造标签使用的是类似 html { &amp;hellip; } 这样的函数，这个函数接收一个 Lambda 表达式作为参数，并返回一个 HTML 对象：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">inline&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="nf">html&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">init&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">HTML&lt;/span>&lt;span class="p">.()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">Unit&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="n">HTML&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">html&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">HTML&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">html&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">init&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">html&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
这里使用内联函数来避免 Lambda 表达式的开销
&lt;/div>
&lt;p>&lt;/p>
&lt;p>其他标签同理，所以我们可以构建一个泛型函数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">abstract&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">Tag&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">children&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">arrayListOf&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">Tag&lt;/span>&lt;span class="p">&amp;gt;()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">inline&lt;/span> &lt;span class="k">protected&lt;/span> &lt;span class="k">fun&lt;/span> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nc">T&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nc">Tag&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="nf">initTag&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tag&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">T&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">init&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">T&lt;/span>&lt;span class="p">.()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">Unit&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="n">T&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tag&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">init&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">children&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tag&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">tag&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">....&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后传入对应的标签类型即可创建不同的标签：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">HTML&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="n">Tag&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fun&lt;/span> &lt;span class="nf">head&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">init&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Head&lt;/span>&lt;span class="p">.()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">Unit&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">initTag&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Head&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="k">init&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fun&lt;/span> &lt;span class="nf">body&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">init&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Body&lt;/span>&lt;span class="p">.()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">Unit&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">initTag&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Body&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="k">init&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样我们就完成了一个简单的 HTML Builder，完整的代码见此：&lt;a class="link" href="https://try.kotlinlang.org/#/Examples/Longer%20examples/HTML%20Builder/HTML%20Builder.kt" target="_blank" rel="noopener"
>HTML Builder&lt;/a>。&lt;/p>
&lt;h2 id="限制作用域">限制作用域&lt;/h2>
&lt;p>思考如下代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">html&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;html scope&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">head&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;head scope&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">head&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;head scope&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>显然 &lt;code>head&lt;/code> 标签中嵌套另一个 &lt;code>head&lt;/code> 标签是没有意义的，而且编译却不会报错。这是因为 Kotlin 会隐式推断接收者为最顶层 Lambda 表达式中 this 指向的 HTML 对象。所以我们要修正这个问题就要禁止这种隐式推断：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@DslMarker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">annotation&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">HtmlTagMarker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@HtmlTagMarker&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">abstract&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">Tag&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">....&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用 &lt;code>@DslMarker&lt;/code> 声明一个注解 &lt;code>@HtmlTagMarker&lt;/code>，然后为所有标签的基类 &lt;code>Tag&lt;/code> 加上这个注解即可。再次尝试使用这种不规范的写法就会报错：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span class="line">&lt;span class="cl">&amp;#39;fun head(init: Head.() -&amp;gt; Unit): Head&amp;#39; can&amp;#39;t be called in this context by implicit receiver.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是我们仍通过指定的接收者进行调用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">html&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;html scope&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">head&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;head scope&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="nd">@html&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">head&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">+&lt;/span>&lt;span class="s2">&amp;#34;head scope&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Kotlin DSL 简介</title><link>https://blog.lv5.moe/p/introduction-to-kotlin-dsl</link><pubDate>Mon, 29 Apr 2019 11:38:00 +0800</pubDate><guid>https://blog.lv5.moe/p/introduction-to-kotlin-dsl</guid><description>&lt;img src="https://blog.lv5.moe/p/introduction-to-kotlin-dsl/kotlin-logo.png" alt="Featured image of post Kotlin DSL 简介" />&lt;h2 id="什么是-dsl">什么是 DSL&lt;/h2>
&lt;p>DSL(Domain Specific Language) 中文名称&lt;strong>特定领域专用语言&lt;/strong>，与 GPL(General Purpose Language) 即&lt;strong>通用编程语言&lt;/strong>相对。&lt;/p>
&lt;p>维基百科中对 DSL 的定义如下：&lt;/p>
&lt;blockquote>
&lt;p>A &lt;strong>domain-specific language&lt;/strong> (&lt;strong>DSL&lt;/strong>) is a computer languagespecialized to a particular application domain.&lt;/p>
&lt;/blockquote>
&lt;p>也就是说，DSL 是一种表达能力有局限的语言。相比于常见的 C、Java、PHP 等语言，DSL 并不能解决所有问题；相反，DSL 的优势在于高效、跨平台。&lt;/p>
&lt;p>常见的 DSL 语言：&lt;/p>
&lt;ul>
&lt;li>Regex&lt;/li>
&lt;li>SQL&lt;/li>
&lt;li>HTML &amp;amp; CSS&lt;/li>
&lt;/ul>
&lt;p>以前端开发为例，一般都是使用 HTML 或 XML 描述界面，然后用 js (Web) 或 java/kotlin (Android) 等语言编写逻辑。你会发现 DSL 描述界面比用代码创建组件要方便的多，而且写出的 HTML 或 XML 文件在任何语言任何系统下都可以解析。&lt;/p>
&lt;h2 id="在-kotlin-中使用-dsl">在 Kotlin 中使用 DSL&lt;/h2>
&lt;p>上一节提到的 DSL 是作为一门单独的语言存在的 External DSL，某些语言为了引入 DSL 的特性加入了 Internal DSL 这种东西。比如在 Kotlin 中用代码构建一段 HTML：&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 221;
flex-basis: 531px"
>
&lt;a href="https://blog.lv5.moe/p/introduction-to-kotlin-dsl/html-builder.jpg" data-size="574x259">
&lt;img src="https://blog.lv5.moe/p/introduction-to-kotlin-dsl/html-builder.jpg"
width="574"
height="259"
loading="lazy"
alt="HTML Builder">
&lt;/a>
&lt;figcaption>HTML Builder&lt;/figcaption>
&lt;/figure>&lt;br />
你可以在 &lt;a class="link" href="https://try.kotlinlang.org/#/Examples/Longer%20examples/HTML%20Builder/HTML%20Builder.kt" target="_blank" rel="noopener"
>Try Kotlin&lt;/a> 上找到这段示例。&lt;/p>
&lt;p>所以，我们可以用这种特性极大的精简我们的代码（第二个例子使用的是 &lt;a class="link" href="https://github.com/Kotlin/anko" target="_blank" rel="noopener"
>Kotlin/anko&lt;/a> 这个库）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Create Alert Dialog
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Android developer guide
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">val&lt;/span> &lt;span class="py">builder&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">AlertDialog&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Builder&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">it&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">setTitle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Hi, I&amp;#39;m Roy&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="n">setMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Have you tried turning it off and on again?&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="n">setPositiveButton&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">R&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">string&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">fire&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">DialogInterface&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">OnClickListener&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">dialog&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">id&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">toast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Oh…&amp;#34;&lt;/span>&lt;span class="p">)})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">builder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Use Kotlin DSL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">alert&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Hi, I&amp;#39;m Roy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Have you tried turning it off and on again?&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">yesButton&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">toast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Oh…&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}.&lt;/span>&lt;span class="n">show&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或是在代码中编写界面布局：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="n">verticalLayout&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">val&lt;/span> &lt;span class="py">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">editText&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">button&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Say Hello&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">onClick&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">toast&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Hello, &lt;/span>&lt;span class="si">${name.text}&lt;/span>&lt;span class="s2">!&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以看到我们构建了由一个 EditText 和一个 Button 构成的界面，并且我们在描述界面的同时就完成了逻辑的编写。&lt;/p></description></item><item><title>操作系统原理 —— 中断</title><link>https://blog.lv5.moe/p/operating-system-principle-interrupt</link><pubDate>Tue, 19 Mar 2019 22:19:00 +0800</pubDate><guid>https://blog.lv5.moe/p/operating-system-principle-interrupt</guid><description>&lt;img src="https://blog.lv5.moe/p/operating-system-principle-interrupt/interrupt-timeline.jpg" alt="Featured image of post 操作系统原理 —— 中断" />&lt;blockquote>
&lt;p>&lt;strong>中断&lt;/strong>（英语：Interrupt）是指处理器接收到来自硬件或软件的信号，提示发生了某个事件，应该被注意，这种情况就称为中断。&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>In summary, interrupts are used throughout modern operating systems to handle asynchronous events.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/p>
&lt;/blockquote>
&lt;p>我们考虑这样一个场景：从键盘读入一个字符。从 CPU 的角度来看这种 I/O 操作的速度远慢于 CPU 频率，所以在 CPU 发出读入字符的指令后需要消耗数百上千个时钟周期来轮询是否有数据返回（这被称为忙等待 Busy waiting），在这个过程中其他操作被阻塞，直到键盘返回数据。显然这样的低效率逻辑是不可接受的。我们需要的是一种&lt;strong>异步机制&lt;/strong>：需要时再通知 CPU 来处理，这样可以最大程度的减少 CPU 的等待时间。&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 228;
flex-basis: 549px"
>
&lt;a href="https://blog.lv5.moe/p/operating-system-principle-interrupt/interrupt-timeline.jpg" data-size="1495x653">
&lt;img src="https://blog.lv5.moe/p/operating-system-principle-interrupt/interrupt-timeline.jpg"
width="1495"
height="653"
loading="lazy"
alt="Interrupt timeline">
&lt;/a>
&lt;figcaption>Interrupt timeline&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>如图所示，每当 I/O 设备完成动作时都会触发中断，然后 CPU 停止正在进行的工作，去处理相应的中断&lt;/p>
&lt;p>下面我们通过&lt;strong>硬件中断&lt;/strong>（Hardware Interrupt）来详细介绍一下中断机制：&lt;/p>
&lt;ol>
&lt;li>CPU 通知设备驱动程序开始一个 I/O 操作，然后去处理其他应用程序&lt;/li>
&lt;li>设备控制器处理 I/O 操作并在完成时发送中断信号&lt;/li>
&lt;li>CPU 检测到中断信号暂停正在处理的程序转而处理中断&lt;/li>
&lt;/ol>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 99;
flex-basis: 238px"
>
&lt;a href="https://blog.lv5.moe/p/operating-system-principle-interrupt/interrupt-process.png" data-size="589x592">
&lt;img src="https://blog.lv5.moe/p/operating-system-principle-interrupt/interrupt-process.png"
width="589"
height="592"
loading="lazy"
alt="Interrupt process">
&lt;/a>
&lt;figcaption>Interrupt process&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>这就是整个硬件中断的原理，但是我们还需要讨论几个细节问题&lt;/p>
&lt;ul>
&lt;li>CPU 如何检测中断信号&lt;/li>
&lt;li>CPU 如何处理不同类型的中断&lt;/li>
&lt;li>如何决定是否延迟执行或优先执行某个中断&lt;/li>
&lt;/ul>
&lt;p>为了回答上面 3 个问题，我们首先要知道什么是中断信号。每个设备的驱动程序都提供&lt;strong>中断服务程序&lt;/strong>（interrupt service routine，ISR）用于响应中断，这些中断服务程序的地址储存在一个被称为&lt;strong>中断向量&lt;/strong>（interrupt vector）的数组中，而中断信号给出了对应设备的中断服务程序在中断向量中的索引。&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
在大多数的操作系统中，中断向量并不直接储存中断服务程序的地址，而是存储着多个数组的首地址，每个数组中包含一类中断处理程序的地址。CPU 通过中断信号的索引找到对应类别的数组，然后通过遍历该数组来找到相应的处理程序。这种机制被称为 &lt;strong>interrupt chaining&lt;/strong>，下图是 Intel 处理器的中断向量。
&lt;/div>
&lt;p>&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 116;
flex-basis: 280px"
>
&lt;a href="https://blog.lv5.moe/p/operating-system-principle-interrupt/intel-interrupt-vector.png" data-size="627x537">
&lt;img src="https://blog.lv5.moe/p/operating-system-principle-interrupt/intel-interrupt-vector.png"
width="627"
height="537"
loading="lazy"
alt="Intel interrupt vector">
&lt;/a>
&lt;figcaption>Intel interrupt vector&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>CPU 硬件有被称为&lt;strong>中断请求线&lt;/strong>（interrupt-request line）的机制，CPU 在执行每条指令后检测到该线中是否有中断信号，如果有则以这个中断信号作为索引从中断向量中取出中断服务程序的地址，然后去执行相应的程序。&lt;/p>
&lt;p>为了解决第 3 个问题，操作系统需要多级中断机制，以便操作系统可以区分高优先级和低优先级中断，并且可以对不同紧急程度的中断进行响应。大多数 CPU 有两个中断请求线。一种是&lt;strong>不可屏蔽的中断&lt;/strong>（nonmaskable interrupt）：它被用于及时响应不可恢复的错误。第二种是&lt;strong>可屏蔽的中断&lt;/strong>（maskable interrupt）：CPU 可以通过不同中断的优先级决定是否延迟响应或者先响应高优先级中断。&lt;/p>
&lt;p>除了硬件中断外还有一种“&lt;strong>软中断&lt;/strong>”（Software interrupt），它通常在程序抛出异常以及系统调用中触发。软中断相关内容在内核态及系统调用中介绍。&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr />
&lt;ol>
&lt;li id="fn:1" role="doc-endnote">
&lt;p>维基百科&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2" role="doc-endnote">
&lt;p>《Operating System Concepts》&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>php 几种非阻塞方式分析</title><link>https://blog.lv5.moe/p/analysis-of-several-nonblocking-modes-of-php</link><pubDate>Sat, 02 Mar 2019 12:11:00 +0800</pubDate><guid>https://blog.lv5.moe/p/analysis-of-several-nonblocking-modes-of-php</guid><description>&lt;p>本文介绍了几种非阻塞执行 php 代码的方法，并分析他们的性能与适用环境&lt;/p>
&lt;p>本文用如下代码来测试这几种非阻塞实现的性能：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 1.php:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nv">$stime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">microtime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$stime&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="nx">PHP_EOL&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 执行非阻塞逻辑
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">file_put_contents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">__DIR__&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="s1">&amp;#39;/debug.log&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;stime：&amp;#39;&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="nv">$stime&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">FILE_APPEND&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 2.php:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 模拟耗时操作
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$etime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">microtime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$etime&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="nx">PHP_EOL&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">file_put_contents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">__DIR__&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="s1">&amp;#39;/debug.log&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;etime：&amp;#39;&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="nv">$etime&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">FILE_APPEND&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="curl">curl&lt;/h2>
&lt;p>利用 php curl 函数设置一个较小的 timeout，来间接达成非阻塞。这种方法会主动断开连接，所以对于有这方面限制的 api 是不适用的&lt;/p>
&lt;h3 id="curlopt_timeout">CURLOPT_TIMEOUT&lt;/h3>
&lt;p>较老的 curl 拓展只支持 &lt;code>CURLOPT_TIMEOUT&lt;/code> 这个参数，也就是最低 timeout 为 1s&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">function&lt;/span> &lt;span class="nf">curl&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$ch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">curl_init&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_URL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;127.0.0.1/aaa.php&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_RETURNTRANSFER&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_TIMEOUT&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 一秒后关闭连接
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_HEADER&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_exec&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">3.002&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">3.001&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.9951&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="curlopt_timeout_ms">CURLOPT_TIMEOUT_MS&lt;/h3>
&lt;p>这个参数在 curl 7.16.2 中被加入，用于设置毫秒级的 timeout&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
如果 libcurl 编译时使用系统标准的名称解析器（ standard system name resolver），那部分的连接仍旧使用以秒计的超时解决方案，最小超时时间还是一秒钟
&lt;/div>
&lt;p>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">function&lt;/span> &lt;span class="nf">curl&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$ch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">curl_init&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_URL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;127.0.0.1/aaa.php&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_RETURNTRANSFER&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_TIMEOUT_MS&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 10 毫秒后关闭连接
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_HEADER&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_exec&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0141&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0043&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0127&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="curl_multi">curl_multi&lt;/h2>
&lt;p>利用 cURL 中的 &lt;code>curl_multi_*&lt;/code> 函数发送异步请求，并且可以并发请求&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">function&lt;/span> &lt;span class="nf">curl_multi&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$mh&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">curl_multi_init&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$ch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">curl_init&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_setopt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">CURLOPT_URL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;127.0.0.1/aaa.php&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_multi_add_handle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$mh&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_multi_exec&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$mh&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$active&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_multi_remove_handle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$mh&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$ch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">curl_multi_close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$mh&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0134&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0226&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0157&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="fsocketopen-或-stream_socket_client">fsocketopen 或 stream_socket_client&lt;/h2>
&lt;p>用 &lt;code>fsocketopen()&lt;/code> 打开一个连接，然后用 &lt;code>stream_set_blocking()&lt;/code> 设置非阻塞模式&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">function&lt;/span> &lt;span class="nf">sock&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$fp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">fsockopen&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;127.0.0.1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">80&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$error_code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$error_msg&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nv">$fp&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">array&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;error_code&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nv">$error_code&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;error_msg&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="nv">$error_msg&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">stream_set_blocking&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$fp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$header&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;GET /aaa.php HTTP/1.1&lt;/span>&lt;span class="se">\r\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$header&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="s2">&amp;#34;Host: 127.0.0.1&lt;/span>&lt;span class="se">\r\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$header&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="s2">&amp;#34;Connection: close&lt;/span>&lt;span class="se">\r\n\r\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fwrite&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$fp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$header&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fclose&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$fp&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">array&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;error_code&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0348&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0142&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.0149&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="fastcgi_finish_request">fastcgi_finish_request&lt;/h2>
&lt;p>这个函数可以返回缓冲区内的所有响应的数据给客户端并结束请求，但是仍继续运行当前脚本直到运行完成或者达到 timeout。所以我们可以在耗时操作前使用该函数以实现非阻塞&lt;/p>
&lt;p>注意：&lt;/p>
&lt;ol>
&lt;li>虽然这个函数叫 fastcgi 但是这是个实实在在的 FPM 函数，需要在 FPM 环境下调用&lt;/li>
&lt;li>使用该函数后，这个脚本将占用一个 FPM 进程，所以如果对高耗时脚本滥用此函数会导致 502 bad gateway&lt;/li>
&lt;li>使用这个函数会给当前 session 加锁，如不需要修改 session 推荐使用 &lt;code>session_write_close()&lt;/code> 解除占用&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$stime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">microtime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">PHP_SAPI&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;fpm-fcgi&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">function_exists&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;fastcgi_finish_request&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fastcgi_finish_request&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$etime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">microtime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$etime&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="nx">PHP_EOL&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">file_put_contents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">__DIR__&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="s1">&amp;#39;/debugtest.log&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$stime&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nv">$etime&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">FILE_APPEND&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.00048&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.00043&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.00042&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pcntl_fork">pcntl_fork&lt;/h2>
&lt;p>用 &lt;code>pcntl_fork&lt;/code> 创建子进程的方式实现真正的异步，这个函数由 pcntl 扩展提供。&lt;br />
为了防止子进程变成僵尸进程，在父进程使用 &lt;code>pcntl_wait&lt;/code> 等待子进程返回并回收资源&lt;br />
我这里采用了二次 fork 的方式，让第一次 fork 出的子进程 a 再 fork 出实际的工作进程 b，让 a 先行退出，使得 b 成为孤儿进程，被 init 进程托管。这样实现了父进程非阻塞，而且子进程不会成为僵尸进程。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Arrow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">static&lt;/span> &lt;span class="nv">$instance&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">public&lt;/span> &lt;span class="k">static&lt;/span> &lt;span class="k">function&lt;/span> &lt;span class="nf">getInstance&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">null&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">self&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="nv">$instance&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">self&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="nv">$instance&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">self&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">self&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="nv">$instance&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">public&lt;/span> &lt;span class="k">function&lt;/span> &lt;span class="nf">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$rb&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cli_set_process_title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;process_a&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$pid&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">pcntl_fork&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nv">$pid&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">pcntl_wait&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nv">$status&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">elseif&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nv">$pid&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$cid&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">pcntl_fork&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nv">$cid&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cli_set_process_title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;process_b&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">exit&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">elseif&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nv">$cid&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cli_set_process_title&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;process_c&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$rb&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">exit&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">exit&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$stime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">microtime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$stime&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="nx">PHP_EOL&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">Arrow&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="na">getInstance&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="k">use&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nv">$stime&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$etime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">microtime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">$etime&lt;/span> &lt;span class="o">.=&lt;/span> &lt;span class="nx">PHP_EOL&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">file_put_contents&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="no">__DIR__&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="s1">&amp;#39;/debug.log&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nv">$stime&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nv">$etime&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">FILE_APPEND&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">output&lt;/span>&lt;span class="o">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.023&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.004&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="mf">2.010&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们用 &lt;code>ps ax&lt;/code> 查看一下进程：&lt;br />
TTY 为 ? 的进程即为与终端无关，是守护进程（daemon）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ps ax &lt;span class="p">|&lt;/span> grep -v grep &lt;span class="p">|&lt;/span> grep -E &lt;span class="s1">&amp;#39;process_|PID&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> PID TTY STAT TIME COMMAND
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="m">7750&lt;/span> ? S 0:00 process_c
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Laravel 开发准备工作</title><link>https://blog.lv5.moe/p/preparations-for-laravel-development</link><pubDate>Sat, 23 Feb 2019 23:24:00 +0800</pubDate><guid>https://blog.lv5.moe/p/preparations-for-laravel-development</guid><description>&lt;p>因为某些原因需要搞个 laravel 项目，于是乎翻出此前的文章补充记录下 laravel 开发准备工作。&lt;/p>
&lt;p>本教程适用环境：&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://laravel.com/" target="_blank" rel="noopener"
>Laravel 5.5&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.jetbrains.com/phpstorm/" target="_blank" rel="noopener"
>PhpStorm&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="安装-laravel">安装 laravel&lt;/h2>
&lt;ul>
&lt;li>安装 php&lt;/li>
&lt;li>安装 composer&lt;/li>
&lt;li>换源：&lt;code>composer config -g repo.packagist composer https://packagist.laravel-china.org&lt;/code>&lt;/li>
&lt;li>全局安装 &lt;code>laravel/installer&lt;/code>：&lt;code>composer global require laravel/installer&lt;/code>&lt;/li>
&lt;li>创建 laravel 项目：&lt;code>laravel new blog&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="安装-laravel-ide-helper">安装 laravel-ide-helper&lt;/h2>
&lt;p>Laravel 本身的依赖注入，服务提供者等特性使得 IDE 很难做到代码检查和智能提示，所以需要安装 &lt;a class="link" href="https://github.com/barryvdh/laravel-ide-helper" target="_blank" rel="noopener"
>barryvdh/laravel-ide-helper&lt;/a> 来辅助进行代码补全和追踪。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">composer require --dev barryvdh/laravel-ide-helper
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后运行下列代码命令生成代码提示文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php artisan ide-helper:generate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">php artisan ide-helper:meta
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你可以设置 &lt;code>composer.json&lt;/code> 使每次自动更新后重新生成代码提示文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;scripts&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;post-update-cmd&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;Illuminate\\Foundation\\ComposerScripts::postUpdate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;php artisan ide-helper:generate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;php artisan ide-helper:meta&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="可选设置项">可选设置项：&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>添加对 migration 的代码提示的支持：&lt;/p>
&lt;p>发布配置文件 &lt;code>config/ide-helper.php&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php artisan vendor:publish --provider&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider&amp;#34;&lt;/span> --tag&lt;span class="o">=&lt;/span>config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>更改 &lt;code>include_fluent&lt;/code> 设置&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;include_fluent&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后重新生成代码提示文件&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php artisan ide-helper:generate
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>添加对 Model 的代码提示的支持：&lt;/p>
&lt;p>这里使用 &lt;strong>@mixin&lt;/strong> 注解来标注文档&lt;/p>
&lt;blockquote>
&lt;p>PhpStorm interprets &lt;em>@mixin&lt;/em> regardless of PHP version just the same way it interprets “&lt;em>use trait&lt;/em>” (see &lt;a class="link" href="http://youtrack.jetbrains.com/issue/WI-1730" target="_blank" rel="noopener"
>WI-1730&lt;/a> for details)&lt;/p>
&lt;/blockquote>
&lt;p>在你的模型类或者 &lt;code>Illuminate\Database\Eloquent\Model&lt;/code> 前加上 &lt;code>/** @mixin \Eloquent */&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl"> &lt;span class="k">namespace&lt;/span> &lt;span class="nx">Illuminate\Database\Eloquent&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">use&lt;/span> &lt;span class="o">...&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sd">/** @mixin \Eloquent */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">abstract&lt;/span> &lt;span class="k">class&lt;/span> &lt;span class="nc">Model&lt;/span> &lt;span class="k">implements&lt;/span> &lt;span class="nx">ArrayAccess&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Arrayable&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Jsonable&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">JsonSerializable&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">QueueableEntity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">UrlRoutable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="o">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h2 id="安装-laravel-plugin">安装 Laravel Plugin&lt;/h2>
&lt;p>&lt;a class="link" href="https://plugins.jetbrains.com/plugin/7532-laravel-plugin" target="_blank" rel="noopener"
>Laravel Plugin&lt;/a> 是一个增强 PhpStorm 对 Laravel 支持的插件，功能截图如下：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 123;
flex-basis: 297px"
>
&lt;a href="https://blog.lv5.moe/p/preparations-for-laravel-development/screenshot1.png" data-size="573x463">
&lt;img src="https://blog.lv5.moe/p/preparations-for-laravel-development/screenshot1.png"
width="573"
height="463"
loading="lazy"
alt="功能截图1">
&lt;/a>
&lt;figcaption>功能截图1&lt;/figcaption>
&lt;/figure>&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 123;
flex-basis: 297px"
>
&lt;a href="https://blog.lv5.moe/p/preparations-for-laravel-development/screenshot1.png" data-size="573x463">
&lt;img src="https://blog.lv5.moe/p/preparations-for-laravel-development/screenshot1.png"
width="573"
height="463"
loading="lazy"
alt="功能截图2">
&lt;/a>
&lt;figcaption>功能截图2&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>此插件依赖于 &lt;a class="link" href="https://github.com/barryvdh/laravel-ide-helper" target="_blank" rel="noopener"
>barryvdh/laravel-ide-helper&lt;/a>&lt;/p>
&lt;p>在 &lt;code>Settings &amp;gt; Plugins&lt;/code> 中搜索 &lt;strong>Laravel Plugin&lt;/strong> 进行安装&lt;/p>
&lt;h2 id="本地化">本地化&lt;/h2>
&lt;h3 id="生成-application-key">生成 Application Key&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php artisan key:generate
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="修改时区">修改时区&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// config/app.php
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="s1">&amp;#39;timezone&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;Asia/Shanghai&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="安装语言包">安装语言包&lt;/h3>
&lt;p>安装 &lt;a class="link" href="https://github.com/overtrue/laravel-lang" target="_blank" rel="noopener"
>overtrue/laravel-lang&lt;/a> :&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">composer require --dev overtrue/laravel-lang
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>将 &lt;code>config/app.php&lt;/code> 中&lt;/p>
&lt;p>&lt;code>Illuminate\Translation\TranslationServiceProvider::class&lt;/code>&lt;/p>
&lt;p>替换为&lt;/p>
&lt;p>&lt;code>Overtrue\LaravelLang\TranslationServiceProvider::class&lt;/code>&lt;/p>
&lt;p>并修改 &lt;code>locale&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;locale&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;zh-CN&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Typecho 追番列表插件</title><link>https://blog.lv5.moe/p/typecho-anime-list-plugin</link><pubDate>Mon, 18 Feb 2019 21:31:00 +0800</pubDate><guid>https://blog.lv5.moe/p/typecho-anime-list-plugin</guid><description>&lt;img src="https://blog.lv5.moe/p/typecho-anime-list-plugin/typecho-anime-list-plugin.png" alt="Featured image of post Typecho 追番列表插件" />&lt;h2 id="项目介绍">项目介绍&lt;/h2>
&lt;p>Bangumi 追番列表插件 Typecho 版&lt;/p>
&lt;p>使用短代码 &lt;code>[bangumi]&lt;/code> 在任何位置插入你的 Bangumi 追番列表&lt;/p>
&lt;p>插件效果见此处：https://blog.sspirits.top/about&lt;/p>
&lt;p>GitHub 仓库：&lt;a class="link" href="https://github.com/ShadowySpirits/BangumiList" target="_blank" rel="noopener"
>ShadowySpirits/BangumiList&lt;/a>&lt;/p>
&lt;h2 id="使用方法">使用方法&lt;/h2>
&lt;ol>
&lt;li>在 &lt;a class="link" href="https://github.com/ShadowySpirits/BangumiList/releases" target="_blank" rel="noopener"
>Release&lt;/a> 中下载此插件的最新版，上传至网站的 /usr/plugins 目录下；&lt;/li>
&lt;li>启用该插件，正确填写相关信息。&lt;/li>
&lt;li>在你想展示的位置插入短代码 &lt;code>[bangumi]&lt;/code>&lt;/li>
&lt;/ol></description></item><item><title>Typecho 评论 SMTP、Mailgun 邮件通知插件</title><link>https://blog.lv5.moe/p/typecho-comments-smtp-mailgun-mail-notification-plugin</link><pubDate>Mon, 18 Feb 2019 21:08:00 +0800</pubDate><guid>https://blog.lv5.moe/p/typecho-comments-smtp-mailgun-mail-notification-plugin</guid><description>&lt;img src="https://blog.lv5.moe/p/typecho-comments-smtp-mailgun-mail-notification-plugin/mail_screenshot.png" alt="Featured image of post Typecho 评论 SMTP、Mailgun 邮件通知插件" />&lt;h2 id="插件简介">插件简介&lt;/h2>
&lt;p>Comment2Mail 是 Typecho 评论邮件通知插件，支持 SMTP、Mailgun 两种接口，其中 SMTP 接口采用非阻塞方式发送邮件&lt;/p>
&lt;p>在评论审核通过、用户评论文章、用户评论被回复时发送邮件通知&lt;/p>
&lt;p>详见 GitHub：&lt;a class="link" href="https://github.com/ShadowySpirits/Comment2Mail" target="_blank" rel="noopener"
>ShadowySpirits/Comment2Mail&lt;/a>&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 150;
flex-basis: 362px"
>
&lt;a href="https://blog.lv5.moe/p/typecho-comments-smtp-mailgun-mail-notification-plugin/mail_screenshot.png" data-size="1041x690">
&lt;img src="https://blog.lv5.moe/p/typecho-comments-smtp-mailgun-mail-notification-plugin/mail_screenshot.png"
width="1041"
height="690"
loading="lazy"
alt="邮件模板">
&lt;/a>
&lt;figcaption>邮件模板&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="安装方法">安装方法&lt;/h2>
&lt;ol>
&lt;li>至 &lt;a class="link" href="https://github.com/ShadowySpirits/Comment2Mail/releases" target="_blank" rel="noopener"
>Releases&lt;/a> 中下载最新版本插件，上传至网站的 /usr/plugins/ 目录下&lt;/li>
&lt;li>启用该插件，正确填写相关信息&lt;/li>
&lt;/ol>
&lt;h2 id="鸣谢">鸣谢&lt;/h2>
&lt;p>本插件模板参考自 &lt;a class="link" href="https://github.com/ylqjgm/LoveKKComment" target="_blank" rel="noopener"
>ylqjgm/LoveKKComment&lt;/a>&lt;/p></description></item><item><title>KMS 激活 Windows 和 Office</title><link>https://blog.lv5.moe/p/kms-activates-windows-and-office</link><pubDate>Sun, 17 Feb 2019 19:38:00 +0800</pubDate><guid>https://blog.lv5.moe/p/kms-activates-windows-and-office</guid><description>&lt;img src="https://blog.lv5.moe/p/kms-activates-windows-and-office/active-success.jpg" alt="Featured image of post KMS 激活 Windows 和 Office" />&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
kms 激活适用于批量授权版本，即 VL 版的 Windows 和 Office
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="使用方法">使用方法&lt;/h2>
&lt;p>以&lt;strong>管理员&lt;/strong>身份开启一个 CMD 窗口&lt;/p>
&lt;h3 id="windows">Windows&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">slmgr.vbs -ipk &amp;lt;windows-GVLK-key&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">slmgr.vbs -skms &amp;lt;kms-server&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">slmgr.vbs -ato
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a class="link" href="https://github.com/SystemRage/py-kms/wiki/Windows-GVLK-Keys" target="_blank" rel="noopener"
>各版本的 Windows GVLK Keys&lt;/a>&lt;/p>
&lt;h3 id="office">Office&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> &amp;lt;your-office-dir&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cscript ospp.vbs /inpkey:&amp;lt;office-GVLK-key&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cscript ospp.vbs /sethst:&amp;lt;kms-server&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cscript ospp.vbs /act
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a class="link" href="https://github.com/SystemRage/py-kms/wiki/Office-GVLK-Keys" target="_blank" rel="noopener"
>各版本的 Office GVLK Keys&lt;/a>&lt;/p>
&lt;p>激活成功会看到这样的提示：&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 180;
flex-basis: 434px"
>
&lt;a href="https://blog.lv5.moe/p/kms-activates-windows-and-office/active-success.jpg" data-size="340x188">
&lt;img src="https://blog.lv5.moe/p/kms-activates-windows-and-office/active-success.jpg"
width="340"
height="188"
loading="lazy"
alt="active success">
&lt;/a>
&lt;figcaption>active success&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>过期时间可以使用 &lt;code>slmgr.vbs -xpr&lt;/code> 查询&lt;/p></description></item><item><title>Nextcloud15 优化心得</title><link>https://blog.lv5.moe/p/nextcloud-15-optimization-experience</link><pubDate>Sun, 17 Feb 2019 12:07:16 +0800</pubDate><guid>https://blog.lv5.moe/p/nextcloud-15-optimization-experience</guid><description>&lt;img src="https://blog.lv5.moe/p/nextcloud-15-optimization-experience/nextcloud.jpg" alt="Featured image of post Nextcloud15 优化心得" />&lt;p>最近抽空把 Nextcloud 从 14 升级到 15，这里记录一下升级流程和优化心得&lt;/p>
&lt;h2 id="nextcloud15-功能更新">Nextcloud15 功能更新&lt;/h2>
&lt;p>这里列出几个更新的功能，给观望的朋友们做个参考&lt;/p>
&lt;ul>
&lt;li>全文搜索&lt;/li>
&lt;li>工作流：文档自动转 PDF，自动执行 shell 命令&lt;/li>
&lt;li>分享功能升级：现在一个文件可以创建多个分享链接、可以分享只读文件&lt;/li>
&lt;li>协作功能加强&lt;/li>
&lt;li>2-3x loading 速度提升&lt;/li>
&lt;/ul>
&lt;h2 id="升级流程">升级流程&lt;/h2>
&lt;p>因为 Nextcloud14 无法直接升级到 15，所以我采用全新安装的方式。安装完成后用备份恢复数据。详细流程可以参照文档中的 &lt;a class="link" href="https://docs.nextcloud.com/server/15/admin_manual/maintenance/migrating.html#migrating-to-a-different-server" target="_blank" rel="noopener"
>Migrating to a different server&lt;/a>&lt;/p>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock info">
将旧的 data 文件夹复制过去后需要用 sudo -uwww php occ files:scan &amp;ndash;all 扫描更改
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="优化心得">优化心得&lt;/h2>
&lt;h3 id="美化-url">美化 URL&lt;/h3>
&lt;p>这里分享一下我的 nginx 配置（去掉了 URL 中的 index.php 和 remote.php）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="cl">&lt;span class="k">server&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">listen&lt;/span> &lt;span class="mi">443&lt;/span> &lt;span class="s">ssl&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">ssl_certificate&lt;/span> &lt;span class="s">&amp;lt;your&lt;/span> &lt;span class="s">certificate&amp;gt;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">ssl_certificate_key&lt;/span> &lt;span class="s">&amp;lt;your&lt;/span> &lt;span class="s">certificate&lt;/span> &lt;span class="s">key&amp;gt;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">ssl_session_timeout&lt;/span> &lt;span class="mi">5m&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">ssl_protocols&lt;/span> &lt;span class="s">TLSv1&lt;/span> &lt;span class="s">TLSv1.1&lt;/span> &lt;span class="s">TLSv1.2&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">ssl_ciphers&lt;/span> &lt;span class="s">ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256:ECDHE+3DES:RSA+3DES&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">ssl_prefer_server_ciphers&lt;/span> &lt;span class="no">on&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">server_name&lt;/span> &lt;span class="s">&amp;lt;your&lt;/span> &lt;span class="s">server&lt;/span> &lt;span class="s">name&amp;gt;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">root&lt;/span> &lt;span class="s">&amp;lt;your&lt;/span> &lt;span class="s">nextcloud&lt;/span> &lt;span class="s">path&amp;gt;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">error_log&lt;/span> &lt;span class="s">/var/log/nextcloud15_error.log&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">access_log&lt;/span> &lt;span class="s">/var/log/nextcloud15_access.log&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">/robots.txt&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">allow&lt;/span> &lt;span class="s">all&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">log_not_found&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">access_log&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Deny access to the data and config directories, .ht* files,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1"># the README, and the database structure definition
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span> &lt;span class="sr">^/(data|config|\.ht|db_structure\.xml|README)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">deny&lt;/span> &lt;span class="s">all&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Pretty URLs for WebDAV
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span>&lt;span class="sr">*&lt;/span> &lt;span class="s">\/remote\/(?:.*)&lt;/span>$ &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">rewrite&lt;/span> &lt;span class="s">^&lt;/span> &lt;span class="s">/remote.php&lt;/span>&lt;span class="nv">$uri$is_args$args&lt;/span> &lt;span class="s">last&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Pretty URLs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kn">location&lt;/span> &lt;span class="s">/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span>&lt;span class="sr">*&lt;/span> &lt;span class="s">^\/core\/(?=preview)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">rewrite&lt;/span> &lt;span class="s">^&lt;/span> &lt;span class="s">/index.php&lt;/span>&lt;span class="nv">$uri$is_args$args&lt;/span> &lt;span class="s">last&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span>&lt;span class="sr">*&lt;/span> &lt;span class="s">^\/core(/.*)?&lt;/span>$ &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">break&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">rewrite&lt;/span> &lt;span class="s">^&lt;/span> &lt;span class="s">/index.php&lt;/span> &lt;span class="s">last&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span> &lt;span class="sr">^(.+?\.php)(/.*)?$&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$1&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">404&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">include&lt;/span> &lt;span class="s">fastcgi_params&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">fastcgi_param&lt;/span> &lt;span class="s">SCRIPT_FILENAME&lt;/span> &lt;span class="nv">$document_root$1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">fastcgi_param&lt;/span> &lt;span class="s">PATH_INFO&lt;/span> &lt;span class="nv">$2&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">fastcgi_param&lt;/span> &lt;span class="s">front_controller_active&lt;/span> &lt;span class="s">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">fastcgi_pass&lt;/span> &lt;span class="s">unix:/tmp/php-cgi.sock&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">rewrite&lt;/span> &lt;span class="s">/.well-known/carddav&lt;/span> &lt;span class="s">/remote.php/dav&lt;/span> &lt;span class="s">permanent&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">rewrite&lt;/span> &lt;span class="s">/.well-known/caldav&lt;/span> &lt;span class="s">/remote.php/dav&lt;/span> &lt;span class="s">permanent&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">location&lt;/span> &lt;span class="p">~&lt;/span> &lt;span class="sr">\.(?:css|js|woff|svg|gif|png|html|ttf|woff|woff2|ico|jpg|jpeg)$&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">try_files&lt;/span> &lt;span class="nv">$uri&lt;/span> &lt;span class="s">/index.php&lt;/span>&lt;span class="nv">$request_uri&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">add_header&lt;/span> &lt;span class="s">Cache-Control&lt;/span> &lt;span class="s">&amp;#34;public,&lt;/span> &lt;span class="s">max-age=2592000&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kn">access_log&lt;/span> &lt;span class="no">off&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="开启-opcache">开启 OPcache&lt;/h3>
&lt;p>一般的源比如 &lt;code>ppa:ondrej/php&lt;/code> 都会默认安装 OPcache（可以用 &lt;code>php -m | grep Zend\ OPcache&lt;/code> 来检查是否安装）但是一般默认都不会开启 OPcache，需要在 php.ini 中手动开启。官方推荐配置如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="na">zend_extension&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">opcache.so&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.enable&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.enable_cli&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.interned_strings_buffer&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.max_accelerated_files&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">10000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.memory_consumption&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">128&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.save_comments&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">opcache.revalidate_freq&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="开启内存缓存">开启内存缓存&lt;/h3>
&lt;p>官方文档推荐本地缓存用 APCu，分布式缓存和锁用 Redis，配置文件如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// &amp;lt;your-nextcloud-path&amp;gt;/config/config.php
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;memcache.local&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;\OC\Memcache\APCu&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;memcache.distributed&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;\OC\Memcache\Redis&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;memcache.locking&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;\OC\Memcache\Redis&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">&amp;#39;redis&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;host&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s1">&amp;#39;redis-host.example.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;port&amp;#39;&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="mi">6379&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用 APCu 和 Redis 都需要安装相应 PHP 拓展（Redis 还需要安装相应的服务端： &lt;code>apt install redis-server&lt;/code>）我个人推荐自行编译安装：&lt;/p>
&lt;ol>
&lt;li>下载对应源码：&lt;a class="link" href="https://pecl.php.net/package/APCu" target="_blank" rel="noopener"
>APCu&lt;/a>、&lt;a class="link" href="https://pecl.php.net/package/redis" target="_blank" rel="noopener"
>Redis&lt;/a>&lt;/li>
&lt;li>解压到你的服务器上&lt;/li>
&lt;li>进入对应目录并编译安装：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">phpize // 这个命令在 php 的安装路经下的 bin 目录里
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./configure --with-php-config&lt;span class="o">=&lt;/span>&amp;lt;your-php-bin-dir&amp;gt;/php-config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> make install
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="4">
&lt;li>在 php.ini 中启用对应拓展：&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="na">extension&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">redis.so&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">extension&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">apcu.so&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">apc.enabled&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">apc.shm_size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">32M&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">apc.enable_cli&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="后台任务设置为-cron">后台任务设置为 cron&lt;/h3>
&lt;p>在管理员面板基本设置里将后台任务设置成 cron，然后登陆服务器配置 cron：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo -uwww crontab -e // -u 参数为运行 php 的用户
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">// 在打开的文件中添加：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*/15 * * * * php -f &amp;lt;your-next-cloud-path&amp;gt;/cron.php
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Typecho 新版又拍云插件使用教程</title><link>https://blog.lv5.moe/p/new-version-of-upyunfile-plugin-use-tutorial</link><pubDate>Mon, 21 Jan 2019 00:30:00 +0800</pubDate><guid>https://blog.lv5.moe/p/new-version-of-upyunfile-plugin-use-tutorial</guid><description>&lt;img src="https://blog.lv5.moe/p/new-version-of-upyunfile-plugin-use-tutorial/upyun.png" alt="Featured image of post Typecho 新版又拍云插件使用教程" />&lt;h2 id="说明">说明&lt;/h2>
&lt;p>本插件 &lt;a class="link" href="https://github.com/ShadowySpirits/UpyunFile" target="_blank" rel="noopener"
>ShadowySpirits/UpyunFile&lt;/a> 是又拍云文件上传插件，基于 &lt;a class="link" href="https://github.com/codesee/UpyunFile" target="_blank" rel="noopener"
>codesee/UpyunFile&lt;/a> 二次开发。&lt;/p>
&lt;p>相比于原插件：&lt;/p>
&lt;ul>
&lt;li>修复了启用本插件会影响其他替换内容插件生效的 Bug&lt;/li>
&lt;li>修复了某些情况下图片链接替换失败的 Bug&lt;/li>
&lt;li>新增：接入又拍云图片处理功能&lt;/li>
&lt;li>新增：为博客静态资源加入 Token 防盗链&lt;/li>
&lt;/ul>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
又拍云 SDK 仅支持 PHP &amp;gt;= 5.6 的环境
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="使用方法">使用方法&lt;/h2>
&lt;ol>
&lt;li>在 &lt;a class="link" href="https://github.com/ShadowySpirits/UpyunFile/releases" target="_blank" rel="noopener"
>Release&lt;/a> 中下载此插件的最新版，上传至网站的 /usr/plugins/ 目录下。务必保持本插件文件夹名称为 &lt;strong>UpyunFile&lt;/strong>，不能随意更改&lt;/li>
&lt;li>启用该插件，正确填写相关信息，保存即可&lt;/li>
&lt;/ol>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 106;
flex-basis: 256px"
>
&lt;a href="https://blog.lv5.moe/p/new-version-of-upyunfile-plugin-use-tutorial/screenshot.jpg" data-size="991x929">
&lt;img src="https://blog.lv5.moe/p/new-version-of-upyunfile-plugin-use-tutorial/screenshot.jpg"
width="991"
height="929"
loading="lazy"
alt="screenshot">
&lt;/a>
&lt;figcaption>screenshot&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="注意事项">注意事项&lt;/h2>
&lt;ol>
&lt;li>启用又拍云图片处理需在又拍云控制台中创建缩略图版本并填入插件相应位置，文档：&lt;a class="link" href="https://help.upyun.com/knowledge-base/image/#thumb" target="_blank" rel="noopener"
>https://help.upyun.com/knowledge-base/image/#thumb&lt;/a>；又拍云图片处理会忽略带有后缀 &lt;code>_nothumb&lt;/code> 的图片（比如：example_nothumb.png）&lt;/li>
&lt;li>如你创建的缩略图版本开启了转码功能，则需将输出格式填入插件相应位置&lt;/li>
&lt;li>只有 &lt;code>JPG、JPEG、PNG、BMP&lt;/code> 这 4 种格式的图片才会进行处理&lt;/li>
&lt;li>启用 Token 防盗链需在又拍云控制台中启用 Token 防盗链并将密钥填入插件相应位置&lt;/li>
&lt;li>自定义目录结构可以在 Typecho 根目录下的 config.inc.php 中添加代码 &lt;code>define('__TYPECHO_UPLOAD_DIR__', '/path/to/uploads');&lt;/code> 并设置目录结构为 &lt;code>Typecho结构&lt;/code>。&lt;/li>
&lt;/ol>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
Token 防盗链功能只能修改 HTML 代码中的 CDN 链接，如果需要引入字体图片等资源请内联 CSS
&lt;/div>
&lt;p>&lt;/p>
&lt;h2 id="更新记录">更新记录：&lt;/h2>
&lt;p>&lt;strong>v0.9.0：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>升级 SDK，修复 Bug，加入新功能&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>v1.0.0：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>控制台的文件管理中现在可以正常查看有 Token 防盗链保护的图片&lt;/li>
&lt;li>又拍云图片处理会忽略带有后缀 &lt;code>_nothumb&lt;/code> 的图片（比如：example_nothumb.png）&lt;/li>
&lt;li>优化代码&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>v1.0.2：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>修复某些情况下重复添加 Token 的 bug&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>v1.0.3：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>优化代码&lt;/li>
&lt;li>增强兼容性&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>v1.0.4：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>解决兼容性问题&lt;/li>
&lt;/ul>
&lt;p>&lt;/p>
&lt;div class="tip inlineBlock warning">
如果你有使用上的问题请在提问时写清楚你的 &lt;strong>php&lt;/strong> 和 &lt;strong>typecho&lt;/strong> 版本以及&lt;strong>报错信息&lt;/strong>，否则一律不予回复
&lt;/div>
&lt;p>&lt;/p></description></item><item><title>网页字体优化</title><link>https://blog.lv5.moe/p/web-font-optimization</link><pubDate>Thu, 03 Jan 2019 22:32:00 +0800</pubDate><guid>https://blog.lv5.moe/p/web-font-optimization</guid><description>&lt;img src="https://blog.lv5.moe/p/web-font-optimization/blog_title.jpg" alt="Featured image of post 网页字体优化" />&lt;p>如果你想在网页中引入一些花式字体，比如这样：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 265;
flex-basis: 637px"
>
&lt;a href="https://blog.lv5.moe/p/web-font-optimization/blog_title.jpg" data-size="369x139">
&lt;img src="https://blog.lv5.moe/p/web-font-optimization/blog_title.jpg"
width="369"
height="139"
loading="lazy"
alt="blog_title">
&lt;/a>
&lt;figcaption>blog_title&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>可以试试下面几个技巧来优化字体加载速度：&lt;/p>
&lt;h2 id="使用-cdn-加速字体资源加载">使用 CDN 加速字体资源加载&lt;/h2>
&lt;p>你可以把字体托管在各大 CDN 服务商的云储存中，或者使用各种公共 CDN 来分发你的字体。特别是后者，如果用户浏览过的其他网页也使用过这种字体，那么浏览器就可以从缓存中读取，而不用重新下载。&lt;/p>
&lt;h2 id="使用-woff2-优化字体体积">使用 WOFF2 优化字体体积&lt;/h2>
&lt;p>WOFF2 采用自定义预处理和压缩算法，提供的文件大小压缩率比其他格式高大约 30%。下面是一张 ttf vs woff vs woff2 的比较图：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 410;
flex-basis: 984px"
>
&lt;a href="https://blog.lv5.moe/p/web-font-optimization/font_type.jpg" data-size="734x179">
&lt;img src="https://blog.lv5.moe/p/web-font-optimization/font_type.jpg"
width="734"
height="179"
loading="lazy"
alt="font_type">
&lt;/a>
&lt;figcaption>font_type&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>可以看到 woff2 在体积的控制上还是非常有优势的，这里推荐一个 ttf 转 woff2 的工具，提供 CLI 和 API 两种转换方式。&lt;br />
[button color=&amp;ldquo;dark&amp;rdquo; icon=&amp;ldquo;fontello fontello-github&amp;rdquo; url=&amp;ldquo;https://github.com/nfroidure/ttf2woff2&amp;rdquo;]nfroidure/ttf2woff2[/button]&lt;/p>
&lt;p>目前浏览器对 woff2 的兼容性已经相当不错了，可以看到浏览器的支持率达到了 84.03%。移动端 Android 5+/IOS 10+ 的 webview 支持 woff2，目前我测试的几款主流手机浏览器对 woff2 都完美支持。&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 315;
flex-basis: 757px"
>
&lt;a href="https://blog.lv5.moe/p/web-font-optimization/woff2_support.jpg" data-size="1584x502">
&lt;img src="https://blog.lv5.moe/p/web-font-optimization/woff2_support.jpg"
width="1584"
height="502"
loading="lazy"
alt="woff2_support">
&lt;/a>
&lt;figcaption>woff2_support&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="提取部分字体">提取部分字体&lt;/h2>
&lt;p>很多情况下（特别是中文字体体积在通常情况下超大），你并不需要使用完整的字体文件，比如我的签名：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 202;
flex-basis: 486px"
>
&lt;a href="https://blog.lv5.moe/p/web-font-optimization/sign.jpg" data-size="233x115">
&lt;img src="https://blog.lv5.moe/p/web-font-optimization/sign.jpg"
width="233"
height="115"
loading="lazy"
alt="sign">
&lt;/a>
&lt;figcaption>sign&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>对于这样的静态页面，可以提取所需的字体来减小引入的字体文件体积，这里推荐两个工具：&lt;a class="link" href="http://font-spider.org/" target="_blank" rel="noopener"
>字蛛&lt;/a> &amp;amp; &lt;a class="link" href="http://ecomfe.github.io/fontmin" target="_blank" rel="noopener"
>fontmin&lt;/a>，并且 fontmin 提供了 &lt;em>Mac OS X&lt;/em>、&lt;em>Windows&lt;/em> 平台下的客户端：&lt;a class="link" href="https://github.com/ecomfe/fontmin-app" target="_blank" rel="noopener"
>Github: ecomfe/fontmin-app&lt;/a>&lt;/p>
&lt;h2 id="内联字体">内联字体&lt;/h2>
&lt;p>在字体不大的情况下，使用 data 协议内联是个不错的选择，可以减少请求数，并且完全避免了 FOUT 和 FOIT。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">font-face&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">font-family&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">AomemoFont-Regular&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">src&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;data:font/woff2;base64,...&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">format&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;woff2&amp;#34;&lt;/span>&lt;span class="o">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">font-style&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">normal&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">font-weight&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">normal&lt;/span>&lt;span class="o">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这种方式一定程度上拖慢了首屏加载时间，推荐配合提取部分字体使用。&lt;/p></description></item><item><title>个人网站 CDN 选用指北</title><link>https://blog.lv5.moe/p/individual-website-cdn-usage-reference</link><pubDate>Wed, 02 Jan 2019 14:05:00 +0800</pubDate><guid>https://blog.lv5.moe/p/individual-website-cdn-usage-reference</guid><description>&lt;img src="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_logo.jpg" alt="Featured image of post 个人网站 CDN 选用指北" />&lt;h2 id="全能王-cloudflare">全能王 Cloudflare&lt;/h2>
&lt;p>Cloudflare 是一家提供 DNS 和 CDN 等服务的公司，号称 &lt;code>Powering over 39% of managed DNS domains&lt;/code> ，其提供的 CDN 服务在功能性（WAF、Page Rules、Argo、Load Balancing）上不知道甩掉国内同行几条街，而且基础功能和流量完全免费。但是基于国情在国内访问不是很理想，更建议体量大的静态资源使用国内 CDN 服务。&lt;/p>
&lt;p>Cloudflare 的接入方式默认只支持 DNS 接入，CNAME 接入需要 py（大雾）。这里简单介绍下 Cloudflare 的几个功能。&lt;/p>
&lt;h3 id="全站-https">全站 HTTPS&lt;/h3>
&lt;p>Cloudflare 免费提供泛域名证书，只要接入他家的 CDN 就可以体验到全站 HTTPS 的舒爽（手动狗头）。需要注意的是免费证书下载下来是自签名证书，只有在 Cloudflare 网络内受信，用它来开启个回源 HTTPS 还行，浏览器是不认的。控制台的 Crypto 标签下可以配置其他相关选项。&lt;/p>
&lt;h3 id="自动阻止恶意访问">自动阻止恶意访问&lt;/h3>
&lt;p>自动检测出恶意 IP 后使用 challenge page 来进一步甄别和阻止恶意访问（下图是 javascript challenge page），可以在控制台的 Firewall 标签页下调整 Security Level 和 Challenge Passage 的等级，或者在 IP Access Rules 下根据 IP、ASN、 国家等手动配置 。&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 321;
flex-basis: 771px"
>
&lt;a href="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_challenge_page.jpg" data-size="926x288">
&lt;img src="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_challenge_page.jpg"
width="926"
height="288"
loading="lazy"
alt="challenge page">
&lt;/a>
&lt;figcaption>challenge page&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="web-application-firewall">Web Application Firewall&lt;/h3>
&lt;p>Cloudflare 提供的 WAF 可以说是相当强大的应用防火墙，免费用户拥有 5 条规则，可以根据 IP、Cookie、host、URI、威胁等级、是否是机器人等匹配规则指定阻止访问或者需要输入验证码等策略。&lt;/p>
&lt;p>这里给出一个例子，禁止除我的 IP 以外的用户访问某个敏感页：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 212;
flex-basis: 509px"
>
&lt;a href="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_waf.jpg" data-size="1240x584">
&lt;img src="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_waf.jpg"
width="1240"
height="584"
loading="lazy"
alt="waf">
&lt;/a>
&lt;figcaption>waf&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="page-rules">Page Rules&lt;/h3>
&lt;p>对某些页面指定特定的 Cloudflare 设置（缓存等级、过期时间、开启 Always Online 等等），比如对某个子域名的所有 HTML 文件添加自动优化功能：&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 172;
flex-basis: 413px"
>
&lt;a href="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_page_rules.jpg" data-size="967x561">
&lt;img src="https://blog.lv5.moe/p/individual-website-cdn-usage-reference/cf_page_rules.jpg"
width="967"
height="561"
loading="lazy"
alt="page rules">
&lt;/a>
&lt;figcaption>page rules&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h3 id="添加第三方-app">添加第三方 APP&lt;/h3>
&lt;p>在控制台的 Apps 标签下可以添加第三方 App，比如接入 Google Analytics、加入本站使用 Cookie 的提示等等。&lt;/p>
&lt;h3 id="其他功能">其他功能&lt;/h3>
&lt;p>除了一般的 CDN 该有的功能以外还有自动跳转移动端页面、AMP 优化、 Rocket Loader、Always Online 等实用功能。以上介绍的都是免费功能，如果你愿意付费的话还能享受到 Argo、Load Balancing、Rate Limiting 等进阶功能。&lt;/p>
&lt;h2 id="国内-cdn-服务商">国内 CDN 服务商&lt;/h2>
&lt;p>前面说过 Cloudflare 虽然功能非常强大，可惜天朝自有国情在，所以主站使用 Cloudflare CDN，静态资源托管在国内云储存这种动静分离的方式可以有更好的体验。&lt;/p>
&lt;p>本站使用的是又拍云，我个人觉得又拍云性能和功能上比七牛要好一些，而且比阿里云要便宜 qwq。并且又拍云提供融合云储存，可以将增量文件同步到七牛或者阿里云，加入&lt;a class="link" href="https://www.upyun.com/league" target="_blank" rel="noopener"
>又拍云联盟&lt;/a>可以获得 10GB 免费存储空间和 15GB 免费流量（HTTPS 也能用，这点要比七牛良心）&lt;/p></description></item><item><title>Windows 下编译 AV1 codec</title><link>https://blog.lv5.moe/p/compiling-av1-codec-under-windows</link><pubDate>Tue, 18 Dec 2018 10:52:00 +0800</pubDate><guid>https://blog.lv5.moe/p/compiling-av1-codec-under-windows</guid><description>&lt;img src="https://blog.lv5.moe/p/compiling-av1-codec-under-windows/codec-timeline.jpg" alt="Featured image of post Windows 下编译 AV1 codec" />&lt;h2 id="什么是-av1">什么是 AV1&lt;/h2>
&lt;p>AV1 全称 AOMedia Video 1 是 AOM (Alliance for Open Media) 在 2018 年推出的新一代视频编码标准，其设计目的是提供更好的质量、更小的体积，以用于在互联网上传输高质量的视频。目前 Chrome 69 和 Firefox 63 中都添加了对 AV1 的支持（需要手动开启），LAV 在 &lt;a class="link" href="https://github.com/Nevcairiel/LAVFilters/releases/tag/0.73" target="_blank" rel="noopener"
>0.73&lt;/a> 中开始支持对 AV1 的解码。&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 242;
flex-basis: 581px"
>
&lt;a href="https://blog.lv5.moe/p/compiling-av1-codec-under-windows/codec-timeline.jpg" data-size="1400x578">
&lt;img src="https://blog.lv5.moe/p/compiling-av1-codec-under-windows/codec-timeline.jpg"
width="1400"
height="578"
loading="lazy"
alt="codec timeline">
&lt;/a>
&lt;figcaption>codec timeline&lt;/figcaption>
&lt;/figure>&lt;br />
更详细的介绍可以参考以下两篇文章：&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://research.mozilla.org/av1-media-codecs/" target="_blank" rel="noopener"
>mozilla research&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://www.theoplayer.com/blog/av1-hevc-comparative-look-video-codecs" target="_blank" rel="noopener"
>AV1 or HEVC? How to choose the best video codec&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="编译-av1-codec">编译 AV1 codec&lt;/h2>
&lt;h3 id="编译所需环境">编译所需环境：&lt;/h3>
&lt;ol>
&lt;li>&lt;a class="link" href="https://cmake.org/" target="_blank" rel="noopener"
>CMake&lt;/a> version 3.5 or higher.&lt;/li>
&lt;li>&lt;a class="link" href="https://git-scm.com/" target="_blank" rel="noopener"
>Git&lt;/a>.&lt;/li>
&lt;li>&lt;a class="link" href="https://www.perl.org/" target="_blank" rel="noopener"
>Perl&lt;/a>.&lt;/li>
&lt;li>For x86 targets, &lt;a class="link" href="http://yasm.tortall.net/" target="_blank" rel="noopener"
>yasm&lt;/a>, which is preferred, or a recent version of &lt;a class="link" href="http://www.nasm.us/" target="_blank" rel="noopener"
>nasm&lt;/a>.&lt;/li>
&lt;li>Building the documentation requires &lt;a class="link" href="http://doxygen.org/" target="_blank" rel="noopener"
>doxygen&lt;/a>.&lt;/li>
&lt;li>Building the unit tests requires &lt;a class="link" href="https://www.python.org/" target="_blank" rel="noopener"
>Python&lt;/a>.&lt;/li>
&lt;li>Emscripten builds require the portable &lt;a class="link" href="https://kripken.github.io/emscripten-site/index.html" target="_blank" rel="noopener"
>EMSDK&lt;/a>.&lt;/li>
&lt;/ol>
&lt;h3 id="环境搭建">环境搭建：&lt;/h3>
&lt;ul>
&lt;li>cmake：&lt;a class="link" href="https://cmake.org/download/" target="_blank" rel="noopener"
>cmake 3.7.1&lt;/a>&lt;/li>
&lt;li>make/gcc：&lt;a class="link" href="https://mingw-w64.org/" target="_blank" rel="noopener"
>MinGW-W64&lt;/a> （一定要选 64 位的 MinGW）&lt;/li>
&lt;li>perl：&lt;a class="link" href="http://strawberryperl.com/download/5.28.1.1/strawberry-perl-5.28.1.1-64bit.msi" target="_blank" rel="noopener"
>strawberry-perl-5.28.1.1-64bit&lt;/a>&lt;/li>
&lt;li>yasm：&lt;a class="link" href="http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe" target="_blank" rel="noopener"
>yasm-1.3.0-win64&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>安装上述环境并添加环境变量后即可开始编译&lt;/p>
&lt;h3 id="编译流程">编译流程：&lt;/h3>
&lt;ol>
&lt;li>clone 代码&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://aomedia.googlesource.com/aom
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>在 &lt;code>aom&lt;/code> 文件夹外新建一个文件夹 &lt;code>aom_build&lt;/code>&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> aom_build
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cmake ../aom
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>一切正常的话你会在 &lt;code>aom_build&lt;/code> 中看到 &lt;code>aomenc.exe&lt;/code> 和 &lt;code>aomdec.exe&lt;/code>，这就是我们要的 codec&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">// aomenc --help 可以看到编码器版本
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Included encoders:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> av1 - AOMedia Project AV1 Encoder 1.0.0-1047-gf4e775cf3 &lt;span class="o">(&lt;/span>default&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Use --codec to switch to a non-default encoder.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>iptables 白名单过滤</title><link>https://blog.lv5.moe/p/iptables-whitelist-filtering</link><pubDate>Wed, 05 Dec 2018 20:43:00 +0800</pubDate><guid>https://blog.lv5.moe/p/iptables-whitelist-filtering</guid><description>&lt;h2 id="iptables-过滤参数介绍">iptables 过滤参数介绍&lt;/h2>
&lt;h3 id="-a-append">-A (append)&lt;/h3>
&lt;p>向某个规则链尾部添加一条规则，如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">iptables -A INPUT -p icmp -m icmp --icmp-type &lt;span class="m">8&lt;/span> -j ACCEPT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="-i-insert-在规则链的头部插入新的规则">-I (Insert) 在规则链的头部插入新的规则&lt;/h3>
&lt;p>一共有三条规则链：Forward、Input、Output&lt;/p>
&lt;ul>
&lt;li>
&lt;p>Forward：数据包的目的地址不是本机，也就是说，这个包将被转发&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Input：数据包的目的地址是本机&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Output：数据包是由本地系统进程产生的，并通过某个本地端口发送的&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h3 id="-m-module_name">-m (module_name)&lt;/h3>
&lt;p>使用扩展模块来进行数据包的匹配，本文主要介绍 &lt;code>-m state&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport &lt;span class="m">2333&lt;/span> -j ACCEPT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 iptables 上一共有四种状态：NEW、ESTABLISHED、INVALID、RELATED&lt;/p>
&lt;ol>
&lt;li>
&lt;p>NEW：NEW 说明这个包是我们看到的第一个包。意思就是，这是 conntrack 模块看到的某个连接的第一个包，它即将被匹配了。比如，我们看到一个 SYN 包，是我们所留意的连接的第一个包，就要匹配它。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>ESTABLISHED： ESTABLISHED 已经注意到两个方向上的数据传输，而且会继续匹配这个连接的包。处于 ESTABLISHED 状态的连接是非常容易理解的。只要发送并接到应答，连接就是 ESTABLISHED 的了。一个连接要从 NEW 变为 ESTABLISHED，只需要接到应答包即可，不管这个包是发往防火墙的，还是要由防火墙转发的。ICMP 的错误和重定向等信息包也被看作是 ESTABLISHED，只要它们是我们所发出的信息的应答。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>RELATED： RELATED 是个比较麻烦的状态。当一个连接和某个已处于 ESTABLISHED 状态的连接有关系时，就被认为是 RELATED 的了。换句话说，一个连接要想是 RELATED 的，首先要有一个 ESTABLISHED 的连接。这个 ESTABLISHED 连接再产生一个主连接之外的连接，这个新的连接就是 RELATED 的了，当然前提是 conntrack 模块要能理解 RELATED。ftp 是个很好的例子，FTP-data 连接就是和 FTP-control 有关联的，如果没有在 iptables 的策略中配置 RELATED 状态，FTP-data 的连接是无法正确建立的，还有其他的例子，比如，通过 IRC 的 DCC 连接。有了这个状态，ICMP 应答、FTP 传输、DCC 等才能穿过防火墙正常工作。注意，大部分还有一些 UDP 协议都依赖这个机制。这些协议是很复杂的，它们把连接信息放在数据包里，并且要求这些信息能被正确理解。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>INVALID：INVALID 说明数据包不能被识别属于哪个连接或没有任何状态。有几个原因可以产生这种情况，比如，内存溢出，收到不知属于哪个连接的 ICMP 错误信息。一般地，我们 DROP 这个状态的任何东西，因为防火墙认为这是不安全的东西。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>更详细的解释见&lt;a class="link" href="http://os.51cto.com/art/201108/285209.htm" target="_blank" rel="noopener"
>此文&lt;/a>&lt;/strong>&lt;/p>
&lt;h3 id="白名单过滤配置方法">白名单过滤配置方法&lt;/h3>
&lt;ol>
&lt;li>创建白名单&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">iptables -N whitelist
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">iptables -A whitelist -s 127.0.0.1 -j ACCEPT
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>指定规则&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">iptables -A INPUT -p tcp -m tcp --dport &lt;span class="m">3306&lt;/span> -j whitelist
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>在所以规则最后加上&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这条规则表示拒绝未匹配到其他规则的包&lt;/p>
&lt;p>其中 &amp;ndash;reject-with 可以有如下几种值&lt;/p>
&lt;p>icmp-net-unreachable&lt;/p>
&lt;p>icmp-host-unreachable&lt;/p>
&lt;p>icmp-port-unreachable&lt;/p>
&lt;p>icmp-proto-unreachable&lt;/p>
&lt;p>icmp-net-prohibited&lt;/p>
&lt;p>icmp-host-prohibited or icmp-admin-prohibited&lt;/p>
&lt;p>注：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>忽略网络问题的情况下 &lt;code>DROP&lt;/code> 和 &lt;code>REJECT&lt;/code> 分别对应 &lt;code>ERR_CONNECTION_TIMEOUT&lt;/code> 和 &lt;code>ERR_CONNECTION_REFUSED&lt;/code> ，iptables 帮助文档中的解释：This (mean REJECT) is used to send back an error packet in response to the matched packet: otherwise, it is equivalent to DROP&lt;/p>
&lt;/li>
&lt;li>
&lt;p>编辑规则的原则：放行的要先放在前面, 禁止的要放在最后&lt;/p>
&lt;/li>
&lt;/ol></description></item><item><title>原生安卓 WiFi 4G 信号去叹号去叉教程</title><link>https://blog.lv5.moe/p/native-android-wifi-4g-signal-exclamation-defork-tutorial</link><pubDate>Tue, 30 Oct 2018 16:13:00 +0800</pubDate><guid>https://blog.lv5.moe/p/native-android-wifi-4g-signal-exclamation-defork-tutorial</guid><description>&lt;p>适用于 7.1.2+ :&lt;/p>
&lt;p>此版本服务器地址判断逻辑相比 7.1.1 没有更改，但是检测的开关却变了。&lt;/p>
&lt;p>检测开关：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">删除变量：（删除以后默认启用）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings delete global captive_portal_mode
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">关闭检测：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings put global captive_portal_mode &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">查看当前状态：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings get global captive_portal_mode
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>服务器地址相关（同 7.1.1）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">删除（删除默认用HTTPS）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings delete global captive_portal_https_url
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings delete global captive_portal_http_url
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">分别修改两个地址
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings put global captive_portal_http_url http://captive.v2ex.co/generate_204
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">adb shell settings put global captive_portal_https_url https://captive.v2ex.co/generate_204
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>转载自 &lt;a class="link" href="https://www.evil42.com/index.php/archives/17/" target="_blank" rel="noopener"
>原生安卓 WiFi 信号去叹号去叉教程 5.0-Android P&lt;/a>，侵删&lt;/p>
&lt;/blockquote></description></item><item><title>Laravel 中视图 view() 与重定向 redirect() 的使用</title><link>https://blog.lv5.moe/p/use-of-view-and-redirect-in-laravel</link><pubDate>Tue, 04 Sep 2018 12:46:00 +0800</pubDate><guid>https://blog.lv5.moe/p/use-of-view-and-redirect-in-laravel</guid><description>&lt;h2 id="一-view-的使用">一、 view() 的使用&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>简单的返回视图&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 所传的参数是blade模板的路径
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 如果目录是 resources/views/static_pages/home.blade.php 则可以使用
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">return&lt;/span> &lt;span class="nx">view&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;static_pages/home&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">或&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="nx">view&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;static_pages.home&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>向视图传递数据&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$title&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;Hello Laravel&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$user&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">User&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="na">find&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// view() 的第二个参数接受一个数组
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">return&lt;/span> &lt;span class="nx">view&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;static_pages/home&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">compact&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;user&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="nx">view&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;articles.lists&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">with&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="nv">$title&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 所传递的变量在blade模板中用 {{ $title }} 或 {!! $title !!} 输出
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 前者作为文本输出，后者作为页面元素渲染
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h2 id="二redirect-的使用">二、redirect() 的使用&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>基于 Url 的重定向&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 假设我们当前的域名为：http://localhost 则重定向到 http://localhost/home
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">return&lt;/span> &lt;span class="nx">redirect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>基于路由的重定向&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="nx">redirect&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>基于控制器的重定向&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="nx">redirect&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">action&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;UserController@index&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>传递数据&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="nx">redirect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">with&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;Hello Laravel&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 将表单值保存到 Session 中，可以用 {{ old(&amp;#39;param&amp;#39;) }} 来获取
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">return&lt;/span> &lt;span class="nx">redirect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">withInput&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 接收一个字符串或数组，传递的变量名为 $errors
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">return&lt;/span> &lt;span class="nx">redirect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">withErrors&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Error&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>其他用法&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-php" data-lang="php">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 返回登录前的页面，参数为默认跳转的页面
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">redirect&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">intended&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 返回上一个页面，注意避免死循环
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">redirect&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="na">back&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h2 id="三使用-view-或-redirect-的选择">三、使用 view() 或 redirect() 的选择&lt;/h2>
&lt;h3 id="view-和-redirect-的异同">view() 和 redirect() 的异同&lt;/h3>
&lt;p>​ 使用 &lt;code>return view()&lt;/code> 不会改变当前访问的 url ， &lt;code>return redirect()&lt;/code> 会改变改变当前访问的 url&lt;/p>
&lt;p>​ 使用 &lt;code>return view()&lt;/code> 不会使当前 Session 的 Flash 失效 ，但是 &lt;code>return redirect()&lt;/code> 会使 Flash 失效&lt;/p>
&lt;p>​ 在 RESTful 架构中，访问 &lt;code>Get&lt;/code> 方法时推荐使用 &lt;code>return view()&lt;/code> ，访问其他方法推荐使用 &lt;code>return redirect()&lt;/code>&lt;/p></description></item><item><title>MySQL 开启远程访问完全解决方案</title><link>https://blog.lv5.moe/p/mysql-open-remote-access-complete-solution</link><pubDate>Tue, 04 Sep 2018 11:02:00 +0800</pubDate><guid>https://blog.lv5.moe/p/mysql-open-remote-access-complete-solution</guid><description>&lt;img src="https://blog.lv5.moe/p/mysql-open-remote-access-complete-solution/security-groups.jpg" alt="Featured image of post MySQL 开启远程访问完全解决方案" />&lt;h2 id="适用环境">适用环境&lt;/h2>
&lt;ul>
&lt;li>MySQL 5.7&lt;/li>
&lt;li>Ubuntu 16.04&lt;/li>
&lt;/ul>
&lt;p>( 适用但不限于以上环境 ）&lt;/p>
&lt;h2 id="操作步骤">操作步骤&lt;/h2>
&lt;h3 id="一开启-mysql-远程访问权限">一、开启 MySQL 远程访问权限&lt;/h3>
&lt;p>将 mysql.host 字段的值改为 % 就表示能在任何客户端机器上登录到 MySQL 服务器&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mysql&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">use&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">mysql&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">Database&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">changed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">mysql&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">grant&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">all&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">privileges&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">on&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">to&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="s1">&amp;#39;%&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">identified&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;password&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Query&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">rows&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">affected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">00&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sec&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">mysql&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">flush&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">privileges&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Query&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">OK&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">rows&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">affected&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">00&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sec&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="o">//&lt;/span>&lt;span class="err">或者新建一个用户并授权，只要设置&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">host&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">为&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">即可&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;username&amp;#39;&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="s1">&amp;#39;host&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IDENTIFIED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;password&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ALL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">privileges&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">databasename&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tablename&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;username&amp;#39;&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="s1">&amp;#39;host&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">flush&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">privileges&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="o">//&lt;/span>&lt;span class="err">如果授权未生效手动重启&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">mysql&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">服务即可&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>修改密码的方法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mysql&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PASSWORD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;username&amp;#39;&lt;/span>&lt;span class="o">@&lt;/span>&lt;span class="s1">&amp;#39;host&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PASSWORD&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;newpass&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="二-检查-mysql-配置">二、 检查 MySQL 配置&lt;/h3>
&lt;p>查看 MySQL 服务绑定的地址：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="n">ubuntu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="o">~&lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">netstat&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ano&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">|&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">grep&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">3306&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">tcp&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">3306&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LISTEN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">off&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">00&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果不是如上所示（绑定在 0.0.0.0:3306），那么就需要更改 &lt;code>/etc/my.cnf&lt;/code> 中的配置，修改下面一行&lt;/p>
&lt;p>&lt;code>bind-address = 0.0.0.0&lt;/code>&lt;/p>
&lt;h3 id="三-检查防火墙配置">三、 检查防火墙配置&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>在 &lt;code>控制面板\系统和安全\Windows Defender 防火墙&lt;/code> 中关掉 Windows 防火墙&lt;/p>
&lt;/li>
&lt;li>
&lt;p>检查服务器上的防火墙&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">ubuntu:~$ sudo iptables -L -n
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chain INPUT &lt;span class="o">(&lt;/span>policy ACCEPT&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">target prot opt &lt;span class="nb">source&lt;/span> destination
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:21
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:2111
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpts:5500:5600
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:3306
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 icmptype &lt;span class="m">8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chain FORWARD &lt;span class="o">(&lt;/span>policy ACCEPT&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">target prot opt &lt;span class="nb">source&lt;/span> destination
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chain OUTPUT &lt;span class="o">(&lt;/span>policy ACCEPT&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">target prot opt &lt;span class="nb">source&lt;/span> destination
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 MySQL 监听的端口（默认 3306）被设置成 DROP 则需要改成 ACCEPT&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">ubuntu:~$ sudo vim /etc/iptables.rules
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ubuntu:~$ sudo iptables-restore &amp;lt; /etc/iptables.rules
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>检查服务商提供的安全组设置，开放对应的端口&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 424;
flex-basis: 1018px"
>
&lt;a href="https://blog.lv5.moe/p/mysql-open-remote-access-complete-solution/security-groups.jpg" data-size="1455x343">
&lt;img src="https://blog.lv5.moe/p/mysql-open-remote-access-complete-solution/security-groups.jpg"
width="1455"
height="343"
loading="lazy"
alt="安全组">
&lt;/a>
&lt;figcaption>安全组&lt;/figcaption>
&lt;/figure>&lt;/p></description></item><item><title>Android 7.0 之后抓包 unknown 和证书无效的解决方案</title><link>https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7</link><pubDate>Fri, 30 Mar 2018 14:04:00 +0800</pubDate><guid>https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7</guid><description>&lt;img src="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/ssl-handshake-failed.jpg" alt="Featured image of post Android 7.0 之后抓包 unknown 和证书无效的解决方案" />&lt;h2 id="背景">背景&lt;/h2>
&lt;p>使用抓包软件（以 Charles 为例）抓取 APP 的 https 请求时，Android 和 Charles 都正确安装了证书却出现抓包失败，报错：&lt;/p>
&lt;p>Client SSL handshake failed: An unknown issue occurred processing the certificate (certificate_unknown)&lt;br />
&lt;figure
class="gallery-image"
style="
flex-grow: 164;
flex-basis: 394px"
>
&lt;a href="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/ssl-handshake-failed.jpg" data-size="1147x698">
&lt;img src="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/ssl-handshake-failed.jpg"
width="1147"
height="698"
loading="lazy"
alt="抓包失败">
&lt;/a>
&lt;figcaption>抓包失败&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;h2 id="原因">原因&lt;/h2>
&lt;p>Android7.0 之后默认不信任用户添加到系统的 CA 证书：&lt;/p>
&lt;blockquote>
&lt;p>To provide a more consistent and more secure experience across the Android ecosystem, beginning with Android Nougat, compatible devices trust only the standardized system CAs maintained in AOSP.（&lt;a class="link" href="https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html" target="_blank" rel="noopener"
>文档链接&lt;/a>）&lt;/p>
&lt;/blockquote>
&lt;p>也就是说对基于 SDK24 及以上的 APP 来说，即使你在手机上安装了抓包工具的证书也无法抓取 https 请求&lt;/p>
&lt;h2 id="解决方案">解决方案&lt;/h2>
&lt;h3 id="一官方解决方案需修改代码">一、官方解决方案（需修改代码）&lt;/h3>
&lt;ul>
&lt;li>官方文档：https://developer.android.google.cn/training/articles/security-config.html&lt;/li>
&lt;li>详细演示：https://blog.csdn.net/mrxiagc/article/details/75329629&lt;/li>
&lt;/ul>
&lt;h3 id="二将抓包软件的证书安装成系统证书需-root">二、将抓包软件的证书安装成系统证书（需 ROOT）&lt;/h3>
&lt;p>系统证书目录：&lt;code>/system/etc/security/cacerts/&lt;/code>&lt;/p>
&lt;p>其中的每个证书的命名规则如下：&lt;br />
&lt;code>&amp;lt;Certificate_Hash&amp;gt;.&amp;lt;Number&amp;gt; &lt;/code>&lt;br />
文件名是一个 Hash 值，而后缀是一个数字。&lt;/p>
&lt;p>文件名可以用下面的命令计算出来：&lt;/p>
&lt;p>&lt;code>openssl x509 -subject_hash_old -in &amp;lt;Certificate_File&amp;gt;&lt;/code>&lt;/p>
&lt;p>后缀名的数字是为了防止文件名冲突的，比如如果两个证书算出的 Hash 值是一样的话，那么一个证书的后缀名数字可以设置成 0，而另一个证书的后缀名数字可以设置成 1&lt;/p>
&lt;h4 id="操作步骤">操作步骤：&lt;/h4>
&lt;p>将抓包软件的证书用上述命令计算出 Hash 值，将其改名并复制到系统证书目录&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 429;
flex-basis: 1031px"
>
&lt;a href="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/cal-hash.jpg" data-size="460x107">
&lt;img src="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/cal-hash.jpg"
width="460"
height="107"
loading="lazy"
alt="计算 Hash 值">
&lt;/a>
&lt;figcaption>计算 Hash 值&lt;/figcaption>
&lt;/figure>&lt;/p>
&lt;p>此时你应该可以在 设置-&amp;gt;安全-&amp;gt;加密与凭据-&amp;gt;信任的凭据 的系统标签页看到你新加入的证书，将其启用即可顺利抓包&lt;/p>
&lt;p>&lt;figure
class="gallery-image"
style="
flex-grow: 58;
flex-basis: 140px"
>
&lt;a href="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/install-ca-certificate.jpg" data-size="280x477">
&lt;img src="https://blog.lv5.moe/p/solutions-to-certificate-invalidation-after-android-7/install-ca-certificate.jpg"
width="280"
height="477"
loading="lazy"
alt="安装好的CA证书">
&lt;/a>
&lt;figcaption>安装好的CA证书&lt;/figcaption>
&lt;/figure>&lt;/p></description></item></channel></rss>